so I have this custom collection in my controller and I would like to use magic getters but I am getting this error:
Property [title] does not exist on this collection instance.
$test = collect( ["title" => "title", "heading" => "heading"]);
echo $test->title; // This doesnt work
echo $test->get('title'); // this works
Is it possible to use magic getters or I can only access it by get method?
laravel collection class has the magic getter implementation in the following way
public function __get($key)
{
if (! in_array($key, static::$proxies)) {
throw new Exception("Property [{$key}] does not exist on this collection instance.");
}
return new HigherOrderCollectionProxy($this, $key);
}
since your query does not belongs to the static::$proxies (which are used for further modification or actions to be done in the array like sort, group) array it throws the error. so get the value with $test->get('title', 'default value')
For your example, you should be able to use array-like access:
echo $test['title'];
If you have multiple records in your collection you can change how you access the records with the ->keyBy() method. Example:
$collection = collect([
['id' => 13, 'name' => 'Fred'],
['id' => 42, 'name' => 'Mike']
]);
Initially, it acts like a numerically-indexed array. Here, we can call $collection[0] to interact with:
['id' => 13, 'name' => 'Fred']
But calling:
$collection = $collection->keyBy('id');
makes $collection[13] now return this value.
A similar operation can be performed with ->keyBy('name') to be able to access $collection['Fred'].
You can initialize the collection instance and then programmatically populate it using object method
With that you can now access the collection prioperties as you normally do
$collection = collect();
$collect->title = 'title';
$collection->heading = 'heading';
echo $collection->title // this now works
Related
I'm looking at updating my mutators and accessors to the new syntax introduced in Laravel 9 but I can't seem to make it work.
I currently have a model with a MorphMany relationship to a Settings object that acts as a key-value store. As a shorthand I'm using mutators to set a couple of more commonly used settings. Very simplified, it looks like this:
class Item extends Model
public function setSomeSettingAttribute($value)
{
$key = "some_setting";
Setting::updateOrCreate(
["settable_id" => $this->id, "settable_type" => self::class, "key" => $key],
["value" => $value]
);
}
}
$m = Item::find(1);
// typically in a controller this would be $request->all()
$arr = ["some_setting" => 234];
$m->update($arr);
This works fine and the setting is updated. The important thing to note is that there is no column in the database named some_setting.
In the new code, it seems like the mutator should look like this:
public function someSetting(): Attribute
{
$key = "some_setting";
return Attribute::make(
set: function ($value) use ($key): void {
Setting::updateOrCreate(
["settable_id" => $this->id, "settable_type" => self::class, "key" => $key],
["value" => $value]
);
}
);
}
But this is not working; Laravel is attempting to insert something into the some_setting column, resulting in an SQL "column not found" error.
Is there a way around this that doesn't involve editing all my controller code to remove the fake columns? Or, if not, is the old mutator syntax deprecated in any way?
I have a collection object which includes the array of Model objects and I would like to select specific fields from the model.
Illuminate\Database\Eloquent\Collection Object
(
[items:protected] => Array
(
[0] => App\Model Object
[1] => App\Model Object
[2] => App\Model Object
)
)
Now I would like to select some fields from the model object. When I try to do the following syntax
print_r($collection->select('filed a', 'field b'));
then the following error occurs.
BadMethodCallException in Macroable.php line 74: Method select does
not exist.
I think select can work directly with the eloquent model but not with a collection.
Are you looking for only()
$filtered = $collection->only(['list', 'of', 'fields', 'to', 'keep']);
or perhaps mapWithKeys()
You are correct that select is not present on the Collection class.
What you can do is map, filter or transform the collection yourself e.g.
$whiteList = ['filed a', 'field b'];
$filledOnly = $collection->map(function ($item) use ($whiteList) {
$properties = get_object_vars($item);
foreach ($properties as $property) {
if(!in_array($property, $whiteList) {
unset($item->{property});
}
}
return $item;
});
The problem is in PHP once a property (or field) is set on an object, you really have to unset it or create new objects of the same class). This is why I came up with this solution.
Question is: How did you retrieve this collection in the first place, could you not add the select to the query itself?
The best would have been to select the fields you need before you execute the query on the model. However, you can use map() if you want to preserve the initial collection or transform() if you want to override the collection (for example):
$selected_fields = ['filed a', 'field b']
$models->map(function ($zn) use ($selected_fields) {
return $zn->newInstance(array_only($zn->getAttributes(), $selected_fields));
})->toArray();
newInstance() method creates a new empty instance of that model then getAttributes() retrieves the attributes present in the model. So the initial model is preserved in this process.
For reference sake, the implementation of newInstance() can be found on at Illuminate\Database\Eloquent\Model class and it is as follows (on Laravel 5.2):
/**
* Create a new instance of the given model.
*
* #param array $attributes
* #param bool $exists
* #return static
*/
public function newInstance($attributes = [], $exists = false)
{
// This method just provides a convenient way for us to generate fresh model
// instances of this current model. It is particularly useful during the
// hydration of new objects via the Eloquent query builder instances.
$model = new static((array) $attributes);
$model->exists = $exists;
return $model;
}
I want to create an Eloquent Model from an Array() fetched from database which is already toArray() of some model stored in database. I am able to do that using this code:
$model = Admin::hydrate($notification->data);
$notification->data = [
"name" => "abcd"
"email" => "abcd#yahoo.com"
"verified" => 0
"shopowner_id" => 1
"id" => 86
"shopowner" => [
"id" => 1
"name" => "Owner1"
"email" => "owner1#owner.com"
]
];
But i can't access the $model->shopowner->name
I have to use $model->shopowner['name']
I want to use the same class of notification without any specific change to access the data.
If you want to access shopowner as a relationship, you have to hydrate it manually:
$data = $notification->data;
$model = Notification::hydrate([$data])[0];
$model->setRelation('shopowner', ShopOwner::hydrate([$data['shopowner']])[0]);
Solution:
Thanks to #Devon & #Junas. by combining their code I landed to this solution
$data = $notification->data;
$data['shopowner'] = (object) $data['shopowner'];
$model = Admin::hydrate([$data])[0];
I see this as an invalid use of an ORM model. While you could mutate the array to fit your needs:
$notification->data['shopowner'] = (object) $notification->data['shopowner'];
$model = Admin::hydrate($notification->data);
Your model won't be functional because 'shopowner' will live as an attribute instead of a relationship, so when you try to use this model for anything other than retrieving data, it will cause an exception.
You cannot access array data as object, what you can do is override the attribute and create an instance of the object in your model, so then you can use it like that. For example:
public function getShopownerAttribute($value)
{
return new Notification($value); // or whatever object here
}
class Notification {
public function __construct($data)
{
// here get the values from your array and make them as properties of the object
}
}
It has been a while since I used laravel but to my understanding once you use hydrate your getting a Illuminate\Database\Eloquent\Collection Object, which then holds Model classes.
These however could have attributes that are lazy loaded when nested.
Using the collections fresh method could help getting a Full database object as using load missing
Is there a way to invoke eloquent relationship methods without changing the original eloquent collection that the method runs on? Currently I have to employ a temporary collection to run the method immutable and to prevent adding entire related record to the response return:
$result = Item::find($id);
$array = array_values($result->toArray());
$temp = Item::find($id);
$title = $temp->article->title;
dd($temp); //This prints entire article record added to the temp collection data.
array_push($array, $title);
return response()->json($array);
You are not dealing with collections here but with models. Item::find($id) will get you an object of class Item (or null if not found).
As far as I know, there is no way to load a relation without storing it in the relation accessor. But you can always unset the accessor again to delete the loaded relation (from memory).
For your example, this process yields:
$result = Item::find($id);
$title = $result->article->title;
unset($result->article);
return response()->json(array_merge($result->toArray(), [$title]));
The above works but is no very nice code. Instead, you could do one of the following three things:
Use attributesToArray() instead of toArray() (which merges attributes and relations):
$result = Item::find($id);
return response()->json(array_merge($result->attributesToArray(), [$result->article->title]));
Add your own getter method on the Item class that will return all the data you want. Then use it in the controller:
class Item
{
public function getMyData(): array
{
return array_merge($this->attributesToArray(), [$this->article->title]);
}
}
Controller:
$result = Item::find($id);
return response()->json($result->getMyData());
Create your own response resource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ItemResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'title' => $this->article->title,
'author' => $this->article->author,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Which can then be used like this:
return new ItemResource(Item::find($id));
The cleanest approach is option 3. Of course you could also use $this->attributesToArray() instead of enumerating the fields, but enumerating them will yield you security in future considering you might extend the model and do not want to expose the new fields.
I see two ways you can achieve that.
First, you can use an eloquent Resource. Basically it'll allow you to return exactly what you want from the model, so in your case, you'll be able to exclude the article. You can find the documentation here.
The second way is pretty new and is still undocumented (as fas i know), but it actually works well. You can use the unsetRelation method. So in your case, you just have to do:
$article = $result->article; // The article is loaded
$result->unsetRelation('article'); // It is unloaded and will not appear in the response
You can find the unsetRelation documentation here
There is not as far as I know. When dealing with Model outputs, I usually construct them manually like this:
$item = Item::find($id);
$result = $item->only('id', 'name', 'description', ...);
$result['title'] = $item->article->title;
return $result;
Should you need more power or a reusable solution, Resources are your best bet.
https://laravel.com/docs/5.6/eloquent-resources#concept-overview
Yii2 ArrayHelper's helper method toArray doesn't convert nested objects.
Here is my test code.
public function actionTest()
{
$product = \common\models\Product::find()
->where(['id' => 5779])
->with('firstImage')
->one();
$product = \yii\helpers\ArrayHelper::toArray($product);
print_r($product);
}
Recursive property is enabled by default.
public static array toArray ( $object, $properties = [], $recursive =
true)
So this piece of code should work correctly but it doesn't.
Action returns one level array without firstImage object.
What I'm doing wrong here?
PS:
Code was simplified for test purposes. I know that in this certain situation one can simply use asArray() method to get AR record in array.
You should use this instead :
$product = \common\models\Product::find()
->where(['id' => 5779])
->with('firstImage')
->asArray()
->one();
Read more about Retrieving Data in Arrays.
If your really want to use toArray(), and since a relation is not really an attribute or property, you should simply use the second parameter, e.g. :
$product = \yii\helpers\ArrayHelper::toArray($product, [
'common\models\Product' => [
// add needed properties here
// ...
'firstImage',
],
]);
Or, if you are using REST, you could override extraFields() in your model :
public function extraFields()
{
return ['firstImage'];
}
Read more about REST fields.