Die Schnittstelle zwischen Web3 und traditionellen Web-Frameworks ist der Punkt, an dem der reale Nutzen beginnt. Während Hype-Zyklen kommen und gehen, bleibt der Nutzen von Non-fungible Token (NFT) zur Verifizierung des Eigentums – insbesondere bei Event-Tickets – ein solider Anwendungsfall.
In diesem Artikel werden wir das Rückgrat eines Dezentralen Event-Ticketing-Systems mit Symfony 7.4 und PHP 8.3 erstellen. Wir werden über grundlegende Tutorials hinausgehen und eine produktionsreife Architektur implementieren, die die asynchrone Natur von Blockchain-Transaktionen mit der Symfony Messenger-Komponente handhabt.
Ein „Senior"-Ansatz erkennt an, dass PHP kein lang laufender Prozess wie Node.js ist. Daher hören wir nicht in Echtzeit auf Blockchain-Ereignisse innerhalb eines Controllers. Stattdessen verwenden wir einen hybriden Ansatz:
Viele PHP-Web3-Bibliotheken sind aufgegeben oder schlecht typisiert. Während web3p/web3.php die bekannteste ist, kann eine strenge Abhängigkeit davon aufgrund von Wartungslücken riskant sein.
Für diesen Leitfaden werden wir web3p/web3.php (Version ^0.3) für die ABI-Codierung verwenden, aber Symfonys nativen HttpClient für den tatsächlichen JSON-RPC-Transport nutzen. Dies gibt uns volle Kontrolle über Timeouts, Wiederholungen und Protokollierung – entscheidend für Produktions-Apps.
Zuerst installieren wir die Abhängigkeiten. Wir benötigen die Symfony-Laufzeitumgebung, den HTTP-Client und die Web3-Bibliothek.
composer create-project symfony/skeleton:"7.4.*" decentralized-ticketing cd decentralized-ticketing composer require symfony/http-client symfony/messenger symfony/uid web3p/web3.php
Stellen Sie sicher, dass Ihre composer.json die Stabilität widerspiegelt:
{ "require": { "php": ">=8.3", "symfony/http-client": "7.4.*", "symfony/messenger": "7.4.*", "symfony/uid": "7.4.*", "web3p/web3.php": "^0.3.0" } }
Wir benötigen einen robusten Service, um mit der Blockchain zu kommunizieren. Wir werden einen EthereumService erstellen, der die JSON-RPC-Aufrufe umschließt.
//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; } }
Führen Sie einen lokalen Test durch, indem Sie getTicketOwner mit einer bekannten geminteten ID aufrufen. Wenn Sie eine 0x-Adresse erhalten, funktioniert Ihre RPC-Verbindung.
Blockchain-Transaktionen sind langsam (15 Sekunden bis Minuten). Lassen Sie einen Benutzer niemals auf eine Block-Bestätigung in einer Browser-Anfrage warten. Wir werden Symfony Messenger verwenden, um dies im Hintergrund zu handhaben.
//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 ) {} }
Hier geschieht die Magie. Wir werden den web3p/web3.php-Bibliothekshelfer verwenden, um eine Transaktion lokal zu signieren.
Hinweis: In einer Hochsicherheitsumgebung würden Sie einen Key Management Service (KMS) oder eine separate Signierungsenklave verwenden. Für diesen Artikel signieren wir 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; } } }
Der Controller bleibt schlank. Er akzeptiert die Anfrage, validiert die Eingabe, erstellt eine „Ausstehende" Ticket-Entität in Ihrer Datenbank (der Kürze halber ausgelassen) und sendet die Nachricht.
//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); } }
Entsprechend dem Symfony 7.4-Stil verwenden wir strenge Typisierung und Attribute. Stellen Sie sicher, dass Ihre messenger.yaml für asynchronen Transport konfiguriert ist.
#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
Um zu verifizieren, dass diese Implementierung funktioniert, ohne auf dem Mainnet bereitzustellen:
Lokaler Knoten: Führen Sie eine lokale Blockchain mit Hardhat oder Anvil (Foundry) aus.
npx hardhat node
Umgebung: Setzen Sie Ihre .env.local so, dass sie auf localhost zeigt.
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"
Konsumieren: Starten Sie den Worker.
php bin/console messenger:consume async -vv
Anfrage:
curl -X POST https://localhost:8000/api/v1/tickets/mint \ -H "Content-Type: application/json" \ -d '{"wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}'
Sie sollten sehen, dass der Worker die Nachricht verarbeitet, und wenn Sie die Logik zur Signierung von Raw-Transaktionen vollständig implementiert haben, sollte ein Transaktions-Hash in Ihrer Hardhat-Konsole erscheinen.
Das Erstellen von Web3-Anwendungen in PHP erfordert einen Mentalitätswechsel. Sie erstellen nicht nur eine CRUD-App; Sie erstellen einen Orchestrator für dezentralen Status.
Durch die Verwendung von Symfony 7.4 haben wir genutzt:
Diese Architektur skaliert. Egal, ob Sie 10 Tickets oder 10.000 verkaufen, die Nachrichten-Warteschlange fungiert als Puffer und stellt sicher, dass Ihre Transaktions-Nonces nicht kollidieren und Ihr Server nicht hängt.
Die Integration von Blockchain erfordert Präzision. Wenn Sie Hilfe bei der Prüfung Ihrer Smart-Contract-Interaktionen oder der Skalierung Ihrer Symfony-Nachrichten-Consumer benötigen, lassen Sie uns in Kontakt treten.
\

