De kruising van Web3 en traditionele webframeworks is waar de praktische toepasbaarheid begint. Hoewel hype-cycli komen en gaan, blijft het nut van Non-Fungible Tokens (NFTs) voor het verifiëren van eigendom — specifiek bij evenemententickets — een solide gebruiksscenario.
In dit artikel bouwen we de ruggengraat van een Gedecentraliseerd Evenementticketsysteem met Symfony 7.4 en PHP 8.3. We gaan verder dan basistutorials en implementeren een productie-waardige architectuur die het asynchrone karakter van blockchain-transacties afhandelt met behulp van het Symfony Messenger component.
Een "Senior" benadering erkent dat PHP geen langlopend proces is zoals Node.js. Daarom luisteren we niet in real-time naar blockchain-gebeurtenissen binnen een controller. In plaats daarvan gebruiken we een hybride aanpak:
Veel PHP Web3-bibliotheken zijn verouderd of slecht getypeerd. Hoewel web3p/web3.php het bekendst is, kan strikt vertrouwen erop riskant zijn vanwege onderhoudslacunes.
Voor deze handleiding gebruiken we web3p/web3.php (versie ^0.3) voor ABI-codering, maar maken we gebruik van Symfony's native HttpClient voor het daadwerkelijke JSON-RPC-transport. Dit geeft ons volledige controle over timeouts, retries en logging — cruciaal voor productie-apps.
Laten we eerst de afhankelijkheden installeren. We hebben de Symfony runtime, de HTTP-client en de Web3-bibliotheek nodig.
composer create-project symfony/skeleton:"7.4.*" decentralized-ticketing cd decentralized-ticketing composer require symfony/http-client symfony/messenger symfony/uid web3p/web3.php
Zorg ervoor dat je composer.json de stabiliteit weerspiegelt:
{ "require": { "php": ">=8.3", "symfony/http-client": "7.4.*", "symfony/messenger": "7.4.*", "symfony/uid": "7.4.*", "web3p/web3.php": "^0.3.0" } }
We hebben een robuuste service nodig om met de blockchain te communiceren. We creëren een EthereumService die de JSON-RPC-aanroepen omhult.
//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 ) {} /** * Leest de eigenaar van een specifiek Ticket ID (ERC-721 ownerOf). */ public function getTicketOwner(int $tokenId): ?string { // Functie-signatuur voor ownerOf(uint256) is 0x6352211e // We vullen het tokenId aan naar 64 tekens (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; } // Decodeer het adres (laatste 40 tekens van het 64-tekens resultaat) return '0x' . substr($response['result'], -40); } /** * Verstuurt een raw JSON-RPC-verzoek met Symfony HttpClient. * Dit biedt betere observeerbaarheid dan standaardbibliotheken. */ 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; } }
Voer een lokale test uit met getTicketOwner met een bekend gemint ID. Als je een 0x-adres krijgt, werkt je RPC-verbinding.
Blockchain-transacties zijn langzaam (15s tot minuten). Laat een gebruiker nooit wachten op een blokbevestiging in een browserverzoek. We gebruiken Symfony Messenger om dit op de achtergrond af te handelen.
//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 ) {} }
Dit is waar de magie gebeurt. We gebruiken de web3p/web3.php bibliotheekhelper om lokaal een transactie te ondertekenen.
Let op: In een hoogbeveiligde omgeving zou je een Key Management Service (KMS) of een afzonderlijke signing enclave gebruiken. Voor dit artikel ondertekenen we lokaal.
//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, // Onze aangepaste 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("Mintproces starten voor Ticket {$message->ticketId}"); // 1. Transactiegegevens voorbereiden (mintTo-functie) // gedetailleerde implementatie van raw transaction signing komt hier meestal. // Voor de beknoptheid simuleren we de logische stroom: try { // Logica om huidige nonce en gasprijs te verkrijgen via EthereumService // $nonce = ... // $gasPrice = ... // Transactie offline ondertekenen om sleutelblootstelling over netwerk te voorkomen // $tx = new Transaction([...]); // $signedTx = '0x' . $tx->sign($this->privateKey); // Uitzenden // $txHash = $this->ethereumService->sendRawTransaction($signedTx); // In een echte app zou je hier $txHash opslaan in de database-entiteit $this->logger->info("Minttransactie succesvol uitgezonden."); } catch (\Throwable $e) { $this->logger->error("Minten mislukt: " . $e->getMessage()); // Symfony Messenger zal automatisch opnieuw proberen op basis van config throw $e; } } }
De controller blijft dun. Het accepteert het verzoek, valideert de invoer, maakt een "In behandeling" ticket-entiteit in je database (weggelaten voor de beknoptheid) en verzendt het bericht.
//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. Basisvalidatie if (!$walletAddress || !str_starts_with($walletAddress, '0x')) { return $this->json(['error' => 'Ongeldig wallet-adres'], 400); } // 2. Intern ID genereren $ticketId = Uuid::v7(); // 3. Bericht verzenden (Fire and Forget) $bus->dispatch(new MintTicketMessage( $ticketId, $walletAddress, 'https://api.myapp.com/metadata/' . $ticketId->toRfc4122() )); // 4. Direct reageren return $this->json([ 'status' => 'processing', 'ticket_id' => $ticketId->toRfc4122(), 'message' => 'Mintverzoek in wachtrij geplaatst. Controleer later de status.' ], 202); } }
Volgens de Symfony 7.4-stijl gebruiken we strikte typering en attributen. Zorg ervoor dat je messenger.yaml is geconfigureerd voor async transport.
#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
Om te verifiëren dat deze implementatie werkt zonder naar Mainnet te implementeren:
Lokale Node: Draai een lokale blockchain met Hardhat of Anvil (Foundry).
npx hardhat node
Omgeving: Stel je .env.local in om naar localhost te verwijzen.
BLOCKCHAIN_RPC_URL="http://127.0.0.1:8545" WALLET_PRIVATE_KEY="<een van de testsleutels geleverd door hardhat>" SMART_CONTRACT_ADDRESS="<geïmplementeerd contractadres>" MESSENGER_TRANSPORT_DSN="doctrine://default"
Consumeren: Start de worker.
php bin/console messenger:consume async -vv
Verzoek:
curl -X POST https://localhost:8000/api/v1/tickets/mint \ -H "Content-Type: application/json" \ -d '{"wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}'
Je zou de worker het bericht moeten zien verwerken en, als je de raw transaction signing-logica volledig hebt geïmplementeerd, een transactiehash in je Hardhat-console moeten zien verschijnen.
Het bouwen van Web3 applicaties in PHP vereist een verschuiving in mindset. Je bouwt niet alleen een CRUD-app; je bouwt een orchestrator voor gedecentraliseerde toestand.
Door gebruik te maken van Symfony 7.4, hebben we benut:
Deze architectuur schaalt. Of je nu 10 tickets of 10.000 verkoopt, de message queue fungeert als buffer, zodat je transactie-nonces niet botsen en je server niet vastloopt.
Het integreren van blockchain vereist precisie. Als je hulp nodig hebt bij het controleren van je smart contract-interacties of het opschalen van je Symfony-berichtconsumenten, laten we contact opnemen.
\


