Laravel - Dynamically add field to model and prevent its save - laravel

I need to add a field to a model, and this field is not a real column, for example:
function calculateDiscount(Order $order)
{
// some logic
$order->discount = 20; // It's just an example
return $order
}
The field discount is not a real column to database. I need to add this "dynamic" field to use later.
If I perform a save like:
$order->save();
I get the error:
Column not found: 1054 Unknown column 'discount' in 'field list' (SQL: update `orders` set `discount` = 20, where `id` = 67586)
There is a way to add a "dynamic" field and prevent Laravel save it ?

use appends
Models/Order.php
class Order extends Model {
protected $appends = [
'discount'
];
public function getDiscountAttribute() {
// your logic of how to get discount value
return $discount;
}
}
or you can load the discount attribute only when you need
// controller
$order->append('discount'); // load the value
// model
class Order extends Model {
public function getDiscountAttribute() {}
}
// add the getDiscountAttribute function only

Related

Laravel - How to change response format for specific fields

I've "Product" model.
And need to change some value formats for only responses.
For example;
I've "price" on database as decimal (11,2).
I want this as "1.000.000,00" format on response.
Or created_at field to "Carbon::parse($this->created_at)->toDayDatetimeString()"
Or I want to add 3 specific columns with my user attribute, on response. (is_allowed etc.)
How can this be possible on model?
How can I response like that?
You can use Mutator and Accessor to set format :
https://laravel.com/docs/8.x/eloquent-mutators#accessors-and-mutators
public function setDateAttribute($date) {
$this->attributes['date'] = Carbon::createFromFormat('Y-m-d', $date);
}
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
As a best practice in Laravel you can use Eloquent Resources: Eloquent Resources
It's basically a "transformer" between models data and API/Responses Output.
The only one thing to notice is that in the Resource files yout must specify all fields and relations (if needed) of the Model manually.
In the toArray() function you can modify the type of all data of your model as you prefer.
If not, you can access the new field by $model->my_custom_field (Laravel can resolve the name of the getter function automatically).
public function toArray($request)
{
$editedFieldValue = doSomething();
return [
'my_field' => $editedFieldValue,
'other_field' => '',
];
}
If you want to do that in Model, you can create customs fields:
class MuModel extends Model
{
protected $appends = ['my_custom_field'];
public function getMyCustomFiledAttribute(){
$newData = doSomething($this->existent_field);
return $newData;
}
}
The $appends variable add the new fields to all responses generated from the Model, as a normal database field.
P.S.: You can create a getAttribute() function for existent database attribute and return the value as you want!
For example: getCreatedAtAttribute()

Multiple options to questions using eloquent

Im creating a survey app, and basically when a user selects a input type of "select" it appears a option input that can be dynamiclly increase, but that im having some issues in inserting these options in my database, im sugin the sync method to insert these options in my table, but is giving me a error of
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'toufarto.question_id' doesn't exist (SQL: select `option_question_id` from `question_id` where `id` = 11)
Here is my code:
Tables:
questions:
- id;
- input_type;
option_question
- id;
- label_option;
- question_id
My controller:
public function store(Request $request)
{
$this->validate($request, array(
'label_option' => 'max:255',
'input_type' => 'required|integer'
));
$question = new Question();
$question->input_type = $request->input_type;
$question->save();
$question->options()->sync($request->option, false);
Session::flash('success', 'success');
return back();
}
My Question Model:
public function options()
{
return $this->belongsToMany(OptionQuestion::class,'question_id','id');
}
My OptionQuestion Model:
class OptionQuestion extends Model
{
protected $table = "option_question";
}
Note: How i could add the label column to the "sync" method, since i need to insert the label from the option field of the form
As mentioned in the docs of Laravel you need to define your parameters as follows:
$this->belongsToMany(Model,table_name);
or
$this->belongsToMany(Model, table_name, table_1_key, table_2_key);
Looking at your DB schematic however it seems you should make a small adjustment.
questions
- id
- input_type;
options
- id
- label_option
option_question
- id
- question_id
- option_id
Question model
public function options()
{
return $this->belongsToMany(OptionQuestion::class);
}
Options model
public function questions()
{
return $this->belongsToMany(Question::class);
}

