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 Sti Laravel Package

phl/laravel-sti

phl/laravel-sti brings Single Table Inheritance to Laravel Eloquent. Map multiple model subclasses to one database table using a type column, with automatic class resolution, scoped queries, and familiar Eloquent relationships and behavior for cleaner hierarchical models instead of separate tables.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config:

composer require phl/laravel-sti
php artisan vendor:publish --tag=laravel-sti

Add the discriminator column to the table that will back the hierarchy. The default column name is type (confirm in config/laravel-sti.php):

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('type')->index();   // discriminator
    $table->string('title');
    $table->text('body')->nullable();
    $table->string('video_url')->nullable(); // only used by Video
    $table->timestamps();
});

Create a base model that extends the package's StiModel, then extend it for each concrete type:

use PaulhenriL\LaravelSti\StiModel;

class Post extends StiModel
{
    protected $table = 'posts';
}

class Article extends Post {}
class Video extends Post {}

First use case to validate the setup: create one of each child, then confirm that Post::all() returns a mixed collection hydrated as Article/Video, while Article::all() only returns articles.

Article::create(['title' => 'Hello', 'body' => '...']);
Video::create(['title' => 'Demo', 'video_url' => '...']);

Post::count();    // 2
Article::count(); // 1

Where to look first: the README for the exact config keys, then src/StiModel.php — it is small and contains the entire type-resolution and query-scoping behaviour. Read it before building on top of it; that file is the source of truth when the README and reality diverge.

Implementation Patterns

Use the base model as the "all types" query root. Controllers and dashboards that need every row should type-hint Post; anything type-specific should use the child. Keep this convention consistent, otherwise you will scatter where('type', ...) calls through the codebase and defeat the package.

Relationships. Point relation properties at the base class when you want polymorphic behaviour by default, and at the child when you want a narrow set:

class Comment extends Model
{
    public function commentable() { return $this->morphTo(); }
}

class Post extends StiModel
{
    public function comments() { return $this->morphMany(Comment::class, 'commentable'); }
}

$post->comments works for articles and videos identically, because the morph type stored is the base class as long as you always attach through the base model.

Factories and seeders. Child factories should set the discriminator correctly. If the package sets it on create(), use Article::factory(); if it does not, add 'type' => Article::class to the definition and verify in a test that Article::query()->count() reflects the created rows.

Scopes and query building. Global and local scopes defined on the base model are inherited by children. Define narrow, reusable scopes (scopePublished, scopeFeatured) on the base class so they apply to the whole hierarchy; define type-specific scopes on the child.

Route model binding. Binding to a child implicitly filters by type, so /videos/{video} will 404 for an article id instead of exposing the wrong resource. This is usually desirable; if you need cross-type URLs, bind to the base model and validate the concrete class in a policy or form request.

Migrations. Keep columns for all subtypes on the single table and let them be nullable. Document which columns belong to which subtype in the migration comments — this is the main long-term maintenance cost of STI.

Mass assignment. Keep type out of $fillable. Even if the package protects it, treat it as immutable after creation; nothing good comes from reassigning a row's class at runtime.

Gotchas and Tips

  • Age and framework compatibility is the biggest risk. Last release was April 2020 (Laravel 6/7 era), 17 stars, zero dependents. Eloquent method signatures have changed in Laravel 9/10/11 (newInstance, newFromBuilder, newEloquentBuilder, return types). Expect to vendor the package into app/ or fork it rather than rely on a tagged release, and add tests around hydration immediately.
  • Table inference. Eloquent normally derives the table from the class name, so Video would look for videos. Either the package resolves the table from the root parent or you must set protected $table on the base and every child. If you get "table not found" errors, set $table explicitly everywhere.
  • Type value stability. If the discriminator stores the FQCN, renaming or moving a model orphans every row of that type. Decide early whether to override the type resolution (e.g. getStiType() returning a short slug) and add a data migration if you change your mind.
  • Raw queries bypass the scope. DB::table('posts')->get() returns plain rows, not subclass instances. Any reporting, exports, or upsert/insert code must set type manually.
  • withoutGlobalScopes() is dangerous here. Dropping scopes exposes the whole hierarchy to a child query and can hydrate rows into the wrong class or produce type errors. Grep for it in code review.
  • Serialisation leaks the type. Hide the discriminator (protected $hidden = ['type']) unless the frontend actually uses it, and be aware toArray() on a base-model collection will contain a mix of child attribute sets.
  • Index the discriminator, and consider composite indexes like (type, created_at) for lists. STI queries almost always start with the type filter.
  • Config caching. If you change the discriminator column name or any package config, run php artisan config:clear; stale cached config produces confusing "column not found" errors.
  • Testing. Assert with the base model (Post::count()) as well as the child. A passing child test does not prove hydration of mixed collections works.
  • Consider alternatives before committing. tighten/parental is the actively maintained STI package in the Laravel ecosystem and covers the same ground with better framework support. Only choose phl/laravel-sti if its specific API fits and you are prepared to maintain a fork.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin