Thought I'd ask this as Laravel is the most elegant Framework I've come across and wondered if there was a "prettier way" of doing this.
I have a system which records books such that:
class Chapter extends Model
{
public function book()
{
return $this->belongsTo('\App\Book');
}
}
In the system there are number of other models which extend from "Book" such as "Novel", "Biography" etc. Is there a way for Eloquent to provide me with a correctly cast object given the right info (i.e. a namespaced class)? Currently, I am obtaining the book and the casting it using the function at https://gist.github.com/borzilleri/960035 which works but doesn't feel very "tidy".
I can see a few different options here. One would be to write your class like this:
class Chapter extends Model
{
public function book()
{
return $this->belongsTo('\App\Book');
}
public function biography()
{
return $this->belongsTo('\App\Biography')->where('type', 'biography');
}
public function novel()
{
return $this->belongsTo('\App\Novel')->where('type', 'novel');
}
}
You'd then need to know ahead of time which type of book it is though. Another would be to do something like this:
class Chapter extends Model
{
protected function parent_book()
{
return $this->belongsTo('\App\Book');
}
public function getBookAttribute()
{
$book = $this->parent_book;
if (!$book) return $book; // No related book.
if ($book->type == 'novel') return (Novel)$book;
if ($book->type == 'biography') return (Biography)$book;
return $book;
}
}
You still have to do all of the casting yourself, but at least it's all in one place and transparent to the rest of the app, as it can still just reference $chapter->book For this second solution, if you ever set $chapter->book = new Book(), you'd also need to make sure to make a setBookAttribute() function.
One more complicated possibility would be to create your own custom relationship type by extending the BelongsTo class and overriding getResults() to to the casting before returning the result. This would be pretty transparent from the outside and would let you still call $chapter->book() and treat it as a relationship.
This should be attributed to Joshua Dwire as he set me on the path to this solution. I was intrigued by his reference to extending the standard BelongsTo class and make it work for me. Ideally I want to be able to call a custom relationship:
$this->belongsToBook('\App\Book');
And for that function to return a correctly cast object.
Routing through the code I found that it was the trait HasRelationship used by Model which was responsible for returning the relationship. By changing that relationship we can change the implementation and therefore the returned object.
I also wanted to replicate the same methodology that Laravel employs so have mimiced it in my own app.
With all that in mind the first step is to create a new trait HasBookRelationship which can be used in a model to handle the call to $this->belongsToBook('\App\Book'):
trait HasBookRelationship
{
public function belongsToBook($related, $foreignKey = null, $ownerKey = null, $relation = null)
{
if (is_null($relation)) {
$relation = $this->guessBelongsToRelation();
}
$instance = $this->newRelatedInstance($related);
if (is_null($foreignKey)) {
$foreignKey = \Str::snake($relation).'_'.$instance->getKeyName();
}
$ownerKey = $ownerKey ?: $instance->getKeyName();
//We change the return relationship here
**return new BelongsToBook(
$instance->newQuery(), $this, $foreignKey, $ownerKey, $relation
);**
}
}
This is simply copied from the existing belongsTo method in the HasRelationships trait. The key thing here is that we are going to return a custom relationship BelongsToBook and use that to override what is returned. The last line of the method is changed to return our desired relationship class.
The class we use is extended from BelongsTo but we change the get method to cast the object before returning it.
class BelongsToBook extends BelongsTo
{
public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)
{
parent::__construct($query, $child, $foreignKey, $ownerKey, $relationName);
}
public function get($columns = ['*'])
{
$objs = $this->query->get($columns);
//iterate over the collated objects...
$objs->transform(function($item)
{
//..and return a cast object with whatever method you want
return castTheCorrectObject($item);
});
return $objs;
}
}
castTheCorrectObject can be any casting function you like perhaps set up as a helper or another method in the relationship.
Once these are set up, we can empoy it in our own Model:
class Author extends Model
{
use HasBookRelationship;
public function books()
{
return $this->belongsToBook('\App\Book');
}
}
This will return a collection of correctly cast objects and maintains the relationship.
One thing did puzzle me though. The method I overrode in my BelongsToBook class was get() and not getResults() as suggested by Joshua. get() is defined in Relation and is inherited by BelongsTo where as getResults() is defined in BelongsTo. I'm not sure what the difference between getResults() and get() is nor why I had to override get() rather than getResults(). If anyone can shed any light , it would be appreciated.
Related
I have a model called RealEstate, this model has a relation with another model called TokenPrice, I needed to access the oldest records of token_prices table using by a simple hasOne relation, So I did it and now my relation method is like following:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function firstTokenPrice(): HasOne
{
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
By far it's fine and no complexity. But now, I need to involve another relation into firstTokenPrice.
Let me explain a bit more:
As my project grown, the more complexity was added it, like changing firstTokenPrice using by a third table called opening_prices, so I added a new relation to RealEstate called lastOpeningPrice:
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
So the deal with simplicity of firstTokenPrice relation is now off the table, I want to do something like following every time a RealEstate object calls for its firstTokenPrice:
Check for lastOpeningPrice, if it was exists, then firstTokenPrice must returns a different record of token_price table, otherwise the firstTokenPrice must returns oldestOfMany of TokenPrice model.
I did something like following but it's not working:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
class RealEstate extends Model
{
public function lastOpeningPrice(): HasOne
{
return $this->hasOne(OpeningPrice::class)->latestOfMany();
}
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
}
I have checked the $this->hasOne(TokenPrice::class)->where('amount', $lop->amount)->oldestOfMany() using by ->toSql() method and it returns something unusual.
I need to return a HasOne object inside of firstTokenPrice method.
You can use ofMany builder for that purpose:
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount); // this is just a helper function that inserts a new token price into `token_prices` table, if there was none exists already with selected amount
return $this->hasOne(TokenPrice::class)->ofMany([
'id' => 'min',
], function ($query) use ($lop) {
$query->where('amount', $lop->amount);
});
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
I used ->oldest() with a custom scope called amounted in TokenPrice model:
class TokenPrice extends Model
{
public function scopeAmounted(Builder $query, OpeningPrice $openingPrice): Builder
{
return $query->where('amount', $openingPrice->amount);
}
/....
}
And then changed my firstTokenPrice
public function firstTokenPrice(): HasOne
{
$lop = $this->lastOpeningPrice;
if ($lop) {
TokenPriceHelper::getOrCreateFirstToken($this, $lop->amount);
return $this->hasOne(TokenPrice::class)->amounted($lop)->oldest();
}
return $this->hasOne(TokenPrice::class)->oldestOfMany();
}
It's working, but I don't know if it's the best answer or not
I've got a observer that has a update method:
ObserverServiceProvider.php
public function boot()
{
Relation::observe(RelationObserver::class);
}
RelationObserver.php
public function updated(Relation $relation)
{
$this->cache->tags(Relation::class)->flush();
}
So when I update a relation in my controller:
public function update(Request $request, Relation $relation)
{
$relation->update($request->all()));
return back();
}
Everything is working as expected. But now I've got a pivot table. A relation belongsToMany products.
So now my controller method looks like this:
public function update(Request $request, Relation $relation)
{
if(empty($request->products)) {
$relation->products()->detach();
} else {
$relation->products()->sync(collect($request->products)->pluck('id'));
}
$relation->update($request->all());
return back();
}
The problem is that the observer is not triggered anymore if I only add or remove products.
How can I trigger the observer when the pivot table updates aswel?
Thanks
As you already know, Laravel doesn't actually retrieve the models nor call save/update on any of the models when calling sync() thus no event's are created by default. But I came up with some alternative solutions for your problem.
1 - To add some extra functionality to the sync() method:
If you dive deeper into the belongsToMany functionality you will see that it tries to guess some of the variable names and returns a BelongsToMany object. Easiest way would be to make your relationship function to simply return a custom BelongsToMany object yourself:
public function products() {
// Product::class is by default the 1. argument in ->belongsToMany calll
$instance = $this->newRelatedInstance(Product::class);
return new BelongsToManySpecial(
$instance->newQuery(),
$this,
$this->joiningTable(Product::class), // By default the 2. argument
$this->getForeignKey(), // By default the 3. argument
$instance->getForeignKey(), // By default the 4. argument
null // By default the 5. argument
);
}
Or alternatively copy the whole function, rename it and make it return the BelongsToManySpecial class. Or omit all the variables and perhaps simply return new BelongsToManyProducts class and resolve all the BelongsToMany varialbes in the __construct... I think you got the idea.
Make the BelongsToManySpecial class extend the original BelongsToMany class and write a sync function to the BelongsToManySpecial class.
public function sync($ids, $detaching = true) {
// Call the parent class for default functionality
$changes = parent::sync($ids, $detaching);
// $changes = [ 'attached' => [...], 'detached' => [...], 'updated' => [...] ]
// Add your functionality
// Here you have access to everything the BelongsToMany function has access and also know what changes the sync function made.
// Return the original response
return $changes
}
Alternatively override the detach and attachNew functions for similar results.
protected function attachNew(array $records, array $current, $touch = true) {
$result = parent::attachNew($records, $current, $touch);
// Your functionality
return $result;
}
public function detach($ids = null, $touch = true)
$result = parent::detach($ids, $touch);
// Your functionality
return $result;
}
If you want to dig deeper and want to understand what's going on under the hood then analyze the Illuminate\Database\Eloquent\Concerns\HasRelationship trait - specifically the belongsToMany relationship function and the BelongsToMany class itself.
2 - Create a trait called BelongsToManySyncEvents which doesn't do much more than returns your special BelongsToMany class
trait BelongsToManySyncEvents {
public function belongsToMany($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) {
if (is_null($relation)) {
$relation = $this->guessBelongsToManyRelation();
}
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$relatedKey = $relatedKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
return new BelongsToManyWithSyncEvents(
$instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
);
}
}
Create the BelongsToManyWithSyncEvents class:
class BelongsToManyWithSyncEvents extends BelongsToMany {
public function sync($ids, $detaching = true) {
$changes = parent::sync($ids, $detaching);
// Do your own magic. For example using these variables if needed:
// $this->get() - returns an array of objects given with the sync method
// $this->parent - Object they got attached to
// Maybe call some function on the parent if it exists?
return $changes;
}
}
Now add the trait to your class.
3 - Combine the previous solutions and add this functionality to every Model that you have in a BaseModel class etc. For examples make them check and call some method in case it is defined...
$functionName = 'on' . $this->foreignKey . 'Sync';
if(method_exists($this->parent), $functionName) {
$this->parent->$functionName($changes);
}
4 - Create a service
Inside that service create a function that you must always call instead of the default sync(). Perhaps call it something attachAndDetachProducts(...) and add your events or functionality
As I didn't have that much information about your classes and relationships you can probably choose better class names than I provided. But if your use case for now is simply to clear cache then I think you can make use of some of the provided solutions.
When I search about this topic, it came as the first result.
However, for newer Laravel version you can just make a "Pivot" model class for that.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PostTag extends Pivot
{
protected $table = 'post_tag';
public $timestamps = null;
}
For the related model
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->using(PostTag::class);
}
and you have to put your declare your observer in EventServiceProvider
as stated in Laravel Docs
PostTag::observe(PostTagObserver::class);
Reference: Observe pivot tables in Laravel
Just add:
public $afterCommit = true;
at the beginning of the observer class.. It will wait until the transactions are done, then performs your sync which should then work fine..
Please check Laravel's documentation for that.
It seems this solutions was just added in Laravel 8.
I'm not sure I am asking the questions correctly, but this is what I am trying to do.
So we can get the current from
$model = Model::find($id)
Then we can get it's relationships like:
$model->relationships()->id
Then we have actions like:
$model->relationships()->detach(4);
My question is, can we have a custom method like:
$model->relationships()->customMethod($params);?
and in the model it may look like:
public function customMethod($params){
//Do something with relationship id
}
But further more, how in the customMethod would I get the $models info like id?
Sorry if this may be a bit confusing.
First of all, if you want to access a related object, you do this by accessing an attribute with the same name as the relation. In your case, in order to access object(s) from relationships, you need to do this by:
$model->relationships //returns related object or collection of objects
instead of
$model->relationships() //returns relation definition
Secondly, if you want to access attributes on the related object, you can do it the same way:
$relatedObjectName = $model->relationship->name; // this works if you have a single object on the other end of relations
Lastly, if you want to call a method on a related model you need to implement this method in related model class.
class A extends Eloquent {
public function b() {
return $this->belongsTo('Some\Namespace\B');
}
public function cs() {
return $this->hasMany('Some\Namespace\C');
}
}
class B extends Eloquent {
public function printId() {
echo $this->id;
}
}
class C extends Eloquent {
public function printId() {
echo $this->id;
}
}
$a = A::find(5);
$a->b->printId(); //call method on related object
foreach ($a->cs as $c) { //iterate the collection
$c->printId(); //call method on related object
}
You can read more about how to define and use relationships here: http://laravel.com/docs/5.1/eloquent-relationships
I have a pivot table that connects users to workspaces. On the pivot table, I also have a column for role, which defines the users role for that workspace. Can I provide Accessor (Getter) & Mutator (Setter) methods on the role inside the pivot table? I have been trying to look all over, but details on pivot tables in eloquent are pretty sparse.
I am not sure if I have to setup a custom pivot model? If I do, an example would be awesome as the documentation on pivot models is very basic.
Thanks.
If all you need to do is access additional fields on the pivot table, you just need to use the withPivot() method on the relationship definition:
class User extends Model {
public function workspaces() {
return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
}
}
class Workspace extends Model {
public function users() {
return $this->belongsToMany('App\Models\User')->withPivot('role');
}
}
Now your role field will be available on the pivot table:
$user = User::first();
// get data
foreach($user->workspaces as $workspace) {
var_dump($workspace->pivot->role);
}
// set data
$workspaceId = $user->workspaces->first()->id;
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);
If you really need to create accessors/mutators for your pivot table, you will need to create a custom pivot table class. I have not done this before, so I don't know if this will actually work, but it looks like you would do this:
Create a new pivot class that contains your accessors/mutators. This class should extend the default Pivot class. This new class is the class that is going to get instantiated when User or Workspace creates a Pivot model instance.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspacePivot extends Pivot {
getRoleAttribute() {
...
}
setRoleAttribute() {
...
}
}
Now, update your User and Workspace models to create this new pivot table class, instead of the default one. This is done by overriding the newPivot() method provided by the Model class. You want to override this method so that you create an instance of your new UserWorkspacePivot class, instead of the default Pivot class.
class User extends Model {
// normal many-to-many relationship to workspaces
public function workspaces() {
// don't forget to add in additional fields using withPivot()
return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
}
// method override to instantiate custom pivot class
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new UserWorkspacePivot($parent, $attributes, $table, $exists);
}
}
class Workspace extends Model {
// normal many-to-many relationship to users
public function users() {
// don't forget to add in additional fields using withPivot()
return $this->belongsToMany('App\Models\User')->withPivot('role');
}
// method override to instantiate custom pivot class
public function newPivot(Model $parent, array $attributes, $table, $exists) {
return new UserWorkspacePivot($parent, $attributes, $table, $exists);
}
}
I figured out how to use Accessors and Mutators on the Pivot table (I'm using Laravel 5.8)
You must use using() on your belongsToMany relationships, for example:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
public function workspaces() {
return $this->belongsToMany('App\Workspace')->using('App\UserWorkspace');
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Workspace extends Model {
public function users() {
return $this->belongsToMany('App\User')->using('App\UserWorkspace');
}
}
So, use your Pivot model:
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspace extends Pivot {
public function getRoleAttribute() {
// your code to getter here
}
public function setRoleAttribute($value) {
// your code to setter here
}
}
This is a difficult question. The solutions I can think of are smelly and may cause some problems later on.
I am going to extend on Patricus's answer to make it work.
I was going to comment on Patricus's answer but there is simply too much to explain. To make his solution work with attach and sync we must do some ugly things.
The Problem
First let's identify the problem with his solution. His getters and setters do work but the belongsToMany relationship doesn't use the Pivot model when running sync, attach, or detach. This means every time we call one of these with the $attributes parameter the non-mutated data will be put into the database column.
// This will skip the mutator on our extended Pivot class
$user->workspaces()->attach($workspace, ['role' => 'new role value']);
We could just try to remember that every time we call one of these we can't use the second parameter to attach the mutated data and just call updateExistingPivot with the data that must be mutated. So an attach would be what Patricus stated:
$user->workspaces()->attach($workspace);
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);
and we could never use the correct way of passing the pivot attributes as the attach methods second parameter shown in the first example. This will result in more database statements and code rot because you must always remember not to do the normal way. You could run into serious problems later on if you assume every developer, or even yourself, will just know not to use the attach method with the second parameter as it was intended.
The Solution (untested and imperfect)
To be able to call attach with the mutator on the pivot columns you must do some crazy extending. I haven't tested this but it may get you on the right path if you feel like giving it a try. We must first create our own relationship class that extends BelongsToMany and implements our custom attach method:
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class UserWorkspaceBelongsToMany extends BelongsToMany {
public function attach($id, array $attributes = [], $touch = true)
{
$role = $attributes['role'];
unset($attributes['role']);
parent::attach($id, $attributes, $touch);
$this->updateExistingPivot($id, ['role' => $role], $touch);
}
// You will need sync here too
}
Now we have to make each Model::belongsToMany use our new UserWorkspaceBelongsToMany class instead of the normal BelongsToMany. We do this by mocking the belongsToMany in our User and Workspace class:
// put this in the User and Workspace Class
public function userWorkspaceBelongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null)
{
if (is_null($relation)) {
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new UserWorkspaceBelongsToMany($query, $this, $table, $foreignKey, $otherKey, $relation);
}
As you can see, we are still calling the database more but we don't have to worry about someone calling attach with the pivot attributes and them not getting mutated.
Now use that inside your models instead of the normal belongsToMany:
class User extends Model {
public function workspaces() {
return $this->userWorkspaceBelongsToMany('App\Models\Workspace')->withPivot('role');
}
}
class Workspace extends Model {
public function users() {
return $this->userWorkspaceBelongsToMany('App\Models\User')->withPivot('role');
}
}
Its impossible to use setters, will not affect pivot table... make the change in the controller instead.
Usually to eager load a relationship I would do something like this:
Model::with('foo', 'bar', 'baz')...
A solution might be to set $with = ['foo','bar','baz'] however that will always load these three relations whenever I call Model
Is it possible to do something like this: Model::with('*')?
No it's not, at least not without some additional work, because your model doesn't know which relations it supports until they are actually loaded.
I had this problem in one of my own Laravel packages. There is no way to get a list of the relations of a model with Laravel. It's pretty obvious though if you look at how they are defined. Simple functions which return a Relation object. You can't even get the return type of a function with php's reflection classes, so there is no way to distinguish between a relation function and any other function.
What you can do to make it easier is defining a function that adds all the relationships.
To do this you can use eloquents query scopes (Thanks to Jarek Tkaczyk for mentioning it in the comments).
public function scopeWithAll($query)
{
$query->with('foo', 'bar', 'baz');
}
Using scopes instead of static functions allows you to not only use your function directly on the model but for example also when chaining query builder methods like where in any order:
Model::where('something', 'Lorem ipsum dolor')->withAll()->where('somethingelse', '>', 10)->get();
Alternatives to get supported relations
Although Laravel does not support something like that out of the box you can allways add it yourself.
Annotations
I used annotations to determine if a function is a relation or not in my package mentioned above. Annotations are not officially part of php but a lot of people use doc blocks to simulate them.
Laravel 5 is going to use annotations in its route definitions too so I figuered it not to be bad practice in this case. The advantage is, that you don't need to maintain a seperate list of supported relations.
Add an annotation to each of your relations:
/**
* #Relation
*/
public function foo()
{
return $this->belongsTo('Foo');
}
And write a function that parses the doc blocks of all methods in the model and returns the name. You can do this in a model or in a parent class:
public static function getSupportedRelations()
{
$relations = [];
$reflextionClass = new ReflectionClass(get_called_class());
foreach($reflextionClass->getMethods() as $method)
{
$doc = $method->getDocComment();
if($doc && strpos($doc, '#Relation') !== false)
{
$relations[] = $method->getName();
}
}
return $relations;
}
And then just use them in your withAll function:
public function scopeWithAll($query)
{
$query->with($this->getSupportedRelations());
}
Some like annotations in php and some don't. I like it for this simple use case.
Array of supported relations
You can also maintain an array of all the supported relations. This however needs you to always sync it with the available relations which, especially if there are multiple developers involved, is not allways that easy.
protected $supportedRelations = ['foo','bar', 'baz'];
And then just use them in your withAll function:
public function scopeWithAll($query)
{
return $query->with($this->supportedRelations);
}
You can of course also override with like lukasgeiter mentioned in his answer. This seems cleaner than using withAll. If you use annotations or a config array however is a matter of opinion.
There's no way to know what all the relations are without specifying them yourself. How the other answers posted are good, but I wanted to add a few things.
Base Model
I kind of have the feeling that you want to do this in multiple models, so at first I'd create a BaseModel if you haven't already.
class BaseModel extends Eloquent {
public $allRelations = array();
}
"Config" array
Instead of hard coding the relationships into a method I suggest you use a member variable. As you can see above I already added $allRelations. Be aware that you can't name it $relations since Laravel already uses that internally.
Override with()
Since you wanted with(*) you can do that too. Add this to the BaseModel
public static function with($relations){
$instance = new static;
if($relations == '*'){
$relations = $instance->allRelations;
}
else if(is_string($relations)){
$relations = func_get_args();
}
return $instance->newQuery()->with($relations);
}
(By the way, some parts of this function come from the original Model class)
Usage
class MyModel extends BaseModel {
public $allRelations = array('foo', 'bar');
}
MyModel::with('*')->get();
I wouldn't use static methods like suggested since... it's Eloquent ;)
Just leverage what it already offers - a scope.
Of course it won't do it for you (the main question), however this is definitely the way to go:
// SomeModel
public function scopeWithAll($query)
{
$query->with([ ... all relations here ... ]);
// or store them in protected variable - whatever you prefer
// the latter would be the way if you want to have the method
// in your BaseModel. Then simply define it as [] there and use:
// $query->with($this->allRelations);
}
This way you're free to use this as you like:
// static-like
SomeModel::withAll()->get();
// dynamically on the eloquent Builder
SomeModel::query()->withAll()->get();
SomeModel::where('something', 'some value')->withAll()->get();
Also, in fact you can let Eloquent do it for you, just like Doctrine does - using doctrine/annotations and DocBlocks. You could do something like this:
// SomeModel
/**
* #Eloquent\Relation
*/
public function someRelation()
{
return $this->hasMany(..);
}
It's a bit too long story to include it here, so learn how it works: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html
Since i've met with a similar problem, and found a good solution that isn't described here and doesn't require filling some custom arrays or whatever, i'll post it for the future.
What i do, is first create a trait, called RelationsManager:
trait RelationsManager
{
protected static $relationsList = [];
protected static $relationsInitialized = false;
protected static $relationClasses = [
HasOne::class,
HasMany::class,
BelongsTo::class,
BelongsToMany::class
];
public static function getAllRelations($type = null) : array
{
if (!self::$relationsInitialized) {
self::initAllRelations();
}
return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
}
protected static function initAllRelations()
{
self::$relationsInitialized = true;
$reflect = new ReflectionClass(static::class);
foreach($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
/** #var ReflectionMethod $method */
if ($method->hasReturnType() && in_array((string)$method->getReturnType(), self::$relationClasses)) {
self::$relationsList[(string)$method->getReturnType()][] = $method->getName();
}
}
}
public static function withAll() : Builder
{
$relations = array_flatten(static::getAllRelations());
return $relations ? self::with($relations) : self::query();
}
}
Now you can use it with any class, like -
class Project extends Model
{
use RelationsManager;
//... some relations
}
and then when you need to fetch them from the database:
$projects = Project::withAll()->get();
Some notes - my example relation classes list doesn't include morph relations, so if you want to get them as well - you need to add them to $relationClasses variable. Also, this solution only works with PHP 7.
You could attempt to detect the methods specific to your model using reflection, such as:
$base_methods = get_class_methods('Illuminate\Database\Eloquent\Model');
$model_methods = get_class_methods(get_class($entry));
$maybe_relations = array_diff($model_methods, $base_methods);
dd($maybe_relations);
Then attempt to load each in a well-controlled try/catch. The Model class of Laravel has a load and a loadMissing methods for eager loading.
See the api reference.
You can create method in your Model
public static function withAllRelations() {
return static::with('foo', 'bar', 'baz');
}
And call Model::withAllRelations()
Or
$instance->withAllRelations()->first(); // or ->get()
You can't have a dynamic loading of relationships for a certain model. you need to tell the model which relations to support.
composer require adideas/laravel-get-relationship-eloquent-model
https://packagist.org/packages/adideas/laravel-get-relationship-eloquent-model
Laravel get relationship all eloquent models!
You don't need to know the names of the methods in the model to do this. Having one or many Eloquent models, thanks to this package, you can get all of its relationships and their type at runtime
The Best Solution
first create a trait, called RelationsManager:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use ReflectionClass;
use ReflectionMethod;
trait RelationsManager
{
protected static $relationsList = [];
protected static $relationsInitialized = false;
protected static $relationClasses = [
HasOne::class,
HasMany::class,
BelongsTo::class,
BelongsToMany::class,
HasOneThrough::class,
HasManyThrough::class,
MorphTo::class,
MorphOne::class,
MorphMany::class,
MorphToMany::class,
];
public static function getAllRelations($type = null): array
{
if (!self::$relationsInitialized) {
self::initAllRelations();
}
return $type ? (self::$relationsList[$type] ?? []) : self::$relationsList;
}
protected static function initAllRelations()
{
self::$relationsInitialized = true;
$reflect = new ReflectionClass(static::class);
foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
/** #var ReflectionMethod $method */
if ($method->hasReturnType() && in_array((string) $method->getReturnType(), self::$relationClasses)) {
self::$relationsList[(string) $method->getReturnType()][] = $method->getName();
}
}
}
public static function withAll(): Builder
{
$relations = array_flatten(static::getAllRelations());
return $relations ? self::with($relations) : self::query();
}
}
Now you can use it with any class, like -
class Company extends Model
{
use RelationsManager;
//... some relations
}
and then when you need to fetch them from the database:
$companies = Company::withAll()->get();
this solution only works with PHP 7 Or Higher.
Done