Web3 與傳統網頁框架的交會點,正是真實世界應用的起點。儘管炒作週期來來去去,非同質化代幣(NFTs)在驗證所有權方面的實用性——特別是在活動票務領域——仍然是一個可靠的使用案例。
在本文中,我們將使用 Symfony 7.4 和 PHP 8.3 構建去中心化活動票務系統的骨幹架構。我們將超越基礎教學,實作一個生產級架構,使用Symfony Messenger元件來處理區塊鏈交易的異步特性。
「資深」的方法承認 PHP 不像 Node.js 那樣是長時間運行的進程。因此,我們不在控制器內即時監聽區塊鏈事件。相反,我們使用混合方法:
許多 PHP Web3 函式庫已被放棄或型別定義不佳。雖然 web3p/web3.php 是最知名的,但嚴格依賴它可能存在風險,因為存在維護缺口。
在本指南中,我們將使用web3p/web3.php(版本 ^0.3)進行 ABI 編碼,但會利用Symfony 原生的 HttpClient進行實際的 JSON-RPC 傳輸。這讓我們完全控制逾時、重試和日誌記錄——這對生產應用程式至關重要。
首先,讓我們安裝依賴項。我們需要 Symfony 執行時環境、HTTP 用戶端和 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
確保您的composer.json反映了穩定性:
{ "require": { "php": ">=8.3", "symfony/http-client": "7.4.*", "symfony/messenger": "7.4.*", "symfony/uid": "7.4.*", "web3p/web3.php": "^0.3.0" } }
我們需要一個強大的服務來與區塊鏈通訊。我們將建立一個EthereumService來封裝 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; } }
執行本地測試,使用已知的已鑄造 ID 存取 getTicketOwner。如果您獲得 0x 地址,則您的 RPC 連線正常運作。
區塊鏈交易速度緩慢(15秒到數分鐘)。永遠不要讓使用者在瀏覽器請求中等待區塊確認。我們將使用 Symfony Messenger 在背景處理這件事。
//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 ) {} }
這是神奇發生的地方。我們將使用web3p/web3.php函式庫輔助工具在本地簽署交易。
注意: 在高安全性環境中,您會使用金鑰管理服務(KMS)或獨立的簽署隔離區。在本文中,我們在本地簽署。
//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; } } }
控制器保持精簡。它接受請求、驗證輸入、在資料庫中建立「待處理」的票證實體(為簡潔起見省略),並派發訊息。
//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); } }
遵循 Symfony 7.4 風格,我們使用嚴格型別和屬性。確保您的messenger.yaml已配置為異步傳輸。
#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
要在不部署到主網的情況下驗證此實作是否正常運作:
本地節點: 使用Hardhat或Anvil(Foundry)執行本地區塊鏈。
npx hardhat node
環境: 將您的.env.local設定為指向 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"
消費: 啟動工作程式。
php bin/console messenger:consume async -vv
請求:
curl -X POST https://localhost:8000/api/v1/tickets/mint \ -H "Content-Type: application/json" \ -d '{"wallet_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"}'
您應該會看到工作程式處理訊息,如果您完整實作了原始交易簽署邏輯,則會在 Hardhat 控制台中出現交易雜湊值。
在 PHP 中構建Web3應用程式需要思維轉變。您不僅僅是在構建 CRUD 應用程式;您正在構建去中心化狀態的協調器。
透過使用Symfony 7.4,我們利用了:
這個架構具有可擴展性。無論您是銷售 10 張門票還是 10,000 張,訊息佇列都充當緩衝區,確保您的交易 nonce 不會發生衝突,並且您的伺服器不會當機。
整合區塊鏈需要精確性。如果您需要協助審計智能合約互動或擴展 Symfony 訊息消費者,請與我們聯繫。
\


