overtrue/wechat
Overtrue/WeChat (EasyWeChat) is a widely used PHP SDK for building WeChat integrations. It provides clean APIs for official accounts, mini programs, payments, open platforms, enterprise WeChat, and more, handling tokens, messages, menus, and OAuth so you can focus on your application logic.
Install the SDK with Composer. For Laravel, use the framework bridge instead of wiring everything by hand:
composer require overtrue/wechat
# Laravel
composer require overtrue/laravel-wechat
php artisan vendor:publish --provider="EasyWeChat\Laravel\ServiceProvider"
Start by looking at config/wechat.php (Laravel) or your application config array. The key objects you will use daily are:
getClient() — raw HTTP calls to WeChat APIs: users, menus, template messages, QR codes, etc.getServer() — webhook/callback handling for messages and events.getAccessToken() — token inspection/refresh when debugging.getUtils() — JSSDK config, mini program codeToSession, encryption helpers.getOAuth() / getAuth() — web authorization and mini program login.getValidator() / getDecryptor() on the payment application — callback signature verification and resource decryption.A minimal official-account server endpoint, which is usually the first thing to get working:
use EasyWeChat\OfficialAccount\Application;
$app = new Application([
'app_id' => env('WECHAT_APP_ID'),
'secret' => env('WECHAT_SECRET'),
'token' => env('WECHAT_TOKEN'),
'aes_key' => env('WECHAT_AES_KEY'), // if using safe mode
]);
Route::any('/wechat/server', function () use ($app) {
$server = $app->getServer();
$server->with(function ($message) {
return 'received';
});
$response = $server->serve();
return response($response->getBody()->getContents(), $response->getStatusCode())
->withHeaders($response->getHeaders());
});
Configure that URL in the WeChat official account console first. The initial GET verification request is handled by serve(); if that fails, nothing else will work.
Use one application instance per platform. Official account, mini program, payment, open platform, and WeChat Work are separate SDK applications with separate credentials, tokens, and sometimes separate caches. Register them as singletons in a service provider and inject the specific application class rather than passing around raw config arrays.
$this->app->singleton(\EasyWeChat\OfficialAccount\Application::class, function () {
return new \EasyWeChat\OfficialAccount\Application(config('wechat.official_account.default'));
});
Treat getClient() as the primary integration surface. Most WeChat APIs do not need dedicated SDK methods; you call the endpoint and handle the JSON response. This is the normal day-to-day workflow for user management, menus, customer service messages, and template messages.
$response = $app->getClient()->get('/cgi-bin/user/get', [
'query' => ['openid' => $openId],
]);
$user = $response->toArray();
Use the server object for all inbound webhooks. Register message and event listeners rather than parsing XML/JSON yourself. Keep handlers fast: acknowledge WeChat immediately, then dispatch jobs for database writes, notifications, or external API calls.
$server = $app->getServer();
$server->addMessageListener('text', function ($message) {
return 'You said: ' . $message->Content;
});
$server->addEventListener('subscribe', function ($event) {
dispatch(new SyncWechatSubscriber($event->FromUserName));
});
For official-account OAuth, redirect and exchange the code server-side. Persist openid and unionid against your local user record. Do not trust user profile data as current; WeChat profile snapshots can be stale unless you refresh them.
$oauth = $app->getOAuth();
// Redirect
return redirect($oauth->redirect($callbackUrl));
// Callback
$user = $oauth->userFromCode($request->get('code'));
$openId = $user->getId();
$unionId = $user->getOriginal()['unionid'] ?? null;
For mini programs, exchange code for session_key only on the server. Never send session_key to the client. Use the SDK encryptor for encrypted phone number or user data payloads.
$session = $miniApp->getUtils()->codeToSession($code);
// Persist session_key in a short-lived server-side session/cache.
$decrypted = $miniApp->getEncryptor()->decrypt(
$session['session_key'],
$iv,
$encryptedData
);
Payment is a separate application and a separate mental model. Create the order server-side, return the prepay_id or signed client parameters, and handle the asynchronous callback through getValidator() and getDecryptor(). Always verify the signature against the raw request body before decrypting.
$pay = new \EasyWeChat\Pay\Application([
'mch_id' => env('WECHAT_MCH_ID'),
'private_key' => env('WECHAT_PAY_PRIVATE_KEY'),
'certificate' => env('WECHAT_PAY_CERTIFICATE'),
'secret_key' => env('WECHAT_PAY_V3_KEY'),
]);
// Create a JSAPI transaction.
$response = $pay->getClient()->post('/v3/pay/transactions/jsapi', [
'json' => [
'appid' => env('WECHAT_APP_ID'),
'mchid' => env('WECHAT_MCH_ID'),
'description' => 'Order #123',
'out_trade_no' => $orderNo,
'notify_url' => route('wechat.pay.notify'),
'amount' => ['total' => 100, 'currency' => 'CNY'],
'payer' => ['openid' => $openId],
],
]);
// Callback: verify first, decrypt second.
$validator = $pay->getValidator();
$validator->validate($headers, $rawBody);
$decryptor = $pay->getDecryptor();
$message = $decryptor->decrypt($rawBody);
Build JSSDK config through getUtils(). Do not hand-roll nonce/timestamp/signature logic; pass the exact current page URL, including query string, and the list of JS APIs you actually use.
$config = $app->getUtils()->buildJsSdkConfig(
$request->fullUrl(),
['chooseImage', 'previewImage'],
false
);
Configure cache and logging explicitly in production. Access tokens are cached by the SDK, but the default cache is not suitable for multi-process or multi-server deployments. Use Redis or Laravel's shared cache so every worker sees the same token. Add a logger to capture SDK HTTP failures, but scrub secrets and authorization headers.
Test with mocked HTTP clients, not live WeChat calls. Inject a fake PSR-18/Guzzle client or use the SDK's configurable HTTP client with a mock handler. Keep the callback payload fixtures from WeChat in tests and assert signature verification and decryption separately from business logic.
EasyWeChat\OfficialAccount\Application, EasyWeChat\MiniApp\Application, and EasyWeChat\Pay\Application. Older examples using Factory::officialAccount() are for 4.x/5.x and will not work unchanged.40001 / 42001 often means the cached token was invalidated or another process refreshed it. 40164 means your server IP is not in the official account or mini program IP whitelist. Clear the cache and check the whitelist before changing code.$server->serve() returns a PSR-7 response. Convert the body, status, and headers when returning from Laravel/Symfony, or use the Laravel bridge's conventions. Forgetting this produces empty or malformed replies to WeChat.getValidator(). In Laravel use $request->getContent().Wechatpay-Serial header against your configured certificate set.out_trade_no / transaction ID and return a success response quickly. Do not perform long-running fulfillment inside the callback request.token and aes_key. If messages arrive encrypted but aes_key is missing or wrong, the server will fail to decode them. If you change encryption settings in the WeChat console, update the app config and clear cached configuration.invalid signature. Build the signature from the same URL the browser reports.json or query option with getClient().openid, phone numbers, or payment data.setCache(), setLogger(), and the configurable HTTP client as extension points. These are the clean places to integrate Laravel's cache, Monolog, and Guzzle middleware/retries rather than editing vendor code.How can I help you explore Laravel packages today?