Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Ddd Laravel Package

lava83/laravel-ddd

A Laravel package for structuring applications with domain-driven design, offering building blocks such as domain layers, entities, value objects, and repositories to help separate business logic from framework code.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

  • The package is a DDD toolkit for Laravel: domain entities, read models, repositories, and now cursor pagination on both the producing side (Eloquent query → page) and the consuming side (HTTP response → page), plus remote (HTTP-backed) aggregate persistence. v0.8.1 closes the read half of the API story; v0.8.0 supplied the write half.
  • ReadModelCursorPaginator handles query-backed lists; ApiCursorPageMapper + RelayedCursorPaginator handle API-backed lists. Both return Illuminate\Pagination\CursorPaginator, so controller signatures, Resource::collection(), response factories and Scramble docs are untouched. The abstraction seam stays at the paginator, which is the right place for it.
  • Relaying the upstream token verbatim rather than decoding and re-encoding is the correct design decision. Laravel's Cursor::encode() reconstructs a token from decoded parameters and is not guaranteed to return the same string, and deriving a foreign API's ordering would couple the host to a sort it does not control. RelayedCursor makes the token genuinely opaque rather than nominally so.
  • RelayedCursorPaginator deriving nothing from the items is consistent with the constraint that API read models have no sort column. Good separation: the paginator knows no token format, only ApiCursorPageMapper knows the Laravel-resource-collection shape. Third-party response shapes need a small mapper, not a fork of the paginator.
  • Loud-failure posture is consistent across both releases: a malformed answer (missing data, meta, non-integer per_page, missing cursor keys, non-Laravel token) throws rather than silently rendering "last page, no results". Same philosophy as the 0.8.0 missing-data behaviour, and the right one.
  • Opt-in posture unchanged: nothing is bound, no config publish, ReadModelCursorPaginator untouched. DB-only and existing-cursor consumers see no change.

