tourze/pinduoduo-api-bundle
A Symfony bundle that integrates the Pinduoduo open-platform API, wiring configuration and services through dependency injection so PHP apps can call PDD e-commerce endpoints with minimal setup.
0.0.1 is the initial tag. The release notes contain only an auto-generated Full Changelog link, so there is no feature list, migration note, or BC promise to read — treat this as "first public snapshot" and evaluate the code, not the changelog.
composer require tourze/pinduoduo-api-bundle:0.0.1 — pin the exact version, this is pre-1.0.*Bundle class, DependencyInjection/*Extension.php, and YAML/XML service wiring — not a Laravel ServiceProvider. There will be no package auto-discovery, no php artisan vendor:publish, and no config/pinduoduo.php merge. Read composer.json (type, extra.symfony, autoload.psr-4) first.PinduoduoServiceProvider that binds the client as a singleton and mirrors the bundle's expected config keys into config/services.php + .env.client_id, client_secret, access_token, base URL, HTTP client), the base client class (signing + transport), and the per-namespace API classes (Goods, Order, Logistics, AfterSales, auth/token).pdd.time.get (connectivity/sign check) followed by pdd.goods.cats.get (real payload shape). If both succeed you have config, signing, and deserialization proven in one shot.Config as a singleton, tokens as data. The client config carries client_id/client_secret (app-level, static) but access_token is per-shop and expires. Do not bake the token into a singleton. Bind an app-level client and build a short-lived per-shop client from it:
// config/services.php
'pinduoduo' => [
'client_id' => env('PDD_CLIENT_ID'),
'client_secret' => env('PDD_CLIENT_SECRET'),
],
// app/Services/Pinduoduo/ClientFactory.php
public function forShop(Shop $shop): PddClient
{
return new PddClient(
config('services.pinduoduo.client_id'),
config('services.pinduoduo.client_secret'),
$shop->pdd_access_token,
);
}
Wrap the bundle, don't sprinkle it. Create App\Services\Pinduoduo\{OrderService,GoodsService,LogisticsService} that own the API classes. When the bundle inevitably changes a method name at 0.0.2, you change one adapter instead of 40 call sites.
Token lifecycle is your job. PDD access tokens expire (order of a month) and there is a refresh endpoint (pdd.pop.auth.token.refresh). Store access_token, refresh_token, expires_at per shop; refresh in a scheduled command and lazily on a token-invalid error code. A single shared global token is the number one way this breaks in multi-shop apps.
Queue outbound writes. Any *.add / *.update / *.send call goes through a job with RateLimited/redis throttle and exponential backoff ([1, 5, 15, 60]), because PDD enforces per-app QPS and returns errors rather than blocking. Read-heavy calls can also be queued and cached.
Message push (webhook) receiver. PDD pushes order/logistics/refund events to an endpoint. Pattern: verify signature → persist raw payload keyed by msg_id (unique index) → return 200 immediately (< a few seconds or it retries) → dispatch a job to process. Never do the downstream work in the HTTP handler.
Testing. Because the transport is almost certainly Guzzle/PSR-18, stub it with a MockHandler (or Http::fake() if the bundle happens to use the Laravel client) and assert on the signed parameter array — that's where your real bugs live.
0.0.1 exactly; a ^0.0.1 constraint would still allow 0.0.2 breaking changes since Composer treats 0.0.x as fully unstable. Read the diff before any bump.*Bundle class you must instantiate and register the container extension yourself — usually not worth it. Budget an hour for the adapter, or skip it.MD5(secret + k1v1k2v2... + secret), uppercase hex), params dropped because the value was null/'', or a non-string being passed (ints must be cast). If the bundle signs for you, do not pre-sign — double-signing produces the same opaque 10001-style error.{"error_response": {"error_code": ..., "error_msg": ...}} with a 200 status. Your transport layer must throw on error_response presence, not on status code. Otherwise failures silently become null return values.Asia/Shanghai and formatted Y-m-d H:i:s, not Unix epochs. Set the timezone on the formatter explicitly, not on the app global — this bites during DST-free-but-offset confusion in CI.error_code only. Truncate access_token to 6 chars.error_code for insufficient stock, invalid state) → do not retry, surface to the domain layer.request(string $method, array $params) (usually public or protected). Subclass it for methods the bundle hasn't generated yet — that gives you full API coverage without waiting for the package, which matters a lot at 0.0.1.How can I help you explore Laravel packages today?