Laravel Virtual Columns fail to save - php

I added a couple of virtual columns to my database tables using Laravels virtualAs column modifier:
$table->decimal('grand_total')->virtualAs( '(total_value + (total_value*tax_rate))');
Basically it keeps a mysql virtual column that automatically calculates the grand total based on the total and tax rate stored in another column.
However, Laravel does not seem to play nice with virtual columns at all. When saving a record, it attempts to INSERT or UPDATE the virtual column, which is obviously not allowed in mySQL. I could not find a way to configure in the Eloquent model which fields are actually written to the database on an update or insert.
I've tried adding the field to the models $hidden, and $appends but nothing seems to work.
Looking at the Laravel Source code for an insert (https://github.com/laravel/framework/blob/5.6/src/Illuminate/Database/Eloquent/Model.php#L733), it seems to just insert whatever attributes are in $this->attributes. When the record is read from the database the grand_total field is read from the table and set as an attribute and then it is tried to be written again once the record is saved.
Is there any way to get this Laravel to stop trying to save columns that are virtual?

Here's a quick trait I wrote to solve your problem that will filter fields residing in the $virtualFields property before saving. It requires a select (refresh) after the save to get the new value for the virtual field. If you don't need to query this virtual field, I'd highly recommend you look into a mutator instead.
trait HasVirtualFields
{
public function save(array $options = [])
{
if (isset($this->virtualFields)) {
$this->attributes = array_diff_key($this->attributes, array_flip($this->virtualFields));
}
$return = parent::save($options);
$this->refresh(); // Refresh the model for the new virtual column values
return $return;
}
}
class YourModel
{
use HasVirtualFields;
protected $virtualFields = ['grand_total'];
}

Related

Laravel 8 user table column names

I am using laravel 8 with an existing user table. All is working as expected except the password reset link functionality. This is because my table has the email column name as "Email" instead of "email." Other applications use this table, so the column name cannot be changed. I can get the password reset link functionality working if I manually set the column name within the framework itself (example below).
File: /vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php
public function retrieveByCredentials(array $credentials)
{
// framework code that retieves the user record for email address
if ($res) {
$res->email = $res->Email;
}
// rest of frame work code
}
This seems a little "hacky." Is there a better approach to this?
Laravel would benefit greatly from more customization regarding the user's table (custom user table name, column names, etc.).
Laravel has mutators and accessors. This does that you can change behavior of ->email access or assigning it. Add this snippet to your User.php model.
class User {
public function getEmailAttribute()
{
return $this->attributes['Email'];
}
}
You can read the docs about it. The naming convention for the function is get{PropertyName}Attribute, if you define your function like so, you can easily overwrite property logic in Laravel. Making it use the column Email.

Advanced Laravel merged data/models - can it be done at model level?

We have a COMMON database and then tenant databases for each organization that uses our application. We have base values in the COMMON database for some tables e.g.
COMMON.widgets. Then in the tenant databases, IF a table called modified_widgets exists and has values, they are merged with the COMMON.widgets table.
Right now we are doing this in controllers along the lines of:
public function index(Request $request)
{
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->get();
$merged = $widgets->merge($modified);
$merged = array_values(array_sort($merged, function ($value) {
return $value['name'];
}));
return $merged;
}
return $countries;
}
As you can see, we have model for each table and this works OK. We get the expected results for GET requests like this from controllers, but we'd like to merge at the Laravel MODEL level if possible. That way id's are linked to the correct tables and such when populating forms with these values. The merge means the same id can exist in BOTH tables. We ALWAYS want to act on the merged data if any exists. So it seems like model level is the place for this, but we'll try any suggestions that help meet the need. Hope that all makes sense.
Can anyone help with this or does anyone have any ideas to try? We've played with overriding model constructors and such, but haven't quite been able to figure this out yet. Any thoughts are appreciated and TIA!
If you put this functionality in Widget model you will get 2x times of queries. You need to think about Widget as an instance, what I am trying to say is that current approach does 2 queries minimum and +1 if tenant has modified_widgets table. Now imagine you do this inside a model, each Widget instance will pull in, in a best case scenario its equivalent from different database, so for bunch of Widgets you will do 1 (->all())+n (n = number of ModifiedWidgets) queries - because each Widget instance will pull its own mirror if it exists, no eager load is possible.
You can improve your code with following:
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->whereIn('id', $widgets->pluck('id'))->get(); // remove whereIn if thats not the case
return $widgets->merge($modified)->unique()->sortBy('name');
}
return $widgets;
OK, here is what we came up with.
We now use a single model and the table names MUST be the same in both databases (setTable does not seem to work even though in exists in the Database/Eloquent/Model base source code - that may be why it's not documented). Anyway = just use a regular model and make sure the tables are identical (or at least the fields you are using are):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Widget extends Model
{
}
Then we have a generic 'merge controller' where the model and optional sort are passed in the request (we hard coded the 'where' and key here, but they could be made dynamic too). NOTE THIS WILL NOT WORK WITH STATIC METHODS THAT CREATE NEW INSTANCES such as $model::all() so you need to use $model->get() in that case:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class MergeController extends Controller
{
public function index(Request $request)
{
//TODO: add some validations to ensure model is provided
$model = app("App\\Models\\{$request['model']}");
$sort = $request['sort'] ? $request['sort'] : 'id';
$src_collection = $model->where('active', '1')->orderBy('name')->get();
// we setup the tenants connection elsewhere, but use it here
if(Schema::connection('tenant')->hasTable($model->getTable())) {
$model->setConnection('tenant');
$tenant_collection = $model->get()->where('active', '1');
$src_collection = $src_collection->keyBy('id')->merge($tenant_collection->keyBy('id'))->sortBy('name');
}
return $src_collection;
}
}
If you dd($src_collection); before returning it it, you will see the connection is correct for each row (depending on data in the tables). If you update a row:
$test = $src_collection->find(2); // this is a row from the tenant db in our data
$test->name = 'Test';
$test->save();
$test2 = $src_collection->find(1); // this is a row from the tenant db in our data
$test2->name = 'Test2'; // this is a row from the COMMON db in our data
$test2->save();
dd($src_collection);
You will see the correct data is updated no matter which table the row(s) came from.
This results in each tenant being able to optionally override and/or add to base table data without effecting the base table data itself or other tenants while minimizing data duplication thus easing maintenance (obviously the table data and population is managed elsewhere just like any other table). If the tenant has no overrides then the base table data is returned. The merge and custom collection stuff have minimal documentation, so this took some time to figure out. Hope this helps someone else some day!

$model->save() adding 'where id = 1' condition

I'm trying to update a model, I load the model, take all the data from the POST and then save it, easy... But my record was never updating so went to the log and discovered that the update query is adding a weird condition. FYI, MD_ID is my primary key.
So, I load the model, the next line is the SQL produced by Yii:
$model = Ositems::model()->findByPk($id);
SELECT * FROM "MTODETALLADO_INV" "t" WHERE "t"."MD_ID"=249217
If echo the json_encode of the loaded model I get that dictionary in my browser:
echo json_encode($model->getAttributes());
{""MD_BODEGA":"01","MD_PRODUCTO":"0031253","MD_CANTIDAD":"1","MD_PRECIOTOTAL":"1466",,"MD_PORCENTAJEDESCUENTO":"0","MD_IDCABECERA":"97403","MD_ID":"249217","MD_OBSERVACION":null}
At this point everything looks right, now I take the values from post:
$model->attributes = $_POST;
And here if echo the values of the model I get the new values right, now here is the problem: I save the model and this is the SQL Yii runs (I replaced the :yp_ values to make it more readable)
$model->save();
UPDATE "MTODETALLADO_INV" SET
MD_BODEGA"='01'
MD_PRODUCTO"='0020514
MD_CANTIDAD"='10'
MD_PORCENTAJEDESCUENTO"='0
MD_IDCABECERA"=97403
MD_ID"=249218
MD_PRECIOTOTAL"='36210'
MD_OBSERVACION"=''
WHERE "MTODETALLADO_INV"."MD_ID"=1
And there is the problem! WHERE "MTODETALLADO_INV"."MD_ID"=1, Why would it make it 1 if all this time my model id has been 249218 ?
A few considerations:
My model only takes some columns that I need from the actual table, Yii sets the other columns as null and I omitted them in the previous code.
The table is in a foreign db, I use have a custom ActiveRecord which manages the CDbConnection to a database according to the user. (It's a webservice app)
I followed what the function save() did and could finally find the problem was when it tried to get the primary key. I had this method in my model:
public function primaryKey()
{
return array('MS_ID');
}
}
But it had to be:
public function primaryKey()
{
return 'MS_ID';
}
}
Somehow that was causing the problem.

Laravel 4 Eloquent Column Alias

What i am trying to achieve is in my database i have a table named channel
i am using laravel's eloquent class to access these the properties from the table
The problem that i am facing is that
the table name is column and the column name is channel
so when accessing that property looks like this.
User::find(1)->channel->channel
How can i modify this to say
User::find(1)->channel->name
We cannot change the table name in the database.
Options i have thought of:
1)Create views for tables that need columns changed. Too messy...
2)Use column alias.... laravel documentation...sigh.. no clue how?
3)Use a property set with the create_function that would call this->channel
but i am pretty sure it won't work because laravel is using dynamic properties. and when it's fill out in the array im pretty sure it changes it to the name of the column.
I could in my belongs_to/hasOne/hasMany function change the property to the alias of the name i want to use so that later on i can change it. i dunno how well that would work..
any thoughts?
much appreciated
You could probably do it easily with Accessors / Mutators.
class Channel extends Eloquent {
public function getNameAttribute()
{
return $this->attributes['channel'];
}
public function setNameAttribute($value)
{
$this->attributes['channel'] = $value;
}
}
Reference
Laravel Accessors & Mutators

How to make models use defaults in Phalcon PHP Framework?

If a table has defaults on certain fields and NULL is not allowed, one would expect the insert script to use those defaults, as MariaDB/MySQL usually does. For example, if the table products has an AI field "id", a required field "name" and two required fields "active" and "featured" which both default to 1, then the query
INSERT INTO products (name) VALUES ('someName');
automatically inserts 1 as the value of active and featured. However, when using Phalcon's models like so:
$product = new Products();
$product->setName('someName');
$product->save();
returns validation errors saying "active" and "featured" are required.
Is there a flag I should provide during model generation in order for Phalcon tools to harvest and input the defaults into Model classes, or another way to make Phalcon automatically use defaults if found? Best approach would be just ignoring the fields that weren't set, I reckon. Can I make the models do that?
You can use a raw database value to avoid that, in specific inserts:
<?php
use Phalcon\Db\RawValue;
$product = new Products();
$product->setName('someName');
$product->setType(new RawValue('default')); //use default here
$product->save();
Or, general before create/update for specific fields:
use Phalcon\Db\RawValue;
class Products extends Phalcon\Mvc\Model
{
public function beforeValidationOnCreate()
{
$this->type = new RawValue('default');
}
}
Or ignore these fields in every SQL INSERT generated:
use Phalcon\Db\RawValue;
class Products extends Phalcon\Mvc\Model
{
public function initialize()
{
$this->skipAttributesOnCreate(array('type'));
}
}
Although I find twistedxtra's answer fascinating from the aspect that Phalcon contains this wicked method to read the column default, I believe from a architectural point of view this might be the wrong approach as you rely on your database to define the defaults of the properties of your model.
I would set the default value when declaring the property and keep the logic in the application layer. But that's just me.
Use Like below
The skipAttributesOnCreate will make sure Phalcon does not attempt to put a a value in that column. The database will apply the default value.
public function initialize()
{
$this->setSource('table_name');
$this->skipAttributesOnCreate(['name_of_column']);
}

Categories