Sebelum memulai praktik, pastikan Anda telah memahami konsep dasar pada materi kelas AI Agent & PHP di infokoding.

BAB 12

Membuat Tool Database MySQL untuk AI Agent PHP

Implementasi tools database MySQL untuk AI Agent PHP: searchUser, insertData, getProduct, dan searchArticle dengan prepared statements aman dan validasi input ketat.

Terakhir diperbarui:

TL;DR — Ringkasan Tool Database MySQL AI Agent PHP

  • Prepared Statements: Wajib menggunakan parameter binding PDO untuk mengeliminasi risiko SQL Injection saat model AI mengekstrak input dinamis.
  • Whitelist Operasi: Batasi akses AI hanya pada daftar method eksplisit (seperti searchUser, getProduct, insertData).
  • Klausul LIMIT Wajib: Pasang batasan jumlah baris hasil (misal: LIMIT 10) guna mencegah memory exhaustion dan token blowout.
  • Sanitasi Kolom Sensitif: Jangan pernah menyertakan kolom password, hash, atau token ke dalam output JSON yang dikirimkan ke model AI.

Implementasi Class DatabaseTool untuk AI Agent PHP

Arsitektur Kunci:Whitelist Action Pattern & PDO Prepared Statements
Proteksi Utama:Pencegahan mutlak SQL Injection dan Prompt Injection

Dalam materi belajar ai-agent-php tingkat lanjut, memberikan akses database langsung ke Large Language Model (LLM) memerlukan standar arsitektur keamanan yang sangat ketat. Alih-alih mengizinkan model AI menyusun query SQL mentah secara bebas, kita membangun class DatabaseTool dengan pendekatan Whitelist Action & Parameterized Queries menggunakan PDO PHP murni:

Diagram Alur Data: Proteksi Keamanan DatabaseTool MySQL pada PHP
WebP Lossless • 66 KB • Schema Ready
Diagram alur data keamanan modul belajar ai-agent-php yang mengilustrasikan eksekusi DatabaseTool MySQL dengan validasi whitelist dan prepared statement PDO
Gambar 12.1: Arsitektur Keamanan Alur Data DatabaseTool — LLM hanya mengirimkan parameter pencarian terstruktur, backend PHP mengeksekusi prepared query dengan parameter binding, memfilter kolom sensitif, dan membatasi kuota baris demi menjaga efisiensi token context window.

Penjelasan Parameter dan Spesifikasi Operasi DatabaseTool

Operasi Tersedia:searchUser (Pencarian), getProduct (Detail), searchArticle (Artikel), insertData (Form)
Aturan Sanitasi:Kolom password_hash & token disembunyikan, kuota query dibatasi LIMIT 5–10

Berikut adalah tabel rincian parameter fungsi yang diekspos oleh DatabaseTool untuk memandu model AI dalam memilih operasi data yang tepat:

Nama OperasiParameter KunciValidasi & Batasan KeamananOutput Data
searchUserkeyword (string)Minimal 2 karakter, LIKE binding (? , ?), LIMIT 10id, name, email, role (password disembunyikan)
getProductid (integer)Casting (int), wajib id > 0, WHERE is_active = 1id, name, sku, price, stock, category
searchArticlekeyword (string), category (string)Prepared statement dinamis, filter published, LIMIT 5id, title, slug, category, excerpt
insertDatatable (string), data (object)Strict Whitelist table (feedback, contact_messages)success: true, id: lastInsertId()

Database Safety Takeaway: Pemisahan operasi via whitelist mencegah injeksi DROP TABLE atau manipulasi hak akses oleh instruksi berbahaya (*Prompt Injection*).

Kode Sumber Lengkap Class DatabaseTool di PHP

Berikut adalah implementasi class DatabaseTool berbasis ToolInterface:

<?php

namespace App\Tools;

use PDO;

