Laravel model relationship belongsTo with associate() gives infinite loop toArray() - php

I hope someone may help me with the issue I have for a long time, but only now I'm posting it.
The project I'm working on uses models with nested relationships. Just to simplify the context of the issue, let's imagine a parent with many children model relation: hasMany and belongsTo.
I usually create new instances and fill the properties then I relate them by hand using setRelation() and associate() methods (because I like to retrieve the relationships using parent-to-child and child-to-parent methods).
But infinite loops arises after I call toArray() (among many other model methods that traverse its relations).
The question is: Am I doing the right thing caling setRelation and associate for model relationship? If not, how would I retrieve $model->children()/$model->parent() relation?
I'm using Laravel Framework 7.14.1 with PHPUnit 8.5.5 and PHP 7.4.4 (cli)
Here a Unit test:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use Illuminate\Database\Eloquent\Model;
class Team extends Model {
protected $fillable = ['name'];
public function players()
{
return $this->hasMany(Player::class);
}
}
class Player extends Model {
protected $fillable = ['name'];
public function team()
{
return $this->belongsTo(Team::class);
}
}
class CircularReferencesTest extends TestCase
{
public function testCircularReference(): void
{
// new instances
$team = app(Team::class)->fill(['name' => 'team name']);
$player = app(Player::class)->fill(['name' => 'player name']);
// set relations
$team->setRelation('players', collect([$player]));
$player->team()->associate($team);
dd($team->toArray(), $player->toArray()); // ERROR: Segmentation fault (core dumped).
// dd($team->push()); // push calls save() method recursively #see https://laravel.com/docs/7.x/eloquent-relationships#the-push-method
dd($team, $player);
}
}
I'm calling by:
./vendor/phpunit/phpunit/phpunit --stop-on-failure --colors=always ./tests/Unit/CircularReferencesTest.php

according to this
you can't create a relationship between 2 models that are not already represented in the database.
at least one of the models must be saved in advance, in order to create the relationship.
this is the first issue in your code ...
second one:
setting relation of one side is more than enough.. why you set the relation from the other side?
i mean:
one of those two line is enough:
$team->setRelation('players', collect([$player]));
$player->team()->associate($team);
i prefer using 'associate' method is more clearer ...

Related

Extending Eloquent models, more magic methods

