├── LICENSE.txt ├── README.md ├── composer.json └── src ├── HasBelongsToOne.php ├── HasMorphToOne.php └── Relations ├── BelongsToOne.php └── MorphToOne.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Ankur Kumar 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Missing Eloquent Relationships For Laravel 2 | 3 | [![Packagist](https://badgen.net/packagist/v/ankurk91/laravel-eloquent-relationships)](https://packagist.org/packages/ankurk91/laravel-eloquent-relationships) 4 | [![GitHub tag](https://badgen.net/github/tag/ankurk91/laravel-eloquent-relationships)](https://github.com/ankurk91/laravel-eloquent-relationships/tags) 5 | [![License](https://badgen.net/packagist/license/ankurk91/laravel-eloquent-relationships)](LICENSE.txt) 6 | [![Downloads](https://badgen.net/packagist/dt/ankurk91/laravel-eloquent-relationships)](https://packagist.org/packages/ankurk91/laravel-eloquent-relationships/stats) 7 | [![tests](https://github.com/ankurk91/laravel-eloquent-relationships/workflows/tests/badge.svg)](https://github.com/ankurk91/laravel-eloquent-relationships/actions) 8 | [![codecov](https://codecov.io/gh/ankurk91/laravel-eloquent-relationships/branch/main/graph/badge.svg)](https://codecov.io/gh/ankurk91/laravel-eloquent-relationships) 9 | 10 | This package adds some missing relationships to Eloquent in Laravel 11 | 12 | ## Installation 13 | 14 | You can install the package via composer: 15 | 16 | ```bash 17 | composer require ankurk91/laravel-eloquent-relationships 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### BelongsToOne 23 | 24 | BelongsToOne relation is almost identical to 25 | standard [BelongsToMany](https://laravel.com/docs/9.x/eloquent-relationships#many-to-many) except it returns one model 26 | instead of Collection of models and `null` if there is no related model in DB (BelongsToMany returns empty Collection in 27 | this case). Example: 28 | 29 | ```php 30 | belongsToOne(User::class) 49 | ->wherePivot('is_operator', true); 50 | //->withDefault(); 51 | } 52 | 53 | /** 54 | * Get all employees including the operator. 55 | */ 56 | public function employees(): BelongsToMany 57 | { 58 | return $this->belongsToMany(User::class) 59 | ->withPivot('is_operator'); 60 | } 61 | } 62 | ``` 63 | 64 | Now you can access the relationship like: 65 | 66 | ```php 67 | first(); 71 | dump($restaurant->operator); 72 | // lazy loading 73 | $restaurant->load('operator'); 74 | // load nested relation 75 | $restaurant->load('operator.profile'); 76 | // Perform operations 77 | $restaurant->operator()->update([ 78 | 'name'=> 'Taylor' 79 | ]); 80 | ``` 81 | 82 | ### MorphToOne 83 | 84 | MorphToOne relation is almost identical to 85 | standard [MorphToMany](https://laravel.com/docs/9.x/eloquent-relationships#many-to-many-polymorphic-relations) except it 86 | returns one model instead of Collection of models and `null` if there is no related model in DB (MorphToMany returns 87 | empty Collection in this case). Example: 88 | 89 | ```php 90 | morphedByMany(Post::class, 'imageable'); 102 | } 103 | 104 | public function videos(): MorphToMany 105 | { 106 | return $this->morphedByMany(Video::class, 'imageable'); 107 | } 108 | } 109 | ``` 110 | 111 | ```php 112 | morphToOne(Image::class, 'imageable') 131 | ->wherePivot('featured', 1); 132 | //->withDefault(); 133 | } 134 | 135 | /** 136 | * Get all images including the featured. 137 | */ 138 | public function images(): MorphToMany 139 | { 140 | return $this->morphToMany(Image::class, 'imageable') 141 | ->withPivot('featured'); 142 | } 143 | 144 | } 145 | 146 | ``` 147 | 148 | Now you can access the relationship like: 149 | 150 | ```php 151 | first(); 155 | dump($post->featuredImage); 156 | // lazy loading 157 | $post->load('featuredImage'); 158 | ``` 159 | 160 | ## Testing 161 | 162 | ```bash 163 | composer test 164 | ``` 165 | 166 | ## Security 167 | 168 | If you discover any security issues, please email `pro.ankurk1[at]gmail[dot]com` instead of using the issue tracker. 169 | 170 | ## Attribution 171 | 172 | * Most of the code is taken from this [PR](https://github.com/laravel/framework/pull/25083) 173 | * Similar package [fidum/laravel-eloquent-morph-to-one](https://github.com/fidum/laravel-eloquent-morph-to-one) 174 | 175 | ## License 176 | 177 | The [MIT](https://opensource.org/licenses/MIT) License. 178 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ankurk91/laravel-eloquent-relationships", 3 | "description": "Add missing eloquent relationships to Laravel php framework.", 4 | "keywords": [ 5 | "laravel", 6 | "eloquent", 7 | "BelongsToOne", 8 | "MorphToOne" 9 | ], 10 | "homepage": "https://github.com/ankurk91/laravel-eloquent-relationships", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "ankurk91", 15 | "homepage": "https://ankurk91.github.io" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "illuminate/database": "^10 || ^11 || ^12.0", 21 | "illuminate/support": "^10 || ^11 || ^12.0" 22 | }, 23 | "require-dev": { 24 | "laravel/serializable-closure": "^1.3 || ^2.0", 25 | "phpunit/phpunit": "^9.5.7" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Ankurk91\\Eloquent\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Tests\\": "tests/" 35 | } 36 | }, 37 | "config": { 38 | "sort-packages": true, 39 | "preferred-install": "dist" 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit", 43 | "test:coverage": "vendor/bin/phpunit --coverage-clover=coverage.xml" 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /src/HasBelongsToOne.php: -------------------------------------------------------------------------------- 1 | guessBelongsToOneRelation(); 29 | } 30 | 31 | // First, we'll need to determine the foreign key and "other key" for the 32 | // relationship. Once we have determined the keys we'll make the query 33 | // instances as well as the relationship instances we need for this. 34 | $instance = $this->newRelatedInstance($related); 35 | 36 | $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); 37 | 38 | $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey(); 39 | 40 | // If no table name was provided, we can guess it by concatenating the two 41 | // models using underscores in alphabetical order. The two model names 42 | // are transformed to snake case from their default CamelCase also. 43 | if (is_null($table)) { 44 | $table = $this->joiningTable($related); 45 | } 46 | 47 | return $this->newBelongsToOne( 48 | $instance->newQuery(), $this, $table, $foreignPivotKey, 49 | $relatedPivotKey, $parentKey ?: $this->getKeyName(), 50 | $relatedKey ?: $instance->getKeyName(), $relation 51 | ); 52 | } 53 | 54 | /** 55 | * Instantiate a new BelongsToOne relationship. 56 | */ 57 | protected function newBelongsToOne( 58 | Builder $query, 59 | Model $parent, 60 | string $table, 61 | string $foreignPivotKey, 62 | string $relatedPivotKey, 63 | string $parentKey, 64 | string $relatedKey, 65 | ?string $relationName = null 66 | ): BelongsToOne { 67 | return new BelongsToOne( 68 | $query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, 69 | $relatedKey, $relationName 70 | ); 71 | } 72 | 73 | /** 74 | * Get the relationship name of the belongs to many. 75 | */ 76 | protected function guessBelongsToOneRelation(): string 77 | { 78 | list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); 79 | 80 | return $caller['function']; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/HasMorphToOne.php: -------------------------------------------------------------------------------- 1 | guessBelongsToManyRelation(); 27 | 28 | // First, we'll need to determine the foreign key and "other key" for the 29 | // relationship. Once we have determined the keys we'll make the query 30 | // instances as well as the relationship instances we need for this. 31 | $instance = $this->newRelatedInstance($related); 32 | 33 | $foreignPivotKey = $foreignPivotKey ?: $name.'_id'; 34 | 35 | $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey(); 36 | 37 | // If no table name was provided, we can guess it by concatenating the two 38 | // models using underscores in alphabetical order. The two model names 39 | // are transformed to snake case from their default CamelCase also. 40 | $table = $table ?: Str::plural($name); 41 | 42 | return $this->newMorphToOne( 43 | $instance->newQuery(), $this, $name, $table, 44 | $foreignPivotKey, $relatedPivotKey, $parentKey ?: $this->getKeyName(), 45 | $relatedKey ?: $instance->getKeyName(), $caller, $inverse 46 | ); 47 | } 48 | 49 | /** 50 | * Instantiate a new MorphToOne relationship. 51 | */ 52 | protected function newMorphToOne( 53 | Builder $query, 54 | Model $parent, 55 | string $name, 56 | string $table, 57 | string $foreignPivotKey, 58 | string $relatedPivotKey, 59 | string $parentKey, 60 | string $relatedKey, 61 | ?string $relationName = null, 62 | bool $inverse = false 63 | ): MorphToOne { 64 | return new MorphToOne($query, $parent, $name, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, 65 | $relatedKey, 66 | $relationName, $inverse); 67 | } 68 | 69 | /** 70 | * Define a polymorphic, inverse many-to-many relationship but one. 71 | */ 72 | public function morphedByOne( 73 | string $related, 74 | string $name, 75 | ?string $table = null, 76 | ?string $foreignPivotKey = null, 77 | ?string $relatedPivotKey = null, 78 | ?string $parentKey = null, 79 | ?string $relatedKey = null 80 | ): MorphToOne { 81 | $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); 82 | 83 | // For the inverse of the polymorphic many-to-many relations, we will change 84 | // the way we determine the foreign and other keys, as it is the opposite 85 | // of the morph-to-many method since we're figuring out these inverses. 86 | $relatedPivotKey = $relatedPivotKey ?: $name.'_id'; 87 | 88 | return $this->morphToOne( 89 | $related, $name, $table, $foreignPivotKey, 90 | $relatedPivotKey, $parentKey, $relatedKey, true 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Relations/BelongsToOne.php: -------------------------------------------------------------------------------- 1 | first() ?: $this->getDefaultFor($this->getRelated()); 23 | } 24 | 25 | /** 26 | * Initialize the relation on a set of models. 27 | * 28 | * @param array $models 29 | * @param string $relation 30 | * 31 | * @return array 32 | */ 33 | public function initRelation(array $models, $relation) 34 | { 35 | foreach ($models as $model) { 36 | $model->setRelation($relation, $this->getDefaultFor($model)); 37 | } 38 | 39 | return $models; 40 | } 41 | 42 | /** 43 | * Match the eagerly loaded results to their parents. 44 | * 45 | * @param array $models 46 | * @param \Illuminate\Database\Eloquent\Collection $results 47 | * @param string $relation 48 | * 49 | * @return array 50 | */ 51 | public function match(array $models, Collection $results, $relation) 52 | { 53 | $dictionary = $this->buildDictionary($results); 54 | 55 | // Once we have an array dictionary of child objects we can easily match the 56 | // children back to their parent using the dictionary and the keys on the 57 | // the parent models. Then we will return the hydrated models back out. 58 | foreach ($models as $model) { 59 | if (isset($dictionary[$key = $model->{$this->parentKey}])) { 60 | $value = $dictionary[$key]; 61 | $model->setRelation( 62 | $relation, reset($value) 63 | ); 64 | } 65 | } 66 | 67 | return $models; 68 | } 69 | 70 | /** 71 | * Make a new related instance for the given model. 72 | * 73 | * @return \Illuminate\Database\Eloquent\Model 74 | */ 75 | public function newRelatedInstanceFor(Model $parent) 76 | { 77 | return $this->related->newInstance(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Relations/MorphToOne.php: -------------------------------------------------------------------------------- 1 | first() ?: $this->getDefaultFor($this->getRelated()); 23 | } 24 | 25 | /** 26 | * Initialize the relation on a set of models. 27 | * 28 | * @param array $models 29 | * @param string $relation 30 | * 31 | * @return array 32 | */ 33 | public function initRelation(array $models, $relation) 34 | { 35 | foreach ($models as $model) { 36 | $model->setRelation($relation, $this->getDefaultFor($model)); 37 | } 38 | 39 | return $models; 40 | } 41 | 42 | /** 43 | * Match the eagerly loaded results to their parents. 44 | * 45 | * @param array $models 46 | * @param \Illuminate\Database\Eloquent\Collection $results 47 | * @param string $relation 48 | * 49 | * @return array 50 | */ 51 | public function match(array $models, Collection $results, $relation) 52 | { 53 | $dictionary = $this->buildDictionary($results); 54 | 55 | // Once we have an array dictionary of child objects we can easily match the 56 | // children back to their parent using the dictionary and the keys on the 57 | // the parent models. Then we will return the hydrated models back out. 58 | foreach ($models as $model) { 59 | if (isset($dictionary[$key = $model->{$this->parentKey}])) { 60 | $value = $dictionary[$key]; 61 | $model->setRelation( 62 | $relation, reset($value) 63 | ); 64 | } 65 | } 66 | 67 | return $models; 68 | } 69 | 70 | /** 71 | * Make a new related instance for the given model. 72 | * 73 | * @return \Illuminate\Database\Eloquent\Model 74 | */ 75 | public function newRelatedInstanceFor(Model $parent) 76 | { 77 | return $this->related->newInstance(); 78 | } 79 | } 80 | --------------------------------------------------------------------------------