Laravel Eloquent saving an object with children - php

I am trying to save an order with order_items but I am not really finding anything in the docs to support this use case. A hasMany relationship.
Basically there is an orders table with something like id | user_id and an order_items table with id | order_id | product_id.
How can I save() the order and use an array of items at the same time without having to loop over the items and save them individually?
Is this possible?
Pseudo code assuming $items is an array:
$items = Session::get("cart.items");
$order = new Order;
$order->user_id = Auth::user()->id;
$order->order_items = $items;
$order->save();

What you need for a hasMany relation is either saveMany or createMany, depending on what's in your $items array:
// array of attributes:
$items = [
['name'=>'item1','price'=>'price1'],
...
];
// then createMany:
$order->orderItems()->createMany($items);
This will create new rows in Items table.
// array of models:
$items = [
Item::find($someId),
Item::find($anotherId),
// and/or newly instantiated:
new Item(['name'=>'item1','price'=>'price1']),
...
];
// then createMany:
$order->orderItems()->saveMany($items);
This will associate (save) existing models, and create non-existing ones.
Also notice that I use camelCase relation name orderItems instead of your order_items.
This is an important detail, since Eloquent (Laravel v4) looks for camelCased methods on the model when working with relations (dynamic properties).
//Order model
public function orderItems()
{
return $this->hasMany(...);
}
$order->orderItems; // collection
$order->order_items; // collection as well
// --------------------
// BUT
public function order_items()
{
return $this->hasMany(...);
}
$order->orderItems; // null
$order->order_items; // null
// the only way you can work with relation then, is explicitly use method like:
$order->order_items()->get();

Probably not the best solution you are looking for, but this should work.
Let's say that the array is named $items, I'm under the impression that you will be saving it into a pivot table. In my example below I also have a 3rd field on item_order pivot table named item_quantity.
foreach ($items as $item)
{
$order->items()
->attach($item['item_id'], ['item_quantity' => $item['item_quantity']]);
}
Basically you will be looping through the $items array. This will assume that you have defined the relationship on your Order model called items().
Then use the attach() method
->attach([insert the item_id], array('3rd field name' => 'value to be inserted')
Finally, if you don't have a 3rd field on your pivot table you could just do
->attach($item_id)
You can check the example given at the Laravel docs
Note
attach() is the method used when the you are only creating a record on the Database, otherwise you need a different method when you want to update.

#jareks answer helped in a similar scenario except for a mass assignment exception . so on digging up docs i found that you need to set a guarded or fillable property for mass assignment in latest versions of laravel (4.2) .
please refer this along with his answer .
Fillable or guarded properties
When creating a new model, you pass an array of attributes to the model constructor. These attributes are then assigned to the model via mass-assignment. This is convenient; however, can be a serious security concern when blindly passing user input into a model. If user input is blindly passed into a model, the user is free to modify any and all of the model's attributes. For this reason, all Eloquent models protect against mass-assignment by default.
So set the fillable or guarded properties on your model. Docs and Source
class User extends Eloquent {
protected $fillable = array('first_name', 'last_name', 'email');
}

Related

Laravel pluck nested collection to root collection

I have a data structure like you can see in the picture above.
I want some fields from user field in the root collection, but I don't know how can I can get that.
public function index (Request $request) {
$draw = $request->get('draw');
$start = $request->get('start');
$length = $request->get('length');
$order = Order::with('user');
$total_order = $order->count();
$orders = Order::with('user')->offset($start)->limit($length)->get();
/*$draw = ceil($total_order/$length);*/
return response()->json([
'draw' => $draw,
'recordsTotal' => $total_order,
'recordsFiltered' => $total_order,
'data' => $orders,
]);
}
The flatmap approach seems weird, you have two models Order and Users, either you have to add extra properties to orders on the fly, which is weird in Laravel if you want to save the model again. Or you have to convert it to arrays and you loose the Model functionality.
Instead of trying to flatten the user object onto the order. You are already using with(), so your user is eager loaded and can rely on that. Instead use Eloquent Getters and map your user fields to the order.
class Order extends Model
{
public function getUserNameAttribute() {
return $this->user->name;
}
}
Eloquent getters function name has to be in the format getPropertyAttribute. Now you would be able to access it like so, which also can be done in blade etc.
foreach ($orders as $order) {
$userName = $order->userName;
}
If you truly want the related values in the root collection, you could do a left join on the table:
$orders = Order::leftJoin('users', 'users.id', '=', 'orders.user_id')
(Based on the model names, these would be the correct table and column names).
However, like Mrhn just pointed out, it is not very Eloquent-like to try and have data of related tables in the root collection of the main model.
P.S. If you do use the left join, be sure to only select the data you need, otherwise it will overwrite Order data (such as id)

Unable to get selective column names on Laravel Eloquent Nested Eager Relationship

I have a Product Model with a following relationship:
public function recommendedPricing()
{
return $this->hasMany(RecommendedPricing::class);
}
The Recommended Pricing Model is:
protected $fillable = [ 'sku_id', 'unit_type_id', 'base_price', 'min_billable_qty', 'max_billable_qty', 'discount_method'];
protected $with = ['bands'];
public function bands()
{
return $this->hasMany('App\Models\RecommendedPricingBand');
}
The Recommended Pricing Band Model is like:
protected $fillable = ['sku_id','recommended_pricing_id','start','end','percent_change','fixed_price'];
Now In my Controller I am doing this:
Product::where('id', $product->id)->with(['recommendedPricing', 'recommendedPricing.bands'])->get();
which gives the entire result. But I want selected columns from both Recommended Pricing and Recommended Pricing Band. So, I tried this:
Product::where('id', $product->id)->with(['recommendedPricing:id, base_price, discount_method', 'recommendedPricing.bands:id, percent_change, fixed_price'])->get();
But this always results in an error.
My question: Is there any better approach or way to fetch the correct result for this kind of a nested relationship scenario?
When eager loading particular fields you can't have spaces. I usually do a seperate with line for each relationship. I don't think you need the array either. And you always have to get the id first, which you have already :) hope this works...
->with('recommendedPricing:id,base_price,discount_method')
->with('recommendedPricing.bands:id,percent_change,fixed_price')