Integration feasibility

  • Additive by design and the release notes are accurate: no action required for existing code. This is a materially cleaner release than 0.8.0, which hid a behaviour change (Entity::hydrate() version semantics) under a "Fixed" heading.
  • The relayed-read path requires no service-provider work at all, unlike ApiRepository — no ApiClient binding, no authenticator wiring, no FQCN aliasing. Integration effort is: pass a path (your own route, not the API's), supply mapItem, and handle ApiException on the list endpoint.
  • The coupling point is the response shape. The mapper expects exactly what a Laravel resource collection produces (data plus meta.{per_page,next_cursor,prev_cursor}, names configurable). A non-Laravel upstream needs a bespoke mapper feeding RelayedCursorPaginator, which is deliberately format-agnostic — reasonable, but it is host code, not package code.
  • filter serialization is an integration decision that must be made up front: the mapper builds the query, but the upstream must accept the resulting shape (nested arrays vs repeated keys). Getting this wrong is a silent "no results" rather than an exception.
  • For adopters still below 0.8.0, the 0.8.0 migration items all remain: hydrate-version tests, partial-select audit, token cacheKey flushing, exception class/message churn, project-local API layer rename. 0.8.1 adds none of its own.

Technical risk

  • Upstream response schema coupling on a user-facing path (new). The mapper is strict by design. That converts an upstream cosmetic rename (next_cursor → next) into a hard failure on a page users browse. Version the upstream contract, and alert on ApiException volume on read endpoints.
  • Foreign token passthrough (new). Relaying byte-for-byte is required for correctness, but it means the opaque blob in your next/prev links is now the upstream's token. If that token is signed, expiring, or encodes more than a position (sort key, tenant, filter hash), your links inherit those properties: bookmarked or shared page URLs can stop working, and the token may carry more information than the ID it sorts on. Confirm what the upstream puts in its cursors before exposing them publicly.
  • Cursor/filter mismatch (new). A cursor is only valid for the filter set and sort that produced it. If a client changes filter while keeping cursor, the mapper will forward both; nothing detects the inconsistency, and the upstream will return either an error or a page from a different ordering. Dropping the cursor on filter change, or hashing the filter into it, is host responsibility.
  • Link construction (new, low severity). Links are built from the path argument plus the request query. A wrong path (relative vs absolute, missing prefix) emits broken pagination links that tests asserting only on data will miss.
  • Non-atomic writes (highest, unchanged). commands() runs in recording order; if call N fails, calls 1..N-1 have landed, the aggregate still carries the events, and there is no compensation. No idempotency keys, no automatic write retry. Design a saga/compensation or a durability queue before using ApiRepository for any multi-command aggregate.
  • No outbox (unchanged). Events publish after the API calls succeed, in-process. A crash between the last successful call and publication silently loses integration events.
  • Entity::hydrate() version semantics (unchanged, 0.8.0). A model without version previously yielded persistedVersion() === 0 (always ConcurrencyException); it now keeps the default of 1. Partial selects that omit version carry a wrong expected version and fail on save — loud, but only for some rows, which is harder to spot than the previous consistent failure.
  • Token cache collision on deploy (unchanged, 0.8.0). The package caches the access token as a plain string; an old cacheKey holding a serialized DTO breaks deserialization. Deployment must flush the key or leave cacheKey unset.
  • Exception contract churn (unchanged, 0.8.0). New hierarchy (NotFound, Forbidden, Unauthorized, ValidationFailed, Unavailable, base ApiException) and changed messages. Host code branching on message text falls through to a generic handler — and now does so on read paths too.
  • Pre-1.0 (unchanged). Minor releases may break. 0.8.1 is a clean additive release, which is a good predictability signal; 0.8.0 was not. Budget upgrade effort per release rather than assuming passive adoption.

Key questions

  1. What is the supported PHP/Laravel matrix, and is there a published support policy or LTS track before 1.0?
  2. Does ApiRepository participate in any unit-of-work / transaction boundary, or is the host expected to supply compensation for the non-atomic command sequence?
  3. Is a transactional outbox or deferred event publication planned for API-backed aggregates?
  4. Are idempotency keys or safe-retry semantics for writes on the roadmap?
  5. For multi-API setups, how is the correct ApiClient instance resolved per aggregate/repository, and what is the recommended binding pattern?
  6. Does ReadModelCursorPaginator::fromPage() handle the case where the mapper returns items that do not retain their source model (e.g. mapped to plain arrays or DTOs)?
  7. What is the test coverage and static-analysis baseline for Infrastructure\Api and Infrastructure\Pagination?
  8. Is the configurable surface of ApiCursorPageMapper limited to cursor field names and the filter key, or is there a documented extension point for an entirely different payload shape short of writing a mapper?
  9. How is the filter array serialized into the query string, and is that encoding configurable per client?
  10. Does anything guard against a cursor being reused with a different filter/sort, or is dropping it on filter change explicitly host responsibility?
  11. Are upstream meta fields beyond per_page/next_cursor/prev_cursor (e.g. total, has_more) intentionally dropped by RelayedCursorPaginator, and can a host extend it to relay them?
  12. Is path validated or normalized (absolute vs relative, base URL), and are non-pagination query parameters preserved in the generated links?

Integration Approach

Stack fit

  • Laravel-native throughout: both paginators extend the framework's own CursorPaginator, Resource::collection() and generated docs keep working, and the API read path needs no container binding or middleware assumptions.
  • Combined with ApiRepository, a whole aggregate can now be served without local tables — list via relayed cursor pages, detail via RemoteModel. That is the notable architectural milestone in this release: the API path is no longer write-only, so "remote-backed aggregate" is a complete story rather than a half one.
  • The relayed read path sits cleanly alongside both Eloquent read models and ReadModelCursorPaginator; endpoints can be migrated one at a time with no shared state.

Migration path

  1. Adopt 0.8.1 and run the suite. Expect no failures. If you are still below 0.8.0, complete the 0.8.0 remediation first (version-default tests, partial-select audit, cacheKey flush, exception mapping) — 0.8.1 does not change any of it.
  2. Replace hand-rolled API page value objects. Where a project built its own page/links/token wrapper for API lists, delete it and return Resource::collection($page) from ApiCursorPageMapper::fromResponse(). This is the migration the release notes explicitly point at, and it removes bespoke link and token handling.
  3. Decide cursor-vs-filter behaviour before wiring the controller. Drop the incoming cursor when filter changes (simplest and correct), or hash the filter into the cursor. Do not leave it implicit.
  4. Set path to your own route and add a test that asserts on links.next, not only on data — the one class of defect that unit tests on the mapper will not catch.
  5. Record an upstream fixture (a real response body) and test the mapper against it. That fixture becomes the tripwire for upstream schema drift.
  6. For the 0.8.0 API write layer (if not yet adopted): migrate project-local classes in one PR, not incrementally — swap ApiRepository, RemoteModel, exception classes; rebind the client; switch tokenPath; update catch blocks and message assertions together. Sequence the cacheKey flush with the code release.

Compatibility

  • Pagination relay: additive, opt-in, no bindings, return types unchanged, ReadModelCursorPaginator untouched. Lowest-risk item in either release.
  • Hydrate fix (0.8.0): still the only change that can affect existing behaviour without opt-in. Treat it as breaking for planning purposes even though it is filed under "Fixed".
  • API write layer (0.8.0): fully opt-in; zero impact on DB-only deployments.
  • The only assumption the new read path introduces is the Laravel-resource-collection response shape; anything else is host-owned mapper code.

Sequencing

  • Phase 1 (one sprint): upgrade, fix tests, audit partial selects and add version where needed.
  • Phase 2 (low effort): adopt ReadModelCursorPaginator on existing cursor-paginated read endpoints — immediate correctness win, independently deployable.
  • Phase 3 (low effort, new): pilot the relayed read path on one API-backed list endpoint. Prefer this over the write path as the first API-facing pilot: it is read-only, reversible, has no compensation semantics, and exercises the client, auth, token cache and exception mapping in a cheap setting.
  • Phase 4 (design-led): pick one low-risk remote aggregate for ApiRepository. Define the compensation story for multi-command aggregates before writing commands().
  • Phase 5: migrate remaining project-local API code and retire local classes, exception mappings and page value objects.

Operational Impact

Maintenance

  • Two upstream contract surfaces now need tracking: request shape/query encoding (write and read) and response shape/token semantics (read). Both are upstream decisions that surface as host breakages.
  • Pre-1.0 cadence means every minor release deserves a read of the upgrade notes. However, 0.8.1 adds no migration work and touches nothing by default — a good signal that behaviour changes are being separated from features. Budget recurring upgrade capacity anyway; 0.8.0 alone carried three independent migration items.
  • The package owns classes the host previously owned (API repository, exceptions, token DTO, now page mapping). That reduces bespoke code but transfers change control upstream.

Support

  • A new, high-traffic failure surface exists: list endpoints users browse. ApiException from the mapper — missing data/meta, non-integer per_page, non-Laravel token — surfaces as a 500 on a page, not on a background write. Centralize the exception → HTTP status/payload mapping once so controllers do not each invent one.
  • Support scripts should distinguish "our bug" from "upstream token expired" and "client changed the filter while keeping the cursor". Both look like broken pagination to a user and neither is a defect in your code.
  • Document that public next/prev URLs contain an upstream-owned token that may expire; bookmarked and shared links are not guaranteed stable. Set expectations before users report it.
  • For the write path, keep the non-atomicity warning prominent in runbooks: first-line support will otherwise assume a failed request means nothing changed.
  • Log correlation between the aggregate's recorded event order and outbound API calls remains essential for post-incident reconstruction; extend it to read-side upstream request IDs.

Scaling

  • Cursor pagination is a genuine scaling improvement over offset pagination on large read tables, and the source-tracking approach avoids a schema-level workaround that would have defeated the index. Net positive for read-path performance.
  • Relayed pages avoid re-encoding and re-sorting entirely, so paging cost on your side is mapping plus serialization. Cost moves to the upstream: one remote round trip per page turn, plus cached auth. Expect page latency to be dominated by the remote system.
  • Upstream rate limits now bind user-facing throughput directly. Consider a short-TTL cache on first pages and on hot filter combinations, and treat upstream 429 as a distinct, retryable signal rather than a generic failure.
  • API-backed aggregates trade database load for network latency, upstream limits and per-request auth overhead. Several APIs can coexist via per-provider client construction, which scales organisationally but increases configuration surface — keep a registry of which client serves which aggregate and which mapper serves which list.

Failure modes

Failure Behaviour Operational consequence
Upstream response missing data/meta/cursor keys, or non-integer per_page ApiException thrown; page fails Upstream schema drift becomes a hard outage of the list view; needs monitoring and a versioned contract
Cursor carried across a filter change Mapper forwards cursor and filter together Wrong or inconsistent page; undetectable locally; drop the cursor on filter change
Upstream cursor expired or signed with short TTL Upstream rejects the token Shared/bookmarked page URLs stop working; document and expect it
Wrong path passed to fromResponse() Links point at the wrong route or host Broken "next" for API consumers; only caught by link assertions
Upstream 5xx or connection failure on page fetch ApiException (Unavailable) User-facing error on a browsing page; consider cached fallback
Partial command failure (write path) Earlier calls persisted, aggregate retains events, no rollback Requires host compensation; possible duplicate or inconsistent remote state
Crash after last call, before publish Events never published Silent loss of integration events; no outbox to recover from
Stale token cache key on deploy Token deserialization fails Auth outage for API-backed aggregates until key is flushed
Unmapped domain event Thrown before the first call Safe by construction; surfaces as a loud runtime error in a path CI may not exercise
401 on a fixed-credential authenticator No retry (only RefreshableAuthenticator repeats) Hard failure; correct, but needs a clear error message
Write retried by caller or upstream No idempotency key, no automatic write retry Duplicate remote resources possible
Partial-select entity without version persistedVersion() === 1 regardless of row state ConcurrencyException on save — loud but late
Empty cursor string in request Treated as no cursor Correct; avoids a spurious upstream error on first load

Ramp-up

  • The relayed read path is roughly half a day for a Laravel-fluent developer: build the response, pass path, write mapItem, return Resource::collection(). The conceptual load is one idea — you do not own the cursor; do not decode, re-encode or reconstruct it.
  • Explicitly teach the difference between ReadModelCursorPaginator (you own the query and the sort) and ApiCursorPageMapper/RelayedCursorPaginator (you own neither). The names are similar and the wrong choice fails in a confusing way, usually as a plausible-looking empty page.
  • The API write layer remains the real ramp-up cost: DDD aggregates, domain events, command-per-event mapping, and up-front compensation design.
  • Allocate one experienced developer to pilot the first API-backed read endpoint and the first API-backed write aggregate, and produce an internal pattern document (binding, mapper, exception mapping, filter/cursor policy, compensation, cache rollout) before broadening adoption.
  • Run a short team session on the non-atomicity and no-idempotency-key constraints specifically — those are the two facts most likely to be assumed away by developers used to database transactions.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource