Coder Social home page Coder Social logo

single-table-inheritance's Introduction

Single Table Inheritance

Build Status Latest Stable Version Total Downloads Latest Unstable Version License

Single Table Inheritance is a trait for Laravel 5.8+ Eloquent models that allows multiple models to be stored in the same database table. We support a few key features

  • Implemented as a Trait so that it plays nice with others, such as Laravel's SoftDeletingTrait or the excellent Validating, without requiring a complicated mess of Eloquent Model subclasses.
  • Allow arbitrary class hierarchies not just two-level parent-child relationships.
  • Customizable database column name that is used to store the model type.
  • Customizable string for the model type value stored in the database. (As opposed to forcing the use of the fully qualified model class name.)
  • Allow database rows that don't map to known model types. They will never be returned in queries.

Installation

Simply add the package to your composer.json file and run composer update.

"nanigans/single-table-inheritance": "~1.0"

Or go to your project directory where the composer.json file is located and type:

composer require "nanigans/single-table-inheritance:~1.0"

Overview

Getting started with the Single Table Inheritance Trait is simple. Add the constraint and add a few properties to your models. A complete example of a Vehicle super class with two subclasses Truck and Car is given by

use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

class Vehicle extends Model
{
  use SingleTableInheritanceTrait;

  protected $table = "vehicles";

  protected static $singleTableTypeField = 'type';

  protected static $singleTableSubclasses = [Car::class, Truck::class];
}

class Car extends Vehicle
{
  protected static $singleTableType = 'car';
}

class Truck extends Vehicle
{
  protected static $singleTableType = 'truck';
}

There are four required properties to be defined in your classes:

Define the database table

In the root model set the protected property $table to define which database table to use to store all your classes.
Note: even if you are using the default for the root class (i.e. the 'vehicles' table for the Vehicle class) this is required so that subclasses inherit the same setting rather than defaulting to their own table name.

Define the database column to store the class type

In the root model set the protected static property $singleTableTypeField to define which database column to use to store the type of each class.

Define the subclasses

In the root model and each branch model define the protected static property $singleTableSubclasses to define which subclasses are part of the classes hierarchy.

Define the values for class type

In each concrete class set the protected static property $singleTableType to define the string value for this class that will be stored in the $singleTableTypeField database column.

Multi Level Class Hierarchies

It's not uncommon to have many levels in your class hierarchy. Its easy to define that structure by declaring subclasses at each level. For example suppose you have a Vehicle super class with two subclasses Bike and MotorVehicle. MotorVehicle in trun has two subclasses Car and Truck. You would define the classes like this:

use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

class Vehicle extends Model
{
  use SingleTableInheritanceTrait;

  protected $table = "vehicles";

  protected static $singleTableTypeField = 'type';

  protected static $singleTableSubclasses = [MotorVehicle::class, Bike::class];
}

class MotorVehicle extends Vehicle
{
  protected static $singleTableSubclasses = [Car::class, Truck::class];
}

class Car extends MotorVehicle
{
  protected static $singleTableType = 'car';
}

class Truck extends MotorVehicle
{
  protected static $singleTableType = 'truck';
}

class Bike extends Vehicle
{
  protected static $singleTableType = 'bike';
}

Defining Which Attributes Are Persisted

Eloquent is extremely lenient in allowing you to get and set attributes. There is no mechanism to declare the set of attributes that a model supports. If you misuse and attribute it typically results in a SQL error if you try to issue an insert or update for a column that doesn't exist. By default the SingleTableInheritanceTrait operates the same way. However, when storing a class hierarchy in a single table there are often database columns that don't apply to all classes in the hierarchy. That Eloquent will store values in those columns makes it considerably easier to write bugs. There, the SingleTableInheritanceTrait allows you to define which attributes are persisted. The set of persisted attributes is also inherited from parent classes.

class Vehicle extends Model
{
  protected static $persisted = ['color']
}

class MotorVehicle extends Vehicle
{
  protected static $persisted = ['fuel']
}

In the above example the class Vehicle would persist the attribute color and the class MotorVehicle would persist both color and fuel.

Automatically Persisted Attributes

For convenience the model primary key and any dates are automatically added to the list of persisted attributes.

BelongsTo Relations

If you are restricting the persisted attribute and your model has BelongsTo relations then you must include the foreign key column of the BelongsTo relation. For example:

class Vehicle extends Model
{
  protected static $persisted = ['color', 'owner_id'];
  
  public function owner()
  {
    return $this->belongsTo('User', 'owner_id');
  }
}

Unfortunately there is no efficient way to automatically detect BelongsTo foreign keys.

Throwing Exceptions for Invalid Attributes

BY default the SingleTableInheritanceTrait will handle invalid attributes silently It ignores non-persisted attributes when a model is saved and ignores non-persisted columns when hydrating a model from a builder query. However, you can force exceptions to be thrown when invalid attributes are encountered in either situation by setting the $throwInvalidAttributeExceptions property to true.

/**
 * Whether the model should throw an InvalidAttributesException if non-persisted 
 * attributes are encountered when saving or hydrating a model.
 * If not set, it will default to false.
 *
 * @var boolean
 */
protected static $throwInvalidAttributeExceptions = true;

Inspiration

We've chosen a very particular implementation to support single table inheritance. However, others have written code and articles around a general approach that proved influential.

First, Mark Smith has an excellent article (no long live but available in web archive) Single Table Inheritance in Laravel 4 amongst other things is introduces the importance of queries returning objects of the correct type. Second, Jacopo Beschi wrote and extension of Eloquent's Model, Laravel-Single-Table-Inheritance`, that introduces the importance of being able to define which attributes each model persists.

The use of Traits was heavy influence by the Eloquent's SoftDeletingTrait and the excellent Validating Trait.

single-table-inheritance's People

Contributors

boris-glumpler avatar bryanashley avatar cfothergill avatar darron1217 avatar denzonl avatar gvidal avatar isaackearl avatar jonspalmer avatar manandhar avatar martdegraaf avatar reiz avatar thomasschiet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

single-table-inheritance's Issues

Cannot construct newFromBuilder for unrecognized type=

Hi,
I'm trying to create a structure for different types of users but can't seem to get it to work, i get this error:
Cannot construct newFromBuilder for unrecognized type=agency

class User extends Authenticatable
{
    use SingleTableInheritanceTrait;
    protected $table = 'users';
    protected static $singleTableTypeField = 'type';
    protected static $singleTableTypeClasses = [
        Agency::class
    ];    
}

class Agency extends User
{
    protected static $singleTableType = 'agency';
}

User i'm trying to get with User::find($user_id) has the "type" attribute set to "agency".
I'm using Laravel 5.4

Am I missing something?

Using pluck() on Query Leads to Crash

Hi,

first I want to thank you for this great package, it helps us a lot in several cases within our projects.

After upgrading Laravel to version 5.3 we found a bug. When using pluck() with Eloquent queries where the Eloquent class using STI, the application crashes with following message:

Nanigans\SingleTableInheritance\Exceptions\SingleTableInheritanceException with message 'Cannot construct newFromBuilder without a value for type'

Example:
Lets say we have a class EloquentModel that applies and configures STI, the type field is named 'type'. There is a class MyEloquentModel that inherits the EloquentModel class and configures the type. Now I want to get all IDs from MyEloquentModel, I use following code:

MyEloquentModel::pluck('id');

This leads to the error from above. Do you have an idea how to fix this problem?

Many thanks and best wishes
David

Loading from relationship fails due to unknown type

Using the following code as an example:

class Zoo extends Model
{
    public function animals()
    {
        return $this->hasMany(Animal::class);
    }

}

class Animal
{
    use SingleTableInheritanceTrait;

    protected $table = 'animals';

    protected static $singleTableTypeField = 'type';

    protected static $singleTableSubClasses = [ Elephant::class ];

    public function zoo()
    {
        return $this->belongsTo(Zoo::class);
    }
}

class Elephant extends Animal
{
    protected static $singleTableType = 'elephant';
}

Assuming a Zoo has been saved with some animals associated, calling Zoo::with('animals')->get(); fails with Nanigans\SingleTableInheritance\Exceptions\SingleTableInheritanceException with message 'Cannot construct newFromBuilder for unrecognized type=elephant'.

This is because get_called_class at https://github.com/Nanigans/single-table-inheritance/blob/master/src/SingleTableInheritanceTrait.php#L46 returns the base Animal class which doesn't know about the elephant type.

A better solution for this would be to define the type in the sub-class map:

    protected static $singleTableSubClasses = [ 'elephant' => Elephant::class ];

onlyTrashed()->forceDelete() seems to lose the SingleTableInheritance scope, deletes way more than intended

I'm planning on working this until I have a proposed patch, but wanted to give a heads up and see if you had any advice.

Here's my reproduction case in my own codebase: (TP_TradeEstimate and Hours are both subclasses of the same thing)

    public function testOnlyTrashedShouldntBreakSingleTableInheritance(): void
    {
        $liveTP = TP_TradeEstimate::factory()->create();
        $deletedTP = TP_TradeEstimate::factory()->create();
        $deletedTP->delete();
        $liveOther = Hours::factory()->create();
        $deletedOther = Hours::factory()->create();
        $deletedOther->delete();

        self::assertSame(1, TP_TradeEstimate::onlyTrashed()->count());
        $affectedRows = TP_TradeEstimate::onlyTrashed()->forceDelete();
        self::assertSame(1, $affectedRows, 'Affected rows should match counted rows');

        self::assertModelExists($liveTP);
        self::assertFalse(
            DB::table('account_integrations')
                ->where('id', $deletedTP->id)
                ->exists(),
        );
        self::assertModelExists($liveOther);
        self::assertSoftDeleted($deletedOther);
    }

This fails because $affectedRows is actually 2, and if you comment out that assertion, the last assertion fails because $deletedOther has been forceDeleted. Even weirder, obviously onlyTrashed by itself isn't the problem, that count works, and if you get() the collection and do each->forceDelete() there's no problem.

Using single-table-inheritance v1.0.0 with Laravel framework 9.36.2

Class mapping

I noticed that your child class map is a simple array. Unfortunately, this would break if, at a later date, I changed the namespace or class name of one of the children. It would be nice to optionally use a simple hashed class map. I could throw in some code & submit a pull request if you'd like.

Change type attribute to convert a model to another

I'm trying to change the column 'type' that stores le class name. I use:

$instance = Model::find($id);
$instance->type = 'other'; // 'other' stands for Other model which shares the same table
$instance->save();

But when saving, type hasn't change. How should we do that with Nanigans/single-table-inheritance ? Actually is it possible to convert a row in the table changing his type column directly ? Thanks!

Laravel 5.2.*

Hi,

composer require "nanigans/single-table-inheritance:0.5.*"

Doesn't work:

Your requirements could not be resolved to an installable set of packages.

This is because the lastest version on Packagist is 0.5.0 only "supports" Laravel 5.1.*:
https://packagist.org/packages/nanigans/single-table-inheritance

I see that you updated dev-master to Laravel 5.2.*:
https://github.com/Nanigans/single-table-inheritance/blob/master/composer.json#L14

Could you publish a new version (0.5.1) on Packagist with new composer.json & Laravel 5.2.* support?

Thanks.

Configurable $singleTableSubclasses

Hi,

I wanna make $singleTableSubclasses configurable (i.e. store this under config and read it as something like $singleTableSubclasses = config('vehicle.subclasses');. Right now it's a static variable so this is not possible. Is there a quick way to achieve this without rewriting too much of your implementation?

Cheers

hasMany relationship on a sublclass won't work

Contract Model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\RecurringPaymentMethod;
use App\Order;
use App\ContractAddress;

class Contract extends Model
{
    protected static $throwInvalidAttributeExceptions = true;

    protected $attributes = [
        'validated' => false,
        'terminated' => false
    ];

    protected $fillable = ['validated', 'termniated'];

    protected $casts = [
        'validated' => 'boolean',
        'terminated' => 'boolean'
    ];

    public function recurringPaymentMethod()
    {
        return $this->belongsTo(RecurringPaymentMethod::class, 'recurring_payment_method_id');
    }

    public function order()
    {
        return $this->belongsTo(Order::class, 'order_id');
    }

    public function addresses()
    {
        return $this->hasMany(ContractAddress::class, 'contract_id', 'id');
    }
}

ContractAddress:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use App\Contract;

class ContractAddress extends Address
{
    protected static $singleTableType = 'contract_address';
    protected static $persisted = [
        'start_date',
        'end_date',
        'default',
        'contract_id'
    ];

    public function __construct() {
        parent::__construct();
        $this->casts = array_merge($this->casts, [
            'contract_id' => 'integer',
            'default' => 'boolean'
        ]);
    }

    public function contract()
    {
        return $this->belongsTo(Contract::class, 'contract_id');
    }
}

Address:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\ContractAddress;
use App\PersonAddress;

class Address extends Model
{
    use SingleTableInheritanceTrait;
    use SoftDeletes;

    protected $dates = ['deleted_at'];

    protected $table = "addresses";
    protected static $singleTableTypeField = 'type';
    protected static $singleTableSubclasses = [PersonAddress::class, ContractAddress::class];
    protected static $persisted = [
        'street_line1',
        'street_line2',
        'zip_code',
        'city',
        'country',
        'validated',
        'archived'
    ];
    protected $casts = [
        'id' => 'integer',
        'validated' => 'boolean',
        'archived' => 'boolean'
    ];
}

This code won't always work:

$contract = new Contract;
$contract->validated = false;
$contract->terminated = false;
$order_id = $faker->numberBetween(1, 5);
$order = Order::find($order_id);
$contract->order()->associate($order);
$recurrent = RecurringPaymentMethod::all()->random(1);
$contract->recurringPaymentMethod()->associate($recurrent);
$addresses = ContractAddress::all()->random(5);
$contract->addresses()->saveMany($addresses);
$contract->save();

The hasMany relationship between Contract and ContractAddress is broken: most of the time I cannot access/save the addresses trought $contract->addresses.
I also tried to associate a ContractAddress with a Contract like this: $address->contract()->associate(...) but I get the same weird behavior: contract_id is not always set on the "addresses" table.

Update Readme

Could you please include a note about how querying works?
Do query builders return instances of the Child classes, or the table class?
For example, if I have Car and Truck extending Vehicle, and I query the vehicles table, do I get back a 2 instance of Vehicle, or 1 Car and 1 Truck?
Is there anything special we need to do when handling this?
Not knowing the answer to this is making me not want to try this package... but I NEED this package.

Numeric type values fail

Wrong classes gets instantiated when using numeric type values and calling Parent::all().
This is because getSingleTableTypeMap() uses merge_array to cache parent class types (see. SingleTableInheritanceTrait line 65).
This also caused inverted relation to fail. I'd suggest using:

$typeMap += $subclass::getSingleTableTypeMap());

Laravel 5

Hi there! Great package. Any plans to update for L5? Thanks!

Can we set the type of model using the base model?

can we set the type of model using the base model.
this one is failing to save unless i define a
protected static $singleTableType = 'test';
in the base model.

what will this do is it will resolve to that type of model when retreive again.
i want to avoid using if else statement to instantiate the child class.

class Base extends Model {

    use SingleTableInheritanceTrait;

    protected static $singleTableTypeField = 'type';

    protected static $singleTableSubclasses = [
        Child::class,
    ];

    protected $fillable = [
        'name',
        'description',
        'type'
    ];
}
....
$base= new Base();
$base->name = 'sample name';
$base->description = 'sample description';
$base->type = 'child'; // this one 
$base->save();

V0.8.6 causes incorrect attributes to be set

Hey,

Thanks for the great package.

Since v.0.8.6 we are encountering some strange behaviour with getAttribute.

It seems $this->setRawAttributes($filteredAttributes, true); call in nanigans/single-table-inheritance/src/SingleTableInheritanceTrait.php:226 now only sets the pivot attributes instead of all attributes.

As a result, all normal attributes of the model (not loaded via pivot) are unavailable

I'm not fully understanding the full scope of the change, but shouldn't $filteredAttributes = array_intersect_key($attributes, array_flip($persistedAttributes)); be $filteredAttributes = array_diff_key($attributes, array_flip($persistedAttributes));?

For now, I'll redefine

    protected function getPivotAttributeNames($attributes)
    {
        return [];
    }

to fix it on our end, but I suppose It should be fixed here as well

Regards,
Levi

belongsToMany relation doesn't load

Hello,
I met a strange behavior applying SingleTableInheritanceTrait on a model used in a BelongsToMany relation:

Single table inheritance structure:

class Animal extends Model {
    use SingleTableInheritanceTrait;
}

class Elephant extends Animal { }

Many to Many relation with "single table inherited" model:

class Zoo extends Model { 

    public function animals(){
        return $this->belongsToMany(Animal::class, 'animal_zoo', 'zoo_id', 'animal_id');
    }

}

The relation load only when I explicitly "touch" the relation property:

return $zoo->animals;

It doesn't load using "with" or "load" eloquent methods:

// Create relations
Zoo::find(1)->animals()->attach([1, 2, 3]);

// Using "with"
return Zoo::with('animals')->get();

// Using "load" in booted model 
$zoo = Zoo::find(1);
return $zoo->load('animals');

/*
 * In both case the result is
 */
{
  name: 'Super funny Zoo',
  address: '...',
  animals: [] // <-- empty array for belonsToMany relation using SingleTableInheritanceTrait 
}

This example represents a return value for an api call by javascript frontend. It's an easy reproducible issue.

Can I use some tricks to load belongsToMany relation properly?

Type is not inserted when using Laravel factories

Hello, I think I have discovered a bug.

// WORKS
$factory->define(Car::class, function (Faker $faker) {
    return [
        'type' => 'car',
    ];
});
// DOES NOT WORK
$factory->define(Car::class, function (Faker $faker) {
    return [];
});

I would expect that since the factory is defined for the concrete type that the type can be inferred that way. The same way that I do not need to specify the type when using the class constructor or create() method.

// WORKS
$car = new Car();
$car->save();

// WORKS
Car::create();

Can't create a new record directly form the base class

I need to be able to create a record directly from the base class because of the code base that I have.

Obviously I can't refactor everything with out a step by step process. And also I need to support a type field without a class, like 'other' for instance. I know this issue/proposal was also asked here: #47 and I know the trade offs this could imply. We love this trait but as I've already said, having a large base code we need some way to add functionality in small cycles. I'm not the only one in this situation.

Right now we have a workaround idea. For those how have the same problem.
A Base Class that extends from Model in which we have all of our configuration then the normal class extends from it and then the inheritances. So we can call the base anywhere without problems.

class VehicleBase extends Model
{
    protected $table = "vehicles";

    // Conventional Model configuration goes here
}
use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

class Vehicle extends VehicleBase
{
    use SingleTableInheritanceTrait;

    protected static $singleTableTypeField = 'type';
    protected static $singleTableSubclasses = [Car::class, Truck::class];
}
class Car extends Vehicle
{
    protected static $singleTableType = 'car';
}
class Truck extends Vehicle
{
    protected static $singleTableType = 'truck';
}

Anyway, This is a great tool. Thanks for the work

Abstract base types

Does this work with abstract base types? I've had issues overriding FromBuilder with abstract base types.

Laravel 6.0 support

This package doesn't work for Laravel 6.0 because it uses the starts_with helper, which has been removed from the framework.

"Field 'type' doesn't have a default value" when saving a model

First: Making sure I'm using the right package

I'm only using the Eloquent ORM (not the whole Laravel framework), and for some reason, I couldn't get your demo work on my project.
Before posting any of my failing code (nothing fancy, just a simple test), I want to know if this package supports the independent Eloquent ORM provided in illuminate/database.
So, is this package meant to be used with the whole Laravel framework or I might have missed something in my demo?

Second: Now, here's my attempt

use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

class A extends Model
{
    use SingleTableInheritanceTrait;
    protected $table = "a";
    protected static $singleTableTypeField = 'type';
    protected static $singleTableSubclasses = [B::class];
}
class B extends A
{
    protected static $singleTableType = 'b';
}
// In some other file
$b = new B(); // some inits as well
$b->save(); // This line is throwing an exception

I'm getting this error:

Field 'type' doesn't have a default value

Workaround for subclasses not found with Laravel 5.1+

Maybe I am crazy, but I couldn't get the overview examples to work on laravel 5.1 until I changed them to the following:

class Vehicle extends Model
{
  use SingleTableInheritanceTrait;

  protected $table = "vehicles";

  protected static $singleTableTypeField = 'type';

  protected static $singleTableSubclasses = [Car::class, Truck::class];
}

Note the changing of the $singleTableSubclasses array items from strings to the class name resolution scalars. Using the original 'Car' and 'Truck' strings, I was receiving "Class not found" errors when instantiating those particular classes, but using the class name resolution scalar resolves it for me for some reason. Perhaps it is because I had them in a separate models directory, I am not sure.

Also, it seems that there is no Eloquent class to extend in 5.1, but there is a Model class.

extra binding during update

When I try to update an element that uses STI with the following code :

$questionnaire = Questionnaire::where('chain_id', $chain->id)->first();
$questionnaire->body = 'test';
$questionnaire->save();

the update does not work because the trait drops in an extra binding for the type column (type = 'questionnaire') but the type column is not included into the update query.
There are 4 bindings for 3 question marks :

'query' =>
    string(70) "update `form_elements` set `body` = ?, `updated_at` = ? where `id` = ?"
    'bindings' =>
    array(4) {
      [0] =>
      string(4) "test"
      [1] =>
      string(19) "2014-10-03 00:06:13"
      [2] =>
      string(13) "questionnaire"
      [3] =>
      string(1) "1"

Cannot insert record

Hi,

I noticed there is no record inserted into database..but if I changed the below:
$instance = (new $class)->newInstance([], true);
to
$instance = (new $class)->newInstance();

it works. However, the changes will pose the update problem for duplicate entry in database.

Any help will be much appreciated:) Thanks

PHP hangs when parent and child inheritance is not setted properly.

PHP hangs when parent and child inheritance is not setted properly.