Null object pattern with Eloquent relations

There is often the case where an certain eloquent model's relation is unset (i.e. in a books table, author_id is null) and thus calling something like $model->relation returns null.
E.g. say a Book model has an author() (hasOne) relation I might want to do
$author = Book::find(1)->author->name;
If Book 1 has no author set it will throw a "trying to get property of non object" error. Is there a way to avoid this and default to a blank Author so I'll always be able to call name on it regardless of whether the relation has been set for the specific model?
Essentially I want to avoid conditionals to check if $book->author is an actual Author before calling further methods/properties on it. It should default to a new Author instance if the relation isn't set.
I tried something like:
public function getAuthorAttribute($author)
{
return $author ?: new Author;
}
however this doesn't work; $author is being passed in as null, even if it's set on the model. Presumably because it's a relation rather than a direct property of a book. I'd need something like
public function getAuthorAttribute()
{
return $this->author()->first() ?: new Author;
}
which seems pretty inelegant and seems like it would override any eager loading resulting in poor performance.
Update
As of Laravel 5.3.23, there is now a built in way to accomplish this (at least for HasOne relationships). A withDefault() method was added to the HasOne relationship. In the case of your Book/Author example, your code would look like:
public function author() {
return $this->hasOne(Author::class)->withDefault();
}
This relationship will now return a fairly empty (keys are set) Author model if no record is found in the database. Additionally, you can pass in an array of attributes if you'd like to populate your empty model with some extra data, or you can pass in a Closure that returns what you'd like to have your default set to (doesn't have to be an Author model).
Until this makes it into the documentation one day, for more information you can check out the pull requests related to the change: 16198 and 16382.
At the time of this writing, this has only been implemented for the HasOne relationship. It may eventually migrate to the BelongsTo, MorphOne, and MorphTo relationships, but I can't say for sure.
Original
There's no built in way that I know of to do this, but there are a couple workarounds.
Using an Accessor
The problem with using an accessor, as you've found out, is that the $value passed to the accessor will always be null, since it is populated from the array of attributes on the model. This array of attributes does not include relationships, whether they're already loaded or not.
If you want to attempt to solve this with an accessor, you would just ignore whatever value is passed in, and check the relationship yourself.
public function getAuthorAttribute($value)
{
$key = 'author';
/**
* If the relationship is already loaded, get the value. Otherwise, attempt
* to load the value from the relationship method. This will also set the
* key in $this->relations so that subsequent calls will find the key.
*/
if (array_key_exists($key, $this->relations)) {
$value = $this->relations[$key];
} elseif (method_exists($this, $key)) {
$value = $this->getRelationshipFromMethod($key);
}
$value = $value ?: new Author();
/**
* This line is optional. Do you want to set the relationship value to be
* the new Author, or do you want to keep it null? Think of what you'd
* want in your toArray/toJson output...
*/
$this->setRelation($key, $value);
return $value;
}
Now, the problem with doing this in the accessor is that you need to define an accessor for every hasOne/belongsTo relationship on every model.
A second, smaller, issue is that the accessor is only used when accessing the attribute. So, for example, if you were to eager load the relationship, and then dd() or toArray/toJson the model, it would still show null for the relatioinship, instead of an empty Author.
Overriding Model Methods
A second option, instead of using attribute accessors, would be to override some methods on the Model. This solves both of the problems with using an attribute accessor.
You can create your own base Model class that extends the Laravel Model and overrides these methods, and then all of your other models will extend your base Model class, instead of Laravel's Model class.
To handle eager loaded relationships, you would need to override the setRelation() method. If using Laravel >= 5.2.30, this will also handle lazy loaded relationships. If using Laravel < 5.2.30, you will also need to override the getRelationshipFromMethod() method for lazy loaded relationships.
MyModel.php
class MyModel extends Model
{
/**
* Handle eager loaded relationships. Call chain:
* Model::with() => Builder::with(): sets builder eager loads
* Model::get() => Builder::get() => Builder::eagerLoadRelations() => Builder::loadRelation()
* =>Relation::initRelation() => Model::setRelation()
* =>Relation::match() =>Relation::matchOneOrMany() => Model::setRelation()
*/
public function setRelation($relation, $value)
{
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When attempting
* to set to null, override with a new instance of the expected model.
*/
if (is_null($value)) {
// set the value to a new instance of the related model
$value = $this->$relation()->getRelated()->newInstance();
}
$this->relations[$relation] = $value;
return $this;
}
/**
* This override is only needed in Laravel < 5.2.30. In Laravel
* >= 5.2.30, this method calls the setRelation method, which
* is already overridden and contains our logic above.
*
* Handle lazy loaded relationships. Call chain:
* Model::__get() => Model::getAttribute() => Model::getRelationshipFromMethod();
*/
protected function getRelationshipFromMethod($method)
{
$results = parent::getRelationshipFromMethod($method);
/**
* Relationships to many records will always be a Collection, even when empty.
* Relationships to one record will either be a Model or null. When the
* result is null, override with a new instance of the related model.
*/
if (is_null($results)) {
$results = $this->$method()->getRelated()->newInstance();
}
return $this->relations[$method] = $results;
}
}
Book.php
class Book extends MyModel
{
//
}
I had the same problem in my project. In my views there's some rows that are accesing to dinamics properties from null relationships, but instead of returning an empty field, the app was thrwoing and exception.
I just added a foreach loop in my controller as a temporal solution that verifies in every value of the collection if the relationship is null. If this case is true, it assigns a new instance of the desire model to that value.
foreach ($shifts as $shift)
{
if (is_null($shift->productivity)) {
$shift->productivity = new Productivity();
}
}
This way when I access to $this->productivity->something in my view when the relationship is unset, I get a empty value instead of an exception without putting any logic in my views nor overriding methods.
Waiting for a better solution to do this automatically.
You can achieve this using model factories.
Define an author factory inside your ModelFactory.php
$factory->define(App\Author::class, function (Faker\Generator $faker) {
return [
'name' => $faker->firstName, //or null
'avatar' => $faker->imageUrl() //or null
];
});
add values for all the needed attributes I am using dummy values from Faker but you can use anything you want.
Then inside your book model you can return an instance of Author like this:
public function getAuthorAttribute($author)
{
return $author ?: factory(App\Author::class)->make();
}

Mapping field name in model using laravel

Is there a way to map field names to a database to a different attribute name in the model? For example, if the database has a field name of customer_id but I wanted to use eloquent in this way Customer::get(['id']) I've tried using the getAttribute method but that is called after eloquent has attempted to get the value.
You can use accessors to work with such attributes, but there's no way to query them this way with core eloquent.
But fear not! Use this package https://github.com/jarektkaczyk/eloquence and you can easily achieve what you want (Mappable in particular):
// Customer model
protected $maps =[
'id' => 'customer_id',
'name' => 'customer_name',
...
];
// then you can do this:
$customer = Customer::where('name', 'whatever')->first();
// calls WHERE customer_name = ? sql
$customer->id; // customer_id column
$customer->name; // customer_name column
$customer->name = 'different name'; // set mutator works as well
It's in heavy development and currently select is not yet supported, but it's matter of day or two. select support has been pushed already.

getting the value of an extra pivot table column laravel

I have a phone_models, phone_problems, and a phone_model_phone_problem pivot table. The pivot table has an extra column 'price'.
PhoneModel:
class PhoneModel extends \Eloquent
{
public function problems()
{
return $this->belongsToMany('RL\Phones\Entities\PhoneProblem')->withPivot('price');
}
}
PhoneProblem:
class PhoneProblem extends \Eloquent
{
public function models()
{
return $this->belongsToMany('PhoneModel')->withPivot('price');
}
}
What I'm trying to do is get the price of a specific phone with a specific problem.
This is how I have it now but I feel like Laravel has a built in Eloquent feature I can't find to do this in a much simpler way:
$model = $this->phoneService->getModelFromSlug($model_slug);
$problem = $this->phoneService->getProblemFromSlug($problem_slug);
all this does is select the specific model and problem from their slug.
then what I do is with those credentials I get the price like so:
$row = DB::table('phone_model_phone_problem')
->where('phone_model_id', '=', $model->id)
->where('phone_problem', '=', $problem->id)
->first();
so now I can get the price like so $row->price but I feel like there needs to be a much easier and more 'Laravel' way to do this.
When using Many to Many relationships with Eloquent, the resulting model automatically gets a pivot attribute assigned. Through that attribute you're able to access pivot table columns.
Although by default there are only the keys in the pivot object. To get your columns in there too, you need to specify them when defining the relationship:
return $this->belongsToMany('Role')->withPivot('foo', 'bar');
Official Docs
If you need more help the task of configuring the relationships with Eloquent, let me know.
Edit
To query the price do this
$model->problems()->where('phone_problem', $problem->id)->first()->pivot->price
To get data from pivot table:
$price = $model->problems()->findOrFail($problem->id, ['phone_problem'])->pivot->price;
Or if you have many records with different price:
$price = $model->problems()->where('phone_problem', $problem->id)->firstOrFail()->pivot->price;
In addition.
To update data in the pivot you can go NEW WAY:
$model->problems()->sync([$problemId => [ 'price' => $newPrice] ], false);
Where the 2nd param is set to false meaning that you don't detach all the other related models.
Or, go old way
$model->problems()->updateExistingPivot($problemId, ['price' => $newPrice]);
And remind you:
To delete:
$model->problems()->detach($problemId);
To create new:
$model->problems()->attach($problemId, ['price' => 22]);
It has been tested and proved working in Laravel 5.1 Read more.
Laravel 5.8~
If you want to make a custom pivot model, you can do this:
Account.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Account extends Model
{
public function users()
{
return $this->belongsToMany(User::class)
->using(AccountUserPivot::class)
->withPivot(
'status',
'status_updated_at',
'status_updated_by',
'role'
);
}
}
AccountUserPivot.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;
class AccountUserPivot extends Pivot
{
protected $appends = [
'status_updated_by_nice',
];
public function getStatusUpdatedByNiceAttribute()
{
$user = User::find($this->status_updated_by);
if (!$user) return 'n/a';
return $user->name;
}
}
In the above example, Account is your normal model, and you have $account->users which has the account_user join table with standard columns account_id and user_id.
If you make a custom pivot model, you can add attributes and mutators onto the relationship's columns. In the above example, once you make the AccountUserPivot model, you instruct your Account model to use it via ->using(AccountUserPivot::class).
Then you can access everything shown in the other answers here, but you can also access the example attribute via $account->user[0]->pivot->status_updated_by_nice (assuming that status_updated_by is a foreign key to an ID in the users table).
For more docs, see https://laravel.com/docs/5.8/eloquent-relationships (and I recommend press CTRL+F and search for "pivot")

Categories