saintsystems/odata-client
A fluent PHP client for calling OData REST services, inspired by Laravel’s Query Builder. Install with Composer and use Guzzle or any PSR-17/PSR-18 HTTP provider for flexible integration.
Architecture fit. The package is a thin, fluent query builder over OData REST endpoints, deliberately mirroring Laravel's Query Builder (where, select, orderBy, take/skip, get). For a Laravel shop this is a near-zero cognitive-load abstraction: developers who can write Eloquent-style queries can produce correct $filter/$expand/$select/$orderby URLs without learning OData's URL grammar. The explicit HTTP provider abstraction (since 0.10.0, Guzzle recommended) is the architecturally significant decision — it makes the client testable (inject a mock provider), lets us bolt OAuth2 token refresh onto the Guzzle handler stack, and keeps the library out of the transport business. That is the right seam for us to own.
Integration feasibility. Composer-installable, MIT, PSR-4, no framework coupling — it works inside Laravel and outside it equally well, which matters if any of the consuming surface is a WordPress plugin or a queue worker in a separate runtime. It ships no Laravel service provider, so integration is ~30 lines of our own: a config file (base URL, auth mode, timeouts), a singleton binding for ODataClient, and a GuzzleHttpProvider built from a shared Guzzle client. Low effort, no version-conflict risk from a third-party provider.
Technical risk. The dominant risk is not code quality, it is adoption and coverage. 160 stars and 0 dependents means we would be among the first production consumers of this library — there is no crowd-sourced hardening of edge cases behind us. OData is a large specification, and any client library implements a subset; the material risk is silent gaps in $batch, $apply/aggregation, delta links, If-Match/ETag optimistic concurrency, @odata.nextLink pagination, and structured error-payload parsing. The version history also shows a breaking seam at 0.10.0 (mandatory HTTP provider) and semver only promised from 1.0.0 — reasonable, but it tells us the API surface was in motion until recently. Single-vendor maintenance (Saint Systems, whose commercial interest is a Gravity Forms → Dynamics 365 add-on) is a continuity consideration: the library is adjacent to their product rather than their product, so roadmap attention is likely proportional to their own D365 needs.
Key questions to put to the vendor / spike team:
$metadata used for typing or is everything untyped arrays?$batch supported, and does it surface per-operation partial failures?@odata.nextLink automatically, or must we paginate by hand?If-Match / 412 and 409 responses handled on update and delete?error.code / error.message payloads normalized into exceptions?Stack fit. Laravel 10/11/12, PHP 8.1+, Guzzle 7. The library is transport-agnostic enough that we can register it as a singleton in our service container and inject it behind an internal CrmGateway interface, so no application code imports SaintSystems\* directly. That single decision converts a vendor-continuity risk into a swap cost of a few days.
Migration path. Most teams reaching for this are replacing hand-rolled Guzzle strings like $client->get("Accounts?\$filter=...") or a tangle of Http::withToken()->get() calls with manually concatenated query strings. The migration is mechanical but should be done behind tests: capture the current request URLs/responses as fixtures, then re-express them as builder calls and assert URL parity. Prioritize read paths first — they carry most of the value and none of the write-side concurrency risk.
Compatibility. No Laravel service provider means no extra.laravel constraints and no collision with our other packages — an underrated benefit. Verify the package's composer.json PHP constraint against our runtime before committing; the Guzzle dependency is the only likely conflict surface, and Guzzle 7 is ubiquitous enough that this is usually a non-issue.
Sequencing:
select/filter/expand/pagination and inspect raw payloads.findAccountByNumber, listOpenOpportunities) rather than leaking the fluent builder upward.If-Match/ETag handling and idempotency keys before any create/update path ships.Retry-After; circuit breaker per entity set.$batch for bulk sync jobs.Maintenance. Low code-churn footprint — the builder API is small and stable at 1.x — but we are the maintainers of last resort. Budget a named owner and a fork-ready posture: MIT licensing and a compact codebase make a vendor fork a half-day exercise if the project stalls. Pin the version in composer.json, subscribe to releases, and run an upgrade only with a green contract-test suite. Realistically: a few hours per quarter of attention, plus a fork contingency.
Support. GitHub issues only; no SLA, no paid support tier for the library itself. For a revenue-adjacent D365 integration that is a genuine gap — mitigate with the adapter boundary and our own test coverage rather than hoping for upstream responsiveness.
Scaling. The client is synchronous and per-request; throughput is bounded by concurrency at the Guzzle layer, not by this package. The real ceiling is the OData server: Dynamics 365 enforces service-protection limits (roughly 6,000 requests per 5 minutes per user, plus execution-time budgets) and returns 429 with Retry-After. Consequences for us: retry/backoff middleware is mandatory, not optional; bulk operations must use $batch (chunked, ~100–1000 ops) rather than loops; and pagination must consume one page at a time via nextLink with a bounded page cap, never "fetch all and filter in PHP." Horizontal PHP-FPM/queue workers scale fine, but a naive foreach over 50k records will get the tenant throttled and the job killed.
Failure modes to instrument for:
$expand depth limits, $top caps, and server-side page-size defaults returning fewer rows than expected without error.$filter literal escaping — OData string literals and datetimes have strict quoting rules; malformed input yields confusing 400s rather than validation errors. Validate at the domain boundary.Ramp-up. For a mid-level Laravel developer, the library itself is roughly half a day — the fluent API is genuinely familiar. The real curve is OData and the target service: navigation properties, $metadata/CSDL, the filter grammar, auth/tenant/app-registration setup, and Dynamics' throttling model. Plan 1–2 weeks to first productive commit and 3–4 weeks to confident production ownership, front-loaded on authentication and sandbox access, which are typically the actual critical path rather than the PHP code. Add a spike ticket purely for "prove pagination and error handling against the sandbox" — it will answer more of the questions above than any amount of README reading.
How can I help you explore Laravel packages today?