يبدأ التقاطع بين Web3 وأطر الويب التقليدية حيث تبدأ الفائدة الحقيقية في العالم الواقعي. بينما تأتي دورات الضجة وتذهب، تظل فائدة الرموز غير القابلة للاستبدال (NFTs) للتحقق من الملكية - خاصة في تذاكر الأحداث - حالة استخدام قوية.
في هذا المقال، سنبني العمود الفقري لـ نظام تذاكر الأحداث اللامركزي باستخدام Symfony 7.4 و PHP 8.3. سننتقل إلى ما هو أبعد من البرامج التعليمية الأساسية وننفذ هندسة معمارية على مستوى الإنتاج تتعامل مع الطبيعة غير المتزامنة لمعاملات البلوكتشين باستخدام مكون Symfony Messenger.
يقر النهج "الأقدم" بأن PHP ليست عملية طويلة الأمد مثل Node.js. لذلك، نحن لا نستمع إلى أحداث البلوكتشين في الوقت الفعلي داخل وحدة التحكم. بدلاً من ذلك، نستخدم نهجًا هجينًا:
تم التخلي عن العديد من مكتبات PHP Web3 أو أنها ضعيفة الكتابة. بينما تعتبر web3p/web3.php الأكثر شهرة، فإن الاعتماد الصارم عليها يمكن أن يكون محفوفًا بالمخاطر بسبب فجوات الصيانة.
لهذا الدليل، سنستخدم web3p/web3.php (الإصدار ^0.3) لترميز ABI ولكن سنستفيد من HttpClient الأصلي لـ Symfony لنقل 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; } }
قم بتشغيل اختبار محلي للوصول إلى 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
للتحقق من أن هذا التنفيذ يعمل دون النشر على 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"
الاستهلاك: ابدأ العامل.
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 الخاصة بك.
يتطلب بناء تطبيقات Web3 في PHP تحولاً في العقلية. أنت لا تبني فقط تطبيق CRUD؛ أنت تبني منسقًا للحالة اللامركزية.
باستخدام Symfony 7.4، استفدنا من:
هذه الهندسة المعمارية قابلة للتوسع. سواء كنت تبيع 10 تذاكر أو 10,000 تذكرة، يعمل قائمة انتظار الرسائل كمنطقة عازلة، مما يضمن عدم تصادم معاملاتك ولا يتوقف خادمك.
يتطلب دمج البلوكتشين الدقة. إذا كنت بحاجة إلى مساعدة في تدقيق تفاعلات العقد الذكي الخاصة بك أو توسيع نطاق مستهلكي رسائل Symfony، فلنكن على اتصال.
\

