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.
As I think it is typically as the documentaion I have tried to use saveMany regarding its parameters as Model objects.
I have a model named Set and it hasMany Equipment so, from the from that creates a set I submit many fields of Equipment in-order to be saved as the following:
//Store method of SetController
$set = new Set();
$set->title = request('title');
$set->eqtype_id = request('eqtype');
$set->product_id = request('product');
$set->parts_count = request('parts_count');
$eq = request('equipments');
$set->equipments()->saveMany(array_map(function($n){return new \App\Equipment(['title' => $n, 'eqtype_id' => request('eqtype')]);}, $eq));
$set->save();
However, I always get the error message about that eqtype_id does not has a default value:
SQLSTATE[HY000]: General error: 1364 Field 'eqtype_id' doesn't have a
default value (SQL: insert into equipments (title, set_id,
updated_at, created_at) values (A-1, , 2017-03-18 12:07:30,
2017-03-18 12:07:30))
I thought that, may be, request('eqtype') is not accessible from the array_map callback, so I replaced it with a fixed number as follows:
$set->equipments()->saveMany(array_map(function($n){return new \App\Equipment(['title' => $n, 'eqtype_id' => 1]);}, $eq));
But also the error is same, it seems like a problem with either saveMany or array_map. I don't know how to fix this issue!
Notice
The model Equipment is related with the both model Set and Eqytpe as follows:
//Equipment model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* App\Equipment
*
* #mixin \Eloquent
*/
class Equipment extends Model
{
public $table = "equipments";
protected $fillable = ['title'];
public function set()
{
return $this->belongsTo(Set::class);
}
public function eqtype()
{
return $this->belongsTo(Eqtype::class);
}
I kind of had the same problem and this is what I did to fix it:
I had a One to Many relationship between a Product and Image and I wanted on a product creation add all the images related to it
// Create post
public function store(Request $request)
{
$newProduct = Product::create([
'name' => $request->name,
'description' => $request->description,
'price' => $request->price,
'stock_items' => $request->stock_items,
]);
for ($i = 0; $i < count($request->image_path); $i++) {
$newProduct->image()->saveMany([
new Image(['image_path' => $request->image_path[$i]])
]);
}
}
Don't forget the relationship functions & to create multiple input with the same name : name='image_path[]'
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']);
});
After creating model, when I try to get his attributes, i get only fields in database that are filled.
----------------------------------------------
DB: | id | shopID | name | bottleID | capacity |
----------------------------------------------
| 1 | 8 | Cola | 3 | |
----------------------------------------------
In this case I need capacity attribute too, as empty string
public function getDrinkData(Request $request)
{
$drink = Drink::where('shopId', $request->session()->get('shopId'))->first();
if($drink) {
$drink = $drink->attributesToArray();
}
else {
$drink = Drink::firstOrNew(['shopId' => $request->session()->get('shopId')]);
$drink = $drink->attributesToArray(); // i want to get even empty fields
}
return view('shop.drink')->(['drink' => $drink])
}
But for later usage (in view) I need to have all attributes, including empty ones. I know that this code works as it should, but I don't know how to change it to detect all attributes.
The model attributes are populated by reading the data from the database. When using firstOrNew() and a record doesn't exist, it makes a new instance of the model object without reading from the database, so the only attributes it has will be the ones manually assigned. Additionally, since there is no record in the database yet, you can't just re-read the database to get the missing data.
In this case, you can use Schema::getColumnListing($tableName) to get an array of all the columns in the table. With that information, you can create a base array that has all the column names as keys, and null for all the values, and then merge in the values of your Drink object.
public function getDrinkData(Request $request)
{
// firstOrNew will query using attributes, so no need for two queries
$drink = Drink::firstOrNew(['shopId' => $request->session()->get('shopId')]);
// if an existing record was found
if($drink->exists) {
$drink = $drink->attributesToArray();
}
// otherwise a new model instance was instantiated
else {
// get the column names for the table
$columns = Schema::getColumnListing($drink->getTable());
// create array where column names are keys, and values are null
$columns = array_fill_keys($columns, null);
// merge the populated values into the base array
$drink = array_merge($columns, $drink->attributesToArray());
}
return view('shop.drink')->(['drink' => $drink])
}
If you were using firstOrCreate(), then a new record is created when one doesn't exist. Since there a record in the database now, you could simply re-read the record from the database to populated all of the model attributes. For example:
public function getDrinkData(Request $request)
{
$drink = Drink::firstOrCreate(['shopId' => $request->session()->get('shopId')]);
// If it was just created, refresh the model to get all the attributes.
if ($drink->wasRecentlyCreated) {
$drink = $drink->fresh();
}
return view('shop.drink')->(['drink' => $drink->attributesToArray()])
}
What if you were to explicitly declare all the fields you want back.
An example of something I do that is a bit more basic as I'm not using where clauses and just getting all from Request object. I still think this could be helpful to someone out there.
public function getDrinkData(Request $request)
{
// This will only give back the columns/attributes that have data.
// NULL values will be omitted.
//$drink = $request->all();
// However by declaring all the attributes I want I can get back
// columns even if the value is null. Additional filtering can be
// added on if you still want/need to massage the data.
$drink = $request->all([
'id',
'shopID',
'name',
'bottleID',
'capacity'
]);
//...
return view('shop.drink')->(['drink' => $drink])
}
You should add a $fillable array on your eloquent model
protected $fillable = ['name', 'email', 'password'];
make sure to put the name of all the fields you need or you can use "*" to select all.
If you already have that, you can use the ->toArray() method that will get all attributes including the empty ones.
I'm using the same thing for my Post model and it works great with all fields including empty ones.
My model looks like this:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ["*"];
public function comments()
{
return $this->hasMany(Comment::class);
}
}
Do you need to set the $drink variable again after checking the $drink variable?
you can check the following code?
public function getDrinkData(Request $request)
{
$drink = Drink::where('shopId', $request->session()->get('shopId'))->first();
if(!count($drink)>0) {
$drink = Drink::firstOrNew(['shopId' => $request->session()->get('shopId')]);
}
return view('shop.drink')->(['drink' => $drink]); or
return view('shop.drink',compact('drink'));
}
hope it will help. :) :)
You can hack by overriding attributesToArray method
class Drink extends Model
{
public function attributesToArray()
{
$arr = parent::attributesToArray();
if ( !array_key_exist('capacity', $arr) ) {
$arr['capacity'] = '';
}
return $arr;
}
}
or toArray method
class Drink extends Model
{
public function toArray()
{
$arr = parent::toArray();
if ( !array_key_exist('capacity', $arr) ) {
$arr['capacity'] = '';
}
return $arr;
}
}
then
$dirnk->toArray(); // here capacity will be presented even it null in db
I get a very unhelpful error when I try and insert a new record in to my db. Does anyone have any idea where to start to solve this error?
user_id
That's all it says. (This is the name of one of my required fields in the table I'm saving to but it's not very descriptive.)
For reference here's my code:
$data = array(
'user_id'=>1,
'post_type'=>'0',
'post_title'=>'blah',
'description'=>'blah');
//it fails on this line
$id = Post::create($data);
Here's what my model looks like:
class Post extends Eloquent{
public static $rules = array(
'user_id'=>'required',
'post_type'=>'required',
'post_title' => 'required|max:25',
'description'=>'required'
);
public static function validate($data){
return Validator::make($data, static::$rules);
}
public function user(){
return $this->hasOne('User', 'id', 'user_id');
}
}
Tried getting rid of all relationships and validation in the model. That didn't work.
This is called mass-assignment in Laravel, what you need to do to get it working on your model is to set a protected array called $guarded to be empty. Add this to your model.
protected $guarded = array();
What this array does, it can prevent attributes to be mass-assigned with an array, if you don't want an attribute to be filled with the Model::create() method, then you need to specify that attribute in the $guarded array.
If instead you want to specify only the fillable attributes, Laravel's Eloquent also provides an array called $fillable where you specify only the attributes that you want to fill via the mass-assigment way.
Further reading:
Mass Assignment:
http://laravel.com/docs/eloquent#mass-assignment
Create Method:
http://laravel.com/docs/eloquent#insert-update-delete