Skip to content

Rewrite SDK in the economic-php-sdk style#5

Merged
loevgaard merged 9 commits into
2.xfrom
rewrite-sdk-economic-style
Jun 18, 2026
Merged

Rewrite SDK in the economic-php-sdk style#5
loevgaard merged 9 commits into
2.xfrom
rewrite-sdk-economic-style

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Summary

Re-architects the SDK to mirror setono/economic-php-sdk, preserving exactly the functionality already supported — no new endpoints. Targets the 2.x major.

Scope preserved: paymentGateways (list + getById), pickupPoints (search), salesOrders (list/getById/create), shipmentTemplates (list + getById), webhooks (list/create/delete/deleteAll), and the ShipmentTemplateResolver.

Architecture

  • Immutable Client — collaborators constructor-injected, sandbox: true flag (replaces setDebug() and all setters), HTTP Basic auth, host/port credential guard, match-based exception mapping. get/post/delete return decoded arrays; request() is the PSR-7 escape hatch.
  • Base-class endpoints (EndpointResourceEndpointCollectionEndpoint) replace every *EndpointInterface and capability trait.
  • Resource responses with $raw, stamped by RawStamper outside Valinor's converter pipeline (avoids the per-node reflection leak).
  • Payload requests serialized via the Valinor Normalizer (null-strip + camelCase→snake_case + DateTime→ISO; native enums in src/Enum/).
  • Header-based pagination (Collection carries the X-* header values; paginate() walks pages). pickupPoints returns a bare list<PickupPoint>; paymentGateways/shipmentTemplates look up via ?id=.
  • Typed exception hierarchy under ShipmondoException, exposing getError() for Shipmondo's {"error": "..."} body.

