# tighten/parental: Single Table Inheritance for Laravel Eloquent

> tighten/parental adds single table inheritance to Eloquent by keeping parent and child models in one database table. It suits Laravel applications that need type-specific model behavior without separate tables, although the model design requires a discriminator column and careful handling of model transitions.

**tighten/parental** — Use single table inheritance in your Laravel app

- Repository: https://github.com/tighten/parental
- Stars: 1,540 · Forks: 104
- Language: PHP
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/tighten-parental

## The shared-table problem Parental addresses

tighten/parental is a PHP package for Laravel applications that want several Eloquent model classes to represent records in one database table. Without this package, the README says that querying a child such as `Admin::first()` makes Laravel look for an `admins` table. That is Laravel's normal class-based table naming behavior, and it becomes a problem when `Admin`, `Guest`, and `User` are intended as representations of records stored in `users`.

The package expresses this relationship with two traits. A parent model uses `HasChildren`, while each child uses `HasParent`. This design is useful when records share attributes and storage but need different behavior. A `User` base model might represent the common account record, while `Admin` adds an `impersonate()` method. The approach avoids introducing separate tables merely to hold subclasses.

This is single table inheritance, or STI. It is not a claim that the records share only a table name. Their database shape remains shared, so every child must also tolerate the parent model's attributes and constraints. That makes the package most appropriate when the differences are primarily behavioral rather than schema-heavy.

## How HasChildren and HasParent change Eloquent mapping

The first Parent mapping change is table resolution. In the README's example, `User` extends `Model` and uses `HasChildren`, while `Admin` extends `User` and uses `HasParent`. The documented result is that `Admin::first()` queries the `users` table and returns an `Admin` instance. The parent class supplies the storage context, while the child adds its own methods.

The second mechanism is discriminator-based hydration. Adding `HasChildren` to the parent enables a type discriminator column. With a `type` column on `users`, a query such as `User::all()` can return `Admin` or `Guest` instances according to the value stored for each row. Without this mechanism, `User::first()` returns only the parent representation. The README notes that a nullable `type` column can be added through Laravel's schema builder, and the parent model must allow `type` through `$fillable` when using the demonstrated create calls.

The package can store fully qualified class names by default, such as `App\Models\Admin`, or shorter aliases declared in `$childTypes`. Aliases are useful when an existing database uses values such as `admin` and `guest`, or when the stored values should not expose application class names. A custom discriminator can be selected with `$childColumn`; the README uses `parental_type` as the replacement column.

## Install tightenco/parental and create a first child model

Install the package through Composer with the package name documented in the README:

```bash
composer require tightenco/parental
```

After installation, create the parent model with `HasChildren` and the child with `HasParent`. The following is the smallest mapping shown in the README. The child extends the parent, so Laravel can use the parent's `users` table while the child retains its own methods.

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Parental\HasChildren;

class User extends Model
{
    use HasChildren;
}
```

```php
namespace App\Models;

use Parental\HasParent;

class Admin extends User
{
    use HasParent;

    public function impersonate($user) {
        //...
    }
}
```

A first query should use the imported child class. According to the README, `Admin::first()` returns an `Admin` model while reading from `users`. The example then calls behavior that is specific to `Admin`, which is the practical reason for defining the child class rather than only adding a label to the parent.

## Discriminator columns and stable type aliases

To make parent queries return child instances, the README requires a discriminator column on the shared table. This example adds a nullable `type` column to `users`:

```php
Schema::table('users', function ($table) {
    $table->string('type')->nullable();
});
```

The parent then permits the column to be populated:

```php
class User extends Model
{
    use HasChildren;

    protected $fillable = ['type'];

    protected $childTypes = [
        'admin' => Admin::class,
        'guest' => Guest::class,
    ];
}
```

With these aliases, the README states that `Admin::create()` stores `admin` in the `type` column instead of `App\Models\Admin`. The same pattern applies to `Guest`. This has a concrete migration consequence: changing class names does not necessarily require changing stored values if aliases remain stable.

If the schema already uses another discriminator name, set `$childColumn` on the parent and allow that field through `$fillable`. The README demonstrates `parental_type`, not `type`, for this case. The package does not document a command for backfilling rows created before a discriminator or alias scheme is introduced. Existing records therefore need an application-specific migration plan if they cannot hydrate correctly without a type value.

## Changing an Order with become and save

Parental also supports changing the representation of an existing record without creating a second database row. The README models an `Order` with `PendingOrder` and `ShippedOrder`, then calls `become()` to return the desired child model while retaining the original model's attributes.

```php
class Order extends Model
{
    use HasChildren;

    protected $fillable = ['type', 'total'];

    protected $childTypes = [
        'pending' => PendingOrder::class,
        'shipped' => ShippedOrder::class,
    ];
}
```

After loading an order, the transition is explicit:

```php
use App\Models\Order;

$order = Order::first();
$order = $order->become(ShippedOrder::class);
$order->save();
```

The `become()` call produces a `ShippedOrder` instance with the existing attributes. It does not persist the discriminator change by itself. Calling `save()` is required, and the README states that this updates the `type` column to `shipped`. The returned model also exposes child-specific behavior before or around persistence.

A `becoming()` callback can register behavior for the transition. The documented callback receives the new child instance before the model is saved:

```php
ShippedOrder::becoming(function ($shippedOrder) {
    // Do something before the model is saved...
});
```

This can fit observers or model callbacks that should react when an order moves from pending to shipped. It does not make the transition atomic by itself. The README gives no transaction recipe, concurrency control, or validation rules, so those concerns remain outside what the package documentation establishes.

## Eager loading is limited to Laravel 11 and above

Parental includes helpers for eager loading relationships that exist only on particular child classes. This avoids the mismatch between loading a parent collection and invoking a relationship that exists on only some of its possible instances. The README carries an explicit warning that eager loading relationships is supported only on Laravel 11 and above.

For example, `TextMessage` can define `mentions`, while `ImageMessage` defines `attachments`. From a concrete `Message` instance, `loadChildren()` maps each child class to the relationships that should be loaded for that type:

```php
$message = Message::first();

$message->loadChildren([
    TextMessage::class => ['mentions'],
    ImageMessage::class => ['attachments'],
]);
```

If the message is a `TextMessage`, the package attempts to eager-load `mentions`. If it is an `ImageMessage`, it attempts to eager-load `attachments`. The documentation also mentions `loadChildrenCount()` for relationship counts, but the README does not show its complete call or expected result.

This limitation matters when selecting a Laravel version. The package can still provide table mapping, discriminator hydration, aliases, and model transitions without using these relationship helpers, but applications that depend on `loadChildren()` must use Laravel 11 or a later supported version. The repository's `composer.json` is listed among the top-level files, but its exact version constraints are not included in the README, so a specific compatible version range cannot be confirmed here.

## Release history, maintenance, and MIT adoption costs

The repository is not archived. Its last push was on 2026-05-25. The recorded release history includes `v1.6.0` on 2026-03-03, `v1.5.0` on 2026-01-03, and `v1.4.3` on 2025-04-15. Those dates indicate published updates across recent release lines, although they do not by themselves describe the scope, compatibility, or risk of future upgrades.

The package is licensed under the MIT License. For organizations evaluating adoption, that means the repository lists a permissive license, but software dependency review still needs to account for the supported Laravel and PHP versions declared by the installed package metadata. No compatibility matrix or upgrade guide appears in the README, so teams should inspect `composer.json` before selecting a package version.

Single table inheritance also has a permanent schema cost. Adding `type` or a custom discriminator means the shared table needs a field whose values participate in model hydration and creation. Adding child behavior later does not require an additional table, but existing data must be distinguishable if parent queries are expected to return child instances. Renaming a class is also easier with stable aliases, although the README does not describe an automatic migration from old fully qualified names to aliases.

## When STI is the wrong Eloquent design

Parental is a poor fit when subclasses require materially different database structures. STI keeps every child in the parent table, so a child that needs unique columns, indexes, foreign keys, or storage rules may be better represented by separate Eloquent models and tables. The package addresses class-based table inference and type-specific behavior, not normalization of substantially different schemas.

It can also add avoidable branching to a model hierarchy. Every parent query may need to resolve a discriminator, and every write through a child may need to preserve a stable type value. A database design that already models these concepts through explicit relationships may be clearer than encoding each kind as a subclass of one record.

The documented alternative within Laravel is to use separate models with their own tables and normal Eloquent relationships. That approach duplicates some attributes but gives each model an independent schema and table name. Parental is preferable when shared columns and one common record set are intentional domain constraints. Separate tables are preferable when the distinctions between types extend beyond methods and attributes.

## Conclusion

Adopt tighten/parental when the domain naturally has one shared table with type-specific Eloquent behavior, such as users with Admin and Guest classes or orders with lifecycle subclasses. Do not adopt it when separate tables, independent schemas, or native Laravel relationships are better matches. Before adopting it, verify the installed Laravel version against the package's relationship-loading limits, define the discriminator and aliases, and test how existing rows will hydrate.

## FAQ

### How do I install tighten/parental in Laravel?

Run `composer require tightenco/parental`. The README then shows configuring a parent with `HasChildren` and each child with `HasParent`.

### How does tighten/parental return child models from a parent query?

The parent uses `HasChildren`, and a discriminator column stores the child type. A query on the parent can then hydrate rows as their corresponding child models.

### Does tighten/parental persist changes made with become()?

No. The README says `become()` returns a new instance of the requested child model, and you must call `save()` to persist the type change and model attributes.

### Which Laravel versions support child relationship eager loading in tighten/parental?

The README explicitly limits eager loading relationships to Laravel 11 and above.

### Can tighten/parental store short type names instead of PHP class names?

Yes. Define aliases in the parent's `$childTypes` property. For example, an `admin` alias can be stored instead of a fully qualified model class name.

## Sources

- [Issues](https://github.com/tighten/parental/issues)
- [License: MIT](https://github.com/tighten/parental/blob/main/LICENSE)
- [README](https://github.com/tighten/parental/blob/main/README.md)
- [Releases](https://github.com/tighten/parental/releases)
- [tighten/parental on GitHub](https://github.com/tighten/parental)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tighten-parental
