Pre-1.0. Not intended for public use yet. The API is subject to undocumented change, and these pages may already be wrong about it.

Skip to content

Named Feeds ​

Introduction ​

Most apps show their activity to more than one audience. At Scoops Ahoy, the kitchen needs every order as it moves, the menu has its own change log, and a customer should see only their own order. When each screen filters activities in its own controller, those filters drift apart, and a newly recorded verb can appear on the customer's page without anyone deciding that it should.

A named feed defines an audience once, by name:

  • Every screen shows the same verbs. A controller, a Filament widget and an API endpoint that retrieve the 'customer' feed all apply the same filter.
  • Verb coverage can be checked in CI. With restricted feeds and no unrestricted feed, the doctor warns about app-declared or recorded verbs that no feed includes or excludes. See Checking Verb Coverage for the exceptions.
  • Each feed can link somewhere different. An order can open its ticket on the kitchen's board and its status page on the customer's.
  • A feed class requires its subject. The customer's feed is always scoped to their order.

Defining Named Feeds ​

Define separate feeds for the kitchen's order board and the menu's change log:

Register each feed as a closure in a service provider's boot method. The closure receives the feed builder and configures its filters and mode:

app/Providers/AppServiceProvider.phpboot()
php
use Storyfeed\Facades\Storyfeed;
use Storyfeed\FeedBuilder;

Storyfeed::feeds([
    // The kitchen's order board: every order as it moves, with a customer's
    // repeated orders folded into one row.
    'kitchen' => fn (FeedBuilder $feed) => $feed
        ->only(['place', 'confirm', 'ready'])
        ->live(),

    // The menu's change log: items added and prices changed, one row each.
    'menu' => fn (FeedBuilder $feed) => $feed
        ->only(['publish', 'reprice'])
        ->log(),
]);

Retrieving a Named Feed ​

Pass the feed name to the feed method on the Storyfeed facade:

A controller, or wherever the feed is read
php
use Storyfeed\Facades\Storyfeed;

Storyfeed::feed('kitchen')->get();
SH
Steve Harrington marked Order #1035 ready
SH

To retrieve the menu's change log:

A controller, or wherever the feed is read
php
Storyfeed::feed('menu')->get();
SH
Steve Harrington changed the price of USS Butterscotch
SH
Steve Harrington put USS Butterscotch on the menu
USS Butterscotch
Price
$2.95
Section
Sundaes
Available
At the counter
Orders
12

To retrieve a named feed for one model, pass the name to its storyfeed method. For example, $order->storyfeed('kitchen') applies the kitchen feed to that order.

An unknown feed name throws an UnknownFeed exception.

Defining Feed Classes ​

Use a feed class when a feed requires a subject, such as an order. Generate it with the make:feed Artisan command:

bash
php artisan make:feed Customer --subject='App\Models\Order' --role=involving

The command creates app/Feeds/CustomerFeed.php with an order constructor parameter. Edit the generated class to define its allowed verbs and mode:

app/Feeds/CustomerFeed.php
php
<?php

namespace App\Feeds;

use App\Models\Order;
use Storyfeed\Feed;
use Storyfeed\FeedBuilder;

class CustomerFeed extends Feed
{
    public function __construct(protected Order $order) {}

    // What a customer may see: their order being placed, confirmed and made
    // ready, one row each. Nothing about prices or staff notes.
    public function define(FeedBuilder $feed): void
    {
        $feed->only(['place', 'confirm', 'ready'])->log();
    }

    // Which order: the one this feed was made for, on every read.
    protected function scope(FeedBuilder $feed): void
    {
        $feed->involving($this->order);
    }
}

Pass the order to the feed class's make method:

A controller, or wherever the feed is read
php
use App\Feeds\CustomerFeed;

CustomerFeed::make($order)->get();
SH
Steve Harrington marked Order #1035 ready
SH

See Commands for all make:feed options.

Defining and Scoping Hooks ​

The define method configures the feed without constructor values, including when the doctor checks verb coverage. The scope method applies the subject constraint whenever you retrieve the feed.

Additional query filters may narrow the results but cannot replace the constraints set by scope:

A controller, or wherever the feed is read
php
// throws FeedMisconfigured
CustomerFeed::make($order)->involving($other);

// fine
CustomerFeed::make($order)->only(['place'])->live();

For a feed without a subject, omit the constructor and scope method:

app/Feeds/KitchenFeed.php
php
<?php

namespace App\Feeds;

use Storyfeed\Feed;
use Storyfeed\FeedBuilder;

class KitchenFeed extends Feed
{
    public function define(FeedBuilder $feed): void
    {
        $feed->only(['place', 'confirm', 'ready'])->live();
    }
}

Registering Feed Classes ​

Register a feed class to access it by name. You may register classes and closures together:

app/Providers/AppServiceProvider.phpboot()
php
use App\Feeds\CustomerFeed;
use App\Feeds\KitchenFeed;
use Storyfeed\Facades\Storyfeed;
use Storyfeed\FeedBuilder;

Storyfeed::feeds([
    'customer' => CustomerFeed::class,  // named explicitly
    KitchenFeed::class,                 // named 'kitchen', from the class
    'menu' => fn (FeedBuilder $feed) => $feed->only(['publish', 'reprice'])->log(),
]);

Using a Feed's Name ​

Linking Per Feed ​

A model's link resolver can call the context's feed method to get the registered feed name. Use it to return a kitchen ticket URL, customer status URL, or no link:

app/Models/Order.phpbooted()
php
use Storyfeed\FeedContext;

static::feedMediaUsing(
    fn (FeedContext $context) => match ($context->feed()) {
        'kitchen' => route('kitchen.ticket', $context->routeKey()),
        'customer' => route('orders.status', $context->routeKey()),
        // an ad-hoc feed reports no name; without this arm the match throws
        default => null,
    },
);

On the kitchen feed:

On a feed with no name:

ES
Erica Sinclair placed Order #1035 with Scoops Ahoy

Checking Verb Coverage ​

The doctor checks app-declared and recorded verbs; unused package-default verbs are excluded. When at least one restricted feed exists and no unrestricted feed is declared, a verb that no restricted feed includes or excludes produces a feeds.unclassified warning. Run with --fail-on=warning in CI to fail on these warnings.

Use ->unrestricted() to declare that a feed includes every verb. Alongside a restricted feed, this changes undecided-verb findings to informational feeds.unrestricted, which does not fail at the warning threshold. With no restricted feeds, the check reports informational feeds.none_restricted and returns. With no registered feeds, it returns without a finding.

Narrowing a Named Feed ​

You may change a named feed's mode or narrow its verb filters. The only method cannot add verbs excluded by its definition:

A controller, or wherever the feed is read
php
use Storyfeed\Facades\Storyfeed;

// reads only 'place': 'note' is not in the declared list
Storyfeed::feed('kitchen')->only(['place', 'note'])->get();

See Filtering by Verb for the only and except methods.

Authorizing Feed Access ​

A named feed filters activities. Your application must authorize access:

  • Check a policy in your controller to determine whether the customer may access the order.
  • Each included activity returns its complete data payload. Store only values that the feed's audience may access.

For customer-facing feeds, use the only method with explicit verb names. The except method allows new verbs unless excluded, and wildcards allow new verbs that match.

Released under the MIT License.