Extending Eloquent models, more magic methods - php

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.

Related

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

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 ...

Eloquent doesn't get the "belongsTo" item

I have the Project model and the Contract model. When i execute Project:all() it gets me only the projects without the contract, same for contract. I tried to dd() inside contract and doesn't do anything, like is never executed. I also tried with App\ prefix and without.
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
protected $table = 'project';
public function contract() {
return $this->belongsTo('Contract');
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
class Contract extends Model
{
protected $table = 'contract';
public function project() {
return $this->hasMany('Project', 'ContractID', 'ContractID');
}
}
I try to retrieve them like this:
$projects = Project::all()->take(10);
You have a few problems here.
Project::all()->take(10);
This only returns a collection of projects. You havent specified that you want the contracts also.
$projects = Project::with('contract')->get();
In your belongsTo - You havent specified the column that the table should join on. You need to do this, because you have not used a standard id for primary key and contract_id for foreign key.
unrelated to specific question, but your relationship in contract model is also wrong.
public function project() {
return $this->hasMany('Project', 'ContractID', 'ContractID');
}
If one contract has many projects, then your public function project() should be public function projects();
Finally - Why are you using non-standard table / column naming conventions? What's wrong with contract_id? Are you aware that mysql is non-case sensitive? Also the project table could be renamed projects and the contract table could be renamed contracts. It will make you writing your eloquent relations much easier and makes more sense!
If you used standard naming conventions, then you could just do this to declare your model relations.
namespace App;
use Illuminate\Database\Eloquent\Model;
class Contract extends Model
{
public function projects() {
return $this->hasMany('Project');
}
}
Notice you dont need to specify the table name in the model, or how the table is related to the Project.

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. )

separating relationships and model functions in Laravel

Everytime I'm writing a Laravel model it just gives me a feeling of messy code. I have relationships and other model functions specially when using domain driven design. So I though about separating relationships and functions.
Example I have a User class that extends Eloqeunt:
class User extends Eloquent{}
and inside this class I have register functions and password hashing functions etc. Also, we can declare the relationships so:
class User extends Eloquent{
function post(){
return $this->hasMany('POST');
}
}
For some reason this smells funky to me. My solution was to create a Entities folder and inside create a User folder which will hold 2 files one would be UserRelationship which would hold of the the relationships for this class:
class UserRelationship extends Eloquent{
function post(){
return $this->hasMany('POST');
}
}
and a second which would be the actual User class where I would write all of the functions and this class would extend the UserRelationship class instead of Eloquent:
class User extends UserRelationship{
public static function register($email, $password, $activate_token)
{
$user = new static(compact('email', 'password', 'activate_token'));
$user->raise(new UserWasRegistered($user));
return $user;
}
}
What do you guys think of this approach I am relatively new to all this so I don't know if this is bad practice or to much work for little reward. What do you guys recommend?
For a user model, it is too much work. The easiest way and still a better approach is to define the relationship in the user model. If for example it is a post model where you have relationships for post to "user, comment, reply etc" then you can attempt splitting your relationships

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