Chain an array of model relationships to query builder - php

How can I chain an array of relations to a query builder instance in Laravel?
I currently have an array of relations passed in from another part of the system. Something like this:
$relations = [
"company",
"staff",
"category",
]
I've then got an instance of a model, and want to loop and add each relation, then get a certain property on the model. The length of the relations array is dynamic.
$instance->company->staff->category->name;
How can I achieve something like this?
Edit: To add context as to why I'd like to do this, I have a class which is using reflection to retrieve the class to instantiate. I have a property being passed to it which is generated something like organization.company.staff.category.name, where the first item organization is the model, which is instantiated with reflection and the last item name is the property which should be retrieved.
The varying items between the first and last are relations to go through to retrieve the property. We are retrieving this from a JSON value for a reporting platform, so we want to do something like this where we are able to re-use the same code to retrieve data system-wide. The content organization.company.staff.category.name will obviously change per JSON value.

you can make a foreach, see my example
Example
$relations = [
"company",
"staff",
"category",
];
$producto = Producto::find(1);
foreach($relations as $relation){ $producto = $producto->$relation; )}
return $producto->name;

Related

Access keys in json type column for Eloquent collections?

A table column in my database is saved as json type. I want to display a keyed value in the saved json object on my blade templates but I'm unsure how.
Let's say I have $table->json('meta') in my table schema for Newsletter model, with meta column having e.g. {"foo": "bar"} as value.
How would I retrieve something like $newsletter->meta->foo? Since $newsletter->meta returns string instead of json object by default on Laravel 5.5 requiring a trivial json_decode to convert it.
A cleaner solution besides json_decode on every call, would be to use an accessor on that column e.g. getMetaAttribute but that's still messy. I want automatic json column to PHP object detection, how can I make this happen?
You can declare a protected $casts array inside your model in which you can instruct Eloquent to automatically convert types of model attributes. In your case that would look like this:
/*
* #property string $meta - json is actually just a string
*/
class Newsletter extends Model {
protected $casts = [
'meta' => 'array',
];
}
// Now you can use `$meta` as array:
$newsletter = Newsletter::find(1);
$foo = array_get($newsletter->meta, 'foo');
But this still does not convert it into an object. Although object is mentioned in the docs as being a valid cast type, I can not tell you exactly what it does.
As a note, these $casts are not bi-directional. If you want to set meta on a Newsletter instance, you'd have to create your own facilities to convert any object or array into a json string. Eloquent allows you to define mutators on your model to get the job done.

Eloquent - Get specific columns from logged-in user

In Laravel $user = Auth::user() returns the currently logged in user. From this user I want to retrieve only a few columns into a key-value array. What are my options?
Example:
$user = Auth::user()->fetch('id', 'username', 'email');
should give me
[
"id" => 1,
"username" => "John Doe",
"email" => "john#doe.example"
]
I've searched the Eloquent documentation, but haven't found the function I'm looking for. Have I missed it? Does such a function not exist?
If your main objective is to get an array instead of an object, and extra fields in the array are not a problem given that your required fields are there, you could just cast the user model to array:
Auth::user()->toArray();
(array) Auth::user();
However, if you need the output to only have these 3 properties - you have to do a little bit of extra coding. What I would propose is to create a property in User model that contains a list of publicly exposed fields. Then override toArray() method (or create another method) that only returns fields from the list.
Auth::user() fetches a user object for you, so you are no longer in Eloquent and can't use Eloquent methods.
There's no method on a model that returns an array of given attributes (reference: https://laravel.com/api/5.2/Illuminate/Database/Eloquent/Model.html), but you can make one on your User model, for instance like this:
function myData($keys) {
return array_intersect_key($this->toArray(), array_flip($keys))
}
Then you can call it using $user->myData(['id', 'username', 'keys'])

Use collection->get() instead of collection->pluck()

I am using laravel-permission for managing roles and displaying content. Per the docs you can retrieve a users roles by using $roles = $user->roles()->pluck('name'). My problem is that the data returned is ["admin"] rather than just admin. I was reviewing the collections methods and it looked like get('name') would return what I was looking for. When I try to use the following command Auth::user()->roles()->get('name') I get
1/1
ErrorException in BelongsToMany.php line 360:
Argument 1 passed to Illuminate\Database\Eloquent\Relations\BelongsToMany::getSelectColumns() must be of the type array, string given
It seems to me like the get() method is expecting an array however, I'm trying to reference an item in the array. The raw output of Auth::user()->roles()->get() is [{"id":1,"name":"admin","created_at":"2016-03-10 06:24:47","updated_at":"2016-03-10 06:24:47","pivot":{"user_id":1,"role_id":1}}]
I have found a workaround for pulling the correct content, but it is using regex for removing the unwanted characters that are included in the pluck() method.
preg_replace('/\W/i','', Auth::user()->roles()->pluck('name'))
It seems like I'm missing something or approaching using the get() method incorrectly. Any advice is appreciated.
I think pluck() will return the value of the given column for each model in the collection, which would explain the array. In your case it looks like the user has only one role, so you get an array with only one item. If the user had multiple roles, you would likely get an array with multiple items in it.
On the other hand, the get() method is used to execute a query against the database after a query is built. The results of the query are what is returned. To return a collection of models with only a single value you will need to pass an array with just the one column you want, but that will just select models, which does not appear to be what you ultimately need.
You can try this instead: $roles = $user->roles()->first()->name
The call to first() will grab the first model in the collection returned by roles(), and then you can grab the name of the role from that model.
I typically throw some error checking around this:
$role = $user->roles()->first();
if (is_null($role)) {
//Handle what happens if no role comes back
}
$role_name = $role->name;
That's because an user can have many roles, so roles is a collection.
If you are 100% sure that a user will have only one role, you can easily do
Auth::user()->roles()->first()->name
That will get the first item of that collection (the role) and then its name.

