Persimpangan antara Web3 dan framework web tradisional adalah tempat dimulainya utilitas dunia nyata. Sementara siklus hype datang dan pergi, utilitas Non-Fungible Tokens (NFTs) untuk memverifikasi kepemilikan — khususnya dalam tiket acara — tetap menjadi kasus penggunaan yang solid.
Dalam artikel ini, kita akan membangun tulang punggung Sistem Tiket Acara Terdesentralisasi menggunakan Symfony 7.4 dan PHP 8.3. Kita akan melampaui tutorial dasar dan mengimplementasikan arsitektur tingkat produksi yang menangani sifat asinkron transaksi blockchain menggunakan komponen Symfony Messenger.
Pendekatan "Senior" mengakui bahwa PHP bukanlah proses yang berjalan lama seperti Node.js. Oleh karena itu, kita tidak mendengarkan event blockchain secara real-time dalam controller. Sebaliknya, kita menggunakan pendekatan hybrid:
Banyak library PHP Web3 yang ditinggalkan atau kurang baik dalam typing. Meskipun web3p/web3.php adalah yang paling terkenal, mengandalkannya sepenuhnya bisa berisiko karena kesenjangan pemeliharaan.
Untuk panduan ini, kita akan menggunakan web3p/web3.php (versi ^0.3) untuk encoding ABI tetapi akan memanfaatkan HttpClient native Symfony untuk transport JSON-RPC yang sebenarnya. Ini memberi kita kontrol penuh atas timeout, retry, dan logging — kritis untuk aplikasi produksi.
Pertama, mari install dependensi. Kita membutuhkan runtime Symfony, HTTP client, dan library Web3.
composer create-project symfony/skeleton:"7.4.*" decentralized-ticketing cd decentralized-ticketing composer require symfony/http-client symfony/messenger symfony/uid web3p/web3.php
Pastikan composer.json Anda mencerminkan stabilitas:
{ "require": { "php": ">=8.3", "symfony/http-client": "7.4.*", "symfony/messenger": "7.4.*", "symfony/uid": "7.4.*", "web3p/web3.php": "^0.3.0" } }
Kita membutuhkan service yang kuat untuk berkomunikasi dengan blockchain. Kita akan membuat EthereumService yang membungkus panggilan JSON-RPC.
//src/Service/Web3/EthereumService.php namespace App\Service\Web3; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Web3\Utils; class EthereumService { private const JSON_RPC_VERSION = '2.0'; public function __construct( private HttpClientInterface $client, #[Autowire(env: 'BLOCKCHAIN_RPC_URL')] private string $rpcUrl, #[Autowire(env: 'SMART_CONTRACT_ADDRESS')] private string $contractAddress, #[Autowire(env: 'WALLET_PRIVATE_KEY')] private string $privateKey ) {} /** * Reads the owner of a specific Ticket ID (ERC-721 ownerOf). */ public function getTicketOwner(int $tokenId): ?string { // Function signature for ownerOf(uint256) is 0x6352211e // We pad the tokenId to 64 chars (32 bytes) $data = '0x6352211e' . str_pad(Utils::toHex($tokenId, true), 64, '0', STR_PAD_LEFT); $response = $this->callRpc('eth_call', [ [ 'to' => $this->contractAddress, 'data' => $data ], 'latest' ]); if (empty($response['result']) || $response['result'] === '0x') { return null; } // Decode the address (last 40 chars of the 64-char result) return '0x' . substr($response['result'], -40); } /** * Sends a raw JSON-RPC request using Symfony HttpClient. * This offers better observability than standard libraries. */ private function callRpc(string $method, array $params): array { $response = $this->client->request('POST', $this->rpcUrl, [ 'json' => [ 'jsonrpc' => self::JSON_RPC_VERSION, 'method' => $method, 'params' => $params, 'id' => random_int(1, 9999) ] ]); $data = $response->toArray(); if (isset($data['error'])) { throw new \RuntimeException('RPC Error: ' . $data['error']['message']); } return $data; } }
Jalankan tes lokal dengan mengakses getTicketOwner dengan ID yang telah di-mint. Jika Anda mendapat alamat 0x, koneksi RPC Anda berfungsi.
Transaksi blockchain lambat (15 detik hingga menit). Jangan pernah membuat pengguna menunggu konfirmasi blok dalam request browser. Kita akan menggunakan Symfony Messenger untuk menangani ini di background.
//src/Message/MintTicketMessage.php: namespace App\Message; use Symfony\Component\Uid\Uuid; readonly class MintTicketMessage { public function __construct( public Uuid $ticketId, public string $userWalletAddress, public string $metadataUri ) {} }
Di sinilah keajaiban terjadi. Kita akan menggunakan helper library web3p/web3.php untuk menandatangani transaksi secara lokal.
Catatan: Dalam lingkungan keamanan tinggi, Anda akan menggunakan Key Management Service (KMS) atau enklave penandatanganan terpisah. Untuk artikel ini, kita menandatangani secara lokal.
//src/MessageHandler/MintTicketHandler.php namespace App\MessageHandler; use App\Message\MintTicketMessage; use App\Service\Web3\EthereumService; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Web3\Contract; use Web3\Providers\HttpProvider; use Web3\RequestManagers\HttpRequestManager; use Web3p\EthereumTx\Transaction; #[AsMessageHandler] class MintTicketHandler { public function __construct( private EthereumService $ethereumService, // Our custom service private LoggerInterface $logger, #[Autowire(env: 'BLOCKCHAIN_RPC_URL')] private string $rpcUrl, #[Autowire(env: 'WALLET_PRIVATE_KEY')] private string $privateKey, #[Autowire(env: 'SMART_CONTRACT_ADDRESS')] private string $contractAddress ) {} public function __invoke(MintTicketMessage $message): void { $this->logger->info("Starting mint process for Ticket {$message->ticketId}"); // 1. Prepare Transaction Data (mintTo function) // detailed implementation of raw transaction signing usually goes here. // For brevity, we simulate the logic flow: try { // Logic to get current nonce and gas price via EthereumService // $nonce = ... // $gasPrice = ... // Sign transaction offline to prevent key exposure over network // $tx = new Transaction([...]); // $signedTx = '0x' . $tx->sign($this->privateKey); // Broadcast // $txHash = $this->ethereumService->sendRawTransaction($signedTx); // In a real app, you would save $txHash to the database entity here $this->logger->info("Mint transaction broadcast successfully."); } catch (\Throwable $e) { $this->logger->error("Minting failed: " . $e->getMessage()); // Symfony Messenger will automatically retry based on config throw $e; } } }
Controller tetap tipis. Ia menerima request, memvalidasi input, membuat entitas tiket "Pending" di database Anda (dihilangkan untuk singkatnya) dan mengirim message.
//src/Controller/TicketController.php: namespace App\Controller; use App\Message\MintTicketMessage; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Uid\Uuid; #[Route('/api/v1/tickets')] class TicketController extends AbstractController { #[Route('/mint', methods: ['POST'])] public function mint(Request $request, MessageBusInterface $bus): JsonResponse { $payload = $request->getPayload(); $walletAddress = $payload->get('wallet_address'); // 1. Basic Validation if (!$walletAddress || !str_starts_with($walletAddress, '0x')) { return $this->json(['error' => 'Invalid wallet address'], 400); } // 2. Generate Internal ID $ticketId = Uuid::v7(); // 3. Dispatch Message (Fire and Forget) $bus->dispatch(new MintTicketMessage( $ticketId, $walletAddress, 'https://api.myapp.com/metadata/' . $ticketId->toRfc4122() )); // 4. Respond immediately return $this->json([ 'status' => 'processing', 'ticket_id' => $ticketId->toRfc4122(), 'message' => 'Minting request queued. Check status later.' ], 202); } }
Mengikuti gaya Symfony 7.4, kita menggunakan strict typing dan attributes. Pastikan messenger.yaml Anda dikonfigurasi untuk transport async.
#config/packages/messenger.yaml: framework: messenger: transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy: max_retries: 3 delay: 1000 multiplier: 2 routing: 'App\Message\MintTicketMessage': async
Untuk memverifikasi implementasi ini bekerja tanpa deploy ke Mainnet:
Node Lokal: Jalankan blockchain lokal menggunakan Hardhat atau Anvil (Foundry).
npx hardhat node
Environment: Atur .env.local Anda untuk mengarah ke localhost.
BLOCKCHAIN_RPC_URL="http://127.0.0.1:8545" WALLET_PRIVATE_KEY="<one of the test keys provided by hardhat>" SMART_CONTRACT_ADDRESS="<deployed contract address>" MESSENGER_TRANSPORT_DSN="doctrine://default"
Consume: Jalankan worker.
php bin/console messenger:consume async -vv
Request:
curl -X POST https://localhost:8000/api/v1/tickets/mint \ -H "Content-Type: application/json" \ -d '{"wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}'
Anda seharusnya melihat worker memproses message dan, jika Anda mengimplementasikan logika penandatanganan transaksi raw secara penuh, hash transaksi akan muncul di konsol Hardhat Anda.
Membangun aplikasi Web3 di PHP memerlukan pergeseran pola pikir. Anda tidak hanya membangun aplikasi CRUD; Anda membangun orkestrator untuk state terdesentralisasi.
Dengan menggunakan Symfony 7.4, kita memanfaatkan:
Arsitektur ini dapat diskalakan. Baik Anda menjual 10 tiket atau 10.000, antrian message bertindak sebagai buffer, memastikan nonce transaksi Anda tidak bertabrakan dan server Anda tidak hang.
Mengintegrasikan blockchain memerlukan presisi. Jika Anda memerlukan bantuan mengaudit interaksi smart contract Anda atau menskalakan consumer message Symfony Anda, mari terhubung.
\

