Laravel eloquent Mass assignment from multi-dimentional array - php

I'm creating a Restful application, so I'm recieving a POST request that could seem like this
$_POST = array (
'person' => array (
'id' => '1',
'name' => 'John Smith',
'age' => '45',
'city' => array (
'id' => '45',
'name' => 'London',
'country' => 'England',
),
),
);
I would like to save my person model and set its city_id.
I know that the easiest way is to set it manually with $person->city_id = $request['city']['id]; but this way isn't helping me....this code is only an example, in my real code, my model has 15 relationships
Is there any way to make it in a similar such as $person->fill($request);?
My models look like:
City
class City extends Model {
public $timestamps = false;
public $guarded= ['id'];//Used in order to prevent filling from mass assignment
public function people(){
return $this->hasMany('App\Models\Person', 'city_id');
}
}
Person
class Person extends Model {
public $timestamps = false;
public $guarded= ['id'];//Used in order to prevent filling from mass assignment
public function city(){
return $this->belongsTo('App\Models\City', 'city_id');
}
public static function savePerson($request){//Im sending a Request::all() from parameter
$person = isset($request['id']) ? self::find($request['id']) : new self();
$person->fill($request);//This won't work since my $request array is multi dimentional
$person->save();
return $person;
}
}

This is a bit tricky, but you can override fill method in your model, and set deeplyNestedAttributes() for storing attributes thats will be looking for in the request
class Person extends Model {
public $timestamps = false;
public $guarded= ['id'];//Used in order to prevent filling from mass assignment
public function city(){
return $this->belongsTo('App\Models\City', 'city_id');
}
public static function savePerson($request){//Im sending a Request::all() from parameter
$person = isset($request['id']) ? self::find($request['id']) : new self();
$person->fill($request);//This won't work since my $request array is multi dimentional
$person->save();
return $person;
}
public function deeplyNestedAttributes()
{
return [
'city_id',
// another attributes
];
}
public function fill(array $attributes = [])
{
$attrs = $attributes;
$nestedAttrs = $this->deeplyNestedAttributes();
foreach ($nestedAttrs as $attr) {
list($relationName, $relationAttr) = explode('_', $attr);
if ( array_key_exists($relationName, $attributes) ) {
if ( array_key_exists($relationAttr, $attributes[$relationName]) ) {
$attrs[$attr] = $attributes[$relationName][$relationAttr];
}
}
}
return parent::fill($attrs);
}
}

Related

Save model outside of for-each loop

Assuming the Model Order
class Order extends Model {
use HasFactory;
protected $table = 'order';
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
public function extra(){
return $this->hasOne(Extra::class);
}
public function products(){
return $this->hasMany(Product::class);
}
}
and the Model Extra
class Extra extends Model {
use HasFactory;
protected $table = 'extra';
protected $guarded = [];
public function order(){
$this->belongsTo(Order::class);
}
}
and the Model product
class Product extends Model {
use HasFactory;
protected $table = 'product';
protected $guarded = [];
public function order(){
return $this->belongsTo(Order::class);
}
}
Now, from an API I receive data. With these data, I want to feed the models and then store the info to DB.
The approach there is atm is:
foreach ($list as $item) {
$order = new Order();
$order->id = $item['id'];
$order->title = $item['title'];
$order->save();
$extra = new Extra();
$extra->foo= $item['path']['to']['foo'];
$extra->bar= $item['path']['to']['bar'];
$order->extra()->save($extra)
$order->products()->createMany($item['path']['to']['products']);
}
The problem is that this code saves three times for each loop, one for order, one for extra, one for the product.
I would like to know if there is another way that I can use in order to gather the data inside the for-each and outside of it, to make something like
Order::insert($array_of_data);
I imagine it would look something like this, try it and if doesn't work please let me know i'll delete answer
$orders = [];
$extras = [];
$products = [];
foreach ($list as $item) {
$orders[] = [
'id' => $item['id'],
'title' => $item['title'],
];
$extras[] = [
'foo' => $item['path']['to']['foo'],
'bar' => $item['path']['to']['bar'],
];
$products[] = [
'order_id' => $item['id'],
'foo' => $item['path']['to']['products']['foo'] // or data it has
];
}
Order::insert($orders);
Extra::insert($extras);
Product::insert($products); // make sure each product has order id and data which is not visible here
I also suggest looking into converting $list into collection and then iterating over it, if the data is quite big you might make a use of LazyCollection which is the same as collection but better for processing larger data sets
Here's an example how you'd do it using lazy collection
LazyCollection::make($list)
->each(function (array $item) {
$order = Order::create(
[
'id' => $item['id'],
'title' => $item['title']
],
);
Extra::create(
[
'order_id' => $item['id'],
'foo' => $item['path']['to']['foo'],
'bar' => $item['path']['to']['bar'],
],
);
$order->products()->createMany($item['path']['to']['products']);
});
While it doesn't necessarily create many at once, it it memory saviour and will process quite quickly

How to validate model object instance in Laravel?

I've got the following model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $guarded = ['id'];
public static function rules()
{
return [
'created_at' => 'nullable',
'updated_at' => 'nullable',
'name' => 'required|string|between:1,255',
'description' => 'required|string|between:1,255',
'date_added' => 'nullable',
'date_edited' => 'nullable',
'unit' => 'required|string|between:1,255',
'unit_type' => 'required|integer',
'stock' => 'nullable|string|between:0,255',
'barcode' => 'nullable|string|between:0,32',
'tax' => 'nullable|float',
'price' => 'nullable|float',
'category_id' => 'required|integer|gt:0'
];
}
}
And there's a controller ProductController that has an action insertProduct.
class class ProductController extends ApiController { // controller class
public function insertProduct(Request $request) {
$inputJson = $request->input('data_json', null);
if(empty($inputJson)) {
$inputJson = $request->getContent();
if(empty($inputJson)) {
return $this->errorResponse(
'Either data_json formdata parameter or request body should contain a JSON string.'
);
}
}
try {
$product = $this->extractProductFromJSON($inputJson);
} catch(\Exception $e) {
return $this->errorResponse($e->getMessage(), 400);
}
// When I dump the Product ($product) instance using dd(), it is just as expected and its
// properties contain the right values.
$validator = Validator::make($product, Product::rules());
/* Above line causes exception:
TypeError: Argument 1 passed to Illuminate\Validation\Factory::make() must be of the type
array, object given,
called in /.../vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
on line 261 in file /.../vendor/laravel/framework/src/Illuminate/Validation/Factory.php
on line 98
*/
if($validator->fails()) {
return $this->errorResponse($validator->errors()->first());
}
// ...
}
// How I extract the data from the JSON string (which is input).
// Although I don't think this has anything to do with my problem.
private function extractProductFromJSON(string $json) {
$data = \json_decode($json);
if(\json_last_error() != JSON_ERROR_NONE) {
throw new \Exception('Error parsing JSON: ' . \json_last_error_msg());
}
try {
$productData = $data->product;
$productId = empty($productData->id) ? null : $productData->id;
// Product id is allowed to be absent
$product = new Product(); // My \App\Product model instance.
$product->id = $productId;
$product->name = $productData->name;
$product->description = $productData->description;
$product->date_added = $productData->date_added;
$product->date_edited = $productData->date_edited;
$product->unit = $productData->unit;
$product->unit_type = $productData->unit_type;
$product->stock = $productData->stock;
$product->barcode = $productData->barcode;
$product->tax = $productData->tax;
$product->price = $productData->price;
$product->category_id = $productData->category_id;
return $product;
} catch(\Exception $e) {
echo 'EXCEPTION...';
}
}
} // end of controller class
It seems pretty clear there's something wrong with the following line:
$validator = Validator::make($product, Product::rules());
The simplest cause I can think of, is that the validator simply does not accept objects and only wants arrays.
If not, what could be the problem?
If Laravel's validation only works with arrays, is it somehow possible to validate an object?
validator = Validator::make($product, Product::rules());
the problem is not Product::rules() but $product. Product::rules() is correct because it's return an array, but $product is an object instead of an array. You should change/convert $product to an array an example:
validator = Validator::make((array)$product, Product::rules());
Yes! You're right. If we look at the make method, we can see that it accepts rules as an array.
* #param array $data
* #param array $rules
* #param array $messages
* #param array $customAttributes
* #return \Illuminate\Validation\Validator
* #static
*/
public static function make($data, $rules, $messages = [], $customAttributes = [])
{
/** #var \Illuminate\Validation\Factory $instance */
return $instance->make($data, $rules, $messages, $customAttributes);
}
I usually validate data directly in the controller
$validator = Validator::make($info, [
'shortDescription' => 'required',
'description' => 'required',
'countryId' => 'required',
'cities' => 'required | array | min:1',
]);
class Cliente extends Eloquent
{
public static $autoValidates = true;
protected static $rules = [];
protected static function boot()
{
parent::boot();
// or static::creating, or static::updating
static::saving(function($model)
{
if ($model::$autoValidates) {
return $model->validate();
}
});
}
public function validate()
{
}
}