For example, the class MiniCar is not proper inheritance. But it never emits error, just hang PHP which makes hard to debug.

use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

class Vehicle extends Model
{
  use SingleTableInheritanceTrait;

  protected $table = "vehicles";

  protected static $singleTableTypeField = 'type';

  protected static $singleTableSubclasses = [Car::class];
}

class Car extends Vehicle
{
  protected static $singleTableType = 'car';

  protected static $singleTableSubclasses = [MiniCar::class];
}

// This is wrong, but it never emits error.
class MiniCar extends Vehicle
{
  protected static $singleTableType = 'minicar';
}
// This will lead you to infinite execution of PHP script.
Vehicle::getSingleTableTypeMap();

Is there a way to emit errors in this situation?

Model subclass using SingleTableInheritanceTrait returns model indicated by parent's $singleTableSubclasses?

Hi folks:

This may or may not be a weird one.
I've got a model (User) which uses Single Table Inheritance to identify six different classes of user. Basically, my parent class looks a bit like this:

namespace MyNamespace\Models;

class User extends Illuminate\Database\Eloquent\Model
{
    use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

    protected static $singleTableTypeField = 'user_role';
    protected static $singleTableSubclasses = [
        'MyNamespace\Models\UserRole1',
        'MyNamespace\Models\UserRole2',
        'MyNamespace\Models\UserRole3',
        'MyNamespace\Models\UserRole4',
        'MyNamespace\Models\UserRole5',
        'MyNamespace\Models\UserRole6',
    ];
}

Then I have this child class (in the MyNamespace\API\Models namespace this time):

namespace MyNamespace\API\Models;

class User extends MyNamespace\Models\User
{
    protected static $singleTableTypeField = 'user_role';
    protected static $singleTableSubclasses = [
        'MyNamespace\Models\API\UserRole1',
        'MyNamespace\Models\API\UserRole2',
        'MyNamespace\Models\API\UserRole3',
        'MyNamespace\Models\API\UserRole4',
        'MyNamespace\Models\API\UserRole5',
        'MyNamespace\Models\API\UserRole6',
    ];
}

The problem I'm finding is that when I do a Model::find(...) on the child user class, I get a model as specified by the parent class's $singleTableSubclasses property.

Am I doing something stupid? The documentation suggests that this should work?

Things that may be helpful:

  1. This worked fine before I had the need to subclass the models.
  2. I'm using Eloquent outside of Laravel, booting it using the Capsule Manager.

Any wisdom on this would be appreciated.
Thanks.

Subclass query building

Using Laravel 5.3

I'm not sure if I'm expecting something that should be there or not.

class Base extends Model { }

class Sub extends Base { }

class SubAlt extends Base { }

I would expect doing something like this:

Sub::all()

Expectation:

\Collection {
    all: [
        \Sub { }
    ]
}

Getting:

\Collection {
    all: [
        \Sub { }
        \SubAlt { }
    ]
}

Not sure if this is part of the Laravel 5.3 ticket #23 or not.

Enhence performance when querying root model of subclasses

I found that if model has subclasses, trait adds whereIn condition to query.

But the root model (which queries every type) don't need whereIn condition to select specific types. It will just make query slow.

I think it would be nice to remove whereIn condition when querying root model.

SingleTableInheritance should not filter out pivot attributes

Thanks for a great package, we have started to use it a lot.

Recently we have run into a problem when using Single Table Inheritance with Eloquent eager loading.

If I run the below I would get the expected results.

$model = new App\MyModel;
$model = $model->find(1);
dd($model->myRelation);

If I run the below I would get an empty result.

$model = new App\MyModel;
$model = $model->with('myRelation')->find(1);
dd($model->myRelation);

After some digging I found that it worked when there was no Single Table Inheritance.

This lead me to find that SingleTableInheritanceTrait::setFilteredAttributes() was stripping the pivot table attributes add by the relation.

$attributes before filtering.

Array
(
    [id] => 21
    [type] => sub_type
    [user_id] => 1
    [name] => Test Information
    [description] =>     
    [status] => true
    [created_at] => 2016-10-01 10:56:39
    [updated_at] => 2016-10-01 10:56:39
    [pivot_parent] => 6
    [pivot_other_id] => 21
    [pivot_extra] => 0
)

$attributes after filtering.

Array
(
    [id] => 21
    [type] => sub_type
    [user_id] => 1
    [name] => Test Information
    [description] =>     
    [status] => true
    [created_at] => 2016-10-01 10:56:39
    [updated_at] => 2016-10-01 10:56:39
)

