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

Activity Scopes ​

Introduction ​

Storyfeed::actor() and Storyfeed::context() supply a default actor or context to every activity published inside a callback. HTTP middleware can supply either role for a whole request.

Sharing Roles Within a Callback ​

Sharing Context ​

php
Storyfeed::context($order->shop, function () use ($request, $order) { 
    Storyfeed::activity()
        ->by($request->user())
        ->action('place', $order)
        ->publish();
});
php
Storyfeed::context($order->shop, function () use ($request, $order) { 
    Storyfeed::record(
        verb: 'place',
        object: $order,
        actor: $request->user(),
    );
});

With :actor placed :object in :context declared as the headline:

[
  {
    "kind": "activity",
    "id": "j84",
    "verb": "place",
    "published_at": "1985-07-02T12:00:00.000000Z",
    "headline_template": ":actor placed :object in :context",
    "headline": null,
    "glyph": "shopping-bag",
    "glyph_intent": null,
    "actor": {
      "type": "user",
      "id": "113",
      "label": "Erica Sinclair",
      "url": "/users/113",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": null,
      "body": null,
      "tombstone": null
    },
    "object": {
      "type": "order",
      "id": "1035",
      "label": "Order #1035",
      "url": "/orders/1035",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": null,
      "body": null,
      "tombstone": null
    },
    "target": null,
    "context": {
      "type": "venue",
      "id": "101",
      "label": "Scoops Ahoy",
      "url": "/venues/scoops",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": {
        "icon": null,
        "image": null,
        "files": [],
        "preview": {
          "src": "/media/worlds/stranger-things/parlour.jpg",
          "mediaType": "image/jpeg",
          "width": 960,
          "height": 720,
          "alt": "An ice cream counter"
        },
        "url": null
      },
      "body": [
        {
          "$body": "Storyfeed/Body/Image",
          "$v": 1,
          "caption": "Scoops Ahoy",
          "alt": "Scoops Ahoy",
          "width": null,
          "height": null,
          "image": "preview"
        }
      ],
      "tombstone": null
    },
    "origin": null,
    "result": null,
    "instrument": null,
    "data": null,
    "tombstoned": [],
    "redundant": false,
    "missing_headline_template": null,
    "missing_headline": null
  }
]

Every activity published inside the callback inherits the context, including activities published by methods the callback calls.

The scope accepts an Eloquent model or a declared party name and returns the callback's result. Without a callback, Storyfeed::context($model) returns an activity builder with that context set.

Sharing an Actor ​

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

Storyfeed::actor($request->user(), function () use ($order) {
    Storyfeed::activity()
        ->action('place', $order)
        ->context($order->shop)
        ->publish();
});
php
use Storyfeed\Facades\Storyfeed;

Storyfeed::actor($request->user(), function () use ($order) {
    Storyfeed::record(
        verb: 'place',
        object: $order,
        context: $order->shop,
    );
});
[
  {
    "kind": "activity",
    "id": "j84",
    "verb": "place",
    "published_at": "1985-07-02T12:00:00.000000Z",
    "headline_template": ":actor placed :object in :context",
    "headline": null,
    "glyph": "shopping-bag",
    "glyph_intent": null,
    "actor": {
      "type": "user",
      "id": "113",
      "label": "Erica Sinclair",
      "url": "/users/113",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": null,
      "body": null,
      "tombstone": null
    },
    "object": {
      "type": "order",
      "id": "1035",
      "label": "Order #1035",
      "url": "/orders/1035",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": null,
      "body": null,
      "tombstone": null
    },
    "target": null,
    "context": {
      "type": "venue",
      "id": "101",
      "label": "Scoops Ahoy",
      "url": "/venues/scoops",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": {
        "icon": null,
        "image": null,
        "files": [],
        "preview": {
          "src": "/media/worlds/stranger-things/parlour.jpg",
          "mediaType": "image/jpeg",
          "width": 960,
          "height": 720,
          "alt": "An ice cream counter"
        },
        "url": null
      },
      "body": [
        {
          "$body": "Storyfeed/Body/Image",
          "$v": 1,
          "caption": "Scoops Ahoy",
          "alt": "Scoops Ahoy",
          "width": null,
          "height": null,
          "image": "preview"
        }
      ],
      "tombstone": null
    },
    "origin": null,
    "result": null,
    "instrument": null,
    "data": null,
    "tombstoned": [],
    "redundant": false,
    "missing_headline_template": null,
    "missing_headline": null
  }
]

The actor method accepts an Eloquent model or declared party name and returns the callback's result. Without a callback, Storyfeed::actor($user) returns an activity builder with that actor set.

Nested Scopes ​

Nested callbacks use the innermost actor or context. The previous scope is restored when the callback ends, including when it throws an exception.

Sharing Roles Within an HTTP Request ​

Context From Route Parameters ​