Yii2 Activerecord get fields from junction table and order by according to them

I have items and units table that have many to many relationship. In other words, the item has many units and the unit has many items. I managed the relation through a junction table item_units. The junction table has some extra field more than item_id and unit_id, i.e it has price, and weight (it is an integer to manage the order of units for each item for display purposes).
I managed the relations in the models as follows:
//In Items model
/**
* #return \yii\db\ActiveQuery
*/
public function getItemUnits()
{
return $this->hasMany(ItemUnits::className(), ['item_id' => 'id'])->orderBy(['item_units.weight' => SORT_DESC]);
}
public function getUnits()
{
return $this->hasMany(Units::className(), ['id'=> 'unit_id'])->select(['id','title'])->via('itemUnits');
}
//
//In Units model
public function getItemUnits()
{
return $this->hasMany(ItemUnits::className(), ['unit_id' => 'id'])->orderBy(['price' => SORT_DESC]);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getItems()
{
return $this->hasMany(Items::className(), ['id' => 'item_id'])->via('itemUnits');
}
//
//In ItemUnits model
public function getItem()
{
return $this->hasOne(Items::className(), ['id' => 'item_id']);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getUnit()
{
return $this->hasOne(Units::className(), ['id' => 'unit_id']);
}
In the controller I'm able to get the data of all related units to an item by something like the following:
$item = Items::findOne($id);
return Json::encode($item->units);
The following is a demo of the JSON object obtained:
[{"id":"4","title":"قرص"},{"id":"5","title":"شريط 10"},{"id":"6","title":"علبة 2 شريط"}]
However, I could not able to order the results according to the weight field in item_units table and also I could not able to include the price field there in the demo result above -JSON Object-.
I only able to get data in item_units as a separate result like the following:
return Json::encode($item->itemUnits);
Update
According to the two answers (#Александр Шалаев & #Onedev.Link) , I have overridden the fields method in Units model as follows:
public function fields() {
parent::fields();
return [
'price' => function($model){
return $model->id; //Here I could not able to get the corresponding price field value from item_units -junction table-
},
'id',
'title',
];
}
However, I could not able to get the price field value from the junction table, temporary, I set it to current model id to prevent error generation. Also, I still has no any mean to set order by using weight field in that junction table.
Update 2
In other words, how could Yii2 Activerecords perform the following SQL query:
SELECT units.id UnitID, units.title Unit, iu.weight, iu.price
FROM units
Left JOIN item_units AS iu
ON iu.item_id = 1 AND iu.unit_id = units.id
WHERE
units.id = iu.unit_id
ORDER BY iu.weight;
Finally I have found a solution. It depends on findBySql method. I'm going to use the above SQL query regarded in Update 2 -just I have removed some selected fields to be suitable for my current task-.
public function actionUnitsJson($id){
$sql = 'SELECT units.id, units.title
FROM units
Left JOIN item_units AS iu
ON iu.item_id = :id AND iu.unit_id = units.id
WHERE
units.id = iu.unit_id
ORDER BY iu.weight DESC;';
$units = \common\models\Units::findBySql($sql,[':id' => $id])->asArray()->all();
return Json::encode($units);
}
You need fields or extraFields in your ActiveRecord model with asArray.
Example:
/**
* #return array
*/
public function fields()
{
return [
'itemUnit', //will get getItemUnit method
];
}
or
/**
* #return array
*/
public function extraFields()
{
return [
'itemUnits', //it is relation name
];
}
Usage:
$model->toArray(); //will contains fields and extra fields relations
... sort array & return
By default, yii\base\Model::fields() returns all model attributes as fields, while yii\db\ActiveRecord::fields() only returns the attributes which have been populated from DB.
You can override fields() to add, remove, rename or redefine fields. The return value of fields() should be an array. The array keys are the field names, and the array values are the corresponding field definitions which can be either property/attribute names or anonymous functions returning the corresponding field values.

How to update field when delete a row in laravel

Let I have a table named customer where customer table has a field named deleted_by.
I implement softDelete in customer model. Now I want to update deleted_by when row delete. So that I can trace who delete this row.
I do search on google about it But I don't found anything.
I use laravel 4.2.8 & Eloquent
You may update the field using something like this:
$customer = Customer::find(1); // Assume 1 is the customer id
if($customer->delete()) { // If softdeleted
DB::table('customer')->where('id', $customer->id)
->update(array('deleted_by' => 'SomeNameOrUserID'));
}
Also, you may do it in one query:
// Assumed you have passed the id to the method in $id
$ts = Carbon\Carbon::now()->toDateTimeString();
$data = array('deleted_at' => $ts, 'deleted_by' => Auth::user()->id);
DB::table('customer')->where('id', $id)->update($data);
Both is done within one query, softDelete and recorded deleted_by as well.
Something like this is the way to go:
// override soft deleting trait method on the model, base model
// or new trait - whatever suits you
protected function runSoftDelete()
{
$query = $this->newQuery()->where($this->getKeyName(), $this->getKey());
$this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp();
$deleted_by = (Auth::id()) ?: null;
$query->update(array(
$this->getDeletedAtColumn() => $this->fromDateTime($time),
'deleted_by' => $deleted_by
));
}
Then all you need is:
$someModel->delete();
and it's done.
I would rather use a Model Event for this.
<?php
class Customer extends \Eloquent {
...
public static function boot() {
parent::boot();
// We set the deleted_by attribute before deleted event so we doesn't get an error if Customer was deleted by force (without soft delete).
static::deleting(function($model){
$model->deleted_by = Auth::user()->id;
$model->save();
});
}
...
}
Then you just delete it like you would normally do.
Customer::find(1)->delete();
I know this is an old question, but what you could do (in the customer model) is the following....
public function delete()
{
$this->deleted_by = auth()->user()->getKey();
$this->save();
return parent::delete();
}
That would still allow the soft delete while setting another value just before it deletes.

Laravel Eloquent - $fillable is not working?

I have set the variable $fillable in my model. I wanted to test the update functionality, and I get this error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column '_method' in 'field list' (SQL: update positions set name = Casual Aquatic Leader, _method = PUT, id = 2, description = Here is my description, updated_at = 2014-05-29 17:05:11 where positions.client_id = 1 and id = 2)"
Why is this yelling at _method when my fillable doesn't have that as a parameter? My update function is:
Client::find($client_id)
->positions()
->whereId($id)
->update(Input::all());
Change following:
->update(Input::all());
to this (exclude the _method from the array)
->update(Input::except('_method'));
Update:
Actually following update method is being called from Illuminate\Database\Eloquent\Builder class which is being triggered by _call method of Illuminate\Database\Eloquent\Relations class (because you are calling the update on a relation) and hence the $fillable check is not getting performed and you may use Input::except('_method') as I answered:
public function update(array $values)
{
return $this->query->update($this->addUpdatedAtColumn($values));
}
If you directly call this on a Model (Not on a relation):
Positions::find($id)->update(Input::all());
Then this will not happen because fillable check will be performed within Model.php because following update method will be called from Illuminate\Database\Eloquent\Model class:
public function update(array $attributes = array())
{
if ( ! $this->exists)
{
return $this->newQuery()->update($attributes);
}
return $this->fill($attributes)->save();
}
write a parent class
class BaseModel extends Model
public static function getFillableAttribute(Model $model, $data){
$array = $model->getFillable();
$arr = [];
foreach ($array as $item){
if( isset($data["$item"])){
$arr["$item"] = $data["$item"];
}
}
return $arr;
}
I experienced this breaking after updating from Laravel 5.2 to 5.4 - I can't find anything in the documentation / migration guide that covers it.
As covered in this github issue the correct fix/use of Eloquent seems to be:
Positions::find($id)->fill(Input::all())->save();
To trigger the fillable check by laravel and then perist the changes.
You could also do this
$data = request()->all();
//remove them from the array
unset($data['_token'],$data['_method']);
//then
Client::find($client_id)
->positions()
->whereId($id)
->update($data);
This removes the _method and _token from the array

Resources