Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/archtechx/laravel-hasmanywithinverse
Define HasMany while also setting the inverse relationship in Laravel.
https://github.com/archtechx/laravel-hasmanywithinverse
eloquent laravel
Last synced: 27 days ago
JSON representation
Define HasMany while also setting the inverse relationship in Laravel.
- Host: GitHub
- URL: https://github.com/archtechx/laravel-hasmanywithinverse
- Owner: archtechx
- License: mit
- Created: 2020-04-01T23:16:31.000Z (almost 5 years ago)
- Default Branch: master
- Last Pushed: 2023-02-16T10:35:39.000Z (almost 2 years ago)
- Last Synced: 2024-12-06T07:24:16.686Z (about 1 month ago)
- Topics: eloquent, laravel
- Language: PHP
- Size: 29.3 KB
- Stars: 70
- Watchers: 2
- Forks: 5
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# stancl/laravel-hasmanywithinverse
## Why?
[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)
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.
This probably sounds confusing, so just read the blog post. It's very good.
Jonathan's approach suggests using something like this:
```php
$category->products->each->setRelation('category', $category);
```This works, but it's not very clean and there are cases when it doesn't work. For example, on model creation.
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.)
## Practical Example & Benchmarks
I have an e-commerce application where an `Order` has child models: `OrderProduct`, `OrderStatus` and `OrderFee` (think shipping costs, payment fees, etc).
When some of those models are **being created** (`creating` Eloquent event), they are accessing the parent model.
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.
This results in order creation being expensive, resulting in a large amount of n+1 queries.
### Benchmark
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.
I have created an order with 6 products.
#### This is the amount of queries made with regular `hasMany()`
![Query count with hasMany()](https://i.imgur.com/Yss7aVl.png)
And now I just replace all of these calls:
```php
return $this->hasMany(...);
```
with these calls
```php
return $this->hasManyWithInverse(..., 'order');
```inside the `Order` model.
#### And this is the amount of queries made with `hasManyWithInverse()`
![Query count with hasManyWithInverse()](https://i.imgur.com/XimW6T7.png)
See the query count reduction.
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.
This is pretty impressive for **a free improvement that only requires changing a few simple calls to a similar method**.
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).
## Installation
Laravel 9.x and 10.x are supported.
```
composer require stancl/laravel-hasmanywithinverse
```## Usage
```php
namespace App;use Stancl\HasManyWithInverse\HasManyWithInverse;
class Order extends Model
{
use HasManyWithInverse;public function products()
{
// 'order' is the name of the relationship in the other model, see below
return $this->hasManyWithInverse(OrderProduct::class, 'order');
}
}class OrderProduct extends Model
{
public function order()
{
return $this->belongsTo(Order::class);
}
}
```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.
## Details
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.
**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.
To disable setting the relationship during child **creation**, do this:
```php
class Parent extends Model
{
public function children()
{
return $this->hasManyWithInverse(Child::class, 'parent', null, null, ['setRelationOnCreation' => false]);
}
}
```To disable setting the relationship during child **resolution**, do this:
```php
class Parent extends Model
{
public function children()
{
return $this->hasManyWithInverse(Child::class, 'parent', null, null, ['setRelationOnResolution' => false]);
}
}
```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.
## Laravel Nova
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).
Here's an example implementation using a base model and adding config to filter out Nova requests.
```php
abstract class Model extends EloquentModel
{
use HasManyWithInverse {
hasManyWithInverse as originalHasManyWithInverse;
}public function hasManyWithInverse($related, $inverse, $foreignKey = null, $localKey = null, $config = [])
{
$config = array_merge(['setRelationOnResolution' => function () {
if (request()->route() && in_array('nova', request()->route()->middleware())) {
return false;
}
}], $config);return $this->originalHasManyWithInverse($related, $inverse, $foreignKey, $localKey, $config);
}
}
```