All pivot attributes are added with the hard coded prefix pivot_. The below code works for me.

  public function setFilteredAttributes(array $attributes) {
    $persistedAttributes = $this->getPersistedAttributes();
    if (empty($persistedAttributes)) {
      $filteredAttributes = $attributes;
    } else {
      // The query often include a 'select *' from the table which will return null for columns that are not persisted.
      // If any of those columns are non-null then we need to filter them our or throw and exception if configured.
      // array_flip is a cute way to do diff/intersection on keys by a non-associative array
      $extraAttributes = array_filter(array_diff_key($attributes, array_flip($persistedAttributes)), function($value) {
        return !is_null($value);
      });
      if (!empty($extraAttributes) && $this->getThrowInvalidAttributeExceptions()) {
        throw new SingleTableInheritanceInvalidAttributesException("Cannot construct " . get_called_class() . ".", $extraAttributes);
      }

      $filteredAttributes = array_intersect_key($attributes, array_flip($persistedAttributes));
    }
+
+    // All pivot attributes start with 'pivot_'
+    // Add pivot attributes back in
+    $filteredAttributes += $this->getPivotAttributeNames($attributes);

    $this->setRawAttributes($filteredAttributes, true);
  }

+  protected function getPivotAttributeNames($attributes)
+  {
+    $pivots = [];
+    foreach ($attributes as $key => $value) {
+      if (starts_with($key, 'pivot_')) {
+        array_set($pivots, $key, $value);
+      }
+    }
+    return $pivots;
+  }

Alternatively you could documenting that when eager loading is used and if a relation adds pivot properties using the withPivot() method then all all keys and additional properties need to be added to the $persisted array property.

My preference would be to not filter out pivot_* properties.

Duplicate data always throw SingleTableInheritanceException

Hello,

I have a table with no limitation for duplicate data. But every trying to add new entry, it always throw SingleTableInheritanceException.

Here is my table schema:

CREATE TABLE `base_posts` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `content` text COLLATE utf8_unicode_ci NOT NULL,
  `excerpt` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `slug` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `post_type` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  KEY `base_posts_post_type_index` (`post_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Models:

class BasePost extends Model
{
    use SingleTableInheritanceTrait;
    protected $table = 'base_posts';
    protected $fillable = ['title', 'content', 'excerpt', 'slug'];
    protected static $singleTableTypeField = 'post_type';
    protected static $singleTableSubclasses = [Story::class];
}

class Story extends BasePost
{
    protected static $singleTableType = 'story';
}

Sample code:

$ ./artisan tinker
Psy Shell v0.7.2 (PHP 5.6.24-1+deb.sury.org~trusty+1 โ€” cli) by Justin Hileman
>>> App\Models\Story::create(['title' => 'title', 'content' => 'content']);
=> App\Models\Story {#667
     title: "title",
     content: "content",
     slug: "title",
     post_type: "story",
     updated_at: "2016-08-12 03:59:29",
     created_at: "2016-08-12 03:59:29",
     id: 1,
   }
>>> App\Models\Story::create(['title' => 'title', 'content' => 'content']);
Nanigans\SingleTableInheritance\Exceptions\SingleTableInheritanceException with message 'Cannot construct newFromBuilder without a value for post_type'
>>> 

Using Laravel 5.2.43 and nanigans/single-table-inheritance 0.6.1.

Unable to update models - data not persisting

I'm using STI in Laravel with mysql on a businesses model so it has a type Business, then other types extend it (Hotel, Restaurant, etc). I can create new businesses of any type just fine using STI models (eg. Hotel::create(['name' => 'test'])). I can also fetch them (eg. Business::all() or Hotel::all()). However, I cannot update them.

For example, I narrowed the code down to a very basic update or save command on a Hotel and neither of these work:

public function update()
{
    $business = Business::find(1);
    return $business->update(['name' => 'lol']);
}

OR

public function update()
{
    $business = Hotel::find(1);
    return $business->update(['name' => 'lol']);
}

They both will return true, but no change is reflected in the database. If I remove the 'use SingleTableInheritanceTrait;' from my model, the update works correctly in both cases above - the database reflects the change.

Here are the models. I have $throwInvalidAttributeExceptions set to true just in case.

Business.php

<?php namespace Directory\Businesses;

use Cviebrock\EloquentSluggable\SluggableInterface;
use Cviebrock\EloquentSluggable\SluggableTrait;
use Eloquent;
use Nanigans\SingleTableInheritance\SingleTableInheritanceTrait;

class Business extends Eloquent implements SluggableInterface{

    use SluggableTrait;
    use SingleTableInheritanceTrait; // <---- removing this makes it work

    protected $table = "businesses";

    protected $fillable = ['id', 'name', 'category', 'slug'];

    protected static $persisted = ['name', 'category', 'slug'];

    protected static $throwInvalidAttributeExceptions = true;

    /**
     * Columns for sluggable business name
     *
     * @var array
     */
    protected $sluggable = array(
        'build_from' => 'name',
        'save_to'    => 'slug',
    );

    /**
     * Table column for subclass name
     *
     * @var string
     */
    protected static $singleTableTypeField = 'category';

    /**
     * Subclass models
     *
     * @var array
     */
    protected static $singleTableSubclasses = ['Directory\Businesses\Hotel', 'Directory\Businesses\Restaurant', 'Directory\Businesses\Retail'
    ];
}

Hotel.php

<?php

namespace Directory\Businesses;

class Hotel extends Business {

    /**
     * Subclass identifier in db table
     *
     * @var string
     */
    protected static $singleTableType = 'hotels';

    protected static $persisted = ['hotel_capacity', 'hotel_type_id'];

}

Any idea what may be going on? Thanks!

Eager loading child model relationship

I have a parent model called Category, and two category types, Link & Dynamic. Only one of them has the relationship criteriable()

When I try to load like,

$categories->load('criteria');

I get the error, Call to undefined relationship [criteria] on model [App\Models\Category\Category]' Is eager loading that relationship possible?

Laravel 5.7 Support

As the title says, it would be very nice to have Laravel 5.7 support as it has been tagged today. From what I've seen in the upgrade guide, not too much has changed, so perhaps it's nothing more than a composer.json update.

edit: Added a PR: #53

Sync?

Hello!

I have tried to sync a belongsToMany relationship and the filter seams to don't apply: all table (even brother rows) is cleared except for those who were sent on form

Have a nice day!

Compatibility with laravel-mongodb ?

hi
I just found this library when i was trying to implement something similar. Great stuff, thanks.

In fact, I am new to laravel and eloquent.
As I am using mongodb , i also found this mongodb version eloquent https://github.com/jenssegers/laravel-mongodb

However, when I try to use SingleTableInheritanceTrait to that 'Moloquent' I have to make two changes in order to make it work:

1.at line 171 of SingleTableInheritanceTrait.php

$classType = isset($attributes->$typeField) ? $attributes->$typeField : null;
to
$classType = isset($attributes[$typeField]) ? $attributes[$typeField] : null;

this one i guess is something related to 'mongo' as it will return pure array, i didn't have time to check with 'mysql'

2.at line 195 of SingleTableInheritanceTrait.php

return $this->getTable() . '.' . static::$singleTableTypeField;
to
return static::$singleTableTypeField;

same thing here as the query column key needs to be the field name itself without 'table' name

Any thought about this?

Double persistence

I'm stuck in a scenario where im using Mutators an Accessors in my model. This works fine up until i want to create a model. Because the attributes are set twice. Let me explain.

These are my accessors and mutators. You have to give somekind of enumeration class on those fields. And the mutator tranforms it into a string.

        /**
         * @param $value
         * @return AccessorType
         */
        public function getAccessorTypeAttribute($value)
        {
            return new AccessorType($value);
        }

        /**
         * @param AccessorType $accessorType
         */
        public function setAccessorTypeAttribute(AccessorType $accessorType)
        {
            $this->attributes['accessor_type'] = $accessorType->getValue();
        }

This goes fine the first time and the internal attributes table is set with the string.
But after the saved event is catched by the Observer.

The model tries to fill itself a second time but now with the data from the attributes table without using the mutators first.

Problem is internally AccessorType is a string but when used you or set you need it wrapped in the enumeration class. So PHP throws an error because the typehinting fails.

The first setAttribute is called in the
Model->fill() function

The second after
setSingleTableInheritanceType();
and then it calls the magic method __set()

screenshot 2015-07-23 13 38 47

Support for Laravel 5.5

Hello,

I tried to make a pull-request for Laravel 5.5, but I can't figure out how to make the test-cases succeed. Before I made any changes, I ran vendor/bin/phpunit which gave no errors. Then I made the following changes to composer.json:

  • Added 5.5.* to lluminate/database and lluminate/support.
  • Added ~6.0 to phpunit/phpunit
  • Updated orchestra/testbench to 3.5.*

It looks like the migrations are not executed, I've read the documentation on orchestra/testbench, but I don't understand why the migrations are not running.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.