Save model outside of for-each loop - php

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

Related

laravel eloquent foreach loop error Trying to get property 'location_id' of non-object

This one has happened to me before but I have no idea why and how to avoid it. So I have a static function in a Model which gets all the database rows and uses a foreach loop to read another table but I am unable to correctly read the row data:
public static function test()
{
$accounts = self::where( 'is_enabled', 1 )->get();
foreach ( $accounts as $account ) {
$map = AccountMap::where( 'account_id', $account->id )->first();
$location = Location::getLocation( $map->location_id );
$data = $location->getData();
}
}
So the above function gathers an array of items ($accounts) this is then passed into a foreach loop all is fine to this point but if i now use $account->id it is null. The id is shown in the Account object in its attributes folder.
A very similar function is used elsewhere in this model but it uses a passed id and this one works (however $account->id is null). The issue is not the database or column names:
public static function getThisLocation( $id )
{
$account = self::find( $id );
$map = AccountMap::where( 'account_id', $id )->first();
location = Location::getLocation( $map->location_id );
$data = $location->getData();
return $data;
}
*** EDIT ***
Account, AccountMap and Location are all Eloquent models
namespace App\Models;
use Eloquent;
use App\Notifications\AccountMessages;
use Kyslik\ColumnSortable\Sortable;
use Illuminate\Notifications\Notifiable;
/**
* #method static find(int $id)
*/
class Account extends Eloquent
{
use Sortable;
use Notifiable;
public $sortable = [
'id',
'name',
'lastupdate',
'url'
];
public static function test()
{
$accounts = self::where( 'is_enabled', 1 )->get();
foreach ( $accounts as $account ) {
$map = AccountMap::where( 'account_id', $account->id )->first();
$location = Location::getLocation( $map->location_id );
$data = $location->getData();
}
}
public static function getThisLocation( $id )
{
$account = self::find( $id );
$map = AccountMap::where( 'account_id', $id )->first();
location = Location::getLocation( $map->location_id );
$data = $location->getData();
return $data;
}
}
namespace App\Models;
use Eloquent;
use Kyslik\ColumnSortable\Sortable;
/**
* #method static where(string $string, int $id)
*/
class AccountMap extends Eloquent
{
use Sortable;
public $sortable = [
'id',
'account_id',
'location'
];
}
*** MORE EDIT ***
I have confirmed that using $account->attributes['id'] has worked but I've no idea why what I expected to work didn't ($account->id)
The problem must be something related to communication of your model and migration.
Add this dd() to your current test function:
public static function test()
{
$accounts = self::where( 'is_enabled', 1 )->get();
foreach ( $accounts as $account ) {
if ($account->id){
$map = AccountMap::where( 'account_id', $account->id )->first();
$location = Location::getLocation( $map->location_id );
$data = $location->getData();
} else {
dd($account)
}
}
}
Then Check the result and see is there the id filed on your response? If not, The id field doesn't exist on your self Model and it's Your problem's cause.
Finally, Check your model fields easily with :
public function testReturnOfSelfModel()
{
$data= self::all();
dd($data);
}
If you have id on this dd function, Your Model working properly. If not, you dont have id field.
Also, it is more professional to change Capitalize your model's first charachter. It sholud be Self, not self.
I'd suggest to setup two proper data Model (a migration would need to create these tables):
class Account extends Model {
protected $table = 'accounts';
public $timestamps = false;
/**
* The attributes that are mass assignable.
* #var array
*/
protected $fillable = [];
/**
* The attributes that should be hidden for arrays.
* #var array
*/
protected $hidden = [];
}
Unless defining protected $table it will definitely not know what to do.
It's rather unclear what you're even trying to accomplish with AcountMap, but it may need a relation defined; which eg. with return $this->belongsTo(Account::class); ...while simply adding lat & lng to class Account would be far less complex and perfectly fine, while it's only 1 location.

Laravel hasMany relationship not resturning values without parentheses

So I have this model, say City. And it has a OneToMany relationship with another model, say, Citizen.
On the city model, I have defined a relationship helper function
public function citizens()
{
return $this->hasMany(Citizen::class, 'city_id', 'id');
}
Now my problem is that, in a command, I have :
$cities = City::with('citizens')->get();
foreach ($cities as $city) {
$citizens = $city->citizens->pluck('user');
}
Yet it doesn't return anything. To get values I must turn this line to
$cities = City::all();
foreach ($cities as $city) {
$citizens = $city->citizens()->get()->pluck('user');
}
Does anyone have a clue how this might happen ? This started happening today with no apparent reason.
EDIT
To further illustrate the situation,
$cities = City::with('citizens')->get();
foreach ($cities as $city) {
dd($city->citizens()->count()); // => 5
dd($city->citizens->count()); // => 0
}
Here are the models definitions
// City.php
class City extends Model
{
use Searchable;
protected $table = 'cities';
public $incrementing = false;
protected $perPage = 20;
protected $fillable = [
'name',
'unique_code',
'extra_attributes'
];
protected $casts = [
'id' => 'string',
'codes' => 'array',
'extra_attributes' => SchemalessAttributes::class,
];
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->id = $model->id ?: Str::orderedUuid();
});
}
public function toSearchableArray(): array
{
return [
'name' => $this->name,
];
}
public function citizens()
{
return $this->hasMany(Citizen::class, 'city_id', 'id');
}
}
// Citizen.php
class Citizen extends Model
{
public $incrementing = false;
protected $perPage = 20;
protected $table = "citizens";
protected $fillable = [
'user_id',
'level_id',
'city_id',
];
public static function boot()
{
parent::boot();
self::creating(function ($model) {
$model->id = $model->id ?: Str::orderedUuid();
});
}
public function user() {
return $this->hasOne(User::class, 'id', 'user_id')->withTrashed();
}
public function city() {
return $this->hasOne(City::class, 'id', 'city_id');
}
}
the relation between City and Citizen is City hasMany Citizens...
In Laravel, hasMany relation reverse is belongsTo Not hasOne, see Laravel doc
so you should correct the relation In Citizen Model like this:
public function city() {
return $this->belongsTo(City::class, 'city_id');
}
There are two issues, one of which OMR has highlighted (you've used an incorrect relationship in your Citizen class), but that isn't the main issue. You're trying to pluck another relationship but unless you explicitly tell it to, Laravel won't eager load that relationship. You've only told it to eager load the Citizen relationship, not the User relationship. Thankfully, Laravel does support nested relationships. You need to update your query thusly:
$cities = City::with('citizens.user')->get();
The best way to solve this issue is by eager loading the citizens when you're getting your cities, that way you wont be executing too many queries (as you will have to get citizens for each city individually), it will save you a lot of execution time in the future when the database gets bigger if you do this :
$cities = City::with('citizens')->get();
foreach($cities as $city) {
$items = $city->citizens->pluck('...');
}

Updating a one to many relationship without the use of implode laravel

I am trying to update my data into my database where the relationship is that I have many schools, so when I try to save inside my database, I want to get something like this:
[![enter image description here][1]][1]
But I only know how to use implode function to do it, and I can only make it like this:
I tried doing this but it doesn't work:
public function update1(Request $request, $user_id){
$rows = qualification::where('user_id', $user_id)->get();
foreach ($rows as $row){
switch ($row['meta_key']){
case 'school_name':
$row['meta_value'] = $request->input('School');
break;
case 'start_date':
$row['meta_value'] = $request->input('SDate');
break;
case 'end_date':
$row['meta_value'] = $request->input('EDate');
break;
case 'qualification_list':
$row['meta_value'] = $request->input('qualification');
break;
}
$row->save();
}
return redirect('/home');
}
When I try using that code it will give me this error, Array to string conversion
PersonalInfo model:
class PersonalInfo extends Eloquent
{
use SoftDeletes;
protected $table = 'personal_infos';
protected $primaryKey = 'id';
protected $dates = ['deleted_at'];
public function userQualifications()
{
return $this->hasMany('App\Qualification','user_id');
}
Qualification model:
class Qualification extends Model
{
protected $table = "qualifications";
public function PersonalInfos() {
return $this->belongsTo('App\PersonalInfo');
}
}
No error message:
Assuming you have your relationship setup properly in your User model, you could try something like this (not tested):
public function update1(Request $request, $user_id){
// get the user
$user = PersonalInfo::find($user_id);
// create the new array of meta data
$data = array();
// loop through the inputs array count
for($i=0; $i < count($request->input('School')); $i++) {
$data[] = [
'meta_key' => 'school_name',
'meta_value' => $request->input('School')[$i]
];
$data[] = [
'meta_key' => 'start_date',
'meta_value' => $request->input('SDate')[$i]
];
$data[] = [
'meta_key' => 'end_date',
'meta_value' => $request->input('EDate')[$i]
];
$data[] = [
'meta_key' => 'qualification_list',
'meta_value' => $request->input('qualification')[$i]
];
}
// create the relationships
$user->userQualifications()->createMany($data);
return redirect('/home');
}

Laravel eloquent Mass assignment from multi-dimentional array

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

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

Categories