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. )
Related
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 ...
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.
Imagine I have a couple of simple objects like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function posts()
{
return $this->hasMany("App\Post");
}
}
class Post extends Model
{
public function user()
{
return $this->belongsTo("App\User");
}
}
We'll say the \App\Post object has an database column called jsondata which contains JSON-encoded data. When I want to display the user's posts in a view with that column decoded, I need to do this in the controller:
$posts = Auth::user()->posts()->get();
foreach ($posts as $post) {
$post->jsondata = json_decode($post->jsondata);
}
return view("user.show", ["posts"=>$posts]);
Is there a way to avoid that foreach loop in my controller and do the JSON decoding at a lower level?
I'm sure I could do this in App\User::posts() but that doesn't help other places where I need to display the decoded data. I tried defining App\Post::get() to override the parent method, but it doesn't work because hasMany() doesn't seem to return an instance of the model at all.
It can be done in different places/ways, but I would suggest to use an append for this property in your model if you want this data is always decoded everywhere and every time you retrieve a Post model, or simply a mutator.
see https://laravel.com/docs/master/eloquent-mutators
In your model you can define:
protected $appends = [
'name_of_property'
];
// calculated / mutated field
public function getNameOfPropertyAttribute()
{
return jsondecode($this->jsondata);
}
You then can always access this property with:
$post->name_of_property
Note the conversion from CamelCase to snake_case and the conversion from getNameOfPropertyAttribute > name_of_property. By default you need to respect this convention to get it working automagically.
You can substitute the name_of_property and NameOfProperty with what you want accordingly.
Cheers
Alessandro's answer seemed like the best one; it pointed me to the Laravel documentation on accessors and mutators. But, near the bottom of the page is an even easier method: attribute casting.
In typical Laravel fashion, they don't actually list all the types you can cast to, but they do mention being able to cast a JSON-formatted database column to an array. A little poking through the source and it turns out you can do the same with an object. So my answer, added to the App\Post controller, is this:
/**
* The attributes that should be casted to native types.
*
* #var array
*/
protected $casts = ["jsondata"=>"object"];
This automatically does the decode and makes the raw data available. As a bonus, it automatically does a JSON encode when saving!
Here is a raw SQL query:
SELECT name, area, point(area) AS center FROM places;
I want to get an Eloquent model based on this query. Here is the Model:
class Place extends Model
{
protected $visible = ['name', 'area'];
}
So, I want to get the center property if I run this code:
return response()->json( Place::all() );
center is missing. I don't know how to add the center property in my Place object. I don't want to build a raw query in my controller, is there any solution with mutator or something like this? (The only thing I want to call is Place::all(), I really want to use Eloquent Model in controllers, not an SQL query).
Use a combination of Mutators and the $appends property. Here's an example:
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class Place extends Model {
protected $appends = ['center'];
public function getCenterAttribute()
{
return $this->point($this->getRawAttribute("area"));
}
protected function point($area)
{
// logic for SQL POINT(), etc
}
}
The $appends property will mean that the mutated attribute is included in JSON/array output when $model->toJson() or $model->toArray() is called (which Response::json() does)
The reason for doing point logic in code is because with eloquent models, you'd hit the N+1 query problem when fetching a list of places and their centers, and that's not a great idea for your database.
Your query wouldn't be used when fetching data for the model from the database, either, since the default query for models is
select * from `table` where id = :id
Which is then figured out internally to set up data on the model.
You may want to take a look at this:
http://laravel.com/docs/5.0/eloquent#global-scopes. It should help you build the query that will always get center along with the rest of data.
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