简介:面向PHP开发者与区块链初学者的以太坊私链操作资源,聚焦web3.php库在PHP环境下的实际应用,覆盖区块信息读取、交易发送、智能合约交互与事件监听等核心场景,同时兼顾composer依赖管理与私链RPC连接等基础操作,适合有一定PHP基础、希望进入区块链开发的读者。压缩包共包含1935个文件,以php源码与phpt测试文件为主,辅以xml配置、md文档、json数据及yml脚本等,整体大小约2.29MB;其中php与phpt用于核心库及测试用例,xml与json承担配置和ABI定义,md文档提供使用说明;目录结构完整,src核心库、examples示例、scripts辅助脚本分层清晰,并提供composer.json、phpunit.xml、README.md等标准工程文件。已有4156人学习下载。通过该资源,读者可以对照代码掌握以太坊账户私钥管理、发送交易时的wei单位换算、合约ABI解析及事件订阅等具体用法,也能参考项目中的测试配置与示例脚本,快速搭建自己的PHP以太坊开发调试环境。 做PHP的人去碰以太坊,第一反应往往是“这玩意儿不是Node和Go的天下吗”。真到自己上手才会发现,业务后端是PHP写的,支付回调、用户资产流水、管理员审核这些流程全在Laravel或者ThinkPHP里,总不能让前端拿MetaMask去替你签一堆后台逻辑。这时候用web3.php去操作以太坊,就是PHP开发者最顺手的解法。
web3.php是目前PHP生态里维护最活跃、资料相对齐全的以太坊交互客户端,本质上是把以太坊节点的JSON-RPC接口包成了一个个PHP类和方法。它能帮你完成余额查询、ETH转账、智能合约读写、事件监听这些链上操作,适合做钱包系统后端、NFT项目方后台、链上数据同步脚本,以及任何需要PHP业务系统去对接以太坊的场景。这篇文章我会从一个实际项目角度,把安装配置、常用API、合约交互、踩坑实录完整过一遍。
1. 为什么是web3.php:PHP做链上业务的选型真相
1.1 web3.php到底能干什么
先把这个库的能力边界说清楚。web3.php不是一条链,也不是一个节点,它只是一个RPC客户端。它通过HTTP或WebSocket去连接以太坊节点(比如你自己跑的geth、或云端节点服务),然后把这些底层接口映射成PHP方法。
平日开发里我高频用到的能力是这几块:
- 链信息查询:区块高度、链ID、客户端版本、gasPrice,这些是很多后台页面和脚本的基础输入。
- 账户管理:通过
personal_*系列接口创建地址、解锁账户,也可以配合私钥管理工具做离线签名。 - 交易发送:构造ETH转账、调用合约方法,广播到链上并获取交易哈希。
- 合约交互:读取链上数据(如代币余额、NFT元数据),发送写交易(如转账、mint、盲盒开盒)。
- 历史查询:根据区块号或交易哈希查询交易详情、交易回执,以及事件日志。
如果你要做的功能落在这几类里,web3.php都能覆盖。
1.2 为什么不要自己封装RPC
很多PHP开发者拿到节点地址后,第一反应是用cURL直接POST JSON-RPC。比如查余额就手写一个eth_getBalance请求,查合约就自己拼eth_call的data字段。短时间看起来工作量不大,但一旦深入就会遇到三个绕不开的麻烦。
第一个麻烦是大整数精度。以太坊的余额和交易金额以wei为单位,动辄几十个十进制位,远超PHP浮点数能安全表达的精度范围。手写RPC用json_decode出来的数字会被转成float,精度直接丢失。web3.php内部用BigNumber对象处理链上数值,加减乘除都在整数域内完成,解决的就是这个基础问题。
第二个麻烦是ABI编解码。调用合约函数时,函数名和参数需要按ABI规范编码成一个十六进制data字段,返回值也需要按同样规则解码。人手拼ABI编码极其容易出错,web3.php的Contract类把 encode/decode 都封装好了。
第三个麻烦是类型系统。节点返回的地址是40位十六进制字符串,区块号可能返回十六进制或十进制,不统一处理到处是坑。这些细节库都做掉了,我们只需要关心业务。
所以我的一贯建议是:除非你想彻底搞懂底层协议,不然直接用web3.php,省下的时间拿去排查业务问题更有价值。
2. 环境准备:先让扩展和依赖站好位置
2.1 PHP版本与必须扩展
web3.php对PHP版本要求不算苛刻,7.3以上都能跑,但我实测推荐8.0或8.1,8.2也正常。真正决定能不能安装成功的是下面这几个PHP扩展,缺一个都会在运行时报错:
- gmp:用于大整数运算,web3.php处理wei、gasLimit、nonce都依赖它。
- bcmath:任意精度数学运算库,部分版本和功能分支会用到。
- openssl:生成私钥、签名以及和节点通信时的某些加密操作需要。
- curl:默认的HTTP请求依赖。
- mbstring:字符串处理,尤其涉及地址和ABI编解码时。
如果你用的是宝塔面板,在“软件商店 → PHP设置 → 安装扩展”里把gmp和bcmath勾上就行。自己用Docker部署的话,在Dockerfile里加一行即可:
RUN docker-php-ext-install gmp bcmath装完后用php -m | grep -E 'gmp|bcmath|openssl|curl'检查一遍,确认扩展都已加载。
2.2 composer安装web3.php
web3.php的项目地址是sc0vu/web3.php,但要注意它的版本分支有点混乱。我建议直接安装最新分支:
composer require sc0vu/web3.php:dev-main如果你的项目对稳定性要求很高,也可以锁定一个已经在生产环境跑过的tag。安装完成后看下vendor/sc0vu/web3.php/README.md,不同分支的初始化写法略有差别,以你自己装的版本为准。
安装时如果碰到proc_open、putenv这类函数被禁用导致composer无法运行,去PHP配置里临时放开,装完可以再关掉。
2.3 验证安装
写一个最简PHP脚本,请求节点返回客户端版本:
<?php require 'vendor/autoload.php'; use Web3\Web3; $web3 = new Web3('http://127.0.0.1:8545'); $web3->clientVersion(function ($err, $version) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } echo 'Client version: ' . $version . PHP_EOL; });能输出类似Geth/v1.13.14/linux-amd64/go1.21.5的信息,就说明整条链路已经通了。
3. 第一次连接节点:跑通一次真实的RPC调用
3.1 准备一个可以访问的以太坊节点
开发调试阶段不建议直接连主网,成本高又不安全。我常用的方案有两个。
一个是在本地跑ganache,一条命令就能拉起来一个带200个测试账户的开发链,每个账户自带100个测试ETH,对转账和合约测试非常友好:
npx ganache默认监听8545端口,RPC地址就是http://127.0.0.1:8545。
另一个是连公共节点服务,比如Infura或Alchemy,申请一个项目ID后得到主网或测试网的HTTPS地址。格式类似:
https://mainnet.infura.io/v3/YOUR_PROJECT_ID本地开发建议跑私链,正式脚本再切测试网或主网节点。
3.2 用web3.php查询链信息
连接节点后,我习惯先查询区块高度和gasPrice,确认节点同步正常:
<?php require 'vendor/autoload.php'; use Web3\Web3; $web3 = new Web3('http://127.0.0.1:8545'); $web3->eth->blockNumber(function ($err, $blockNumber) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } echo 'Block number: ' . $blockNumber->toString() . PHP_EOL; }); $web3->eth->gasPrice(function ($err, $gasPrice) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } echo 'Gas price (wei): ' . $gasPrice->toString() . PHP_EOL; });注意blockNumber和gasPrice返回的都是BigNumber对象,不能直接echo,要调用toString()方法。这是web3.php最明显的使用习惯之一。
3.3 回调风格并不难:FPM下的执行真相
web3.php的大部分方法都是回调风格,签名类似:
$web3->eth->blockNumber(function ($err, $result) { // ... });第一次接触的人会担心:是不是需要await?会不回调不执行?其实在传统PHP-FPM的单线程模式下,这些RPC请求是同步阻塞的,回调函数是在请求返回后立即执行的,只是写成了异步风格而已。
也就是说,你不用像Node.js那样为回调地狱担忧。但要注意:如果在Swoole或Workerman这类常驻内存环境里使用,就要小心回调里的长耗时操作会阻塞整个Worker,建议开启协程或放到独立进程处理。
4. 账户、余额与转账:最常用的三个动作
4.1 创建账户和获得地址
在开发链上,我们可以通过personal_newAccount接口创建账户:
<?php require 'vendor/autoload.php'; use Web3\Web3; $web3 = new Web3('http://127.0.0.1:8545'); $password = 'your-password'; $web3->personal->newAccount($password, function ($err, $address) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } echo 'New account: ' . $address . PHP_EOL; });这里创建的账户由节点管理,私钥存在节点本地。生产环境出于安全考虑不太建议这样做,更稳妥的是在PHP侧自己生成私钥和地址。web3.php早期版本没有完整封装离线生成地址的工具,我的做法是直接用kornrunner/keccak之类的库配合椭圆曲线库实现,私钥只在内存里出现,加工完成就销毁。
4.2 查询余额:先弄懂wei和BigNumber
查询一个地址的ETH余额,代码非常短:
$address = '0x...'; $web3->eth->getBalance($address, function ($err, $balance) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } $eth = $balance->toString() / 1e18; echo 'Balance: ' . $eth . ' ETH' . PHP_EOL; });这里的关键点是$balance的单位是wei,要转成ETH得除以1e18。而且不能直接在BigNumber对象上做浮点除法,必须先toString()再用字符串或高精度函数计算。很多人第一次查余额得到一长串数字,以为接口坏了,其实是没做单位换算。
4.3 发起一笔ETH转账
发送ETH转账,需要组装一个交易数组:
$transaction = [ 'from' => '0xFromAddress', 'to' => '0xToAddress', 'value' => '0x' . dechex(0.01 * 1e18), // 0.01 ETH 转成十六进制wei 'gas' => '0x5208', // 21000 'gasPrice' => '0x3b9aca00', // 1 gwei 'nonce' => '0x0', ]; $web3->eth->sendTransaction($transaction, function ($err, $txHash) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } echo 'Tx hash: ' . $txHash . PHP_EOL; });有几个细节我每次写都会确认一遍:
- value的单位:节点要求十六进制字符串表示的wei值,所以要先转成整数wei,再用
dechex转十六进制。直接用0.01或者十进制字符串都会报错。 - gas:普通ETH转账固定21000,合约调用要按实际情况估算。
- nonce:如果交易发送失败或想覆盖pending交易,nonce很重要。
4.4 gas和nonce怎么拿
不要手动写死gasPrice,链上gas波动很厉害。正确做法是请求节点当前推荐gasPrice:
$web3->eth->gasPrice(function ($err, $gasPrice) { $gasPriceHex = '0x' . $gasPrice->toHex(); // 组装交易时使用 $gasPriceHex });nonce的获取也有讲究,尤其一个地址连续发多笔交易时。要用pending参数才能拿到包含pending交易的nonce,否则后发的交易会因为nonce冲突被拒绝:
$web3->eth->getTransactionCount($fromAddress, 'pending', function ($err, $nonce) { $nonceHex = '0x' . $nonce->toHex(); });5. 智能合约交互:读数据和写数据
5.1 ABI与合约实例化
要操作一个智能合约,除了合约地址,还需要它的ABI(Application Binary Interface)。ABI本质是一个JSON数组,定义了合约有哪些函数、参数类型和返回值类型。开发合约时会自动生成,比如用Hardhat编译后会在artifacts/contracts/xxx.sol/xxx.json里找到。
拿到ABI后,实例化合约:
use Web3\Contract; $abi = json_decode(file_get_contents('path/to/abi.json'), true); $contractAddress = '0xContractAddress'; $contract = new Contract($web3->provider, $abi); $contract->at($contractAddress);如果你的web3.php分支初始化方式和这个不同,以官方示例为准,核心流程是一样的。
5.2 视读函数调用:以balanceOf为例
查询一个地址的ERC20代币余额是一个典型的视读(view)函数调用,不消耗gas,不需要from地址,也不会上链:
$userAddress = '0xUserAddress'; $contract->call('balanceOf', $userAddress, function ($err, $result) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } $balanceWei = $result[0]->toString(); $balance = $result[0]->toString() / 1e18; // 按代币精度调整 echo 'Balance: ' . $balance . PHP_EOL; });注意$result是一个数组,返回的每个值对应函数签名里的返回值。balanceOf返回一个uint256,所以取$result[0]。
5.3 写操作:ERC20 transfer的两种调用方式
转账代币和转账ETH不一样,你实际是在调用合约的transfer函数,交易目标地址是合约地址,data字段里编码了transfer(to, amount)的信息。web3.php里有send方法专门处理写操作:
$toAddress = '0xReceiver'; $amount = '1000000000000000000'; // 1个代币,按精度补零 $contract->send('transfer', $toAddress, $amount, [ 'from' => $userAddress, 'gas' => '0x2dc6c0', 'gasPrice' => '0x3b9aca00' ], function ($err, $txHash) { if ($err !== null) { echo 'Error: ' . $err->getMessage() . PHP_EOL; return; } echo 'Tx hash: ' . $txHash . PHP_EOL; });这个调用会广播交易上链,返回的是交易哈希而不是结果。等交易被打包后,你还需要用eth_getTransactionReceipt查询交易收据,确认执行状态。有些开发者会误以为send返回的$txHash就等于执行成功,其实不一定,得看receipt里的status字段是否等于0x1。
gasLimit在写操作里最好调用estimateGas先估算,尤其是合约逻辑复杂时,写死21000肯定不行,写死几十万又会浪费手续费。后续交易可以直接用估算值乘以1.2作为冗余。
6. 实战排坑:以太坊开发里最常见的错误
6.1 错误速查表
这部分是我调试时踩过的坑,整理成一个速查表,遇到报错直接对照。
| 错误信息 | 原因 | 解决办法 |
|---|---|---|
Call to undefined function gmp_init() | PHP缺少gmp扩展 | 安装并启用gmp扩展 |
Cannot connect to Ethereum node | 节点URL错误、节点未启动或端口不对 | 检查节点进程和RPC地址 |
nonce too low | 同一地址有pending交易,或nonce被重复使用 | 用pending模式获取新的nonce |
insufficient funds for gas * price + value | 账户余额不足以支付手续费和转账金额 | 补充余额,或降低gasPrice |
Invalid address | 地址格式不正确,比如大小写校验失败 | 转换为EIP-55 checksum地址或全小写 |
Contract function call returned empty | 合约方法名、参数类型或个数不匹配 | 核对ABI和函数签名 |
Transaction has been reverted | 合约执行失败,比如transfer被拒 | 查询receipt里的revert原因 |
request timeout | 节点响应慢 | 增加HttpRequestManager超时时间 |
超时问题的解决方法是初始化时把超时时间调大:
use Web3\Providers\HttpProvider; use Web3\RequestManagers\HttpRequestManager; $requestManager = new HttpRequestManager('http://127.0.0.1:8545', 30); $provider = new HttpProvider($requestManager); $web3 = new Web3($provider);6.2 生产环境的性能与安全建议
用web3.php做生产服务时,有几点建议来自我的真实教训。
一是不要用节点账户管理私钥。personal_*接口虽然方便,但私钥落在节点进程内存里,一旦节点被攻破就是灾难。正确做法是生成私钥后存到硬件安全模块或配置中心,交易用离线签名,PHP脚本只组装交易和广播。web3.php本身对离线签名的支持不算特别完善,需要配合其他库来实现ECDSA签名,这也是我踩过最多的坑之一。
二是不要把链上轮询任务放FPM进程里做。PHP-FPM是有请求才执行的,不适合做常驻监听。我的做法是写CLI脚本,用while(true)循环批量拉取区块和交易,然后用supervisor守护进程。这样即使脚本挂了也会自动重启。
三是web3.php的每次调用都是一次HTTP请求,性能天花板受节点RPC能力限制。如果需要高频率批量查询,可以加一层Redis缓存,把短时间内不变的数据(比如区块头、合约元数据)缓存起来,大幅减少节点压力。
最后再分享一个小技巧
我现在做链上业务时,习惯把web3.php的所有调用封装成一个独立的Service类,对外只暴露返回数组或抛异常的方法,不让业务代码感知BigNumber和回调。这样测试时只需要mock这个Service,也方便将来换成其他语言编写的微服务。用web3.php操作以太坊本质上就是在填平PHP业务逻辑和链上状态之间的沟壑,封装得越干净,后面维护越省心。希望这份实操梳理能帮你少走一些磕磕绊绊的路。
本文还有配套的精品资源,点击获取