skip to content

In a Laravel logistics app, how would you collect every carrier's rate calculator with tag() and tagged(), and decorate each one with extend()?

level: seniorimportance: nice to knowfreq 22%

answer

  1. tag a list of abstracts under a name
  2. tagged() returns a lazy RewindableGenerator
  3. each iteration calls make() again
  4. extend() closure gets object and container
  5. resolved shared instance: decorated at once

basics

~10 s

Bind each calculator, group them with $this->app->tag([...], 'rate-calculators'), and build the quote service from $app->tagged('rate-calculators'), a lazy iterable. Wrap a calculator with $this->app->extend(Calculator::class, fn ($c) => new CachedRates($c)), which decorates it whenever it resolves.

solid answer

~40 s

`tag($abstracts, $tags)` records one or more abstracts under one or more tag names. `tagged($tag)` returns a `RewindableGenerator`, iterable and countable but not an array, that calls `make()` on each abstract as you iterate; an unknown tag returns an empty array. A `CarrierRateRegistry` built from `$app->tagged('rate-calculators')` can loop and pick the calculator whose `supports($carrier)` is true, so adding a carrier is one binding plus one tag entry. `extend($abstract, $closure)` registers a decorator: the closure receives the built object and the container and returns what consumers get, such as a caching wrapper. If the abstract already has a cached shared instance, `extend()` decorates it immediately and fires rebinding callbacks; otherwise the closure runs whenever the abstract is built, so a non-shared calculator is re-wrapped on every resolution.

code

php · 23 lines
php
<?php

namespace App\Services;

use App\Contracts\ShippingRateCalculator;
use InvalidArgumentException;

class CarrierRateRegistry
{
    /** @param iterable<ShippingRateCalculator> $calculators */
    public function __construct(private iterable $calculators) {}

    public function for(string $carrier): ShippingRateCalculator
    {
        foreach ($this->calculators as $calculator) {
            if ($calculator->supports($carrier)) {
                return $calculator;
            }
        }

        throw new InvalidArgumentException("No rate calculator for [{$carrier}].");
    }
}

go deeper

for a junior

Recall that tag() groups bindings under a name, tagged() returns them for iteration, and extend() wraps a service with a closure.

for a middle

Explain that tagged() is a lazy, countable generator that calls make() per item, and the two branches extend() takes depending on whether an instance is cached.

for a senior

Design the registry so adding a carrier is configuration only, choose singleton lifetimes to avoid rebuilding on each pass, and register decorators before first resolution.

for a principal

Weigh container-level decoration and tagging against explicit composition in code, considering how discoverable the wiring is for the next engineer reading the registry.

## The scenario A logistics app quotes shipping prices from several carriers. Each carrier has its own **rate calculator** implementing one interface, `ShippingRateCalculator`, with `supports(string $carrier): bool` and `quote(Shipment $s): Money`. A `CarrierRateRegistry` must receive **all** of them and pick the right one per shipment, and every calculator should be wrapped in a **caching decorator** because carrier rate tables are slow to query. Two container features cover this without editing the registry each time a carrier is added: **tagging** and **extending**. ## Grouping with tag() and tagged() `tag($abstracts, $tags)` takes a single abstract or an array of them, and one or more tag names (an array, or extra arguments). It only records names; nothing is built. `tagged($tag)` resolves the group: - It returns an `Illuminate\Container\RewindableGenerator`, which implements `IteratorAggregate` and `Countable`. You can `foreach` over it and `count()` it, but it is **not an array**, so `array_map()` on it fails; use `iterator_to_array()` or `collect()` first. - Iteration is **lazy**: each step calls `make()` for the next abstract, so objects are built only when reached. - Each new iteration calls `make()` again. Calculators bound with `bind()` are **rebuilt on every pass**; ones bound with `singleton()` come back from the cache. - A tag that was never registered returns an empty array, not an exception. The registry depends on the group, not on each class, so adding a carrier means one binding and one entry in the `tag()` call. ## Decorating with extend() `extend($abstract, Closure $closure)` lets you modify or replace a service when it is resolved. The closure receives **the built object and the container** and must return the object consumers will get. There are two branches: 1. If the container already holds a **cached instance** for the abstract (a resolved singleton, or one registered with `instance()`), the closure runs **immediately** on that instance, the result replaces it in the cache, and any rebinding callbacks fire. 2. Otherwise the closure is stored as an **extender** and applied every time the abstract is built. For a shared binding that happens once and the decorated object is cached; for a non-shared binding it happens on every resolution. Objects that already captured the old, undecorated instance in a constructor keep it. Registering extenders in a provider's `register()` method, before anything resolves the service, avoids that. ## Wiring it together ```php $this->app->singleton(GroundFreightCalculator::class); $this->app->singleton(AirFreightCalculator::class); foreach ([GroundFreightCalculator::class, AirFreightCalculator::class] as $class) { $this->app->extend($class, fn ($calc, $app) => new CachedRates($calc, $app->make('cache.store'))); } $this->app->tag([GroundFreightCalculator::class, AirFreightCalculator::class], 'rate-calculators'); $this->app->singleton(CarrierRateRegistry::class, fn ($app) => new CarrierRateRegistry($app->tagged('rate-calculators'))); ``` Every calculator the registry iterates arrives already wrapped in `CachedRates`, because `tagged()` resolves through `make()`, and `make()` applies extenders. ## Trade-offs and traps | Concern | Effect | Mitigation | |---|---|---| | Non-shared calculators | Rebuilt and re-decorated on every iteration | Bind them with `singleton()` or materialise the list once | | Treating the result as an array | `array_map()` or `array_filter()` throws a `TypeError` | `iterator_to_array()` or `collect()` | | Late `extend()` | Consumers built earlier keep the undecorated object | Register extenders before first resolution | | Decorator changes the type | Consumers type-hinting the interface break | Make the decorator implement the same interface | | Order of tagged services | Follows the order abstracts were tagged | Tag in the order the registry should try them | ## Why not a hard-coded array? The alternative is a closure that builds the registry with `new CarrierRateRegistry([new GroundFreightCalculator(...), ...])`. That works, but every calculator's dependencies must then be constructed by hand, the decorator must be applied by hand, and adding a carrier means editing the closure. With tagging: - each calculator is still **auto-wired**, so its own constructor can change freely; - the decorator is declared **once per abstract**, independent of who consumes it; - the registry depends only on `iterable`, which keeps it trivial to unit test with a plain array. The cost is indirection: a reader of `CarrierRateRegistry` cannot see which classes it receives without finding the `tag()` call in a provider. Keeping the tag name, the `tag()` call and the registry binding next to each other in one provider limits that cost. Injecting a tag straight into a constructor parameter, instead of through a closure binding, is a contextual-binding feature with its own rules.

  • In Laravel, why might iterating tagged() calculators twice build every calculator twice?
    `tagged()` stores only abstract names, and each pass through the `RewindableGenerator` calls `make()` for each of them. Calculators bound with `bind()` are rebuilt, and re-decorated, on every pass, while `singleton()` bindings return their cached objects. Bind heavy calculators as singletons, or convert the group once with `iterator_to_array()`.
  • In Laravel, what happens if extend() is called after a singleton was already injected into another service?
    `extend()` decorates the cached instance at once and fires rebinding callbacks, so later resolutions get the decorated object. The service that already received the old instance through its constructor keeps the undecorated copy. Registering extenders before anything resolves the abstract avoids the split.
  • In Laravel, can an extend() closure return an object of a different class?
    Yes; the container uses whatever the closure returns. If that object no longer satisfies the type consumers hint, injection fails with a `TypeError`, so a decorator should implement the same interface as the object it wraps.

saying these in an interview costs you the question

  • tagged() returns a plain array of objects built when tag() was called.
  • Tagging a class also registers it as a singleton.
  • extend() on an already-resolved singleton waits until the next request to apply.
  • An extend() closure receives only the container, not the built object.
  • Asking tagged() for an unknown tag throws an exception.