Laravel get data from db array not working

When I run the code I get no error but the data I am trying to display is not displaying it's just blank.. can someone tell me what I'm doing wrong?
My controller:
public function openingPage($id) {
$this->getGames();
$games = $this->getGames();
return view('caseopener')->with('games',$games);
}
private function getGames() {
$games = array();
foreach ($this->data->items as $item) {
$game = new Game($item);
$games[] = array(
'id' => $game['id'],
'name' => $game['name'],
'price' => $game['price'],
'image' => $game['image'],
);
}
return $games;
}
The 'Game' Model that is used in 'getGames function':
class Game extends Model
{
private $id;
public $data;
public function __construct($id) {
parent::__construct();
$this->id = $id;
$this->data = $this->getData();
}
private function getData() {
$game = DB::table('products')->where('id', 1)->first();
if(empty($game)) return array();
return $game;
}
}
The view:
#foreach ($games as $game)
<div class="gold">$ {{ $game['price'] }}</div>
#endforeach
I think you are over-complicating things. You could simplify your flow like this:
Given your provided code, it seems like you are using a custom table name ('products') in your Game model. So we'll address this first:
Game.php
class Game extends Model
{
protected $table = 'products'; //
}
Now, it seems like you're searching an array of Game ids ($this->data->items). If so, you could make use of Eloquent for your query, specially the whereIn() method:
YourController.php
public function openingPage($id)
{
$games = Game::whereIn('id', $this->data->items)->get();
return view('caseopener')->with('games', $games);
}
Optionally, if you want to make sure of just returning the id, name, price and image of each Game/product, you could format the response with API Resources:
php artisan make:resource GameResource
Then in your newly created class:
app/Http/Resources/GameResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class GameResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'image' => $this->image,
];
}
}
So now just update your controller:
YourController.php
use App\Http\Resources\GameResource;
public function openingPage($id)
{
$games = Game::whereIn('id', $this->data->items)->get();
return view('caseopener')->with('games', GameResource::collection($games));
} // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Laravel save many-to-many relationship in Eloquent mutators

