Skip to content

Feedable models

Anything that appears in the feed — actor, object, target, or context — implements Feedable:

php
use Illuminate\Database\Eloquent\Model;
use Storyfeed\Concerns\InteractsWithFeed;
use Storyfeed\Contracts\Feedable;
use Storyfeed\FeedEntity;
use Storyfeed\FeedLink;

class Document extends Model implements Feedable
{
    use InteractsWithFeed;

    public function toFeed(): FeedEntity
    {
        return FeedEntity::make(
            label: $this->name,
            data: ['id' => $this->id, 'project_id' => $this->project_id],
        );
    }

    public static function toFeedLink(array $data): ?FeedLink
    {
        return FeedLink::make(url: route('documents.show', $data['id']));
    }
}

The two methods split along the cache boundary:

methodruns atproduces
toFeed()publish time (refreshed on save)the cached snapshot: label + data
toFeedLink()read time, statically, from the cached dataa fresh URL

Reads never touch your domain tables — a feed page is served entirely from snapshots. URLs are regenerated live so they never go stale.

TIP

toFeedLink() receives exactly what toFeed() put in data — include the key you need to build the URL. Throwing inside it is safe: the failure is reported and the entity degrades to url: null. One broken link never breaks a feed.

FeedLink carries more than a URL when you need it:

php
FeedLink::make(url: $url, attributes: ['target' => '_blank']);
FeedLink::modal($url);   // hint the renderer to open as a modal

Keeping snapshots fresh

InteractsWithFeed wires the model events: saving refreshes the snapshot, deleting removes the entity's feed presence.

methoduse
updateFeedSnapshot()force a refresh outside a save
deleteFromFeed()remove feed presence (soft)
forceDeleteFromFeed()remove permanently

For entities recorded before they had snapshots (imports, backfills), schedule the trickle:

php
Schedule::command('storyfeed:trickle')->everyMinute();

Un-snapshotted entities still appear — with label: null, url: null — and renderers show a neutral placeholder. Activities are never hidden by the read path.

Morph aliases

Storyfeed stores morph aliases, never class names, so entities survive a namespace refactor. Enforce a map:

php
Relation::enforceMorphMap([
    'document' => Document::class,
    'project' => Project::class,
    'user' => User::class,
]);

Aliases can also be registered in config/storyfeed.php under morph_map, which merges into the app's map at boot.

Rich rendering

FeedEntity optionally names a frontend component and passes it props:

php
FeedEntity::make(
    label: $this->name,
    component: 'Resource',
    data: ['status' => $this->status],
);

The payload carries component and data on the entity; what your renderer does with them is yours.

Released under the MIT License. Everything MIT today stays MIT.