Intersecția dintre Web3 și framework-urile web tradiționale este locul unde utilitatea din lumea reală începe. Deși ciclurile de hype vin și pleacă, utilitatea Token-urilor Non-Fungibile (NFT-uri) pentru verificarea proprietății — în special în vânzarea de bilete la evenimente — rămâne un caz de utilizare solid.
În acest articol, vom construi coloana vertebrală a unui Sistem Descentralizat de Ticketing pentru Evenimente folosind Symfony 7.4 și PHP 8.3. Vom depăși tutorialele de bază și vom implementa o arhitectură de nivel de producție care gestionează natura asincronă a tranzacțiilor blockchain folosind componenta Symfony Messenger.
O abordare „Senior" recunoaște că PHP nu este un proces de lungă durată precum Node.js. Prin urmare, nu ascultăm evenimentele blockchain în timp real într-un controller. În schimb, folosim o abordare hibridă:
Multe biblioteci PHP Web3 sunt abandonate sau slab tipizate. Deși web3p/web3.php este cea mai cunoscută, a te baza strict pe ea poate fi riscant din cauza lacunelor de întreținere.
Pentru acest ghid, vom folosi web3p/web3.php (versiunea ^0.3) pentru codificarea ABI, dar vom valorifica HttpClient nativ Symfony pentru transportul JSON-RPC efectiv. Acest lucru ne oferă control complet asupra timeout-urilor, reîncercărilor și jurnalizării — critice pentru aplicațiile de producție.
Mai întâi, să instalăm dependențele. Avem nevoie de runtime-ul Symfony, clientul HTTP și biblioteca 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
Asigurați-vă că composer.json reflectă stabilitatea:
{ "require": { "php": ">=8.3", "symfony/http-client": "7.4.*", "symfony/messenger": "7.4.*", "symfony/uid": "7.4.*", "web3p/web3.php": "^0.3.0" } }
Avem nevoie de un serviciu robust pentru a comunica cu blockchain-ul. Vom crea un EthereumService care încapsulează apelurile 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 ) {} /** * Citește proprietarul unui ID de Bilet specific (ERC-721 ownerOf). */ public function getTicketOwner(int $tokenId): ?string { // Semnătura funcției pentru ownerOf(uint256) este 0x6352211e // Umplem tokenId la 64 caractere (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; } // Decodificăm adresa (ultimele 40 caractere din rezultatul de 64 caractere) return '0x' . substr($response['result'], -40); } /** * Trimite o cerere JSON-RPC brută folosind Symfony HttpClient. * Acest lucru oferă o mai bună observabilitate decât bibliotecile standard. */ 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('Eroare RPC: ' . $data['error']['message']); } return $data; } }
Rulați un test local accesând getTicketOwner cu un ID mintat cunoscut. Dacă obțineți o adresă 0x, conexiunea RPC funcționează.
Tranzacțiile blockchain sunt lente (15s până la minute). Nu faceți niciodată un utilizator să aștepte confirmarea unui bloc într-o cerere de browser. Vom folosi Symfony Messenger pentru a gestiona acest lucru în fundal.
//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 ) {} }
Aici se întâmplă magia. Vom folosi helper-ul bibliotecii web3p/web3.php pentru a semna o tranzacție local.
Notă: Într-un mediu de înaltă securitate, ați folosi un Serviciu de Gestionare a Cheilor (KMS) sau o enclavă de semnare separată. Pentru acest articol, semnăm local.
//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, // Serviciul nostru personalizat 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("Se inițiază procesul de mint pentru Biletul {$message->ticketId}"); // 1. Pregătirea Datelor Tranzacției (funcția mintTo) // implementarea detaliată a semnării tranzacției brute se plasează de obicei aici. // Pentru scurtare, simulăm fluxul logic: try { // Logică pentru obținerea nonce-ului curent și a prețului gazului prin EthereumService // $nonce = ... // $gasPrice = ... // Semnarea tranzacției offline pentru a preveni expunerea cheii peste rețea // $tx = new Transaction([...]); // $signedTx = '0x' . $tx->sign($this->privateKey); // Difuzare // $txHash = $this->ethereumService->sendRawTransaction($signedTx); // Într-o aplicație reală, ați salva $txHash în entitatea din baza de date aici $this->logger->info("Tranzacția de mint a fost difuzată cu succes."); } catch (\Throwable $e) { $this->logger->error("Mint-ul a eșuat: " . $e->getMessage()); // Symfony Messenger va reîncerca automat pe baza configurației throw $e; } } }
Controller-ul rămâne subțire. Acceptă cererea, validează input-ul, creează o entitate de bilet „În așteptare" în baza de date (omisă pentru scurtare) și trimite mesajul.
//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. Validare de bază if (!$walletAddress || !str_starts_with($walletAddress, '0x')) { return $this->json(['error' => 'Adresă de portofel invalidă'], 400); } // 2. Generare ID intern $ticketId = Uuid::v7(); // 3. Trimiterea Mesajului (Fire and Forget) $bus->dispatch(new MintTicketMessage( $ticketId, $walletAddress, 'https://api.myapp.com/metadata/' . $ticketId->toRfc4122() )); // 4. Răspuns imediat return $this->json([ 'status' => 'processing', 'ticket_id' => $ticketId->toRfc4122(), 'message' => 'Cerere de minting în coadă. Verificați starea mai târziu.' ], 202); } }
Urmând stilul Symfony 7.4, folosim tipizare strictă și atribute. Asigurați-vă că messenger.yaml este configurat pentru 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
Pentru a verifica că această implementare funcționează fără a implementa pe Mainnet:
Nod Local: Rulați un blockchain local folosind Hardhat sau Anvil (Foundry).
npx hardhat node
Mediu: Setați .env.local să indice către localhost.
BLOCKCHAIN_RPC_URL="http://127.0.0.1:8545" WALLET_PRIVATE_KEY="<una dintre cheile de test furnizate de hardhat>" SMART_CONTRACT_ADDRESS="<adresa contractului implementat>" MESSENGER_TRANSPORT_DSN="doctrine://default"
Consumare: Porniți worker-ul.
php bin/console messenger:consume async -vv
Cerere:
curl -X POST https://localhost:8000/api/v1/tickets/mint \ -H "Content-Type: application/json" \ -d '{"wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}'
Ar trebui să vedeți worker-ul procesând mesajul și, dacă ați implementat complet logica de semnare a tranzacției brute, un hash de tranzacție apare în consola Hardhat.
Construirea aplicațiilor Web3 în PHP necesită o schimbare de mentalitate. Nu construiți doar o aplicație CRUD; construiți un orchestrator pentru starea descentralizată.
Folosind Symfony 7.4, am valorificat:
Această arhitectură se scalează. Indiferent dacă vindeți 10 bilete sau 10 000, coada de mesaje acționează ca un buffer, asigurându-se că nonce-urile tranzacțiilor nu se ciocnesc și serverul nu se blochează.
Integrarea blockchain-ului necesită precizie. Dacă aveți nevoie de ajutor pentru auditarea interacțiunilor cu contractele inteligente sau scalarea consumatorilor de mesaje Symfony, să luăm legătura.
\

