تلاقی وب 3 و فریمورکهای سنتی وب جایی است که کاربرد واقعی آغاز میشود. در حالی که چرخههای هیجانی میآیند و میروند، کاربرد توکن غیر قابل تعویض (NFT) برای تأیید مالکیت — بهویژه در بلیط رویدادها — همچنان یک مورد استفاده قوی باقی میماند.
در این مقاله، ما ستون فقرات یک سیستم بلیط رویداد غیرمتمرکز را با استفاده از Symfony 7.4 و PHP 8.3 خواهیم ساخت. ما فراتر از آموزشهای پایه حرکت میکنیم و یک معماری سطح تولید را پیادهسازی میکنیم که ماهیت ناهمزمان تراکنشهای بلاک چین را با استفاده از کامپوننت Symfony Messenger مدیریت میکند.
رویکرد "ارشد" تصدیق میکند که PHP یک فرآیند طولانیمدت مانند Node.js نیست. بنابراین، ما به رویدادهای بلاک چین بهصورت real-time در یک کنترلر گوش نمیدهیم. در عوض، از یک رویکرد ترکیبی استفاده میکنیم:
بسیاری از کتابخانههای PHP وب 3 رها شده یا تایپ ضعیفی دارند. در حالی که web3p/web3.php معروفترین است، تکیه کامل بر آن میتواند به دلیل شکافهای نگهداری خطرناک باشد.
برای این راهنما، ما از web3p/web3.php (نسخه ^0.3) برای کدگذاری ABI استفاده خواهیم کرد اما از HttpClient بومی Symfony برای انتقال واقعی JSON-RPC بهره خواهیم برد. این به ما کنترل کامل بر روی تایماوتها، تلاشهای مجدد و ثبت وقایع میدهد — که برای برنامههای تولید حیاتی است.
ابتدا، بیایید وابستگیها را نصب کنیم. ما به runtime Symfony، کلاینت HTTP و کتابخانه وب 3 نیاز داریم.
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; } }
یک تست محلی را با دسترسی به 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) یا یک enclave امضای جداگانه استفاده خواهید کرد. برای این مقاله، ما به صورت محلی امضا میکنیم.
//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 شما برای انتقال 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
برای تأیید این که این پیادهسازی بدون استقرار در Mainnet کار میکند:
گره محلی: یک بلاک چین محلی با استفاده از 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"
مصرف: worker را شروع کنید.
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"}'
باید ببینید که worker پیام را پردازش میکند و، اگر منطق امضای تراکنش خام را به طور کامل پیادهسازی کردهاید، یک هش تراکنش در کنسول Hardhat شما ظاهر میشود.
ساخت برنامههای وب 3 در PHP نیاز به یک تغییر در ذهنیت دارد. شما فقط در حال ساخت یک برنامه CRUD نیستید؛ شما در حال ساخت یک هماهنگکننده برای حالت غیرمتمرکز هستید.
با استفاده از Symfony 7.4، ما از موارد زیر بهره بردیم:
این معماری مقیاسپذیر است. چه در حال فروش 10 بلیط باشید یا 10000، صف پیام به عنوان یک بافر عمل میکند و اطمینان حاصل میکند که nonce های تراکنش شما با هم تصادم نمیکنند و سرور شما متوقف نمیشود.
یکپارچهسازی بلاک چین نیاز به دقت دارد. اگر به کمک برای حسابرسی قرارداد هوشمند تعاملات خود یا مقیاسبندی مصرفکنندگان پیام Symfony خود نیاز دارید، با ما در تماس باشید.
\