How to index returned array by key with eager loading in Laravel Eloquent?

I'm working on a side project in which we are trying to implement a "like" functionality for user's posts. We are using Laravel's ORM and we would like to use eager loading to make things easier, I'll outline the issue below. Some information first, the Post.php Model contains this:
public function likes()
{
return $this->hasMany('App\Models\PostLike', 'post_id', 'post_id');
}
The PostController.php implementing the API call to load the posts and their likes looked like:
$posts = Post::with("likes")->where("group_id", "=", $group_id)->get();
An example JSON response from the posts API might look like this:
[
{
"post_id":1,
"group_id":1,
"author_id":1,
"text":"Some text here.",
"created_at":"2015-08-13 00:15:08",
"updated_at":"2015-08-13 00:15:08",
"likes":[
{"post_id":1,"user_id":1,"updated_at":"2015-08-14 03:05:48"}
]
}
]
The issue with this is that the "likes" are not indexed by the user_id, so you have to search through the array to determine if a user has liked the post or not, it would be much easier to have it indexed by the unique user_id key. That would yield something like:
[
{
"post_id":1,
"group_id":1,
"author_id":1,
"text":"Some text here.",
"created_at":"2015-08-13 00:15:08",
"updated_at":"2015-08-13 00:15:08",
"likes":{
"1": {"post_id":1,"user_id":1,"updated_at":"2015-08-14 03:05:48"}
}
}
]
So the ultimate question is how do we index the eager loading response by one of the columns it returns?
In order to achieve that, you'll need to add extra post-processing step to your code that would index the array by id. This is very easy using Collection's helper method keyBy().
What you need is a accessor that would load the relation if needed and reindex the array. This accessor can be used in different scenarios, even without eagerly loading the relation, that's why it needs to handle relation loading if needed.
Adding that method to your Post model should do the trick:
public function getLikesAttribute() {
return $this->getRelationValue('likes')->keyBy('user_id');
}
Custom accessors take priority above relation definitions in cases like that, when botu likes() and getLikesAttribute() exist. That's why your accessor will be called when you do $post->likes and will reindex the table.

How can you update and create a new row in Laravel from a JSON object with relationships?

I started using Laravel yesterday, the ORM seems powerful. Does it have any way of updating rows in related models? This is what I tried:
Step 1: Generate a JSON object with the exact structure the database has. The JSON object has certain fields that are subarrays which represent relationships in the database.
Step 2: Send the JSON object via POST to Laravel for processing, here it gets tricky:
I can change the JSON object into an array first
$array = (array) $JSONobject;
Now I need to update, I would expect this to work:
Product::update($JSONobject->id,$array);
But because the array has subarrays, the update SQL that is executed cannot find the sub-array column in the table, it should instead look for the associated table. Can this be done? Or do I have to call the other models as well?
Thanks in advance!
This is something that Eloquent does not handle for you. The array that you supply to the update() method should contain columns only for, in your case, the Product model. You might try something like this to update relations. This is all off the top of my head and is by no means tested. Take it with a grain of salt.
$update = (array) $JSONobject;
$relations = [];
foreach ($update as $column => $value)
{
// If the value is an array then this is actually a relation. Add it to the
// relations array and remove it from the update array.
if (is_array($value))
{
$relations[$column] = $value;
unset($update[$column]);
}
}
// Get the product from the database so we can then update it and update any of the
// the products relations.
$product = Product::find($update['id']);
$product->update($update);
foreach ($relations as $relation => $update)
{
$product->{$relation}()->update($update);
}
The above code assumes that the key for your nested relation arrays is the name of the relation (method name used in your model). You could probably wrap this up in a method on your Product model. Then just call something like Product::updateRecursively($JSONobject); I'm terrible with names but you get the idea.
This probably won't work with more complex relations either. You'd have to take it a few steps further for things like many to many (or probably even one to many).

Categories