routes/web.php
php
use App\Http\Controllers\PlaceOrderController;
use Illuminate\Support\Facades\Route;

Route::post('/shops/{shop}/orders/{order}/place', PlaceOrderController::class)
    ->middleware(['auth', 'storyfeed.context:shop']);

The storyfeed.context:shop middleware uses the bound shop route parameter. It must be an Eloquent model; missing or unbound values throw an exception. For implicit binding, type-hint the parameter in the controller:

app/Http/Controllers/PlaceOrderController.php
php
<?php

namespace App\Http\Controllers;

use App\Models\Shop;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Storyfeed\Facades\Storyfeed;

class PlaceOrderController extends Controller
{
    public function __invoke(Request $request, Shop $shop, Order $order): RedirectResponse
    {
        $order->update(['status' => 'placed']);

        Storyfeed::activity()->by($request->user())->action('place', $order)->publish();

        return to_route('orders.show', $order);
    }
}
php
<?php

namespace App\Http\Controllers;

use App\Models\Shop;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Storyfeed\Facades\Storyfeed;

class PlaceOrderController extends Controller
{
    public function __invoke(Request $request, Shop $shop, Order $order): RedirectResponse
    {
        $order->update(['status' => 'placed']);

        Storyfeed::record(
            verb: 'place',
            object: $order,
            actor: $request->user(),
        );

        return to_route('orders.show', $order);
    }
}
[
  {
    "kind": "activity",
    "id": "j84",
    "verb": "place",
    "published_at": "1985-07-02T12:00:00.000000Z",
    "headline_template": ":actor placed :object in :context",
    "headline": null,
    "glyph": "shopping-bag",
    "glyph_intent": null,
    "actor": {
      "type": "user",
      "id": "113",
      "label": "Erica Sinclair",
      "url": "/users/113",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": null,
      "body": null,
      "tombstone": null
    },
    "object": {
      "type": "order",
      "id": "1035",
      "label": "Order #1035",
      "url": "/orders/1035",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": null,
      "body": null,
      "tombstone": null
    },
    "target": null,
    "context": {
      "type": "venue",
      "id": "101",
      "label": "Scoops Ahoy",
      "url": "/venues/scoops",
      "attributes": {},
      "modal": false,
      "data": {},
      "media": {
        "icon": null,
        "image": null,
        "files": [],
        "preview": {
          "src": "/media/worlds/stranger-things/parlour.jpg",
          "mediaType": "image/jpeg",
          "width": 960,
          "height": 720,
          "alt": "An ice cream counter"
        },
        "url": null
      },
      "body": [
        {
          "$body": "Storyfeed/Body/Image",
          "$v": 1,
          "caption": "Scoops Ahoy",
          "alt": "Scoops Ahoy",
          "width": null,
          "height": null,
          "image": "preview"
        }
      ],
      "tombstone": null
    },
    "origin": null,
    "result": null,
    "instrument": null,
    "data": null,
    "tombstoned": [],
    "redundant": false,
    "missing_headline_template": null,
    "missing_headline": null
  }
]

The middleware supplies the context to every activity published in this request.

Declared Party Actors ​

routes/web.php
php
use App\Http\Controllers\PlaceOrderController;
use Illuminate\Support\Facades\Route;

Route::post('/shops/{shop}/orders/{order}/place', PlaceOrderController::class)
    ->middleware(['auth', 'storyfeed.context:shop', 'storyfeed.actor:System']);

The storyfeed.actor:System middleware wraps the request in Storyfeed::actor('System', ...). Declare the party in your service provider. An explicit ->by($request->user()) takes precedence; activities without an explicit actor use System.

Role Precedence ​

An actor or context set on the activity takes precedence over the corresponding scope. An explicit by(null) records no actor even inside an actor scope. Without an explicit value, the activity uses the scoped role; a nested scope applies only for its callback.

See Resolving Role Precedence for the full order, including middleware, verb actors, and default resolvers.

Carrying Roles Into Queued Jobs ​

The Authenticated User ​

A job dispatched during a request publishes as the request's authenticated user, even though the worker has no logged-in user. Jobs dispatched from that job inherit the same user.

Actor Scopes ​

A job dispatched inside Storyfeed::actor() runs as that actor on the worker:

php
Storyfeed::actor('System', fn () => SyncMenu::dispatch());

Activities published by the job use the System party unless they specify an actor. Jobs it dispatches inherit the scope, which ends when the job completes or throws an exception. A job dispatched with ->afterResponse() runs after the scope closes, so it does not carry the actor.

Context Scopes ​

Jobs dispatched inside Storyfeed::context() run inside that context on the worker, and jobs they dispatch inherit it. The scope ends with the job, even when the job throws.

See Carrying Request-Based Actors Into Jobs for the actor selected by a verb during a request.

Released under the MIT License.