Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Mutable Content Tasks Laravel Package

amarenkov/laravel-mutable-content-tasks

A Laravel package for defining and running tasks that mutate content, offering a flexible workflow for managing content-changing jobs in your application.

View on GitHub
Deep Wiki
Context7

Getting Started

Start by installing the package and publishing its migrations so the new point_status column (and its point_status LOV) lands in your schema:

composer require <vendor>/<package>
php artisan vendor:publish --tag="<package>-migrations"
php artisan migrate

Look first at:

  1. The Point model shipped by the package — specifically Point::FIELD_STATUS and isClosed(). Treat FIELD_STATUS as the single source of truth for the allowed values (open, closed) instead of hardcoding strings in your app.
  2. The Points entry point (facade/manager) — that's where Points::of($point)->start(), stop(), close(), and reopen() live.
  3. The PointClosed / PointReopened events under the package's Events namespace.

Your first use case should be the simplest lifecycle: create a point, run time on it, close it when work is done, and list what's still active.

use Points;

$point = Point::create([...]);          // new points are open by default

Points::of($point)->start();
// ... work happens ...
Points::of($point)->close();            // stops running time + closes

Point::whereOpen()->get();              // dashboard / "my active work" list

That gives you the full loop — create → start → close → filter — and exercises every piece of what v0.2.0 added.

Implementation Patterns

Use the scopes, not raw where clauses. whereOpen() and whereClosed() understand the null-means-open rule; hand-written where('status', 'open') does not. Build every list, report, and dashboard filter on top of the scopes:

Point::whereOpen()->where('user_id', $user->id)->get();
Point::whereClosed()->latest('closed_at')->paginate();

Guard the start, don't just catch. Starting time on a closed point now throws DomainException. In UI-driven flows, check first so you can render a sensible message; in queued/automated flows, catch:

if ($point->isClosed()) {
    // offer reopen, or show "this point is closed" state
}

// or, in jobs / webhooks where state is assumed:
try {
    Points::of($point)->start();
} catch (DomainException $e) {
    // point was closed between read and write
}

Model close/reopen as an explicit workflow. close() stops running time and flips status in one call — don't call stop() first, or you'll write a redundant time entry. reopen() only opens the point; it does not restart the timer. The reopen flow is therefore always two steps:

Points::of($point)->reopen();
Points::of($point)->start();   // only if you actually want the clock running again

This split is useful: reopen-and-leave-idle is the correct behaviour when correcting historical data, and reopen-then-start is correct when resuming work.

Hang audit logging, notifications, and broadcasting off the events. PointClosed and PointReopened are the clean seam for anything that shouldn't live in your controller:

Event::listen(PointClosed::class, function (PointClosed $event) {
    AuditLog::record('point.closed', $event->point);
    ClosePointInProjectTracker::dispatch($event->point);
});

Keep listeners side-effect-only and idempotent — they may fire from console commands, imports, or tests.

Wrap multi-step transitions in a transaction. Close and reopen touch both status and time state; if you follow them with your own writes (invoices, summaries, counters), wrap the whole thing:

DB::transaction(function () use ($point) {
    Points::of($point)->close();
    $point->project->recalculateBillable();
});

Validate against the LOV. If you expose a status filter or an admin edit form, build the in: rule from Point::FIELD_STATUS so a future status value doesn't require touching your validators:

'status' => ['nullable', Rule::in(Point::FIELD_STATUS)],

Backfill deliberately. Points created before v0.2.0 have no status and count as open — which is almost certainly the semantics you want, so no migration is required. But if you later add a NOT NULL constraint or a generated index, backfill open explicitly and do it in a chunked command.

Gotchas and Tips

  • DomainException is not an HTTP exception. Uncaught, it's a 500. Map it in bootstrap/app.php (or your exception handler) to a 409/422 with a readable message before you ship the start endpoint.
  • Null status is invisible to raw SQL. where('status', '!=', 'closed') and where('status', 'open') both silently miss legacy null rows on some drivers. Always go through whereOpen() / whereClosed(), and if you must write raw SQL, use whereNull('status')->orWhere('status', 'open').
  • Don't double-stop. close() already stops running time. A stop() immediately before it is at best a no-op and at worst a second time entry, depending on how the package handles a stop with nothing running. Pick one call.
  • reopen() is not a resume. The most common bug after this upgrade is reopening a point and assuming the clock restarted. It didn't — call start() explicitly.
  • Race conditions bite at exactly this boundary. "Read isClosed(), then start" is not atomic; a concurrent close() in another request still throws. Either accept the exception path as your real guard, or lockForUpdate() the point row inside a transaction for anything financial.
  • Status is not time state. A point can be open with nothing running, and isClosed() says nothing about whether a timer is active. Don't conflate the two in your UI — show status and running state separately.
  • Faking events in tests. Use Event::fake([PointClosed::class, PointReopened::class]) when asserting event dispatch, but remember that faking suppresses your listeners — if a listener is what actually stops the clock, don't fake in that test.
  • Extension point. If you need extra statuses, resist editing the LOV in place. Add your own column or a mapping layer; the scopes and isClosed() are built around a two-value model and will quietly misreport anything else.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify