Composites
Introduction
A composite is one activity whose object is a collection, such as several tasks completed together.
Recording Composites
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Storyfeed\Facades\Storyfeed;
class CompleteTasksController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$tasks = Task::whereIn('id', $request->input('tasks'))->get();
$tasks->each->update(['completed_at' => now()]);
Storyfeed::activity()
->by($request->user())
->action('complete')
->objects($tasks)
->publish();
return back();
}
}<?php
namespace App\Http\Controllers;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Storyfeed\Facades\Storyfeed;
class CompleteTasksController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$tasks = Task::whereIn('id', $request->input('tasks'))->get();
$tasks->each->update(['completed_at' => now()]);
Storyfeed::record(
verb: 'complete',
objects: $tasks,
actor: $request->user(),
);
return back();
}
}This writes a parent activity and one activity per task. log() returns each task separately. live() returns the parent as one item with axis: 'composite'. In summary(), it contributes a phrase to its actor's summary row. keepLatest() never replaces a composite.
Defining Composite Headlines
Define two headlines on the verb: one for the group of tasks and one for the composite activity. Both describe the collection, so neither belongs on a single task type:
use Storyfeed\Facades\Story;
use Storyfeed\Grouping\GroupBuilder;
Story::verb('complete')->grouped(
fn (GroupBuilder $group) => $group->composite(
':actor completed :count tasks', // the group
':actor completed tasks', // the activity itself; required
),
);Bundling Activities Automatically
Marking Models Bundleable
Implement Bundleable to combine batched activities on a model into a composite:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Storyfeed\Concerns\InteractsWithFeed;
use Storyfeed\Contracts\Bundleable;
use Storyfeed\Contracts\Feedable;
class Task extends Model implements Feedable, Bundleable
{
use InteractsWithFeed;
}For a model you cannot edit, register its morph alias:
use Storyfeed\Facades\Storyfeed;
Storyfeed::bundleables(['task']);Within a batch, automatic bundling considers unclaimed activities with a bundleable object. Activities must share the verb, object type, target, context, and publication date. The minimum counts distinct objects within each such set; a burst spanning those boundaries can remain separate.
Automatic bundling is enabled by default. Configure it in config/storyfeed.php:
| Key | Default | Meaning |
|---|---|---|
grouping.composite.auto | true | combine batched Bundleable activities into composites |
grouping.composite.min_objects | 2 | minimum distinct objects per composite |
Closing Batches
Bundling runs when the actor's batch closes, so leave batching enabled. The batch closes on the actor's next publication after the window expires. Schedule storyfeed:close-batches to close it on time without another publication.
Bundling Existing Activities
Bundleable applies only to new activities. To bundle existing activities:
php artisan storyfeed:bundle
php artisan storyfeed:bundle --window=30 # only batches closed in the last 30 daysYou may run this command more than once. Creating a composite changes the sync_token, so clients must discard accumulated items and fetch the feed from the start. See the sync token rule.