Add custom attribute in eloquent model - php

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.

Related

Extend a Laravel model that always attaches a `where status = 'cancelled'` to select statements?

I'm working with Laravel and Nova. So basically in Laravel, I have a model like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
}
Then, Nova helps me create a nice CMS web interface at https://example.com/admin/resouces/flights that lists all my flights and https://example.com/admin/resouces/flights/<id> to let me CRUD against a particular record. All I have to do to make this happen is create the file app/Nova/Flight.php with the content:
<?php
namespace App\Nova;
//... import other classes like Text Fields, WYSIWYG editors etc.. etc.., that are the nice CMS UI fields to modify each column in my flights table
class Flight extends Resource
{
public static $model = 'App\Flight';
// ... list of fields I want to modify ...
}
This works fine and all, except now I want to make two different urls like:
* `https://example.com/admin/resouces/flights-cancelled` - this should only list the equivalent of `SELECT * FROM flights WHERE status = 'cancelled'`
* `https://example.com/admin/resouces/flights-active` - this should only list the equivalent of `SELECT * FROM flights WHERE status = 'active'`
Things will get a bit more complicated as my project evolves, so I was wondering if there's a way to define a new model called App\FlightCancelled that is exactly the same as App\Flight, except all queries to the database will always include a WHERE status='cancelled' condition. That way I can assign App\FlightCancelled as the model to my Nova resource.
Curious how that's done? Or if there's a better way to achieve my objective?
You can modify $table and override newQuery() - the method that Eloquent use to construct a new query.
FlightCancelled
protected $table = 'flights'
public function newQuery($excludeDeleted = true)
{
return parent::newQuery($excludeDeleted)
->where('status', '=', 'cancelled');
}
In this case, I recommend using lenses, allow you to fully customize the underlying resource Eloquent query.
class FlightCancelled extends Lens
{
public static function query(LensRequest $request, $query)
{
// Query here..
}
//
}

Laravel on some kind of Model Ready method

Well i don't know how to format the title of this post in very clear way, but here's my question:
Say i have
Posts::find('1);
Photos:find('1');
... and so on, every mode db request
now by default i can access db columns, for instance the id: through model->id
$Photos = Photos::find('1')->first();
echo $Photos->id; // will return 1
what i want is that i need all those kind of requests to add a custom field automatically like hashed_id, which is not in the database, which in return will make all models have a hashed_id as well, i know i can add that field to database and then grab it but i need it for different reasons/implementations
i did create a BaseModel and every Model will extend that BaseModel, so Photos extends BaseModel, BaseModel extends Model... and all that etc etc.
but i need some kind of constructor, upon retrieving data to process the data automatically without having to add -let's say- a hash_id() after retrieving the data.
something like, onAfterGet(), onReady()....sort of commands.
i hope my question is clear.
Thanks.
What you're looking for is an Accessor. Accesors can be used to add custom attributes to the model. Combine this with the $appends property and you have exactly what you need. The $appends property adds the custom accessor in every result.
You can do this by creating a base model like you've stated in the question or by using traits. I'll show you an example on how to achieve this using a base model.
Let's create base model called BaseModel. All other models that need this custom attribute will extend this.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BaseModel extends Model
{
protected $appends = ['hashed_id'];
public function getHashedIdAttribute()
{
return some_hash_function($this->id);
}
}
We have a Image model which extends our BaseModel.
<?php
namespace App;
class Image extends BaseModel
{
}
Now every result from the Image model will have the hashed_id field added by default.
Accesor documenation https://laravel.com/docs/5.4/eloquent-mutators#defining-an-accessor
If I understand you right, all you need to do is to define mutator, for example:
<?php
class Photo extends Model
{
/* ... model implementation ... */
public function getHashedIdAttribute()
{
return md5($this->id);
}
}
Then you can access property like it was in database:
echo Photo::find(5)->hashed_id;

How to override result of hasMany relationship?

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!

Why eloquent model relationships do not use parenthesis and how do they work?

I kept searching the web for an hour but couldn't figure this out. If we look at the eloquent relationships documentation:
https://laravel.com/docs/5.2/eloquent-relationships
The example User model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* Get the phone record associated with the user.
*/
public function phone()
{
return $this->hasOne('App\Phone');
}
}
Just below it, how to access the phone number of a user with id=1:
$phone = User::find(1)->phone;
Why is it phone and not phone() and what is the difference?
Also how does it work? If I try to call an object->name without parenthesis in my code PHP thinks I am looking for a class variable named name?
Some extra information:
It looks like phone is returning object(App\Models\Phone) and phone() is returning object(Illuminate\Database\Eloquent\Relations\HasOne)
If I run the code below:
User::find(1)->phone->count()
Framework executes following SQL statements:
select * from `phone` where `phone`.`user_id` = '1' and `phone`.`user_id` is not null limit 1
select count(*) as aggregate from `phone`
If I run the code below:
User::find(1)->phone()->count()
Framework executes following SQL statement:
select count(*) as aggregate from `phone` where `phone`.`user_id` = '1' and `phone`.`user_id` is not null
One way of thinking about it is that the public function phone() function defines the relationship, so using $obj->phone() would get you the Eloquent relationship itself (not the results of that relationship) which you could then modify with various query builder elements if you wanted.
Leaving out the brackets is the Eloquent shorthand for adding ->get() or ->first() at the end of the expression (Eloquent knows which to use based on if it's a hasOne, hasMany, etc. relationship, as defined in the function), which returns an Eloquent collection.
So, $obj->phone is the same as $obj->phone()->first().
I don't know Laravel/Eloquent and you would need to show the find() method for more information, but User::find(1) returns an object so the ->phone accesses the phone property of that object. This has nothing to do with the find() method that you have shown. It is shorter than this that would do the same:
$obj = User::find(1);
$phone = $obj->phone;
Do a var_dump($obj); and you should see a phone property. If not, then another possibility is that the class implements a __get() magic method so that when you attempt to access the phone property it runs the phone() method and returns the value.
As for the first explanation, the same can be done with arrays:
function test() { return array('phone'=>'713-555-1212'); }
echo test()['phone'];

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

Categories