I've got 2 models with a many-to-many relationship. I want to be able to set a specific attribute with an array of ids and make the relationship in the mutator like this:
<?php
class Profile extends Eloquent {
protected $fillable = [ 'name', 'photo', 'tags' ];
protected $appends = [ 'tags' ];
public function getTagsAttribute()
{
$tag_ids = [];
$tags = $this->tags()->get([ 'tag_id' ]);
foreach ($tags as $tag) {
$tag_ids[] = $tag->tag_id;
}
return $tag_ids;
}
public function setTagsAttribute($tag_ids)
{
foreach ($tag_ids as $tag_id) {
$this->tags()->attach($tag_id);
}
}
public function tags()
{
return $this->belongsToMany('Tag');
}
}
<?php
class Tag extends Eloquent {
protected $fillable = [ 'title' ];
protected $appends = [ 'profiles' ];
public function getProfilesAttribute()
{
$profile_ids = [];
$profiles = $this->profiles()->get([ 'profile_id' ]);
foreach ($profiles as $profile) {
$profile_ids[] = $profile->profile_id;
}
return $profile_ids;
}
public function profiles()
{
return $this->belongsToMany('Profile');
}
}
However the setTagsAttribute function isn't working as expected. I'm getting the following error: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'profile_id' cannot be null (SQL: insert intoprofile_tag(profile_id,tag_id) values (?, ?)) (Bindings: array ( 0 => NULL, 1 => 1, ))
You can't attach many-to-many relations until you've saved the model. Call save() on the model before setting $model->tags and you should be OK. The reason for this is that the model needs to have an ID that Laravel can put in the pivot table, which needs the ID of both models.
It looks like you're calling the function incorrectly or from an uninitialized model. The error says that profile_id is NULL. So if you're calling the function as $profile->setTagsAttribute() you need to make sure that $profile is initialized in the database with an ID.
$profile = new Profile;
//will fail because $profile->id is NULL
//INSERT: profile->save() or Profile::Create();
$profile->setTagsAttribute(array(1,2,3));
Additionally, you can pass an array to the attach function to attach multiple models at once, like so:
$this->tags()->attach($tag_ids);
You can also pass it the model instead of the ID (but pretty sure array of models won't work)
Try using the sync method:
class Profile extends Eloquent {
protected $fillable = [ 'name', 'photo', 'tags' ];
protected $appends = [ 'tags' ];
public function getTagsAttribute()
{
return $this->tags()->lists('tag_id');
}
public function setTagsAttribute($tag_ids)
{
$this->tags()->sync($tagIds, false);
// false tells sync not to remove tags whose id's you don't pass.
// remove it all together if that is desired.
}
public function tags()
{
return $this->belongsToMany('Tag');
}
}
Don't access the tags through the tags() function, rather use the tags property. Use the function name if you want to pop additional parameters onto the relationship query and the property if you just want to grab the tags. tags() works in your getter because you're using get() on the end.
public function setTagsAttribute($tagIds)
{
foreach ($tagIds as $tagId)
{
$this->tags->attach($tagId);
}
}

Default Fields and values in MongoDB, using cakePHP

I am building an application with cakephp 2.1 and mongodb 2.03 , I am using ishikaway's mongodb datasource
I need to set some default values that would be added to a model , I am doing it like so
<?php
class DynamicFormResponse extends AppModel{
public $useDbConfig = 'mongodb';
public $useTable = 'dynamicFormsResponse';
public $primaryKey = '_id';
public $validate=array();
public $mongoschema = array(
'created' => array('type' => 'datetime'),
'modified' => array('type' => 'datetime'),
'escalation'=>array(
'type'=>"integrer",
"default"=>0
),
"status"=>array(
"type"=>"string",
"default"=>"pending"
),
);
public function setSchema($schema) {
$this->_schema=$schema;
}
public function getSchema(){
return $this->_schema;
}
}
Obviously I cannot set default values directly in MongoDb like MySQL, and obviously since I am asking the question the above method is not working.
Any suggestions on how I can solve this ?
Ps:
This did not help CakePHP - Set Default Field Values in the Model
The documentation is not clear either
Full code available here
EDIT:
I have currently solved this problem by committing an MVC sin,
I am adding the default values in the controller before saving the data with the model
<?php
class DynamicFormResponse extends AppModel {
public $name="DynamicFormResponse";
public $useDbConfig = 'mongodb';
public $useTable = 'dynamicFormResponse';
public $primaryKey = '_id';
public $validate = array();
public function getDefaults(){
$defaultValues=array(
"escalation"=>0,
"status"=>"pending",
"department_id"=>NULL,
"user_agent"=>env("HTTP_USER_AGENT")
);
return $defaultValues;
}
...
...
class DynamicFormsController extends AppController {
...
...
public function getForm($id=null){
...
...
/**
* Set defaults values
*/
foreach ($this->DynamicFormResponse->getDefaults() as $fieldName => $defaultValue) {
if (empty($this-> request-> data[$this-> DynamicFormResponse -> alias][$fieldName]))
$this->request->data[$this-> DynamicFormResponse -> alias][$fieldName] = $defaultValue;
}
/**
* Data Validation
*/
if($this->DynamicFormResponse->save($this->request->data) == true ){
$this->set("ticket_id", $this->DynamicFormResponse->id);
$this->render('ticket_successfully_saved');
return;
}
Is there a better solution ? because that seems like a bad way to do it .
it's not really a mongoDB question but anyways, i suggest you to merge your userdata with your default values in beforeSave().
We declare default values in each Model like this:
public $defaultValues = array(
'report' => 't',
'reportinterval' => '7',
'type' => '0'
);
And merge it in beforeSave():
/**
* Extends beforeSave() to add default values
*
* #param array $options
* #return bool
*/
public function beforeSave($options = array()) {
// Add default values if not set already
foreach ($this->defaultValues as $fieldName => $defaultValue) {
if (empty($this->data[$this->alias][$fieldName]))
$this->data[$this->alias][$fieldName] = $defaultValue;
}
return parent::beforeSave($options);
}

Categories