Tooling

  • PHP >=8.1 retained (economic's 8.4-only idioms down-adapted).
  • Psalm → PHPStan level max (+ strict-rules / phpunit / webmozart-assert).
  • cuyz/valinor pinned ^2.2.2 <2.4 — 2.4 drops PHP 8.1 (CI 8.1 matrix guards this).
  • PHPUnit ^10.5 (attribute-based tests; ScriptedHttpClient fake PSR-18, no mocking framework).
  • Dropped psr/log — consumers add logging via PSR-18 middleware.
  • README + CLAUDE.md updated for the new caching/constructor API.

Behavior changes / BC breaks (appropriate for the 2.x major)

  • New constructor signature; all setters removed.
  • get()/post()/delete() return array (was ResponseInterface).
  • pickupPoints()->search(PickupPointSearch): list<PickupPoint> (was get(): Collection).
  • Collection::first() returns null (was false); Collection gains totalCount.
  • Webhook/order-line constants → native enums; SalesOrder.orderId now required.

Verification

  • composer phpunit — 60 tests, 132 assertions (3 live tests skipped)
  • composer analyse — PHPStan level max, no errors
  • composer check-style, vendor/bin/rector --dry-run, composer validate --strict, composer normalize — clean
  • vendor/bin/infection — MSI 58% / covered 69% (thresholds 50/60)

Re-architect the SDK to mirror setono/economic-php-sdk while preserving exactly the currently supported functionality (payment gateways, pickup points, sales orders, shipment templates, webhooks + the shipment template resolver).

Highlights:
- Immutable, constructor-injected Client with a sandbox flag (replaces setters/setDebug); HTTP Basic auth; typed exception hierarchy under ShipmondoException with match-based status mapping.
- Base-class endpoints (Endpoint/ResourceEndpoint/CollectionEndpoint) replace the per-endpoint interfaces and capability traits.
- Resource responses carrying $raw (stamped by RawStamper, outside Valinor's converter pipeline); Payload requests serialized via the Valinor Normalizer (null-strip + camelCase->snake_case + date->ISO + native enums).
- Header-based Collection pagination; pickup points returns a bare list; payment gateways/shipment templates look up by ?id=.

Tooling: PHP >=8.1 retained; switch Psalm -> PHPStan level max; cuyz/valinor pinned ^2.2.2 <2.4 (2.4 drops 8.1); PHPUnit ^10.5; drop psr/log. README + CLAUDE.md updated.

Verified: PHPUnit 60 tests green, PHPStan max clean, ECS clean, Rector clean, composer validate/normalize clean, Infection MSI 58%/covered 69%.
Comment thread composer.json Outdated
"php": ">=8.1",
"cuyz/valinor": "^1.10",
"php-http/discovery": "^1.14",
"cuyz/valinor": "^2.2.2 <2.4",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"cuyz/valinor": "^2.2.2 <2.4",
"cuyz/valinor": "^2.2.2",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — applied in fb2d722. You're right that the <2.4 cap was redundant for the 8.1 floor: Valinor 2.4 requires PHP ≥8.2, so composer's platform constraint already keeps a PHP-8.1 install on ≤2.3. Dropping the cap also means the highest CI on 8.2/8.3 now actually exercises Valinor 2.4 — I verified 2.4 keeps the same PHPStan-extension paths, Source::camelCaseKeys(), and NormalizerBuilder, and CI is green on it.

Comment thread src/Client/Endpoint/Endpoint.php Outdated

/**
* Takes a response and returns a Valinor Source representation
* Recursively convert snake_case array keys to camelCase so they match DTO property names.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the valinor library can do this. Else the Symfony string component also has this capability I think: https://symfony.com/doc/6.4/components/string.html

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done in fb2d722. Switched to Valinor's native Source::array($data)->camelCaseKeys() for mapping and removed the hand-rolled converter. That also let me delete the custom RawStamper: $raw is now just the untouched snake_case payload assigned on the top-level resource (which is arguably a better meaning of "raw" anyway, and matches the key names in the Shipmondo docs).


abstract class ShipmondoTestCase extends TestCase
{
protected const BASE = 'https://sandbox.shipmondo.com/api/public/v3';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this used for?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BASE is the sandbox API base URL (https://sandbox.shipmondo.com/api/public/v3). The tests drive the client through ScriptedHttpClient, a fake PSR-18 keyed by the full request URL, so each test scripts a response against self::BASE . '/sales_orders?page=1&per_page=20' etc. Centralising it on the base test case keeps those URL literals consistent across the endpoint tests. Happy to inline or rename it if you'd prefer.

Verified the integration end-to-end against the Shipmondo sandbox (reads for all endpoints, sales order create + getById, webhook create/list/delete/deleteAll). Captured the real response payloads and wired them into the endpoint tests as fixtures (tests/Fixtures), added a ShipmentTemplatesEndpointTest, and made LiveClientTest sandbox-aware via SHIPMONDO_SANDBOX.
Capture the real GET /sales_orders list payload as a fixture and assert getPage() maps it. Document in the README that newly created sales orders are indexed asynchronously and may not appear in getPage()/paginate() immediately (use getById with the id returned by create() for read-after-write).
Two CI failures, both reproduced locally with --prefer-lowest:

- Static analysis fataled on the lowest matrix: Valinor's suppress-pure-errors PHPStan extension implements PHPStan\Analyser\IgnoreErrorExtension, which only exists in PHPStan >= 2.2, but ^2.1 allowed 2.1.0. The extension is needed (our registerTransformer closures aren't provably pure), so bump phpstan/phpstan to ^2.2.
- Dependency analysis failed: composer-require-checker flagged Composer\InstalledVersions. Drop the version lookup in the User-Agent for a static string (and fix the corresponding ClientTest assertion).
Swap the CI dependency-analysis tool to shipmonk/composer-dependency-analyser (via setup-php). Add composer-dependency-analyser.php config to ignore the unused-dependency error on the virtual psr/http-client-implementation and psr/http-factory-implementation packages (they force a concrete implementation but expose no symbols). Verified locally: no composer issues found.
- Rename top-level request bodies SalesOrder/Webhook -> SalesOrderRequest/WebhookRequest (de-aliases the endpoints; responses stay bare).
- Map via Valinor's native Source::camelCaseKeys() instead of a hand-rolled converter; drop the RawStamper class and set $raw to the untouched snake_case payload on the top-level resource.
- Relax cuyz/valinor to ^2.2.2 (composer's platform constraint keeps PHP 8.1 on <=2.3; 8.2+ CI now exercises 2.4, verified compatible).
Add a full 1.x->2.x upgrade guide. Document the $raw escape hatch in the README, and capture the request-DTO naming, valinor/phpstan constraints, composer-dependency-analyser, and the snake_case $raw decision in CLAUDE.md.
@loevgaard loevgaard merged commit eeec6ff into 2.x Jun 18, 2026
21 checks passed
@loevgaard loevgaard deleted the rewrite-sdk-economic-style branch June 18, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant