Laravel: Using belongsTo() add unexpected attributes to my Model - php

I'm writing a REST API using Lumen. I have for my example 2 models User and Post. Post model use the method belongsTo to get the User model which created this post. My goal was to define an accessor so I can get the author's username of the post just like that Post::find($id)->author. So according to the doc I do this:
Post.php :
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $table = 'posts';
protected $appends = ['author'];
protected $fillable = [
'title',
'description'
];
protected $hidden = [
'user_id',
'created_at',
'updated_at'
];
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
public function getAuthorAttribute()
{
return $this->user->username;
}
}
Now the getter works well and I can easily get the author of the given Post.
But if I tried to return the Post in a JSON response, it's also return me weird attributes like user that seems to come from my user() method that call a belongsTo():
return response()->json(Post::find(2), 200);
{
"id": 2,
"title": "Amazing Post",
"description": "Nice post",
"author": "FooBar",
"user": {
"id": 4,
"username": "FooBar"
}
}
If I use the attributesToArray() it's work as expected:
return response()->json(Post::find(2)->attributesToArray(), 200);
{
"id": 2,
"title": "Amazing Post",
"description": "Nice post",
"author": "FooBar"
}
Moreover if I remove the getter getAuthorAttribute() and the $appends declaration, I don't get the unexpected user attribute.
But I don't want to use this method each time and it doesn't make it work if I want to return all my Post using:
return response()->json(Post::all(), 200);
Have someone an idea why I get this additional attribute using belongsTo?

This behavior is because of performance.
When you call $post->user for first time, The Laravel read it from the database and keep it in $post->relation[] for next usage. So next time Laravel can read it from the array and prevent from executing a query again(it will be useful if you use it in multiple places).
Plus, the user is also an attribute and the Laravel merges
$attributes and $relations array together when you call $model->toJson() or $model->toArray()
The Laravel's Model source code:
public function toArray()
{
return array_merge($this->attributesToArray(), $this->relationsToArray());
}
public function jsonSerialize()
{
return $this->toArray();
}

Your first approach was good, you just need to add 'user' into the $hidden array
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $table = 'posts';
protected $appends = ['author'];
protected $fillable = [
'title',
'description'
];
protected $hidden = [
'user_id',
'created_at',
'updated_at',
'user', // <-- add 'user' here
];
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
public function getAuthorAttribute()
{
return $this->user->username;
}
}
And your resulting model would be:
{
"id": 2,
"title": "Amazing Post",
"description": "Nice post",
"author": "FooBar"
}

Related

Laravel PUT relationship

Fiddling with Laravel and coming from Symfony, I'm trying to replicate some code.
I'm trying to PUT a Suggestion model (overwritting anything, even relationships) and wanted to know the proper way to overwrite the model.
Since tags attribute in fillable doesn't exist, I certainly get an error (Undefined column: 7 ERROR: column "tags" of relation "suggestions" does not exist).
Suggestions and tags both have their own tables and a pivot table that contains two foreign keys to both tables id.
Request & Response :
{
"id":2,
"content":"Magni.",
"tags":[{"id":13,"name":"MediumAquaMarine"}]
}
{
"id":2,
"content":"Magni.",
"tags":[{"id":10,"name":"Navy"},{"id":13,"name":"MediumAquaMarine"}]
}
public function update(Request $request, Suggestion $suggestion)
{
$validator = Validator::make($request->all(), [
'content' => 'required',
'tags.id' => 'numeric',
]);
if ($validator->fails()) {
return response()->json($validator->messages(), Response::HTTP_BAD_REQUEST);
}
$suggestion->fill($request->only($suggestion->getFillable()))->save();
return new SuggestionResource($suggestion);
}
class Suggestion extends Model
{
use HasFactory;
protected $fillable = ['content', 'tags'];
protected $with = ['tags'];
public function tags()
{
return $this->belongsToMany(Tag::class, 'suggestions_tags')->withTimestamps();
}
}
class Tag extends Model
{
use HasFactory;
protected $hidden = ['pivot'];
public function suggestions()
{
return $this->belongsToMany(Suggestion::class, 'suggestions_tags')->withTimestamps();
}
}
You could just pass an array of IDs for tags instead of the whole object.
Do:
"tags":[10, 13]
Instead of:
"tags":[{"id":10,"name":"Navy"},{"id":13,"name":"MediumAquaMarine"}]
Change the validation rules accordingly and then you can remove tags from $fillable and do something like:
$suggestion->update($request->validated());
$suggestion->tags()->sync($request->tags);

How to attach image path on getting data from database using laravel

I am getting data from database and i have a field profile_pic and i am saving image name in my database my response looks like:
"basicInfo": {
"id": 205,
"first_name": "new name",
"middle_name": "middle",
"profile_pic": "3q4Vs8iHdY.png",
}
As you can see profile_pic i am getting image name and there folder public/profile_images where my images is storing and when i hit get Api I GOT following response with image name in profile_pic.
but i want to get full path in my response by attaching path from public/profile_images:
My code where I got this response
$userBasicInfo = $this->userBasicInfo->where('user_id', $user_id)->first();
This gets all data but i want to attach image path name in my profile_pic as well
How I can do that?
I am stuck here
class UserBasicInfo extends Model
{
use SoftDeletes;
const DELETED_AT = 'deletedAt';
protected $table = "user_basic_info";
protected $fillable = [
'first_name','city','state','zip','social_security','middle_name', 'last_name', 'profile_pic', 'date_of_birth', 'gender', 'area_id', 'user_id', 'created_by', 'updated_by', 'created_at', 'deletedAt','title','cell_no','address','work_phone','fax','extension','primary_facility','affiliated_facility','employed_by','emergency_phone','designation','department','employment_type','biography','hiring_date','from','to'
];
protected $hidden = ['deletedAt'];
function user() {
return $this->belongsTo('App\User', 'id', 'user_id');
}
public function getFromAttribute($value){
$createdAt= Carbon::parse($value);
return $createdAt->toIso8601String();
}
public function getToAttribute($value){
$createdAt= Carbon::parse($value);
return $createdAt->toIso8601String();
}
}
if (!empty($userBasicInfo->profile_pic)){
$deleteImage =$userBasicInfo->profile_pic;
unlink(public_path('profile_images').'/'.$deleteImage);
}
You can use accessor in your UserBasicInfo model :
public function getProfilePicAttribute($value)
{
return 'path/to/image' . $value;
}
Guide to Accessors from laravel documentation:
https://laravel.com/docs/5.8/eloquent-mutators#defining-an-accessor

get all atributes in nested object that have auto relationship

I am creating a Rest API project that I have an Artist that have a Musical Genre object as an attribute, and I created these two classes like this:
Artist:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Artist extends Model
{
    public $table = 'artist';
public $timestamps = false;
    
    protected $fillable = [
     'name', '...'
    ];
//...
public function musical_genre() {
        return $ this-> belongsTo (MusicalGenre::class, 'musical_genre');
    }
}
Musical Genre:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class MusicalGenre extends Model
{
public $table = 'musical_genre';
public $timestamps = false;
protected $fillable = [
'name', '...'
];
// ...
function parent_genre(){
return $this->hasMany(MusicalGenre::class, 'parent_genre');
}
}
As you can see, Genre Musical have an auto relationship, because an Genre Musical can have an Parent Genre. This works nice, but the response I get is like this:
{
"artist": [
{
"id": 14,
"name": "Britney Spears",
"musical_genre": {
"id": 4,
"name": "electropop",
"parent_genre": 1
}
}
]
}
But I wish it could be like this:
{
"artist": [
{
"id": 14,
"name": "Britney Spears",
"musical_genre": {
"id": 4,
"name": "electropop",
"parent_genre": {
"id:" 1,
"name": "pop"
}
}
}
]
}
In controller I have this line to call Artist with Genre:
$artist= Artist::with(['...', 'musical_genre'])->get();
I already tried to call it with musical_genre.parent_genre but I got a empty parent.
There is a way to make the response like I wished in Laravel, with all atributes and not only the id? I didnt found how can I do this in Laravel docs.
EDIT:
I already tried using
$artist->load('parent_genre');
too.
EDIT 2:
My last try was in show() method inside the controller, like this:
public function show($id)
{
$artist = Artist::with(['login', 'musical_genre'])->findOrFail($id);
$musical_genre = MusicalGenre::findOrFail($artist->musical_genre);
$parent_genre = MusicalGenre::findOrFail($musical_genre->parent_genre);
if ($parent_genre){
$musical_genre->parent_genre = $parent_genre;
}
$artist->musical_genre = $musical_genre;
return response()->json(['artist' => $artist], 200);
}
And I got the same response, just with the "parent_genre": 1. I tried to get only $musical_genre->parent_genre and I got this body:
"musical_genre": {
"id": 4,
"name": "electropop",
"parent_genre": {
"id:" 1,
"name": "pop"
"parent_genre": null
}
}
like I wished. Maybe it's some Laravel limitation, not showing artist->musical_genre->parent_genre full body?
Add a $with attribute to the MusicalGenre model like so:
class MusicalGenre extends Model
{
public $table = 'musical_genre';
public $timestamps = false;
protected $with = ['parent_genre:id,name'];
.....
}
See: https://laravel.com/api/5.6/Illuminate/Database/Eloquent/Model.html#property_with
When you call the MusicalGenre model, it loads the parent_genre automatically. The 'id,name' in it limits the retrieved model to the two fields, if you want to to load all, just use only 'parent_genre' only.
Simply call it this way and you have it loaded with the response:
$artist= Artist::with('musical_genre')->get();
Please try this for nested eager loading:
$artist= Artist::with(['musical_genre'=> function ($query) {
$query->with(['parent_genre']);
}])->get();

Laravel query builder get custom attribute

I'm trying to get a custom attribute (https://laravel.com/docs/5.5/eloquent-mutators#defining-an-accessor)
from a query.
Right now I have:
User.php
public function getViewUrlAttribute()
{
return route('admin.users.view', ['id' => $this->id]);
}
public function role()
{
return $this->belongsTo('App\Role')->withDefault([
'name' => 'User'
]);
}
UserController.php
public function dataTable(Request $request)
{
$length = $request->has('length') ? $request->input('length') : 10;
$orderDirection = $request->input('orderDirection');
$searchValue = $request->input('search');
$users = User::select('id', 'name', 'email', 'created_at')->with('role:name')->limit($length);
if ($request->has('orderBy')) {
if ($request->has('orderDirection')) {
$users = $users->orderBy($request->input('orderBy'), $request->input('orderDirection') > 0 ? 'asc' : 'desc');
} else {
$users = $users->orderBy($request->input('orderBy'), 'desc');
}
}
return $users->get();
}
Returns
[
{
"id": 1,
"name": "User",
"email": "user#test.com",
"created_at": "2018-04-24 14:14:12",
"role": {
"name": "User"
}
}
]
So the thing is: there's any way to also get the view_url attribute? (I tried inside the with() but it fails)
Also can I return only the role name and not the whole object as you can see in the "Return" code? (I would like something like: "role": "User").
(Of course I'm trying to avoid running raw sql)
Thanks!
You're almost done...
1- To add a custom attribute you need to append it on the Model with $appends attribute:
protected $appends = ['view_url'];
And define your attribute method:
public function getViewUrlAttribute()
{
return route('admin.users.view', ['id' => $this->id]);
}
2- To add attribute to a model from another related model, I think you should try :
// to add them as attribute automatically
protected $appends = ['view_url', 'role_name'];
// to hide attributes or relations from json/array
protected $hidden = ['role']; // hide the role relation
public function getRoleNameAttribute()
{
// if relation is not loaded yet, load it first in case you don't use eager loading
if ( ! array_key_exists('role', $this->relations))
$this->load('role');
$role = $this->getRelation('role');
// then return the name directly
return $role->name;
}
Then you might not event need ->with('role') eager loading.

Laravel 5.2 change database column name reference

Hello i'm trying to change how to access to a database column name without change the name, for example, my column name is resourceType but I want to call it name and also I want the response json appears name instead resourceType. Looking around internet found I should use protected $maps = ['oldName' => 'newName']; but doesn't work. I want to change resourceType because I think doesn't look good the table name should be equal than a column resourceType->resourceType
This is my model
<?php
namespace Knotion;
use Illuminate\Database\Eloquent\Model;
use Mappable, Mutable;
class CTL_ResourceType extends Model {
public $timestamps = false;
protected $table = "CTL_ResourceType";
protected $primaryKey = "idResourceType";
public $incrementing = false;
public static $snakeAttributes = false;
protected $hidden = ['idCountry', 'idCompany', 'initials', 'thumbnail', 'icon', 'status', 'createTime', 'updateTime'];
protected $fillable = ['name'];
protected $maps = ['resourceType' => 'name'];
protected $appends = ['name'];
public function resource() {
return $this->hasMany('Knotion\CTL_Resource', 'idResource' );
}
public function country() {
return $this->belongsTo('Knotion\CTL_Country', 'idCountry', 'idCountry');
}
public function company() {
return $this->belongsTo('Knotion\CTL_Company', 'idCompany', 'idCompany');
}
}
and this is the response JSON I'm receiving. As you see resourceType stills there instead name
{
"total": 16,
"per_page": 15,
"current_page": 1,
"last_page": 2,
"next_page_url": "http://localhost:8000/krb/api/resources?page=2",
"prev_page_url": null,
"from": 1,
"to": 15,
"data": [
{
"idResource": "4e8f1ece-f666-11e5-8137-0f7932903a75",
"productionKey": "238493ujjsl",
"title": "ElTitle16",
"description": "ElDescription16",
"minimumAge": "4",
"maximumAge": "15",
"fileName": "ElFileName16",
"extension": ".png",
"URL": "ElURL16",
"createTime": "2016-03-30 04:58:16",
"creatorUser": {
"idUser": "85cf125c-f5ff-11e5-8137-0f7932903a75",
"name": "Roberto"
},
"creationCountry": {
"idCountry": "f03a75a0-f5ff-11e5-8137-0f7932903a75",
"country": "Estados Unidos"
},
"resourceType": {
"idResourceType": "5c902028-f601-11e5-8137-0f7932903a75",
"resourceType": "TípodeRecurso3"
},
"tags": [
{
"idTag": "40c6a114-f520-11e5-8137-0f7932903a75",
"name": "ElTag1"
}
],
"quickTags": [
{
"idQuickTag": "679bc8f0-f520-11e5-8137-0f7932903a75",
"name": "ElQuickTag4"
}
],
"relatedTo": [
{
"idRelatedTo": "7beddc6c-f520-11e5-8137-0f7932903a75",
"name": "ElRelatedTo3"
}
]
}
I hadn't heard of the $maps property or Mappable before, so I did a quick search. It looks like they (as well as Mutable) are part of the jarektkaczyk/eloquence package.
In this case, both Mappable and Mutable are traits that are supposed to be added to the class. Additionally, in order for them to work properly, you need to add in the Eloquence trait, as well.
Your use statements at the top of your file need to be changed to properly address the class names in the correct namespace, and then you need to add the traits to your class:
<?php
namespace Knotion;
// import the class names
use Sofa\Eloquence\Mutable;
use Sofa\Eloquence\Mappable;
use Sofa\Eloquence\Eloquence;
use Illuminate\Database\Eloquent\Model;
class CTL_ResourceType extends Model {
// add the traits to the class
use Eloquence, Mappable, Mutable;
// code...
}
Edit
If you wanted to do this without the package, you need to do three things:
You need to add resourceType to your $hidden array, so that it won't show up in your toArray()/toJson() results.
protected $hidden = ['idCountry', 'idCompany', 'initials', 'thumbnail', 'icon', 'status', 'createTime', 'updateTime', 'resourceType'];
You need to create a getNameAttribute() accessor method, which will be called whenever you attempt to access the name attribute.
public function getNameAttribute() {
return $this->resourceType;
}
You need to add name to your $appends array, so that it will be included in your toArray()/toJson() results.
protected $appends = ['name'];
Optionally, if that feels like too much work, you could always just override the toArray() method (called by toJson()) to force your naming convention, as well:
public function toArray() {
// call parent method to get initial array results
$array = parent::toArray();
// set the new key with data
$array['name'] = $array['resourceType'];
// unset the old key
unset($array['resourceType']);
// return the array
return $array;
}

Categories