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
Storyfeed::context($order->shop, function () use ($request, $order) {
Storyfeed::activity()
->by($request->user())
->action('place', $order)
->publish();
});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
use Storyfeed\Facades\Storyfeed;
Storyfeed::actor($request->user(), function () use ($order) {
Storyfeed::activity()
->action('place', $order)
->context($order->shop)
->publish();
});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
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:
<?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
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
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:
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.