Situation: In my work they have conventions on their database, table and column names, which are a bit long and repetitive. Being used to Eloquent I figured it wouldn't be much trouble to reimplement __get and __set methods, and not making lots of getters. Something like this (toConvention implements company's conventions):
use Illuminate\Database\Eloquent\Model;
class CompanyModel extends Model {
public function __get($key){
return $this->getAttribute($this->toConvention($key));
}
public function __set($key){
return $this->setAttribute($this->toConvention($key));
}
}
Which works well, for retrieving attributes, but not for retrieving relationships. Here are the implementations:
use App\CompanyModel as Model
class Location extends Model
{
protected $table = 'tablename';
protected $primaryKey = 'primarykeycolumn';
//...
public function comissionCurrency(){
return $this->belongsTo('App\Currency', 'foreign', 'other');
}
}
use App\CompanyModel as Model
class Currency extends Model
{
protected $table = 'tablename';
protected $primaryKey = 'primarykeycolumn';
//...
}
When requesting for attributes, like $location->name, or $location->comission_currency_id everything works as expected, retrieving the corresponding column name. But when I try to retrieve the belongsTo relationship, after using toSql() I see almost the correct query formed: select * from table where table.column is null the is null part should be comparing with the corresponding id.
I know it's due to my implementation, because when I use Illuminate\Database\Eloquent\Model everything works ok. Funny thing is that when I use Eloquent's model on the child model and the reimplemented on the other, it works to (but I'm not able to use my magic methods in the child model, in the parent one fetched from the relationship I can...)
I haven't figured out which method to reimplement to make this work by reading Eloquent's code, any ideas, or suggestions?
Thanks in advance.

Laravel Call to undefined method Illuminate\Database\Query\Builder::detach/attach() for hasManyThrough relationships

There are two important models Resume and Skill. Resume has many skills through ResumeSkill.
When I submit my form with skills, before adding them to resume, I remove all the skills it already has.
But I'm having this problem when I run the methods attach() and detach():
Call to undefined method Illuminate\Database\Query\Builder::detach()
This is my Resume model class:
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Resume extends BaseModel{
public function skills(){
return $this->hasManyThrough('\myApp\Skill', '\myApp\ResumeSkill');
}
}
And the main script:
$record = \myApp\Resume::find($id);
$record->skills()->detach();
foreach($skills as $skill_id){
$record->skills()->attach($skill_id);
}
What is wrong? Some answers I see say attach() is a BelongsTo method, but they must be old answers: https://laravel.com/docs/5.2/eloquent-relationships#has-many-through. Anyway, associate/dissociate don't work as well.
#solved
I should have used BelongsToMany. And attach / detach DOES work with this relationship.
The hasManyThrough is a short-cut for accessing distant relations via an intermediate relation. For Many-to-many relations is define method belongsToMany.
public function skills() {
return $this->belongsToMany('App\Skill', 'ResumeSkill', 'idResume', 'idSkill');
}

Retrieving all children and grandchildren of Laravel models

I have several models, all of which need the one before it to be accessed. The examples below describe a similar situation
<?php namespace backend\Models;
use Illuminate\Database\Eloquent\Model;
class Geo_Locations extends Model {
protected $table = 'geo_locations';
protected $fillable = ['id', 'name', 'population', 'square_miles', 'comments'];
public function state(){
return $this->hasMany('backend\Models\Geo_State', 'id', 'state_id');
}
}
The "Geo_State" model
<?php namespace backend\Models;
use Illuminate\Database\Eloquent\Model;
class Geo_State extends Model {
protected $table = 'geo_states';
protected $fillable = ['id', 'name', 'political_obligation', 'comments'];
public function city(){
return $this->hasMany('backend\Models\Geo_City', 'id', 'city_id');
}
}
And then the "Geo_City" model would be
<?php namespace backend\Models;
use Illuminate\Database\Eloquent\Model;
class Geo_City extends Model {
protected $table = 'geo_cities';
protected $fillable = ['id', 'name', 'population', 'comments'];
public function miles(){
return $this->hasMany('backend\Models\Geo_Mile', 'id', 'mile_marker');
}
}
and it can continues but for the sake of ease, I'll stop there. What I would like to accomplish, and I'm sure many others would as well. Would be a way to retrieve all data related to one of the main models. For example, I want to get all the cities and their respective states within a location. What I would like for a result is something like
print_r(json_encode($a->getChildren(), JSON_PRETTY_PRINT));
should print out
{
"Geo_Location":{
"name":"United States",
"population":"300 Some Million",
"comments":"they have a lot of issues",
"Geo_State":{
"name":"Texas",
"population":"Some big number",
"comments":"they have a lot of republicans",
"Geo_City":{
"name":"Dallas",
"population":"A lot",
"comments":"Their food is awesome"
}
}
}
}
I have some of the code in a gist, while it may be poorly documented it's all I've been working with and it's my third attempt at doing this.
Individual calls to the route /api/v1/locations/1 would return all the info for United States but will leave out the Geo_State and below. A call to /api/v1/state/1 will return Texas's information but will leave out the Geo_Location and the Geo_City.
The issue in what I have is that it's not recursive. If I go to the route /api/v1/all it's suppose to return a json array similar to the desired one above, but it's not getting the children.
It sounds like what you want is to load the relationships the model has when you call that model. There are two ways I know you can do that in laravel using eager loading.
First way is when you retrieve the model using the with parameter, you can add the relationship and nested relationships like below.
$geoLocationCollectionObject =
Geo_Location::with('state', 'state.city', 'state.city.miles', 'etc..')
->where('id', $id)
->get();
This will return a collection object - which has a function to return the json (toJson). I'm not sure how it handles the relationships (what your calling children) so you may need to make a custom function to parse the collection object and format it how you've specified.
toJson Function
Second option is similar to the first but instead you add the with parameter (as protected $with array) to the model so that the relationships are loaded everytime you call that model.
Geo_Location class
protected $with = array('state');
Geo_State class
protected $with = array('city');
Geo_City class
protected $with = array('miles');
etc...
Now when you retrieve a Geo_Location collection object (or any of the others) the relationship will already be loaded into the object, I believe it should be recursive so that the city miles will also be called when you get a state model for instance - but haven't tested this myself so you may want to verify that.
I use ChromeLogger to output to the console and check this myself.
If you do end up needing a custom json_encode function you can access the relationship by doing $geoLocation->state to retreive all states and then for each state $state->city etc... See the docs for more info.
Laravel Docs on Eager Loading
I wasn't really sure what was going on in the code you linked, but hopefully this helps some.
So after a bit of fiddling around, turns out that what I am looking for would be
Geo_Locations::with('state.city')->get();
Where Geo_Locations is the base model. The "state" and the "city" would be the relations to the base model. If I were to return the query above, I would end up getting an json array of what I am looking for. Thank you for your help Anoua
(To make this more dynamic I'm going to try to find a way to get all of the names of the functions to automate it all to keep on track with what my goal is. )

Laravel 4 automatically create relationships in BaseModel - using "with"-array or scopes or what?

I have successfully created a few relations in a Model. This is the user-model
<?php
class User extends BaseModel{
protected $table = 'users';
public function group(){
return $this -> belongsTo('UserGroup');
}
...
}
?>
and this is the UserGroup-model (UserGroup.php)
<?php class UserGroup extends BaseModel{ ... } ?>
Every user can be in one group (the database-column is called 'group_id' in the users-table). If I want to do eager loading on this relation, it works perfectly fine, also for other models.
The problem is that I have a few models that have a lot of foreign keys and I don't want to create all relations manually. I want a function in the BaseModel that creates all those relations automatically, like
public function group(){
return $this->belongsTo('Group');
}
based on an array that I would provide in each model, looking like this
protected $foreignKeys = array(array('key' => 'group', 'model' => 'UserGroup'), ...);
I have read that there is an array called 'with' that you can use, but it did not work for me. Somewhere else I read I should work with query scopes but I have no idea how that could help me.
Thanks for reading and your support!
Best regards, Marcel
Ardent provides something like you'd want to implement: https://github.com/laravelbook/ardent/blob/master/src/LaravelBook/Ardent/Ardent.php#L347-L350.
As per comments:
You might find Ardent-like solution better for your situation in the end. Having all the relations in an array presents more advantages to defining them as methods - you can search through those arrays if you need etc. So probably this is the way to go for you

Laravel: belongsTo() relation assume one-to-many relationship instead of one-to-one

I'm having a tedious problem with Laravel's ORM.
I have a model with multiple relationships, like this:
class Pages extends Eloquent {
protected $guarded = array('id','config_id');
public function template()
{
return $this->belongsTo('Templates', 'templates_id');
}
public function updateUser()
{
return $this->belongsTo('Users', 'updated_by');
}
Now I can access the template related item in a simple way, like this:
$this->template->name;
And it works out of the bat, because Laravel's ORM assumes it is a one-to-one relationship and internally calls the first() method.
But when I try the same with updateUser it fails, returning an error, saying that it can't call name on a non-object.
But if I try this:
$this->updateUser()->first()->name;
it works, but it doesn't feel right to me.
So my question is, how Laravel's ORM decide if a relationship defined with belongsTo() is one-to-one or one-to-many? Is there a way to force a needed behaviour?
Thanks
You need to define the relationship. You can define 'different' relationships on the perspective.
The ->belongsTo() function is an inverse function - but you havent defined anything on the users table - so it is wrongly assuming the inverse is one-to-many.
Just add this to your users class:
class Users extends Eloquent {
public function pages()
{
return $this->hasMany('Pages');
}
}

Categories