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

Story Classes ​

Introduction ​

A Story class can build an activity from the data you give it. You may also keep verb definitions in routes/feed.php or separate declaration classes.

Generating Story Classes ​

bash
php artisan make:story

The command asks for the class name, then What will this story describe?

ChoiceLaravel AnalogyWhat the Class Contains
One activity, published with its datalike a notificationconstructor data and toFeedActivity()
Every activity for one modellike a resource controllerone declaration method per verb
A single verblike a single action controllerthat verb's headlines in their own class

Each choice prints a registration to add to routes/feed.php without editing that file. The sections below show the command for each class type. See Commands for all options.

A name containing Was supplies the headline's past tense. Otherwise, the command derives it from the verb and asks when the spelling is uncertain. Choosing None of these, or running without a terminal, leaves those headline lines commented out. Select and uncomment a line before compiling the definitions.

Publishing Story Classes ​

Defining the Activity ​

shell
php artisan make:story OrderWasPlaced --verb=place --object=Order
app/Stories/OrderWasPlaced.php
php
<?php

namespace App\Stories;

use App\Models\Order;
use App\Models\User;
use Storyfeed\PendingActivity;
use Storyfeed\Stories\Story;

class OrderWasPlaced extends Story
{
    public function __construct(
        public Order $order,
        public User $customer,
    ) {}

    public function toFeedActivity(): ?PendingActivity
    {
        return $this->activity($this->order)
            ->by($this->customer)
            ->to($this->order->shop);
    }

    public function headline(): string
    {
        // Presentation methods cannot read constructor data.
        return ':actor placed :object with :target';
    }

    public function icon(): ?string
    {
        return 'shopping-bag';
    }
}

The toFeedActivity method builds the activity. The inherited activity method sets the verb registered for this class. Return null to skip publishing.

An event implementing PublishesToFeed uses the same toFeedActivity method and publishes when dispatched. A Story can be published independently, like a notification, so use it when the activity has no corresponding application event.

Declaring Casts on a Story Class ​

A Story class declares its casts in a casts method, as a model does. This alternative order-placement class records a channel and promised time:

app/Stories/OrderPlaced.php
php
<?php

namespace App\Stories;

use App\Enums\Channel;
use App\Models\Order;
use Storyfeed\PendingActivity;
use Storyfeed\Stories\Story;

class OrderPlaced extends Story
{
    public string|array|null $objectType = Order::class;

    public function __construct(public Order $order) {}

    public function toFeedActivity(): ?PendingActivity
    {
        return $this->activity($this->order)->data([
            'channel' => $this->order->channel,
            'promised_at' => $this->order->promised_at,
        ]);
    }

    public function headline(): string
    {
        return ':actor placed :object';
    }

    public function casts(): array
    {
        return [
            'channel' => Channel::class,
            'promised_at' => 'immutable_datetime',
        ];
    }
}

To use this class instead of OrderWasPlaced, bind it to the order's verb:

routes/feed.php
php
use App\Models\Order;
use App\Stories\OrderPlaced;
use Storyfeed\Facades\Story;

Story::for(Order::class)->verb('place', OrderPlaced::class);

Registering the Story ​

Register the class for its object type and verb in the feed file:

routes/feed.php
php
use App\Models\Order;
use App\Stories\OrderWasPlaced;
use Storyfeed\Facades\Story;

Story::for(Order::class)->verb('place', OrderWasPlaced::class);

Publishing the Story ​

Construct the Story with its data and publish it:

php
Storyfeed::publish(new OrderWasPlaced($order, $request->user()));

Storyfeed::publish() returns the activity, or null when toFeedActivity() returns null or the Story is queued.

Queueing Stories ​

Implement ShouldQueue and use Queueable, as a queued notification does:

app/Stories/OrderWasPlaced.php
php
<?php

namespace App\Stories;

use App\Models\Order;
use App\Models\User;
use Illuminate\Bus\Queueable; 
use Illuminate\Contracts\Queue\ShouldQueue; 
use Storyfeed\PendingActivity;
use Storyfeed\Stories\Story;

class OrderWasPlaced extends Story implements ShouldQueue
{
    use Queueable; 

    // ...
}

The Storyfeed::publish method now queues the Story. Use the Queueable trait's methods to select the connection and queue:

app/Http/Controllers/PlaceOrderController.php__invoke()
php
use App\Stories\OrderWasPlaced;
use Storyfeed\Facades\Storyfeed;

Storyfeed::publish(
    (new OrderWasPlaced($order, $request->user()))->onQueue('feed'),
);

After the worker calls toFeedActivity() and publishes its result:

Queued publishing returns null. Use Storyfeed::publishNow() to publish synchronously, as you would use sendNow() for a notification. Model properties are serialized by their identifiers. The publication time is captured when Storyfeed::publish() is called, unless toFeedActivity() sets it explicitly.

A queued Story may implement ShouldBeUnique and define a uniqueId method. Its middleware method declares story middleware, while the Queueable trait's through method sets job middleware. See Queued Publishing for connections, transactions, and missing models.

Presentation Methods ​

Storyfeed calls headline and icon without running the constructor, so these methods cannot use constructor data. Use that data in toFeedActivity instead. The same restriction applies to all definition methods:

MethodDeclares
intent()the icon's intent
groups()group headlines
missing()the roles that make it redundant once deleted
keepFor(), keepForever()its retention
keepLatest()keeping the latest activity
period()its grouping period
middleware()its story middleware

Single-Verb Stories ​

Generating an Invokable Story ​

shell
php artisan make:story PlaceStory --invokable --verb=place --object=Order
app/Stories/PlaceStory.php
php
<?php

namespace App\Stories;

use Storyfeed\Stories\Verb;

class PlaceStory
{
    public function __invoke(Verb $verb): Verb
    {
        return $verb
            ->headline(':actor placed :object with :target')
            ->icon('shopping-bag');
    }
}

Registering an Invokable Story ​

When routes/feed.php becomes difficult to maintain, move a verb's headlines to a single-verb declaration class. It does not require a base class. Register it in place of the inline definition:

routes/feed.php
php
use App\Models\Order;
use App\Stories\PlaceStory;
use Storyfeed\Facades\Story;

Story::for(Order::class)->verb('place', PlaceStory::class);

Publish the verb with the activity builder. An invokable declaration may return a Verb or headline string, as a resource method does. Registering it with Story::verb('place', PlaceStory::class) applies it to all object types, so its headline must describe each supported type.

Resource Stories ​

Generating a Resource Story ​

shell
php artisan make:story OrderStory --model=Order
app/Stories/OrderStory.php
php
<?php

namespace App\Stories;

use Storyfeed\Stories\Verb;

class OrderStory
{
    public function place(Verb $verb): Verb
    {
        return $verb
            ->headline(':actor placed :object with :target')
            ->icon('shopping-bag');
    }

    public function complete(): string
    {
        return ':actor completed :object';
    }
}

Each public method declares a verb. The class requires no base class or order instance. Register it in place of the other place definitions:

routes/feed.php
php
use App\Models\Order;
use App\Stories\OrderStory;
use Storyfeed\Facades\Story;

Story::resource(Order::class, OrderStory::class);

Use the resource class to define verbs and the activity builder to publish activities.

Verbs and Return Types ​

The method name becomes the stored verb in snake_case:

MethodStored Verb
pay()pay
store()store
confirmPayment()confirm_payment
markAsPaid()mark_as_paid

No other name conversion applies: store() records store, and create() records create.

Each method declares its return type:

Return TypeThe Method Returns
Storyfeed\Stories\Verbthe supplied definition with its options set
stringthe headline

Use Verb when setting several options, or string for a headline alone. Keep helpers protected or private, because public methods declare verbs.

Keep a verb's method while stored activities still use it. Storyfeed resolves headlines from the current definitions when retrieving the feed, so removing the method leaves those activities without a headline:

app/Stories/OrderStory.php
php
// Nothing publishes `print` any more; old rows still read.
public function print(): string
{
    return ':actor printed :object';
}

The supplied Verb supports every definition method, including missingHeadline() for deleted objects. See Deleted Models.

Selecting Verbs ​

A resource class adds its verbs to the conventional verbs defined by Story::resource(). A method with a conventional verb's name replaces its complete default definition. Use only() or except() to filter both sets by stored verb name:

routes/feed.php
php
use App\Models\Order;
use App\Stories\OrderStory;
use Storyfeed\Facades\Story;

Story::resource(Order::class, OrderStory::class)->only('place', 'complete');

Replace the unfiltered resource registration with this example. Excluded verbs also lose their resource names.

Registering Multiple Resources ​

routes/feed.php
php
use App\Models\MenuItem;
use App\Models\Order;
use App\Stories\OrderStory;
use Storyfeed\Facades\Story;

Story::resources([
    Order::class => OrderStory::class,
    MenuItem::class => null,
], ['except' => ['restore']]);

The Story::resources method registers several models with shared options, as Route::resources does. A null class defines the four conventional verbs. The options accept only and except. Use this example in place of individual resource registrations. To share middleware or role constraints, wrap it in a group.

Request-Based Actors ​

app/Stories/OrderStory.phpAdd this method and the Request import
php
use Illuminate\Http\Request;
use Storyfeed\Stories\Verb;

public function confirmPayment(Verb $verb, Request $request): Verb
{
    return $verb
        ->headline(':actor confirmed payment for :object')
        ->icon('credit-card')
        ->actor($request->hasHeader('Paddle-Signature') ? 'Paddle' : 'Stripe');
}

The same webhook can choose between two declared parties. Its controller publishes confirm_payment without naming an actor:

app/Http/Controllers/PaymentWebhookController.php__invoke(), after loading $order
php
use Storyfeed\Facades\Storyfeed;

Storyfeed::activity('confirm_payment', $order)->publish();
php
use Storyfeed\Facades\Storyfeed;

Storyfeed::record(verb: 'confirm_payment', object: $order);

For a request without the signature header:

S
Stripe confirmed payment for Order #2031

The verb's actor applies when no actor is assigned explicitly or through a Storyfeed::actor() scope. Only the actor setting may depend on the request; headlines, icons, intents, grouping, and other settings must remain consistent. A request-dependent headline throws an exception when grammar.strict is enabled, as it is by default in local and testing environments.

Carrying Request-Based Actors Into Jobs ​

Jobs dispatched during a request carry the actor selected by the request-based verb actor. The worker retains that selection without needing the original HTTP request. For callback and request scopes, see Carrying Roles Into Queued Jobs.

Generating From Existing Activities ​

bash
php artisan make:story --from-doctor

The command generates an activity class for each recorded type and verb without a headline, using a name such as OrderWasPlaced. It prompts for uncertain past tenses. Without an interactive terminal, it skips those pairs and prints commands for the possible spellings. Run the command with the correct spelling.

Listing and Caching Stories ​

New resource methods become available when definitions are compiled again. If definitions are cached, run storyfeed:cache to include the new verbs. Use storyfeed:list to inspect them; see Listing Definitions and Caching Definitions.

Released under the MIT License.