├── .gitignore ├── tests ├── ChildModel.php ├── ParentModel.php ├── ModelResolutionTest.php ├── TestCase.php ├── ConfigTest.php └── ModelCreationTest.php ├── .travis.yml ├── src ├── HasManyWithInverse.php └── HasManyWithInverseRelationship.php ├── composer.json ├── LICENSE ├── phpunit.xml ├── check ├── .github └── workflows │ └── ci.yml ├── .php-cs-fixer.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .phpunit.result.cache 4 | .php-cs-fixer.cache 5 | -------------------------------------------------------------------------------- /tests/ChildModel.php: -------------------------------------------------------------------------------- 1 | belongsTo(ParentModel::class, 'parent_id'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/ParentModel.php: -------------------------------------------------------------------------------- 1 | HasManyWithInverse(ChildModel::class, 'parent', 'parent_id'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '7.2' 4 | - '7.3' 5 | 6 | env: 7 | matrix: 8 | - LARAVEL='^6.0' 9 | - LARAVEL='^7.0' 10 | - LARAVEL='^8.0' 11 | 12 | jobs: 13 | exclude: 14 | - php: '7.2' 15 | env: LARAVEL='^8.0' 16 | 17 | install: 18 | - travis_retry composer require "illuminate/database:${LARAVEL}" --no-update 19 | - travis_retry composer install --prefer-source --no-interaction 20 | 21 | before_script: 22 | - export CODECOV_TOKEN="6de43b13-9ea8-498c-bb32-c29f3f098517" 23 | 24 | script: vendor/bin/phpunit 25 | 26 | after_success: 27 | - bash <(curl -s https://codecov.io/bash) 28 | -------------------------------------------------------------------------------- /tests/ModelResolutionTest.php: -------------------------------------------------------------------------------- 1 | $parent->id]); 14 | 15 | /** @var ChildModel $child */ 16 | $child = $parent->children->first(); 17 | 18 | $this->assertTrue($child->relationLoaded('parent')); 19 | $this->assertSame($parent->id, $child->getRelations()['parent']->id); 20 | } 21 | } -------------------------------------------------------------------------------- /src/HasManyWithInverse.php: -------------------------------------------------------------------------------- 1 | newRelatedInstance($related); 15 | $foreignKey = $foreignKey ?: $this->getForeignKey(); 16 | $localKey = $localKey ?: $this->getKeyName(); 17 | 18 | return new HasManyWithInverseRelationship($instance->newQuery(), $this, $instance->getTable() . '.' . $foreignKey, $localKey, $inverse, $config); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | }); 19 | 20 | Schema::create('children', function (Blueprint $table) { 21 | $table->bigIncrements('id'); 22 | 23 | $table->unsignedBigInteger('parent_id'); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stancl/laravel-hasmanywithinverse", 3 | "description": "Define HasMany while also setting the inverse relationship in Laravel.", 4 | "type": "package", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Stancl\\HasManyWithInverse\\": "src/" 9 | } 10 | }, 11 | "autoload-dev": { 12 | "psr-4": { 13 | "Stancl\\HasManyWithInverse\\Tests\\": "tests/" 14 | } 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Samuel Štancl", 19 | "email": "samuel.stancl@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "illuminate/database": "^9.0|^10.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^9.0|^10.0", 27 | "orchestra/testbench": "^7.0|^8.0" 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Samuel Štancl 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 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | ./src/routes.php 9 | 10 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | offer_run() { 5 | read -p "For more output, run $1. Run it now (Y/n)? " run 6 | 7 | case ${run:0:1} in 8 | n|N ) 9 | exit 1 10 | ;; 11 | * ) 12 | $1 13 | ;; 14 | esac 15 | 16 | exit 1 17 | } 18 | 19 | if (php-cs-fixer fix --dry-run --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then 20 | echo '✅ php-cs-fixer OK' 21 | else 22 | read -p "⚠️ php-cs-fixer found issues. Fix (Y/n)? " fix 23 | case ${fix:0:1} in 24 | n|N ) 25 | echo '❌ php-cs-fixer FAIL' 26 | offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' 27 | ;; 28 | * ) 29 | if (php-cs-fixer fix --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then 30 | echo '✅ php-cs-fixer OK' 31 | else 32 | echo '❌ php-cs-fixer FAIL' 33 | offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' 34 | fi 35 | ;; 36 | esac 37 | fi 38 | 39 | if (./vendor/bin/phpunit > /dev/null 2>/dev/null); then 40 | echo '✅ PHPUnit OK' 41 | else 42 | echo '❌ PHPUnit FAIL' 43 | offer_run './vendor/bin/phpunit' 44 | fi 45 | 46 | echo '==================' 47 | echo '✅ Everything OK' 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | phpunit: 7 | name: Tests (PHPUnit) L${{ matrix.laravel }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | laravel: [9, 10] 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: '8.1' 21 | 22 | - name: Install dependencies 23 | run: composer require "laravel/framework:^${{matrix.laravel}}.0" 24 | - name: Run tests 25 | run: vendor/bin/phpunit 26 | 27 | php-cs-fixer: 28 | name: Code style (php-cs-fixer) 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Install php-cs-fixer 33 | run: composer global require friendsofphp/php-cs-fixer 34 | - name: Run php-cs-fixer 35 | run: $HOME/.composer/vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php 36 | - name: Commit changes from php-cs-fixer 37 | uses: EndBug/add-and-commit@v5 38 | with: 39 | author_name: Samuel Štancl 40 | author_email: samuel.stancl@gmail.com 41 | message: Fix code style (php-cs-fixer) 42 | -------------------------------------------------------------------------------- /src/HasManyWithInverseRelationship.php: -------------------------------------------------------------------------------- 1 | relationToParent = $relationToParent; 22 | $this->config = $config; 23 | 24 | parent::__construct($query, $parent, $foreignKey, $localKey); 25 | } 26 | 27 | /** 28 | * @param array $attributes 29 | * 30 | * @return \Illuminate\Database\Eloquent\Model 31 | */ 32 | public function create(array $attributes = []) 33 | { 34 | return tap($this->related->newInstance($attributes), function ($instance) { 35 | $this->setForeignAttributesForCreate($instance); 36 | 37 | if ($this->config('setRelationOnCreation', true)) { 38 | $instance->setRelation($this->relationToParent, $this->getParent()); 39 | } 40 | 41 | $instance->save(); 42 | }); 43 | } 44 | 45 | public function getResults() 46 | { 47 | $results = parent::getResults(); 48 | 49 | if ($this->config('setRelationOnResolution', true)) { 50 | $results->each->setRelation($this->relationToParent, $this->getParent()); 51 | } 52 | 53 | return $results; 54 | } 55 | 56 | protected function config(string $key, $default) 57 | { 58 | if (! isset($this->config[$key])) { 59 | return $default; 60 | } 61 | 62 | if (is_callable($this->config[$key])) { 63 | return $this->config[$key](); 64 | } 65 | 66 | return $this->config[$key]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | hasManyWithInverse(ChildModel::class, 'parent', 'parent_id', null, [ 15 | 'setRelationOnCreation' => false, 16 | ]); 17 | } 18 | })::create([]); 19 | 20 | /** @var ChildModel $child */ 21 | $child = $parent->children()->create([]); 22 | 23 | $this->assertFalse($child->relationLoaded('parent')); 24 | } 25 | 26 | /** @test */ 27 | public function setting_relation_on_resolution_can_be_disabled() 28 | { 29 | /** @var ParentModel $parent */ 30 | $parent = (new class extends ParentModel { 31 | public function children() 32 | { 33 | return $this->hasManyWithInverse(ChildModel::class, 'parent', 'parent_id', null, [ 34 | 'setRelationOnResolution' => false, 35 | ]); 36 | } 37 | })::create([]); 38 | 39 | /** @var ChildModel $child */ 40 | $child = $parent->children()->create([]); 41 | 42 | $this->assertFalse($parent->children->first()->relationLoaded('parent')); 43 | } 44 | 45 | /** @test */ 46 | public function config_value_can_be_a_closure() 47 | { 48 | /** @var ParentModel $parent */ 49 | $parent = (new class extends ParentModel { 50 | public function children() 51 | { 52 | return $this->hasManyWithInverse(ChildModel::class, 'parent', 'parent_id', null, [ 53 | 'setRelationOnResolution' => function () { 54 | return false; 55 | }, 56 | ]); 57 | } 58 | })::create([]); 59 | 60 | /** @var ChildModel $child */ 61 | $child = $parent->children()->create([]); 62 | 63 | $this->assertFalse($parent->children->first()->relationLoaded('parent')); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/ModelCreationTest.php: -------------------------------------------------------------------------------- 1 | children()->create([]); 15 | 16 | $this->assertTrue($child->relationLoaded('parent')); 17 | $this->assertSame($parent->id, $child->getRelations()['parent']->id); 18 | } 19 | 20 | /** @test */ 21 | public function children_have_the_parent_relationship_automatically_set_when_being_created_using_createMany() 22 | { 23 | /** @var ParentModel $parent */ 24 | $parent = ParentModel::create([]); 25 | 26 | /** @var ChildModel $child1 */ 27 | /** @var ChildModel $child2 */ 28 | [$child1, $child2] = $parent->children()->createMany([[], []]); 29 | 30 | $this->assertTrue($child1->relationLoaded('parent')); 31 | $this->assertSame($parent->id, $child1->getRelations()['parent']->id); 32 | 33 | $this->assertTrue($child2->relationLoaded('parent')); 34 | $this->assertSame($parent->id, $child2->getRelations()['parent']->id); 35 | } 36 | 37 | /** @test */ 38 | public function children_have_the_parent_relationship_automatically_set_in_creating_event() 39 | { 40 | $parentId = null; 41 | 42 | /** @var ParentModel $parent */ 43 | $parent = ParentModel::create([]); 44 | 45 | ChildModel::creating(function (ChildModel $child) use (&$parentId) { 46 | if ($child->relationLoaded('parent')) { 47 | $parentId = $child->getRelations()['parent']->id; 48 | } 49 | }); 50 | 51 | $parent->children()->create([]); 52 | 53 | $this->assertSame($parent->id, $parentId); 54 | } 55 | 56 | /** @test */ 57 | public function children_have_the_parent_relationship_automatically_set_in_saving_event() 58 | { 59 | $parentId = null; 60 | 61 | /** @var ParentModel $parent */ 62 | $parent = ParentModel::create([]); 63 | 64 | ChildModel::saving(function (ChildModel $child) use (&$parentId) { 65 | if ($child->relationLoaded('parent')) { 66 | $parentId = $child->getRelations()['parent']->id; 67 | } 68 | }); 69 | 70 | $parent->children()->create([]); 71 | 72 | $this->assertSame($parent->id, $parentId); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | ['syntax' => 'short'], 8 | 'binary_operator_spaces' => [ 9 | 'default' => 'single_space', 10 | 'operators' => [ 11 | '=>' => null, 12 | '|' => 'no_space', 13 | ], 14 | ], 15 | 'blank_line_after_namespace' => true, 16 | 'blank_line_after_opening_tag' => true, 17 | 'no_superfluous_phpdoc_tags' => true, 18 | 'blank_line_before_statement' => [ 19 | 'statements' => ['return'], 20 | ], 21 | 'braces' => true, 22 | 'cast_spaces' => true, 23 | 'class_definition' => true, 24 | 'concat_space' => [ 25 | 'spacing' => 'one', 26 | ], 27 | 'declare_equal_normalize' => true, 28 | 'elseif' => true, 29 | 'encoding' => true, 30 | 'full_opening_tag' => true, 31 | 'declare_strict_types' => true, 32 | 'fully_qualified_strict_types' => true, // added by Shift 33 | 'function_declaration' => true, 34 | 'function_typehint_space' => true, 35 | 'heredoc_to_nowdoc' => true, 36 | 'include' => true, 37 | 'increment_style' => ['style' => 'post'], 38 | 'indentation_type' => true, 39 | 'linebreak_after_opening_tag' => true, 40 | 'line_ending' => true, 41 | 'lowercase_cast' => true, 42 | 'constant_case' => true, 43 | 'lowercase_keywords' => true, 44 | 'lowercase_static_reference' => true, // added from Symfony 45 | 'magic_method_casing' => true, // added from Symfony 46 | 'magic_constant_casing' => true, 47 | 'method_argument_space' => true, 48 | 'native_function_casing' => true, 49 | 'no_alias_functions' => true, 50 | 'no_extra_blank_lines' => [ 51 | 'tokens' => [ 52 | 'extra', 53 | 'throw', 54 | 'use', 55 | 'use_trait', 56 | ], 57 | ], 58 | 'no_blank_lines_after_class_opening' => true, 59 | 'no_blank_lines_after_phpdoc' => true, 60 | 'no_closing_tag' => true, 61 | 'no_empty_phpdoc' => true, 62 | 'no_empty_statement' => true, 63 | 'no_leading_import_slash' => true, 64 | 'no_leading_namespace_whitespace' => true, 65 | 'no_mixed_echo_print' => [ 66 | 'use' => 'echo', 67 | ], 68 | 'no_multiline_whitespace_around_double_arrow' => true, 69 | 'multiline_whitespace_before_semicolons' => [ 70 | 'strategy' => 'no_multi_line', 71 | ], 72 | 'no_short_bool_cast' => true, 73 | 'no_singleline_whitespace_before_semicolons' => true, 74 | 'no_spaces_after_function_name' => true, 75 | 'no_spaces_around_offset' => true, 76 | 'no_spaces_inside_parenthesis' => true, 77 | 'no_trailing_comma_in_list_call' => true, 78 | 'no_trailing_comma_in_singleline_array' => true, 79 | 'no_trailing_whitespace' => true, 80 | 'no_trailing_whitespace_in_comment' => true, 81 | 'no_unneeded_control_parentheses' => true, 82 | 'no_unreachable_default_argument_value' => true, 83 | 'no_useless_return' => true, 84 | 'no_whitespace_before_comma_in_array' => true, 85 | 'no_whitespace_in_blank_line' => true, 86 | 'normalize_index_brace' => true, 87 | 'not_operator_with_successor_space' => true, 88 | 'object_operator_without_whitespace' => true, 89 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 90 | 'phpdoc_indent' => true, 91 | 'general_phpdoc_tag_rename' => true, 92 | 'phpdoc_no_access' => true, 93 | 'phpdoc_no_package' => true, 94 | 'phpdoc_no_useless_inheritdoc' => true, 95 | 'phpdoc_scalar' => true, 96 | 'phpdoc_single_line_var_spacing' => true, 97 | 'phpdoc_summary' => true, 98 | 'phpdoc_to_comment' => false, 99 | 'phpdoc_trim' => true, 100 | 'phpdoc_types' => true, 101 | 'phpdoc_var_without_name' => true, 102 | 'psr_autoloading' => true, 103 | 'self_accessor' => true, 104 | 'short_scalar_cast' => true, 105 | 'simplified_null_return' => false, // disabled by Shift 106 | 'single_blank_line_at_eof' => true, 107 | 'single_blank_line_before_namespace' => true, 108 | 'single_class_element_per_statement' => true, 109 | 'single_import_per_statement' => false, 110 | 'single_line_after_imports' => true, 111 | 'no_unused_imports' => true, 112 | 'single_line_comment_style' => [ 113 | 'comment_types' => ['hash'], 114 | ], 115 | 'single_quote' => true, 116 | 'space_after_semicolon' => true, 117 | 'standardize_not_equals' => true, 118 | 'switch_case_semicolon_to_colon' => true, 119 | 'switch_case_space' => true, 120 | 'ternary_operator_spaces' => true, 121 | 'trailing_comma_in_multiline' => true, 122 | 'trim_array_spaces' => true, 123 | 'unary_operator_spaces' => true, 124 | 'whitespace_after_comma_in_array' => true, 125 | ]; 126 | 127 | $project_path = getcwd(); 128 | $finder = Finder::create() 129 | ->in([ 130 | $project_path . '/src', 131 | ]) 132 | ->name('*.php') 133 | ->notName('*.blade.php') 134 | ->ignoreDotFiles(true) 135 | ->ignoreVCS(true); 136 | 137 | return (new Config()) 138 | ->setFinder($finder) 139 | ->setRules($rules) 140 | ->setRiskyAllowed(true) 141 | ->setUsingCache(true); 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stancl/laravel-hasmanywithinverse 2 | 3 | ## Why? 4 | 5 | [Jonathan Reinink](https://github.com/reinink) wrote a great blog post about [Optimizing circular relationships in Laravel](https://reinink.ca/articles/optimizing-circular-relationships-in-laravel) 6 | 7 | By manually setting the (`belongsTo`) relationship to a parent model on related (`hasMany`) child models, you can save unnecessary queries for the parent model -- when the child needs an instance of the parent model. 8 | 9 | This probably sounds confusing, so just read the blog post. It's very good. 10 | 11 | Jonathan's approach suggests using something like this: 12 | 13 | ```php 14 | $category->products->each->setRelation('category', $category); 15 | ``` 16 | 17 | This works, but it's not very clean and there are cases when it doesn't work. For example, on model creation. 18 | 19 | If you're accessing the parent model in `creating` and `saving` events on the children, the `->each->setRelation()` approach won't help you at all. (And if you're building a complex app with [Laravel Nova](https://nova.laravel.com), there's a high chance you're using lots of such events.) 20 | 21 | ## Practical Example & Benchmarks 22 | 23 | I have an e-commerce application where an `Order` has child models: `OrderProduct`, `OrderStatus` and `OrderFee` (think shipping costs, payment fees, etc). 24 | 25 | When some of those models are **being created** (`creating` Eloquent event), they are accessing the parent model. 26 | 27 | For example, `OrderProduct`s convert their prices to `$this->order->currency`. `OrderFee`s check for other order fees, and they prevent creating themselves if a fee with the same code already exists (so that you can't have, say, the shipping cost counted twice). Etc. 28 | 29 | This results in order creation being expensive, resulting in a large amount of n+1 queries. 30 | 31 | ### Benchmark 32 | 33 | I haven't run a huge amount of tests, so I won't present the time differences here. I will only talk about database query count. 34 | 35 | I have created an order with 6 products. 36 | 37 | #### This is the amount of queries made with regular `hasMany()` 38 | 39 | ![Query count with hasMany()](https://i.imgur.com/Yss7aVl.png) 40 | 41 | And now I just replace all of these calls: 42 | 43 | ```php 44 | return $this->hasMany(...); 45 | ``` 46 | with these calls 47 | ```php 48 | return $this->hasManyWithInverse(..., 'order'); 49 | ``` 50 | 51 | inside the `Order` model. 52 | 53 | #### And this is the amount of queries made with `hasManyWithInverse()` 54 | 55 | ![Query count with hasManyWithInverse()](https://i.imgur.com/XimW6T7.png) 56 | 57 | See the query count reduction. 58 | 59 | The duration was also decreased from 114ms to 45ms on my machine, though note that I did not run this test a million times to calculate an average duration, so that benchmark might not be very accurate. 60 | 61 | This is pretty impressive for **a free improvement that only requires changing a few simple calls to a similar method**. 62 | 63 | But note that this is not a silver bullet for solving all n+1 queries. As you can see, even with this implemented, my app still has many duplicated queries. (Although not all are unintentional n+1s as there are a few `$this->refresh()` calls to keep the order up-to-date after state transitions). 64 | 65 | ## Installation 66 | 67 | Laravel 9.x and 10.x are supported. 68 | 69 | ``` 70 | composer require stancl/laravel-hasmanywithinverse 71 | ``` 72 | 73 | ## Usage 74 | 75 | ```php 76 | namespace App; 77 | 78 | use Stancl\HasManyWithInverse\HasManyWithInverse; 79 | 80 | class Order extends Model 81 | { 82 | use HasManyWithInverse; 83 | 84 | public function products() 85 | { 86 | // 'order' is the name of the relationship in the other model, see below 87 | return $this->hasManyWithInverse(OrderProduct::class, 'order'); 88 | } 89 | } 90 | 91 | class OrderProduct extends Model 92 | { 93 | public function order() 94 | { 95 | return $this->belongsTo(Order::class); 96 | } 97 | } 98 | ``` 99 | 100 | You may also want to use the trait in a base Eloquent model and then use `$this->hasManyWithInverse()` without thinking about traits in the specific models. 101 | 102 | ## Details 103 | 104 | The (simple) internals of the package are just methods copied from Eloquent source code, with a few lines added to them. The `hasManyWithInverse()` method signature is the same as `hasMany()` (you can set `$foreignKey` and `$localKey`), except the second argument (`$inverse`) was added to let you define the name of the relationship on the child model, and the last argument (`$config`) was added to let you configure the relation setting's behavior. 105 | 106 | **This package sets the parent relation on children both when creating children (`$child = $parent->children()->create()`) and when resolving parent's children (`$children = $parent->children`).** You can customize this behavior for every relationship. 107 | 108 | To disable setting the relationship during child **creation**, do this: 109 | ```php 110 | class Parent extends Model 111 | { 112 | public function children() 113 | { 114 | return $this->hasManyWithInverse(Child::class, 'parent', null, null, ['setRelationOnCreation' => false]); 115 | } 116 | } 117 | ``` 118 | 119 | To disable setting the relationship during child **resolution**, do this: 120 | ```php 121 | class Parent extends Model 122 | { 123 | public function children() 124 | { 125 | return $this->hasManyWithInverse(Child::class, 'parent', null, null, ['setRelationOnResolution' => false]); 126 | } 127 | } 128 | ``` 129 | 130 | You may also pass a callable as the config value. This is useful if you want to disable this behavior on some requests. See example below. 131 | 132 | ## Laravel Nova 133 | 134 | It's a good idea to disable setting the relationship on resolution for Nova requests. They tend to make a lot of queries and this can slow the page down (or result in 502 errors). 135 | 136 | Here's an example implementation using a base model and adding config to filter out Nova requests. 137 | 138 | ```php 139 | abstract class Model extends EloquentModel 140 | { 141 | use HasManyWithInverse { 142 | hasManyWithInverse as originalHasManyWithInverse; 143 | } 144 | 145 | public function hasManyWithInverse($related, $inverse, $foreignKey = null, $localKey = null, $config = []) 146 | { 147 | $config = array_merge(['setRelationOnResolution' => function () { 148 | if (request()->route() && in_array('nova', request()->route()->middleware())) { 149 | return false; 150 | } 151 | }], $config); 152 | 153 | return $this->originalHasManyWithInverse($related, $inverse, $foreignKey, $localKey, $config); 154 | } 155 | } 156 | ``` 157 | --------------------------------------------------------------------------------