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 Model Status Laravel Package

thefeqy/laravel-model-status

Laravel Model Status adds simple status management to Eloquent models: assign statuses, define allowed transitions, query by status, and integrate status badges into Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

thefeqy/laravel-model-status gives models an "active/inactive" lifecycle: a status column, query scopes, and activate() / deactivate() methods, all supplied by the HasActiveScope trait. As of v1.5.0 the status attribute is cast to a Status value object (StatusCast), so you compare and branch via methods instead of raw strings.

Install and wire it up

composer update thefeqy/laravel-model-status   # v1.5.0 introduces StatusCast + cascadeDeactivate
  1. Add the trait to any model that has a status:
use Thefeqy\ModelStatus\Traits\HasActiveScope;

class Product extends Model
{
    use HasActiveScope;
}
  1. Ensure the table has a status column (string or enum) with a sensible default, and index it — the package's scopes filter on it constantly:
$table->string('status')->default('active')->index();
  1. Run the model through a quick console check:
$product = Product::first();
$product->status->isActive();      // bool
$product->status->isInactive();    // bool
$product->deactivate();

Where to look first

  • The HasActiveScope trait (scopes + activate()/deactivate()).
  • The Status class and StatusCast — this is the new public surface in 1.5.0 and the thing most of your code will touch.
  • Any published config for status value names if you do not use literal active / inactive.

First real use case — filtering a listing without hand-writing status comparisons:

$products = Product::active()->paginate();
$products = Product::inactive()->get();

That is the 80% case: it replaces scattered where('status', 'active') calls with one auditable scope.

Implementation Patterns

Branch on the value object, not the string. In 1.5.0 the cast is the idiomatic check. Prefer it over column comparisons in your domain code:

if ($product->status->isActive()) {
    // ...
}

// Expose in the model rather than in controllers/views:
public function isSellable(): bool
{
    return $this->status->isActive() && $this->stock > 0;
}

Query scopes for lists, value object for single records. Sending a filter to SQL via the scope keeps the database doing the work; loading a row and calling isActive() is for a record you already have. Don't loop over a collection calling isActive() when Model::active() would do.

Cascade deactivation is the headline for aggregate models. Declare the relations that should follow the parent down:

class Category extends Model
{
    use HasActiveScope;

    protected array $cascadeDeactivate = ['products'];

    public function products()
    {
        return $this->hasMany(Product::class);
    }
}
$category->deactivate(); // category + all its products

Use it where "parent off" logically means "children off" — category→products, store→listings, user→api keys. Keep $cascadeDeactivate as a property of the model, not something you call conditionally; it is declarative configuration, so reviewers can see the blast radius in one place.

Wrap cascades in a transaction. A cascade can touch many rows and several relations; a failure halfway leaves children in a mixed state.

DB::transaction(fn () => $category->deactivate());

Chain it into admin actions. The natural home is a controller action or model method rather than a controller doing raw updates:

public function deactivate(Category $category)
{
    DB::transaction(fn () => $category->deactivate());

    return back()->with('status', 'Category and products deactivated.');
}

Pair with a form request validation set if statuses are user-editable, so a caller cannot post an arbitrary string that the Status cast cannot represent.

Testing. Assert through the value object so the test survives a cast change:

$this->assertTrue($product->fresh()->status->isInactive());

For factories, add states like inactive() that set status => 'inactive', since the trait does not add factory helpers.

Gotchas and Tips

The cast changes comparison semantics. $model->status === 'active' was correct in v1.4 and is now false — you are comparing an object to a string. Grep your codebase for status === and status == after upgrading. If you genuinely need the string (logging, an external API payload), cast it explicitly ((string) $model->status or whatever the value object exposes) rather than relying on implicit conversion.

The cast is authoritative — remove the old accessor. If you previously hand-rolled getStatusAttribute() or a custom cast on the same column, the two will fight. Delete the old one so StatusCast owns the attribute.

Cascade deactivation bypasses mass-update paths. Model::where(...)->update(['status' => 'inactive']) and Model::query()->update(...) never instantiate models, so deactivate() and $cascadeDeactivate are skipped entirely. If children must follow parents, you have to load and call deactivate() per model — which also means chunk or batch on large sets, otherwise one admin click can generate thousands of individual updates.

Watch for circular or deep relations. If $cascadeDeactivate chains across models that reference each other, you can recurse. Keep cascade arrays one level deep (parent → direct children) and handle grandchildren with an explicit job if the graph is genuinely deep. If cascade deactivation fires model events, a saved/updated listener that re-touches the parent is a common source of loops.

Keep the cascade list and the relations in sync. A typo in $cascadeDeactivate (or renaming a relation) fails silently — nothing deactivates and no exception fires. Add a test that asserts both sides end up inactive.

Serialization changes. A custom cast on an attribute changes how the model serializes to JSON. Check your API responses: a status that was "active" may now serialize as an object unless the Status class implements JsonSerializable. If you have API tests or consumers pinned to a string, pin it deliberately.

Scope names can collide. active() and inactive() are generic names; a model with a pre-existing scopeActive() will conflict or be shadowed depending on load order. Audit models that define their own active scopes before adding the trait.

Index the column. Both scopes and cascade deactivation filter on status; on a large table an unindexed status column turns listings and cascades into full scans.

Cache invalidation. If you cache counts or "active products" lists, deactivate() (especially a cascade) must bust those keys. There is no visible event contract for this in the release notes, so assume you own it — hook the invalidation into the same service that calls deactivate().

Upgrade checklist. composer update, clear config/cache (php artisan config:clear, cache:clear, and re-run the queue worker so it picks up new code), then search for status string comparisons and any duplicate status accessor. The release notes list no breaking changes, but the cast is a behavior change for anyone comparing raw values.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo