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

Product Decisions This Supports

  • Build vs. buy for the domain foundation — now covering reads, writes and lists. v0.8.0 turned this package into a small integration platform (read-model pagination, a remote/API adapter layer, a typed HTTP exception hierarchy). v0.8.1 closes the last obvious gap: remote lists. The decision is no longer only "do we want aggregates and domain events?" but "do we want one opinionated vocabulary for persistence and third-party integrations — including serving vendor-owned lists through our own API — or two separate in-house layers?"
  • Feature: stable, cursor-based lists over local read models. ReadModelCursorPaginator::fromPage() unblocks cursor pagination on pages whose items are read models (feeds, activity streams, transaction lists, audit views). Product consequence: infinite scroll and "load more" surfaces that stay correct while rows are inserted, without offset drift or a sort column leaking into the read model.
  • Feature (new): vendor-backed lists served as our API. ApiCursorPageMapper + RelayedCursorPaginator let us take a paginated answer from an external API and return it as a normal Laravel cursor page — data, links, meta — with our path in the links and the vendor's ordering and cursor tokens passed through untouched. Product consequence: an "Accounts", "Invoices", "Contacts" or "Tickets" screen can be built over a system we don't own, without reimplementing that system's sort semantics, and without leaking its URLs or token format to clients. This unblocks BFF / aggregation-layer / whitelabel-partner surfaces that would otherwise need bespoke per-vendor glue.
  • Roadmap: treat an external system of record as a first-class aggregate. Opt-in API-backed aggregates (ApiRepository, RemoteModel, ApiClient + ApiClientConfig) let a domain model a vendor system — billing, CRM, identity, logistics, tax — with the same aggregate/domain-event semantics as a local table. With v0.8.1 the same vendor can also own a list surface in our product, so a single vendor integration now covers detail views, mutations and index pages. That is a buy/extend decision: model the vendor behind a port, versus scattering SDK calls through services and controllers.
  • Consistency commitments are now an explicit product decision. ApiRepository writes are command-based (one API call per DomainEvent), run in recording order, and are not atomic — a mid-sequence failure leaves earlier calls done and events on the aggregate. Any roadmap item backed by a remote aggregate needs a compensating-action/saga story and a user-facing failure mode agreed before build. Note the asymmetry that v0.8.1 makes sharper: list reads through the relay are safe today; writes are the part that needs design.
  • Proxy-or-not is now a cheap decision instead of an expensive one. Because relaying a vendor list is a few lines, the question becomes a genuine product choice rather than an engineering cost: do we proxy vendor lists through our backend (one auth story, one error vocabulary, vendor quotas and latency absorbed by us, no vendor URLs exposed), or let clients call the vendor directly (no backend load, but auth, rate limits and token formats leak into every client)? Decide deliberately — proxying means we own vendor rate-limit and latency budgets, and inherit the vendor's page-size ceiling as a hard constraint on our "load more".
  • Reliability posture as a product input. Only GETs are retried; writes never are, and there are no idempotency keys. List relay inherits this favourably — it is read-only. Constrains which user journeys may write through a remote aggregate (safe: reads, lookups, enrichment, lists; risky: payments, order placement, anything a user may double-submit).
  • Fail-loud is now a stated product principle, not an accident. A vendor answer missing data, meta, an integer meta.per_page or the cursor keys — or carrying a token that isn't a Laravel cursor — throws ApiException. A broken upstream answer never renders as "last page, no results". Product consequence: we need an agreed empty/error state for every relayed list, and we should expect vendor-contract drift to surface as visible errors rather than silent truncation. That is the right trade, but it must be staffed.
  • Data-integrity fix that unblocks partial reads. Entity::hydrate() no longer crashes when created_at/version are absent, and no longer coerces a missing version to 0. Product consequence: aggregates hydrated from API payloads or partial SELECTs are viable; the flip side is a documented rule — include version in partial selects or lose optimistic locking on save.
  • Consolidation / de-duplication decision. The upgrade notes ship an explicit migration path for project-local ApiRepository, RemoteModel, HTTP exceptions and token DTOs — and v0.8.1 explicitly offers to replace hand-rolled "page value objects" for API lists with ApiCursorPageMapper. If a team already built either, "replace local layer with package classes" is a scheduled, low-risk refactor rather than a rewrite.
  • Prioritisation signal. v0.8.1 is additive with no action required — no container bindings, ReadModelCursorPaginator unchanged. It can be adopted inside an existing sprint when a feature is blocked on a vendor-backed list, without a platform workstream or a bespoke internal library negotiation.

When to Consider This Package

Adopt when:

  • The team is already on Laravel/PHP and has committed (or is committing) to tactical DDD: aggregates, domain events, repositories, read models, optimistic locking via updateWithVersionGuard().
  • A near-term feature needs cursor pagination over read models and the current workaround is ->through() plus a hack, or a sort column leaking into the read model purely to satisfy the paginator.
  • A near-term feature needs to expose a third-party paginated list through our own API — account/invoice/contact/ticket lists from a system we don't own — and the vendor's ordering, page size and cursor format must not be reimplemented or leaked. ApiCursorPageMapper + RelayedCursorPaginator is a direct fit when the vendor answer already looks like a Laravel resource collection (data, meta.per_page, next_cursor, prev_cursor) and its cursors are Laravel-decodable tokens.
  • A roadmap item depends on a system you don't own, and you want it modelled as an aggregate with domain events rather than as a service class with an SDK inside — now including its index/list surfaces.
  • An in-house API repository or an in-house "API page value object" already exists and matches the shipped shape; one maintained implementation beats two.
  • You want a single, typed failure vocabulary across vendors (Unauthorized, Forbidden, NotFound, ValidationFailed, Unavailable, base ApiException) so error UX and retry policy are decided once — and a single, explicit "fail loud, never return a false empty page" rule for lists.
  • Vendor auth is client-credentials or static bearer, and token caching in the framework cache is acceptable.
  • You want cursor tokens treated as opaque: RelayedCursor exists precisely because Laravel's Cursor::encode() re-encodes from decoded parameters and isn't guaranteed to round-trip — if your vendor is strict about token strings, this is the reason to use the relay rather than hand-rolling.

Look elsewhere or wait when:

  • You need atomic or idempotent writes across a remote system, or exactly-once semantics. No idempotency keys, no write retries, no compensation framework — you would be building the saga layer yourself on top. (Unaffected by v0.8.1, which is read-only.)
  • The vendor's list payload isn't Laravel-shaped, or uses offset/page-number pagination with a total count. ApiCursorPageMapper is deliberately narrow; you'd write your own mapper feeding RelayedCursorPaginator (which is format-agnostic), but you'd also forgo cursor semantics the vendor doesn't offer.
  • The product needs offset pagination with total counts, page numbers, or "jump to page N" — for local or relayed lists. Cursor pagination is a deliberate trade, and this package leans into it hard. Relayed lists cannot show "page 3 of 27" unless the vendor supplies it.
  • Server-side search/filtering beyond what you can pass through the configurable filter query key is a product requirement — the relay forwards filters, it does not normalise or reimplement them per vendor.
  • The team has no DDD experience, or the domain is straightforward CRUD. The abstractions (aggregates, events, read-model mappers, remote models, relay paginators) cost more than they return below a certain complexity threshold.
  • The platform isn't Laravel, or is Laravel but pinned to framework versions the package hasn't caught up with. It's pre-1.0 (v0.8.1) — v0.8.0 shipped hydrate() behaviour changes and a cache-key incompatibility, so expect API churn and budget upgrade time each release, even when a given release (like this one) is a clean no-op.
  • You need first-class event sourcing, projections, or a message bus. This is not that; events are published after persistence, not a log of record.
  • You need broad HTTP client maturity: connection pooling, circuit breakers, per-call idempotency, structured telemetry. ApiClient is deliberately thin — relayed lists multiply the number of upstream calls, so verify the operational story before putting a high-traffic screen behind it.
  • Multiple teams would each need their own configured client and authenticator with distinct caching/refresh semantics, or your own response envelope can't be expressed by "build a RelayedCursorPaginator from items, page size and neighbouring tokens".

How to Pitch It (Stakeholders)

For executives (one paragraph). This release rounds out an open-source Laravel package we already build on, and it finishes a story: we can now not only model third-party systems (billing, CRM, identity) as first-class parts of our domain, but also serve their paginated lists through our API as ordinary cursor pages — with our URLs, our error types and our auth story, and without reimplementing or exposing theirs. That removes a recurring bespoke build: every vendor-backed list screen ("Accounts", "Invoices", "Contacts") currently needs its own plumbing, and this makes it a few lines plus a mapper. It is additive and opt-in — nothing we run today has to change, no container bindings, and any hand-rolled page objects an application already has can be swapped out later. The strategic value is de-duplication: one maintained integration layer instead of one per vendor, with one failure vocabulary and a deliberate "fail loud, never show a false empty list" rule. Two decisions remain at product level: consistency — writes through a remote system are not atomic and are not retried, so any journey that writes to a vendor needs a compensation and failure-UX decision before we build; and proxying — relaying vendor lists through our backend is now cheap, but it means we absorb vendor latency and rate limits, and we inherit their page-size ceiling. Reads, lookups, enrichment and relayed lists are low risk today.

For engineering (bullets).

  • Additive, opt-in, zero upgrade cost. No action required. Nothing new is container-bound, and ReadModelCursorPaginator is unchanged. New surface: Infrastructure\Pagination\ApiCursorPageMapper, RelayedCursorPaginator (a CursorPaginator subclass), RelayedCursor.
  • Pagination fixes are straight swaps. Local: replace ->through(...) on a cursor-paginated page with ReadModelCursorPaginator::fromPage($page, $mapper) — still an Illuminate\Pagination\CursorPaginator, so return types and Scramble keep working; cursors are read from each read model's tracked source model, so the domain stays free of persistence knowledge; filter()/reordering and JsonResource wrappers are handled. Remote: replace a hand-rolled API page value object with ApiCursorPageMapper and return Resource::collection($page) directly.
  • Relayed pages are built from nothing but items, page size and neighbouring tokens. Nothing is derived from the items, so read models need no sort column and no knowledge of the vendor's ordering. Links use the path you pass, so vendor URLs never leak; the request query string (filter, per_page, cursor) is preserved in the links. Query key names are configurable and unset values are omitted.
  • Token passthrough is intentional. RelayedCursor keeps the vendor token byte-for-byte because Cursor::encode() re-encodes from decoded parameters and is not guaranteed to return the same string. An empty cursor string is normalised to "no cursor". Anything else that isn't a Laravel cursor throws.
  • Loud failures, by contract. A response missing data, missing meta, missing an integer meta.per_page, missing either cursor key, or carrying a non-Laravel cursor throws ApiException. A malformed answer never reads as "last page, no results". Budget error states and vendor-contract-drift alerts accordingly.
  • The mapper is narrow on purpose. It expects the shape a Laravel resource collection produces. A vendor with a different envelope needs a small mapper of its own feeding RelayedCursorPaginator — which is format-agnostic and knows no token format.
  • hydrate() behaviour change from v0.8.0 — still the thing to read before merging a dependency bump. A model with no version used to yield persistedVersion() === 0 (guaranteed ConcurrencyException on update); it now keeps the entity default of 1. If you select columns partially, include version or you silently lose optimistic locking. Table-backed models with a version column are unaffected.
  • Remote aggregate semantics are explicit, not hidden. commands() maps every DomainEvent to an API call; an unmapped event throws UnmappedDomainEvent before any call. Calls run in recording order, the aggregate is hydrated from the last response, events publish afterwards. On failure: earlier calls already happened, events stay on the aggregate — design compensation accordingly.
  • HTTP hardening boundaries. Only GETs retry (connection errors, 5xx); writes never do; no idempotency keys. RefreshableAuthenticator gets one retry after a 401; fixed credentials get none. Missing data payloads and missing access_token throw rather than reading as "no results" — fail loud, not empty. Relayed lists add upstream call volume, so per_page choices and any caching belong to the host application.
  • Migration gotchas for anyone with a local layer. Alias the configured client as Lava83\LaravelDdd\Infrastructure\Api\ApiClient (a thin subclass is fine) or the container can't resolve ApiRepository. Pass tokenPath explicitly (default oauth/token). Do not reuse an old cache key holding a serialized token DTO — tokens are cached as a plain string now; leave cacheKey unset or flush on deploy, and drop the cache.serializable_classes entry. Update catch blocks and message assertions (API … prefix).
  • Versioning caveat to carry into planning. Pre-1.0. Pin the version, read the upgrade notes each release, and treat minor bumps as potentially behaviour-changing — v0.8.0 demonstrated that with hydrate(); v0.8.1 is the counter-example, additive and no-op, but the policy stands.
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