class DatabaseTool implements ToolInterface
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function getDefinition(): array
    {
        return [
            'type'     => 'function',
            'function' => [
                'name'        => 'database_query',
                'description' => 'Mengakses basis data relasional untuk mencari pengguna, produk, artikel, atau menyimpan pesan masukan. Gunakan operasi: searchUser, getProduct, searchArticle, insertData.',
                'parameters'  => [
                    'type'       => 'object',
                    'properties' => [
                        'operation' => [
                            'type'        => 'string',
                            'enum'        => ['searchUser', 'getProduct', 'searchArticle', 'insertData'],
                            'description' => 'Pilihan operasi basis data yang diizinkan.',
                        ],
                        'params' => [
                            'type'        => 'object',
                            'description' => 'Parameter objek terstruktur untuk operasi yang dipilih.',
                        ],
                    ],
                    'required' => ['operation'],
                ],
            ],
        ];
    }

    public function execute(array $args): mixed
    {
        $operation = $args['operation'] ?? '';
        $params    = $args['params'] ?? [];

        return match ($operation) {
            'searchUser'    => $this->searchUser($params),
            'getProduct'    => $this->getProduct($params),
            'searchArticle' => $this->searchArticle($params),
            'insertData'    => $this->insertData($params),
            default         => ['status' => 'error', 'message' => "Operasi '$operation' tidak diizinkan."],
        };
    }

    private function searchUser(array $params): array
    {
        $keyword = trim($params['keyword'] ?? '');
        if (empty($keyword) || strlen($keyword) < 2) {
            return ['status' => 'error', 'message' => 'Kata kunci pencarian user minimal 2 karakter.'];
        }

        $stmt = $this->pdo->prepare(
            'SELECT id, name, email, role, created_at
             FROM users
             WHERE name LIKE ? OR email LIKE ?
             ORDER BY name ASC
             LIMIT 10'
        );
        $like = '%' . $keyword . '%';
        $stmt->execute([$like, $like]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return ['status' => 'success', 'count' => count($rows), 'data' => $rows];
    }

    private function getProduct(array $params): array
    {
        $productId = (int) ($params['id'] ?? 0);
        if ($productId <= 0) {
            return ['status' => 'error', 'message' => 'ID produk wajib berupa bilangan bulat positif.'];
        }

        $stmt = $this->pdo->prepare(
            'SELECT id, name, sku, price, stock, category, description
             FROM products
             WHERE id = ? AND is_active = 1
             LIMIT 1'
        );
        $stmt->execute([$productId]);
        $product = $stmt->fetch(PDO::FETCH_ASSOC);

        return $product 
            ? ['status' => 'success', 'data' => $product]
            : ['status' => 'error', 'message' => "Produk ID $productId tidak ditemukan."];
    }

    private function searchArticle(array $params): array
    {
        $keyword  = trim($params['keyword'] ?? '');
        $category = trim($params['category'] ?? '');

        $sql    = 'SELECT id, title, slug, category, excerpt, created_at FROM articles WHERE status = "published"';
        $values = [];

        if (!empty($keyword)) {
            $sql     .= ' AND (title LIKE ? OR content LIKE ?)';
            $values[] = '%' . $keyword . '%';
            $values[] = '%' . $keyword . '%';
        }

        if (!empty($category)) {
            $sql     .= ' AND category = ?';
            $values[] = $category;
        }

        $sql .= ' ORDER BY created_at DESC LIMIT 5';

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($values);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return ['status' => 'success', 'count' => count($rows), 'data' => $rows];
    }

    private function insertData(array $params): array
    {
        $table   = $params['table'] ?? '';
        $data    = $params['data'] ?? [];
        $allowed = ['feedback', 'contact_messages'];

        if (!in_array($table, $allowed, true)) {
            return ['status' => 'error', 'message' => "Tabel '$table' tidak diizinkan untuk operasi insert."];
        }

        if (empty($data) || !is_array($data)) {
            return ['status' => 'error', 'message' => 'Data insert tidak boleh kosong.'];
        }

        $columns      = implode(', ', array_keys($data));
        $placeholders = implode(', ', array_fill(0, count($data), '?'));

        $stmt = $this->pdo->prepare("INSERT INTO $table ($columns) VALUES ($placeholders)");
        $stmt->execute(array_values($data));

        return ['status' => 'success', 'inserted_id' => (int) $this->pdo->lastInsertId()];
    }
}
Komparasi Arsitektur: Direct Raw SQL Execution vs Whitelist Function Calling
Dimensi Keamanan & Stabilitas Direct Raw SQL Execution (Berbahaya) Whitelist Function Calling (Standar Enterprise)
Ketahanan Prompt InjectionSangat Rentan (AI bisa dimanipulasi mengeksekusi DROP / UPDATE)Kebal 100% (Model hanya mengirim parameter, bukan sintaks query)
Proteksi SQL InjectionRawan (Jika interpolasi string SQL tidak diescape sempurna)Sempurna (100% diproteksi via PDO Prepared Statements)
Kebocoran Data SensitifTinggi (SELECT * dapat membocorkan password_hash & token)Nol (Kolom output difilter secara eksplisit di level method PHP)
Kontrol Resource & TokenTidak Terkontrol (Query tanpa LIMIT memicu memory crash)Terkontrol (Setiap query dibatasi LIMIT 5–10 baris teratas)
Audit Logging & TracingSulit (Log SQL mentah tidak memiliki konteks intensi AI)Sangat Mudah (Tiap pemanggilan fungsi tercatat di structured log)

