I've been reviewing the documentation and API for Laravel Collections, but don't seem to find what I am looking for:
I would like to retrieve an array with model data from a collection, but only get specified attributes.
I.e. something like Users::toArray('id','name','email'), where the collection in fact holds all attributes for the users, because they are used elsewhere, but in this specific place I need an array with userdata, and only the specified attributes.
There does not seem to be a helper for this in Laravel? - How can I do this the easiest way?
You can do this using a combination of existing Collection methods. It may be a little hard to follow at first, but it should be easy enough to break down.
// get your main collection with all the attributes...
$users = Users::get();
// build your second collection with a subset of attributes. this new
// collection will be a collection of plain arrays, not Users models.
$subset = $users->map(function ($user) {
return collect($user->toArray())
->only(['id', 'name', 'email'])
->all();
});
Explanation
First, the map() method basically just iterates through the Collection, and passes each item in the Collection to the passed in callback. The value returned from each call of the callback builds the new Collection generated by the map() method.
collect($user->toArray()) is just building a new, temporary Collection out of the Users attributes.
->only(['id', 'name', 'email']) reduces the temporary Collection down to only those attributes specified.
->all() turns the temporary Collection back into a plain array.
Put it all together and you get "For each user in the users collection, return an array of just the id, name, and email attributes."
Laravel 5.5 update
Laravel 5.5 added an only method on the Model, which basically does the same thing as the collect($user->toArray())->only([...])->all(), so this can be slightly simplified in 5.5+ to:
// get your main collection with all the attributes...
$users = Users::get();
// build your second collection with a subset of attributes. this new
// collection will be a collection of plain arrays, not Users models.
$subset = $users->map(function ($user) {
return $user->only(['id', 'name', 'email']);
});
If you combine this with the "higher order messaging" for collections introduced in Laravel 5.4, it can be simplified even further:
// get your main collection with all the attributes...
$users = Users::get();
// build your second collection with a subset of attributes. this new
// collection will be a collection of plain arrays, not Users models.
$subset = $users->map->only(['id', 'name', 'email']);
use User::get(['id', 'name', 'email']), it will return you a collection with the specified columns and if you want to make it an array, just use toArray() after the get() method like so:
User::get(['id', 'name', 'email'])->toArray()
Most of the times, you won't need to convert the collection to an array because collections are actually arrays on steroids and you have easy-to-use methods to manipulate the collection.
Method below also works.
$users = User::all()->map(function ($user) {
return collect($user)->only(['id', 'name', 'email']);
});
You need to define $hidden and $visible attributes. They'll be set global (that means always return all attributes from $visible array).
Using method makeVisible($attribute) and makeHidden($attribute) you can dynamically change hidden and visible attributes. More: Eloquent: Serialization -> Temporarily Modifying Property Visibility
I have now come up with an own solution to this:
1. Created a general function to extract specific attributes from arrays
The function below extract only specific attributes from an associative array, or an array of associative arrays (the last is what you get when doing $collection->toArray() in Laravel).
It can be used like this:
$data = array_extract( $collection->toArray(), ['id','url'] );
I am using the following functions:
function array_is_assoc( $array )
{
return is_array( $array ) && array_diff_key( $array, array_keys(array_keys($array)) );
}
function array_extract( $array, $attributes )
{
$data = [];
if ( array_is_assoc( $array ) )
{
foreach ( $attributes as $attribute )
{
$data[ $attribute ] = $array[ $attribute ];
}
}
else
{
foreach ( $array as $key => $values )
{
$data[ $key ] = [];
foreach ( $attributes as $attribute )
{
$data[ $key ][ $attribute ] = $values[ $attribute ];
}
}
}
return $data;
}
This solution does not focus on performance implications on looping through the collections in large datasets.
2. Implement the above via a custom collection i Laravel
Since I would like to be able to simply do $collection->extract('id','url'); on any collection object, I have implemented a custom collection class.
First I created a general Model, which extends the Eloquent model, but uses a different collection class. All you models need to extend this custom model, and not the Eloquent Model then.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model as EloquentModel;
use Lib\Collection;
class Model extends EloquentModel
{
public function newCollection(array $models = [])
{
return new Collection( $models );
}
}
?>
Secondly I created the following custom collection class:
<?php
namespace Lib;
use Illuminate\Support\Collection as EloquentCollection;
class Collection extends EloquentCollection
{
public function extract()
{
$attributes = func_get_args();
return array_extract( $this->toArray(), $attributes );
}
}
?>
Lastly, all models should then extend your custom model instead, like such:
<?php
namespace App\Models;
class Article extends Model
{
...
Now the functions from no. 1 above are neatly used by the collection to make the $collection->extract() method available.
I had a similar issue where I needed to select values from a large array, but I wanted the resulting collection to only contain values of a single value.
pluck() could be used for this purpose (if only 1 key item is required)
you could also use reduce(). Something like this with reduce:
$result = $items->reduce(function($carry, $item) {
return $carry->push($item->getCode());
}, collect());
This is a follow up on the #patricus answer above:
Use the following to return collection of object instead of array so you can use same as models object i.e. $subset[0]->name
$subset = $users->map(function ($user) {
return (object) $user->only(['id', 'name', 'email']);
});
this is a follow up on the patricus [answer][1] above but for nested arrays:
$topLevelFields = ['id','status'];
$userFields = ['first_name','last_name','email','phone_number','op_city_id'];
return $onlineShoppers->map(function ($user) {
return collect($user)->only($topLevelFields)
->merge(collect($user['user'])->only($userFields))->all();
})->all();
This avoid to load unised attributes, directly from db:
DB::table('roles')->pluck('title', 'name');
References: https://laravel.com/docs/5.8/queries#retrieving-results (search for pluck)
Maybe next you can apply toArray() if you need such format
Context
For instance, let's say you want to show a form for creating a new user in a system where the User has one Role and that Role can be used for many Users.
The User model would look like this
class User extends Authenticatable
{
/**
* User attributes.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password', 'role_id'
];
/**
* Role of the user
*
* #return \App\Role
*/
public function role()
{
return $this->belongsTo(Role::class);
}
...
and the Role model
class Role extends Model
{
/**
* Role attributes.
*
* #var array
*/
protected $fillable = [
'name', 'description'
];
/**
* Users of the role
*
* #return void
*/
public function users()
{
return $this->hasMany(User::class);
}
}
How to get only the specific attributes?
In that specific form we want to create, you'll need data from Role but you don't really need a description.
/**
* Create user
*
* #param \App\Role $model
* #return \Illuminate\View\View
*/
public function create(Role $role)
{
return view('users.create', ['roles' => $role->get(['id', 'name'])]);
}
Notice that we're using $role->get(['id', 'name']) to specify we don't want the description.
this seems to work, but not sure if it's optimized for performance or not.
$request->user()->get(['id'])->groupBy('id')->keys()->all();
output:
array:2 [
0 => 4
1 => 1
]
$users = Users::get();
$subset = $users->map(function ($user) {
return array_only(user, ['id', 'name', 'email']);
});
Related
I have three tables: products, product_inventories and product_inventory_details. The ORM of each model is shown below,
Product Model
class Product extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
...,
'title',
'selected_inventory_id',
...
];
/**
* Get the inventories those belongs to this model.
*/
public function inventory(){
return $this->hasMany('App\Models\ProductInventory');
}
/**
* Get the selected product_inventory_detail that owns this model.
*/
public function selected(){
return $this->hasOne('App\Models\ProductInventoryDetail', 'id', 'selected_inventory_id');
}
...
}
ProductInventory Model
class ProductInventory extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_id',
...
];
/**
* Get the inventory_details those belongs to this model.
*/
public function items(){
return $this->hasMany('App\Models\ProductInventoryDetail');
}
...
}
ProductInventoryDetail Model
class ProductInventoryDetail extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_inventory_id',
'price',
...
];
}
I'm sorting and limiting the results through user input of Sort by dropdown and Show per page dropdown. When sorting by Alphabetical: High to Low option I'm running the query builder method to order the results:
$products = $products->orderBy($sort['column'], $sort['order'])->paginate($limit);
Now with sorting by Price, I can't run the query builder method orderBy() since I'm not using joins and getting the data through relationship properties. Instead I'm using the Laravel's collection method to sort it out:
$products = $products->paginate($limit);
$products = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
The above block is working fine if I don't use pagination methods. But, I need to use the pagination as well since the user can also limit the results per page. I'm also using a Paginator object's method to append some parameters to each page URL:
$products->appends($paramsArr);
Since running the sortBy() method returns a collection instead of Paginator object, it's giving me undefined method exception.
My Question
How can I sort the result set by price in my current scenario without having to implement the joins? Is there a way to achieve that??
I would use QueryBuilder package of Spatie. It will make your life easier for creating sortable and filterable grid table. You use that package this way:
$query = Product::with(['inventory', 'selected']);
$products = \Spatie\QueryBuilder\QueryBuilder::for($query)
->allowedFilters([
'name' => 'name', // name column in the products DB table.
'selected.price' => 'product_inventory_details.column_price', // price column in the product_inventory_details DB table.
])
->defaultSort('name')
->allowedSorts([
'name',
\Spatie\QueryBuilder\AllowedSort::custom('selected.price', new SortSelectedPrice())
])
->paginate(20)
->appends(request()->query());
External custom sort class looks like this:
class SortSelectedPrice implements \Spatie\QueryBuilder\Sorts\Sort
{
public function __invoke(Builder $query, bool $descending, string $property)
{
$direction = $descending ? 'DESC' : 'ASC';
$query->leftJoin('product_inventory_details', 'products.id', '=', 'product_inventory_details.product_id');
$query->orderBy('product_inventory_details.column_price', direction);
}
}
Make sure your URL containing the query string like this for sorting name from A to Z and sorting price from 1xxxxxxxx to 0:
domain.com/products?sort=name,-selected.price
I installed the Spatie package using composer. Don't forget to do that.
I found a way to handle it without having to implement the joins. I added a new variable and stored the Paginator object's items() method result set into it.
$products = $products->paginate($limit);
$productItems = $products->items(); // returns the items array from paginator
And sort that particular variable instead of sorting the whole paginator object. That way my links and URLs are untouched and unmodified in the $products variable and the data of the products are in a separate variable.
if($sort['column'] == 'price'){
if($sort['order'] == 'DESC'){
$productItems = $products->sortByDesc(function($prod, $key){
return $prod->selected->price;
});
} else{
$productItems = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
}
}
I also had to change my rendering variable from $products to $productItems and accessed the pagination links from the old $products variable.
#forelse ($productItems as $product)
#include('site.components.product-grid')
#empty
<div class="col text-center">...</div>
#endforelse
...
{{ $products->links() }}
I'm posting it here for the community to benefit/discuss/criticize if there is a better way.
I have a laravel table with a column I've defined like this in the migration:
$table->json('json');
And in the model, I cast it to an array:
protected $casts = [
'json' => 'array'
];
This works perfectly the majority of the time I need it, but there's one api call I'm making where I actually want my collection of that Model to give me the raw string rather than casting it to the array.
So, assuming my model is called Model, my api call looks like this:
$systemModels = Model::whereNull('user_id')->get();
$userModels = Model::where('user_id', $user->id)->get();
return response()->json([
'user_models' => $userModels->toArray(),
'system_models' => $systemModels->toArray()
]);
This is where I'd like the 'json' column of my Model to be rendered as a string rather than cast to an array. Is there a reliable way to do that?
Inside your model you can define a custom attribute which is added when the model is serialized:
class YourModel extends Model
{
protected $appends = ['json_raw'];
public function getJsonRawAttribute()
{
return $this->attributes['json'];
// or return json_encode($this->attributes['json']);
}
}
And then when doing the toArray() you can do $userModels->makeHidden('json')->toArray(); to remove the casted field you do not want.
In my controller, I want to be able to call the model Category and give it an array ('dbField' => 'value') and I want to be able to use this array in a where clause.
My Controller:
$categories = new Category(['name' => 'category name']);
My Model:
public function __construct($attributes = [])
{
parent::__construct($attributes);
...
}
But it doesn't seem to work that way, whenever you pass attributes to the model constructor it's only for mass assignement, is there any way to do that ?
The constructor is for filling attributes.
You can try an actual where
$attributes = ['name' => 'blah'];
$findIt = Category::where($attributes)->get(); // or first();
// get the first matched record based on the attributes or return a new instance filled with those attributes
$findItorNot = Category::firstOrNew($attributes);
I am loading some data into an Eloquent model via an appended attribute and the returned model's attribute is always null. I have protected $appends = array('survey_status); defined in my model (named Survey), and have the accessor defined as such:
public function getSurveyStatusAttribute($value){
return $value;
}
I have tried setting the attribute both as a property and in bracket notation($this->survey_status = ... & $this->attributes['survey_status'] = ..) and also tried using the setAppends() method prior to returning the model, all to no avail.
This was reported on the Laravel forums back at the end of Sept. 2013 and reported as fixed that Oct. (see: https://github.com/laravel/framework/issues/2336 and http://laravel.io/forum/02-26-2014-model-attribute-accessor-not-working-with-object-get-helper-function)
I am on the most current version of Laravel 4 (v4.2.17) which was released in Feb of this year. And from what I read in the docs and elsewhere it seems as though I'm doing everything correctly. Can any see something I'm not doing or confirm this is still an issue?
UPDATE
So I think I figured out 75% of my issue. I didn't realize you could pass an array to $model->load() to make complex queries using where/orWhere/etc. So this basic example works:
$survey = Survey::find(168);
$survey->load(array('surveyStatus' => function ($q){
$q->where('organization_id', '=', 7485);
}));
return Response::json($survey);
In the response my SurveyStatus model is supplied. My issue now is I am trying to iterate of a collection of Survey models to add a SurveyStatus relation just like the working one above but the attribute isn't there on the response. This is what I'm using to iterate the collection:
$org->surveySets->each(function ($ss) use ($id){
$fye = $ss->fiscal_year_end;
$ss->surveys->each(function ($survey) use ($id, $fye){
$sid = $survey->id;
$survey->load(array('surveyStatus' => function ($q) use($id, $fye){
$q->where('organization_id', '=', $id)->where('fiscal_year_end', '=', $fye);
}));
$survey->content_groups->each(function ($cg) use ($id, $sid, $fye){
$cg->content_rows->each(function ($cr) use ($id, $sid, $fye){
$cr->questions->each(function ($q) use ($id, $sid, $fye){
// do the same thing as surveyStatus but load surveyData relation into question
});
});
});
});
});
Is there some reason the loading doesn't 'stick' when iterating over a collection?
Correct me if I'm wrong, but appends doesn't get passed a $value because it's not mapped to a table column. I always thought of it as a computed property of sorts.
Given we have fillable columns 'first' and 'last' we might create an appends called 'fullname'.
protected $appends = [
'fullname'
];
public function getFullnameAttribute()
{
return $this->attributes['fullname'] = $this->first . ' ' . $this->last;
}
Essentially what I think your confusing is that appends is extra data that your appending to your model attributes. You are expected to return a value from the accessor. Returning $value will be null because $value doesn't exist, which is why your manually appending it. Try returning 'foo' in your accessor then you'll see what I mean.
Hello if you want to append some extra data related to another model you could do something like this.
protected $appends = [
'catName'
];
// relation
public function category()
{
return $this->hasOne(PostCat::class, 'id', 'id_cat');
}
//getters
public function getCatNameAttribute()
{
return $this->category()->getEager()->first()->name;
}
If your related model hold many db row considere this
protected $with = [
'category'
];
protected $appends = [
'catName'
];
// relation
public function category()
{
return $this->hasOne(PostCat::class, 'id', 'id_cat');
}
//getters
public function getCatNameAttribute()
{
return $this->category->name;
}
best regards
Not sure if I set this up correctly. In Laravel I'm creating two models with a many-to-may relationship
The models are Item and Tags. Each one contains a belongsTo to the other.
When I run a query like so:
Item::with('tags')->get();
It returns the collection of items, with each item containing a tags collection. However the each tag in the collection also contains pivot data which I don't need. Here it is in json format:
[{
"id":"49",
"slug":"test",
"order":"0","tags":[
{"id":"3","name":"Blah","pivot":{"item_id":"49","tag_id":"3"}},
{"id":"13","name":"Moo","pivot":{"item_id":"49","tag_id":"13"}}
]
}]
Is there anyway to prevent this data from getting at
you can just add the name of the field in the hidden part in your model like this:
protected $hidden = ['pivot'];
that's it , it works fine with me.
You have asked and you shall receive your answer. But first a few words to sum up the comment section. I personally don't know why you would want / need to do this. I understand if you want to hide it from the output but not selecting it from the DB really has no real benefit. Sure, less data will be transferred and the DB server has a tiny tiny bit less work to do, but you won't notice that in any way.
However it is possible. It's not very pretty though, since you have to override the belongsToMany class.
First, the new relation class:
class BelongsToManyPivotless extends BelongsToMany {
/**
* Hydrate the pivot table relationship on the models.
*
* #param array $models
* #return void
*/
protected function hydratePivotRelation(array $models)
{
// do nothing
}
/**
* Get the pivot columns for the relation.
*
* #return array
*/
protected function getAliasedPivotColumns()
{
return array();
}
}
As you can see this class is overriding two methods. hydratePivotRelation would normally create the pivot model and fill it with data. getAliasedPivotColumns would return an array of all columns to select from the pivot table.
Now we need to get this integrated into our model. I suggest you use a BaseModel class for this but it also works in the model directly.
class BaseModel extends Eloquent {
public function belongsToManyPivotless($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null){
if (is_null($relation))
{
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table))
{
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new BelongsToManyPivotless($query, $this, $table, $foreignKey, $otherKey, $relation);
}
}
I edited the comments out for brevity but otherwise the method is just like belongsToMany from Illuminate\Database\Eloquent\Model. Of course except the relation class that gets created. Here we use our own BelongsToManyPivotless.
And finally, this is how you use it:
class Item extends BaseModel {
public function tags(){
return $this->belongsToManyPivotless('Tag');
}
}
If you want to remove pivot data then you can use as protected $hidden = ['pivot']; #Amine_Dev suggested, so i have used it but it was not working for me,
but the problem really was that i was using it in wrong model so i want to give more detail in it that where to use it, so you guys don't struggle with the problem which i have struggled.
So if you are fetching the data as :
Item::with('tags')->get();
then you have to assign pivot to hidden array like below
But keep in mind that you have to define it in Tag model not in Item model
class Tag extends Model {
protected $hidden = ['pivot'];
}
Two possible ways to do this
1. using makeHidden method on resulting model
$items = Item::with('tags')->get();
return $items->makeHidden(['pivot_col1', 'pivot_col2']...)
2. using array_column function of PHP
$items = Item::with('tags')->get()->toArray();
return array_column($items, 'tags');