Yii2 ArrayHelper::toArray doesn't work recursively - php

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.

Related

How to assert paginations in Laravel?

I have a category model with the following method:
public static function index()
{
return self::has('posts')->paginate(1);
}
My category controller:
public function index()
{
$categories = Category::index();
return view('categories.index', compact('categories'));
}
This is what I've tried, I am using RefreshDatabase trait.
public function test_index_view_is_working()
{
factory(Post::class, 5)->create();
$response = $this->get(route('categories.index'));
$response->assertViewHas('categories', Category::index());
}
This test fails for some reason:
Failed asserting that two objects are equal.
at tests/Feature/CategoryTest.php:38
37| $response->assertViewIs('categories.index');
> 38| $response->assertViewHas('categories', Category::index());
--- Expected
+++ Actual
## ##
'dispatchesEvents' => Array ()
'observables' => Array ()
'relations' => Array (
+ 'posts' => Illuminate\Database\Eloquent\Collection Object (...)
)
'touches' => Array ()
'timestamps' => true
The reason you get this error is because somehow the posts are eager loaded from the view/controller but not from the tests.
I'm guessing return self::has('posts')->with('posts')->paginate(1); could fix it.
Alternatively, you can test if you have the pagination at the bottom the page. Since {{ $categories->links() }} will add something like Previous and Next you can still look for it.
$response = $this->get(route('categories.index'));
$response->assertSee('Next');
Also, you can ensure that you paginate the categories but it won't ensure you have added the links at the bottom of the page.
use Illuminate\Contracts\Pagination\Paginator;
...
$response = $this->get(route('categories.index'));
$this->assertInstanceOf(Paginator::class, $response->viewData('categories'));
Are you running any migrations/factories in the setUp method of the test?
It looks like maybe there are no post records in your database so $categories is coming into the view as null.
Also side note if all you want to do is make sure that the view has the variable $categories you can use $response->assertViewHas('categories');. This is not ideal if you are wanting to make sure your view is getting actual data.

Select specific fields from array of Eloquent collecton | Laravel

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;
}

Laravel: array to Model with relationship tree

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

Laravel 5.6: Invoking eloquent relationships change the collection data

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

Laravel custom collection property doesnt exist

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

Categories