Security Architecture Takeaway: Whitelist Function Calling membatasi ruang serang (attack surface) database secara absolut di lingkungan produksi.

Integrasi Function Calling DatabaseTool ke AI Agent Loop

Metode Registrasi:Dependency Injection instance PDO ke $agent->registerTool(new DatabaseTool($pdo))
Karakteristik Loop:Otomatisasi pengembalian role "tool" dengan matching tool_call_id

Berikut adalah contoh skrip implementasi menghubungkan koneksi database PDO ke objek Agent:

<?php
// run-agent-database.php

require_once __DIR__ . '/vendor/autoload.php';

use App\OpenAIClient;
use App\Agent\Agent;
use App\Tools\DatabaseTool;

// Inisialisasi koneksi PDO aman
$pdo = new PDO('mysql:host=127.0.0.1;dbname=infokoding;charset=utf8mb4', 'db_user', 'secret_pass', [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
]);

$client = new OpenAIClient(getenv('OPENAI_API_KEY'));
$agent  = new Agent($client, maxIterations: 5);

// Registrasikan DatabaseTool
$agent->registerTool(new DatabaseTool($pdo));

// Prompt pencarian multi-kriteria
$prompt = 'Carikan pengguna dengan nama "Rusmawan" dan tampilkan peran (role) akunnya.';
$response = $agent->run($prompt);

echo "Jawaban Agent:\n" . $response . "\n";
Resiliensi Basis Data: Penanganan Transaksi PDO & Connection Timeout
ACID & Fault Tolerant

Ketika AI Agent mengeksekusi tugas mutasi multi-langkah (misalnya: membuat pesanan baru sekaligus memotong stok barang), kegagalan di tengah jalan dapat merusak konsistensi basis data (*dirty reads / partial writes*). Gunakan blok PDO Transaction (Atomic Rollback) dan konfigurasi ATTR_TIMEOUT ketat:

<?php

namespace App\Tools;

use PDO;

class TransactionalDatabaseTool implements ToolInterface
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * Eksekusi mutasi multi-langkah dengan jaminan ACID Transaction.
     */
    public function executeOrderTransaction(int $userId, int $productId, int $qty): array
    {
        try {
            // 1. Mulai transaksi database (Atomic)
            $this->pdo->beginTransaction();

            // 2. Periksa stok dengan lock baris (SELECT ... FOR UPDATE)
            $stmt = $this->pdo->prepare("SELECT stock, price FROM products WHERE id = ? FOR UPDATE");
            $stmt->execute([$productId]);
            $product = $stmt->fetch();

            if (!$product || $product['stock'] < $qty) {
                throw new \RuntimeException("Stok produk tidak mencukupi atau produk tidak ditemukan.");
            }

            // 3. Potong stok produk
            $updateStmt = $this->pdo->prepare("UPDATE products SET stock = stock - ? WHERE id = ?");
            $updateStmt->execute([$qty, $productId]);

            // 4. Catat riwayat pesanan
            $orderStmt = $this->pdo->prepare("INSERT INTO orders (user_id, product_id, quantity, created_at) VALUES (?, ?, ?, NOW())");
            $orderStmt->execute([$userId, $productId, $qty]);
            $orderId = (int) $this->pdo->lastInsertId();

            // 5. Commit perubahan data permanen jika seluruh tahap berhasil
            $this->pdo->commit();

            return [
                'status'   => 'success',
                'message'  => 'Transaksi pesanan berhasil diproses secara atomik.',
                'order_id' => $orderId,
            ];
        } catch (\Throwable $e) {
            // 6. Rollback instan jika terjadi exception / deadlock / timeout
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            return [
                'status'  => 'error',
                'message' => 'Transaksi dibatalkan (Rollback): ' . $e->getMessage(),
            ];
        }
    }
}

Best Practice Connection Timeout & Deadlock Guard:

  • PDO::ATTR_TIMEOUT: Pasang timeout 5 detik pada konstruktor PDO agar worker PHP tidak hang saat database mengalami query lock tinggi.
  • inTransaction Check: Selalu periksa $this->pdo->inTransaction() sebelum memanggil rollBack() guna mencegah fatal error No active transaction.

Transaction Takeaway: Penerapan beginTransaction() dan rollBack() menjaga integritas data 100% konsisten meskipun model AI mengalami interupsi jaringan.

FAQ: Pertanyaan Seputar Tool Database MySQL AI Agent PHP

1. Bagaimana cara mencegah SQL Injection pada Tool AI Agent?

Ringkasan Jawaban:Gunakan selalu PDO Prepared Statements dengan parameter binding (? atau named parameter) dan jangan pernah menggabungkan string input dari AI secara langsung ke dalam query SQL.

Dengan prepared statements, database driver memperlakukan input pengguna murni sebagai nilai data (bukan bagian sintaks perintah SQL yang dapat dieksekusi).

2. Mengapa AI Agent tidak boleh menulis query SQL mentah secara bebas?

Ringkasan Jawaban:Membiarkan AI menghasilkan SQL mentah membuka celah Prompt Injection berbahaya di mana penyerang dapat memanipulasi model untuk mengeksekusi DROP TABLE atau membaca data sensitif.

Pendekatan *Whitelist Function* (seperti searchUser atau getProduct) memastikan model hanya dapat mengakses endpoint database yang telah Anda beri izin khusus.

3. Mengapa klausul LIMIT wajib ditambahkan pada query tool database?

Ringkasan Jawaban:Klausul LIMIT mencegah terjadinya dump ribuan baris data yang dapat menghabiskan memori RAM server (*memory exhaustion*) dan memicu lonjakan biaya token (*token blowout*).

Membatasi output query antara 5 hingga 10 baris teratas menjaga efisiensi context window LLM dan mempercepat waktu inferensi.

4. Bagaimana cara menyembunyikan kolom sensitif seperti password dari model AI?

Ringkasan Jawaban:Cantumkan nama kolom secara eksplisit pada klausul SELECT id, name, email alih-alih menggunakan SELECT *, dan bersihkan array data sebelum dikonversi ke JSON.

Ini menjamin data rahasia seperti hash kata sandi, session token, atau nomor rekening tidak pernah bocor ke log percakapan LLM pihak ketiga.

RA

Rusmawan Abdullah Sani

Lead Software Engineer & System Architect

Praktisi pengembangan backend PHP modern, arsitektur AI Agent, microservices, dan otomasi server Linux. Terhubung melalui profil LinkedIn.