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

Chain Laravel Package

armincms/chain

Armincms Chain is a Laravel/PHP package for building fluent, chainable workflows. Compose reusable steps into readable pipelines, keep logic organized, and process data through sequential operations with a simple, expressive API.

View on GitHub
Deep Wiki
Context7

Getting Started

The package resolves an attribute value by walking an ordered chain of candidate sources (model attributes, relations, config, closures) and returning the first non-null hit. That single idea is what you'll use day-to-day: stop writing nested ?? fallbacks in accessors and declare the precedence once.

Minimal setup:

composer require vendor/laravel-attribute-chain
php artisan vendor:publish --tag="attribute-chain-config"

Add the trait to any model that needs fallback behaviour:

use Vendor\AttributeChain\Concerns\HasAttributeChains;

class Product extends Model
{
    use HasAttributeChains;

    protected array $attributeChains = [
        'display_price' => ['sale_price', 'price', 'category.default_price'],
        'title'         => ['seo_title', 'name'],
    ];
}

Where to look first, in order:

  1. config/attribute-chain.php — global resolution strategy, cache toggle, strict mode.
  2. The trait's resolveChain() method — this is the actual resolution engine and the fastest way to understand precedence semantics.
  3. The package test suite — it is the most accurate documentation of what "missing" vs. "null" means, which is the main source of surprise.

First use case to try: a display_price chain on a Product where an unpublished sale price should fall back to the base price, then to the category default. Read it back with $product->display_price and confirm it behaves identically to a hand-written accessor before rolling it out further.

If you are upgrading from 0.4.1 or earlier, note that 0.4.2 fixes a chain attribute conflict: previously, when the same attribute name appeared in more than one chain (or a chain referenced an attribute that was itself chain-resolved), the resolution could return a stale or cross-contaminated value. Bump the constraint and re-run your resolution tests, particularly any chain whose candidates are themselves chain-backed.

Implementation Patterns

Declarative chains on the model. Keep the chain definition next to the model when the precedence is intrinsic to the domain. This is the default and the easiest to review in a PR.

Config-driven chains for cross-cutting precedence. When marketing or ops needs to change fallback order without a deploy, move the chain into config/attribute-chain.php and reference it by key:

'chains' => [
    'product.title' => ['seo_title', 'name', 'sku'],
],

Resolve with $product->resolveChain('product.title'). Prefer this for anything a non-engineer might want to reorder; keep model-level definitions for structural invariants that should not drift.

Compose with existing accessors, don't replace them. Chains compose cleanly when each candidate is a plain attribute or a relation attribute. If a candidate is an expensive computed accessor, wrap it in a chain only after confirming it is not triggering queries — see the N+1 note below.

Request-scoped overrides. A common workflow is a per-tenant or per-locale precedence override. Register the override in a service provider or middleware rather than branching inside the model:

Chain::prepend('product.title', 'tenant_title');

Keep overrides in one place per request lifecycle so you can reason about the final order; scattered prepend calls are the fastest way to make precedence untraceable.

Testing. Assert on the resolved value, not on the internals. Factories make the precedence matrix cheap:

it('prefers sale price over base price', function () {
    $product = Product::factory()->create(['sale_price' => 100, 'price' => 150]);
    expect($product->display_price)->toBe(100);
});

Add one test per link in each chain, plus one test for the all-missing case. The all-missing case is where behaviour changes silently between minor versions.

Integration with validation and forms. Chains are read-side only. Do not point $fillable or validation rules at a chained attribute name — validate and persist the underlying candidates, and let the chain resolve on read. Mixing the two produces data that round-trips incorrectly.

Caching. If you enable resolution caching, key it by model class, primary key, and the chain name. Invalidate on save of the model and of any relation the chain touches. For chains over relations, it is usually cheaper to skip caching than to get invalidation exactly right.

Gotchas and Tips

Null versus missing is the big one. A candidate that exists but is null and a candidate that does not exist at all may be treated differently depending on the config's strict mode. Decide which semantic you want before you ship, and write a test that pins it. Most "the chain skipped my value" bug reports are this distinction.

Chain attribute conflicts (fixed in 0.4.2). If an attribute appeared in multiple chains, or a candidate was itself chain-resolved, earlier versions could return a value from the wrong resolution context. After upgrading, audit any model where the same attribute name is reused across chains, and any chain whose candidates are themselves chained. If you had a defensive workaround in place — duplicated chain keys, manually flattened precedence, or a wrapper accessor — remove it now, otherwise you are carrying dead complexity that masks the corrected behaviour.

Precedence is order-of-declaration, not order-of-assignment. Do not assume the chain prefers whichever attribute was set most recently. It does not. If you need recency-based resolution, that is a different mechanism and should be modelled explicitly rather than bolted onto a chain.

N+1 queries hide inside relation candidates. A chain like ['relation.foo', 'price'] will lazy-load relation per model in a collection. Eager-load the relation in the query, or add the relation to $with, before wiring it into a chain. Check with DB::listen or Telescope on a ->get() of 100 models; it is easy to miss in a single-model test.

Config caching. Config-driven chains are read at boot. After changing config/attribute-chain.php in a deployed environment, run php artisan config:cache again or your precedence change will appear to do nothing.

Debugging a resolution. When a chain returns something unexpected, do not add dd() to the model. Dump the resolution trace instead — the package exposes the candidate list and which link matched, which tells you instantly whether the problem is precedence, a null candidate, or a candidate that silently threw. If a trace is unavailable in your version, resolve each candidate manually in php artisan tinker against a real record; that reproduces the engine's view without touching code.

Extension points. Custom resolvers let you add non-attribute sources (an API response, a settings table) as chain candidates. Register them once in a service provider and keep them side-effect free — a resolver that writes or logs will fire on every read of the attribute, including inside Blade loops.

Don't chain everything. Chains are for fallback precedence with genuine ambiguity. Wrapping a single attribute in a one-element chain adds indirection and a config surface for no benefit; use a plain accessor or a cast instead.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package