L'intersezione tra Web3 e i framework web tradizionali è dove inizia l'utilità del mondo reale. Mentre i cicli di hype vanno e vengono, l'utilità dei Non-fungible Token (NFT) per verificare la proprietà — specificamente nella biglietteria per eventi — rimane un caso d'uso solido.
In questo articolo, costruiremo la struttura portante di un Sistema di biglietteria per eventi decentralizzato utilizzando Symfony 7.4 e PHP 8.3. Andremo oltre i tutorial di base e implementeremo un'architettura di livello produzione che gestisce la natura asincrona delle transazioni blockchain utilizzando il componente Symfony Messenger.
Un approccio "Senior" riconosce che PHP non è un processo a lunga esecuzione come Node.js. Pertanto, non ascoltiamo gli eventi blockchain in tempo reale all'interno di un controller. Invece, utilizziamo un approccio ibrido:
Molte librerie PHP Web3 sono abbandonate o mal tipizzate. Mentre web3p/web3.php è la più famosa, fare affidamento esclusivamente su di essa può essere rischioso a causa di lacune nella manutenzione.
Per questa guida, utilizzeremo web3p/web3.php (versione ^0.3) per la codifica ABI ma sfrutteremo l'HttpClient nativo di Symfony per il trasporto JSON-RPC effettivo. Questo ci dà il pieno controllo su timeout, retry e logging — elementi critici per le app di produzione.
Innanzitutto, installiamo le dipendenze. Abbiamo bisogno del runtime Symfony, del client HTTP e della libreria 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
Assicurati che il tuo composer.json rifletta la stabilità:
{ "require": { "php": ">=8.3", "symfony/http-client": "7.4.*", "symfony/messenger": "7.4.*", "symfony/uid": "7.4.*", "web3p/web3.php": "^0.3.0" } }
Abbiamo bisogno di un servizio robusto per comunicare con la blockchain. Creeremo un EthereumService che racchiude le chiamate 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 ) {} /** * Legge il proprietario di un ID biglietto specifico (ERC-721 ownerOf). */ public function getTicketOwner(int $tokenId): ?string { // Firma della funzione per ownerOf(uint256) è 0x6352211e // Riempiamo il tokenId a 64 caratteri (32 byte) $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; } // Decodifica l'indirizzo (ultimi 40 caratteri del risultato a 64 caratteri) return '0x' . substr($response['result'], -40); } /** * Invia una richiesta JSON-RPC raw utilizzando Symfony HttpClient. * Questo offre una migliore osservabilità rispetto alle librerie 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('RPC Error: ' . $data['error']['message']); } return $data; } }
Esegui un test locale accedendo a getTicketOwner con un ID mintato conosciuto. Se ottieni un indirizzo 0x, la tua connessione RPC funziona.
Le transazioni blockchain sono lente (da 15 secondi a minuti). Non far mai aspettare un utente per una conferma del blocco in una richiesta del browser. Utilizzeremo Symfony Messenger per gestire questo in 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 ) {} }
È qui che avviene la magia. Utilizzeremo l'helper della libreria web3p/web3.php per firmare una transazione localmente.
Nota: In un ambiente ad alta sicurezza, utilizzeresti un Key Management Service (KMS) o un enclave di firma separato. Per questo articolo, firmiamo localmente.
//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, // Il nostro servizio personalizzato 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. Prepara i dati della transazione (funzione mintTo) // l'implementazione dettagliata della firma della transazione raw solitamente va qui. // Per brevità, simuliamo il flusso logico: try { // Logica per ottenere il nonce corrente e il prezzo del gas tramite EthereumService // $nonce = ... // $gasPrice = ... // Firma la transazione offline per prevenire l'esposizione delle chiavi sulla rete // $tx = new Transaction([...]); // $signedTx = '0x' . $tx->sign($this->privateKey); // Trasmetti // $txHash = $this->ethereumService->sendRawTransaction($signedTx); // In un'app reale, salveresti $txHash nell'entità del database qui $this->logger->info("Mint transaction broadcast successfully."); } catch (\Throwable $e) { $this->logger->error("Minting failed: " . $e->getMessage()); // Symfony Messenger riproverà automaticamente in base alla configurazione throw $e; } } }
Il controller rimane snello. Accetta la richiesta, convalida l'input, crea un'entità biglietto "Pending" nel tuo database (omesso per brevità) e invia il messaggio.
//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. Convalida di base if (!$walletAddress || !str_starts_with($walletAddress, '0x')) { return $this->json(['error' => 'Invalid wallet address'], 400); } // 2. Genera ID interno $ticketId = Uuid::v7(); // 3. Invia messaggio (Fire and Forget) $bus->dispatch(new MintTicketMessage( $ticketId, $walletAddress, 'https://api.myapp.com/metadata/' . $ticketId->toRfc4122() )); // 4. Rispondi immediatamente return $this->json([ 'status' => 'processing', 'ticket_id' => $ticketId->toRfc4122(), 'message' => 'Minting request queued. Check status later.' ], 202); } }
Seguendo lo stile di Symfony 7.4, utilizziamo tipizzazione stretta e attributi. Assicurati che il tuo messenger.yaml sia configurato per il trasporto asincrono.
#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
Per verificare che questa implementazione funzioni senza distribuire su Mainnet:
Nodo locale: Esegui una blockchain locale utilizzando Hardhat o Anvil (Foundry).
npx hardhat node
Ambiente: Imposta il tuo .env.local per puntare a localhost.
BLOCKCHAIN_RPC_URL="http://127.0.0.1:8545" WALLET_PRIVATE_KEY="<una delle chiavi di test fornite da hardhat>" SMART_CONTRACT_ADDRESS="<indirizzo del contratto distribuito>" MESSENGER_TRANSPORT_DSN="doctrine://default"
Consuma: Avvia il worker.
php bin/console messenger:consume async -vv
Richiesta:
curl -X POST https://localhost:8000/api/v1/tickets/mint \ -H "Content-Type: application/json" \ -d '{"wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}'
Dovresti vedere il worker elaborare il messaggio e, se hai implementato completamente la logica di firma della transazione raw, un hash della transazione apparire nella tua console Hardhat.
Costruire applicazioni Web3 in PHP richiede un cambio di mentalità. Non stai solo costruendo un'app CRUD; stai costruendo un orchestratore per lo stato decentralizzato.
Utilizzando Symfony 7.4, abbiamo sfruttato:
Questa architettura scala. Sia che tu stia vendendo 10 biglietti o 10.000, la coda dei messaggi funge da buffer, garantendo che i tuoi nonce di transazione non collidano e che il tuo server non si blocchi.
Integrare la blockchain richiede precisione. Se hai bisogno di aiuto per l'audit delle interazioni del tuo smart contract o per scalare i tuoi consumer di messaggi Symfony, mettiamoci in contatto.
\


