├── CHANGELOG.md ├── LICENSE.md ├── composer.json ├── src └── InheritsFromSuper.php └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `eloquent-super` will be documented in this file. 4 | 5 | ## 1.3.0 - 2024-03-14 6 | 7 | ### Added 8 | 9 | - PHP 8.3 support 10 | - Laravel 11 support 11 | 12 | ### Removed 13 | 14 | - PHP 8.2 support 15 | - Laravel 10 support 16 | 17 | ## 1.2.0 - 2023-04-28 18 | 19 | ### Added 20 | 21 | - PHP 8.2 support 22 | - Laravel 10 support 23 | 24 | ### Removed 25 | 26 | - PHP 8.1 support 27 | - Laravel 9 support 28 | 29 | ## 1.1.0 - 2022-02-11 30 | 31 | ### Added 32 | 33 | - PHP 8.1 support 34 | - Laravel 9 support 35 | - The ability to override the default morph name of the `super` relationship 36 | 37 | ### Removed 38 | 39 | - PHP 8.0 support 40 | - Laravel 8 support 41 | 42 | ## 1.0.0 - 2021-09-11 43 | 44 | - Initial release 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) DIVE bv info@dive.be 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dive-be/eloquent-super", 3 | "description": "Lightweight MTI (Multi-Table Inheritance) support for Eloquent models", 4 | "keywords": [ 5 | "dive", 6 | "eloquent-super", 7 | "mti", 8 | "multi", 9 | "table", 10 | "inheritance" 11 | ], 12 | "homepage": "https://github.com/dive-be/eloquent-super", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Muhammed Sari", 17 | "email": "muhammed@dive.be", 18 | "homepage": "https://dive.be", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "~8.3", 24 | "illuminate/database": "^11.0", 25 | "illuminate/support": "^11.0" 26 | }, 27 | "require-dev": { 28 | "laravel/pint": "^1.0", 29 | "larastan/larastan": "^2.0", 30 | "orchestra/testbench":"^9.0", 31 | "phpunit/phpunit": "^11.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Dive\\EloquentSuper\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Tests\\": "tests" 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "scripts": { 46 | "format": "vendor/bin/pint", 47 | "larastan": "vendor/bin/phpstan analyse --memory-limit=2G", 48 | "test": "vendor/bin/phpunit", 49 | "verify": "@composer larastan && composer test" 50 | }, 51 | "config": { 52 | "sort-packages": true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/InheritsFromSuper.php: -------------------------------------------------------------------------------- 1 | with[] = 'super'; 19 | } 20 | 21 | public function super(): MorphOne 22 | { 23 | return $this->morphOne($this->getSuperClass(), $this->superName())->withDefault(); 24 | } 25 | 26 | public function delete(): ?bool 27 | { 28 | if ($this->usesSoftDeletes()) { 29 | return parent::delete(); 30 | } 31 | 32 | return $this 33 | ->getConnection() 34 | ->transaction(fn () => $this->getRelationValue('super')->delete() && parent::delete()); 35 | } 36 | 37 | public function fill(array $attributes): static 38 | { 39 | [$super, $sub] = $this->partitionAttributes($attributes); 40 | 41 | parent::fill($sub); 42 | $this->getRelationValue('super')->fill($super); 43 | 44 | return $this; 45 | } 46 | 47 | public function save(array $options = []): bool 48 | { 49 | return $this->getConnection()->transaction(function () use ($options) { 50 | parent::save($options); 51 | 52 | return $this 53 | ->getRelationValue('super') 54 | ->setAttribute($this->super()->getForeignKeyName(), $this->getKey()) 55 | ->save($options); 56 | }); 57 | } 58 | 59 | public function update(array $attributes = [], array $options = []): bool 60 | { 61 | [$super, $sub] = $this->partitionAttributes($attributes); 62 | 63 | return $this 64 | ->getConnection() 65 | ->transaction(fn () => $this->getRelationValue('super')->update($super, $options) && parent::update($sub, $options)); 66 | } 67 | 68 | public function setCreatedAt($value): static 69 | { 70 | $this->setAttribute($this->getCreatedAtColumn(), $value); 71 | $this->getRelationValue('super')->setCreatedAt($value); 72 | 73 | return $this; 74 | } 75 | 76 | public function setUpdatedAt($value): static 77 | { 78 | $this->setAttribute($this->getUpdatedAtColumn(), $value); 79 | $this->getRelationValue('super')->setUpdatedAt($value); 80 | 81 | return $this; 82 | } 83 | 84 | protected function superName(): string 85 | { 86 | return Str::of($this->getSuperClass()) 87 | ->classBasename() 88 | ->snake() 89 | ->append('able') 90 | ->value(); 91 | } 92 | 93 | private function usesSoftDeletes(): bool 94 | { 95 | return method_exists($this, 'runSoftDelete'); 96 | } 97 | 98 | private function partitionAttributes(array $attributes): array 99 | { 100 | $superAttributes = $this->newSuper()->getFillable(); 101 | 102 | return Collection::make($attributes) 103 | ->partition(static fn ($_, $attribute) => in_array($attribute, $superAttributes)) 104 | ->toArray(); 105 | } 106 | 107 | private function newSuper(): Model 108 | { 109 | return new ($this->getSuperClass()); 110 | } 111 | 112 | public function __call($method, $parameters): mixed 113 | { 114 | if (! method_exists($super = $this->getRelationValue('super'), $method)) { 115 | return parent::__call($method, $parameters); 116 | } 117 | 118 | return $super->{$method}(...$parameters); 119 | } 120 | 121 | public function __get($key): mixed 122 | { 123 | if (! is_null($value = parent::__get($key))) { 124 | return $value; 125 | } 126 | 127 | if (! $this->relationLoaded('super')) { 128 | return $value; 129 | } 130 | 131 | return $this->getRelationValue('super')->__get($key); 132 | } 133 | 134 | public function __set($key, $value): void 135 | { 136 | if ($this->newSuper()->isFillable($key)) { 137 | $this->getRelationValue('super')->__set($key, $value); 138 | } else { 139 | parent::__set($key, $value); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🦸🏼‍♂️ - Eloquent Super 2 | Lightweight MTI (Multiple Table Inheritance) support for Eloquent models. 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/dive-be/eloquent-super.svg?style=flat-square)](https://packagist.org/packages/dive-be/eloquent-super) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/dive-be/eloquent-super.svg?style=flat-square)](https://packagist.org/packages/dive-be/eloquent-super) 7 | 8 | ## What is "Multiple Table Inheritance" exactly? 9 | 10 | MTI allows one to have separate database tables for each "sub class" that shares a common "super class". 11 | 12 | ### "Can't I just define a type column in my table and call it a day?" 13 | 14 | Well, it depends. If the **only** thing you'd like to do is adding specific behavior to each sub type (`user` - `admin` for example), then [Single Table Inheritance](https://github.com/calebporzio/parental) is definitely the better choice here. 15 | 16 | However, if the sub types have very different data fields, then MTI is the better tool. Using STI in this case will cause the table in question to have **a lot** of `NULL` columns. 17 | 18 | ## What problem does this package solve? 19 | 20 | ### Short answer 21 | 22 | As a matter of fact, it solves absolutely **nothing**. "Why this package, then?" you may ask. Well, read on. 23 | 24 | ### Long answer 25 | 26 | You see, Eloquent already gives us the ability to define polymorphic relationships. The only thing you need to start leveraging MTI capabilities in Eloquent is a `MorphOne` relationship. This package adds a nice DX layer on top of the existing functionality, so it is a tad nicer to work with these kind of tightly coupled relationships. 27 | 28 | So, the "meat and potatoes" of this package is **delegating calls** to the defined `super` relationship (and a couple more things). There is no real "parent" class in an object oriented sense. It is a conscious decision to not sprinkle too much magic on the models. 29 | 30 | ## Installation 31 | 32 | ```shell 33 | composer require dive-be/eloquent-super 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### Super / parent class 👱🏻‍♂️ 39 | 40 | #### Migrations 41 | 42 | The super model should define a morphs relationship that follows Laravel's naming conventions: the model's singular name in snake case + `able`. 43 | 44 | ```php 45 | Schema::create('addresses', static function (Blueprint $table) { 46 | $table->id(); 47 | $table->foreignId('country_id')->constrained(); 48 | $table->morphs('addressable'); // ==> mandatory 49 | 50 | // ... other columns 51 | }); 52 | ``` 53 | 54 | #### Class definition 55 | 56 | The super class **must** define a fillable array in order to determine which attributes belong to which database tables. Without it, there is no way to distinguish the super's columns from the sub's columns. 57 | 58 | ```php 59 | class Address extends Model 60 | { 61 | protected $fillable = ['city', 'country_id', 'street', 'postal_code']; 62 | 63 | public function country(): BelongsTo 64 | { 65 | return $this->belongsTo(Country::class); 66 | } 67 | } 68 | ``` 69 | 70 | ### Sub / child classes 👶🏼 71 | 72 | It is not mandatory for the sub classes to define a fillable array. Setting `$guarded` to an empty array is perfectly fine as well. 73 | 74 | #### Class definition 75 | 76 | ```php 77 | class ShippingAddress extends Model 78 | { 79 | use \Dive\EloquentSuper\InheritsFromSuper; 80 | 81 | protected $fillable = ['email', 'phone', 'contact_person', 'is_expedited', 'courier']; 82 | 83 | protected function getSuperClass(): string 84 | { 85 | return Address::class; 86 | } 87 | } 88 | ``` 89 | 90 | ```php 91 | class InvoiceAddress extends Model 92 | { 93 | use \Dive\EloquentSuper\InheritsFromSuper; 94 | 95 | protected $fillable = ['company_id', 'email', 'fax', 'phone', 'language']; 96 | 97 | protected function getSuperClass(): string 98 | { 99 | return Address::class; 100 | } 101 | } 102 | ``` 103 | 104 | ## Capabilities 💪 105 | 106 | ### Partitioning of data when saving a sub model 107 | 108 | ```php 109 | $address = ShippingAddress::create($request->validated()); 110 | 111 | $address->getAttributes(); // 'email', 'phone', 'contact_person', 'is_expedited', 'courier' 112 | $address->super->getAttributes(); // 'city', 'country_id', 'street', 'postal_code' 113 | ``` 114 | 115 | ### Attribute / relationship retrieval from super model 116 | 117 | ```php 118 | $address->city; // Ghent 119 | $address->super->city; // Ghent 120 | 121 | $address->country; // App\Models\Country { #2981 } 122 | $address->super->country; // App\Models\Country { #2981 } 123 | ``` 124 | 125 | ### Deleting the super along with the sub model 126 | 127 | ```php 128 | $address->delete(); // Database transaction in the background 129 | ``` 130 | 131 | > Note: only the sub model will be trashed if both the super and sub use the "SoftDeletes" trait 132 | 133 | ## A note on always eager loading the "super" relationship 📣 134 | 135 | It does not make sense for the sub model to exist without its complementary data from the super model. By having two tables, we are able to achieve a normalized database, but in code, it only makes sense when they coexist as a whole. 136 | 137 | ## Testing 138 | 139 | ```bash 140 | composer test 141 | ``` 142 | 143 | ## Changelog 144 | 145 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 146 | 147 | ## Contributing 148 | 149 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 150 | 151 | ## Security 152 | 153 | If you discover any security related issues, please email oss@dive.be instead of using the issue tracker. 154 | 155 | ## Credits 156 | 157 | - [Muhammed Sari](https://github.com/mabdullahsari) 158 | - [All Contributors](../../contributors) 159 | 160 | ## License 161 | 162 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 163 | --------------------------------------------------------------------------------