How to calculate and store in DB some field values on Model save hook? - atk4

What I want to accomplish is the following.
I need to save in database some field which value should be automatically calculated using other values in its Model (or related models). I guess I should do this using one of these model hooks - beforeInsert, beforeModify, afterInsert, afterModify, but how exactly I should do this?
Also, this field should be not changeable, but visible in UI forms/grids.
For example,
class Model_Address extends Model_Table{
public $table='address';
function init(){
parent::init();
$this->hasOne('Territory');
$this->addField('street');
$this->addField('house');
$this->addField('number');
$this->addField('name')->readonly(true); // this should be calculated on save
$this->addHook('beforeModify',$this);
}
// How to write this to set name=street+house+number+territory.name ???
function beforeModify($m){
$ter_name = $m->ref('Territory')->get('name');
$m['name'] = $m['street'].' '.$m['house'].' '.$m['number'].', '.$ter_name;
return $m;
}
}
Edit:
Will this solution will be correct? It looks that it's working, but I'm not sure yet.
class Model_Address extends Model_Table{
public $table='address';
function init(){
parent::init();
$this->hasOne('Territory');
$this->addField('street');
$this->addField('house');
$this->addField('number');
$this->addField('name')->readonly(true); // this should be calculated on save
$this->addHook('beforeSave',$this);
}
function beforeSave($m){
$t=$m->ref('territory_id');
if($t->loaded()){
$m=set('name',$m->get('street').' '.$m->get('house').' '.$m->get('number').', '.$t->get('name'));
}
return $this;
}
}

This looks like a correct solution. At least one of them :)
class Model_Address extends Model_Table{
public $table='address';
function init(){
parent::init();
$this->hasOne('Territory');
$this->addField('street');
$this->addField('house');
$this->addField('number');
$this->addField('name')->readonly(true); // this should be calculated on save
$this->addHook('beforeSave',$this);
}
function beforeSave($m){
$t=$m->ref('territory_id');
if($t->loaded()){
$m=set('name',$m->get('street').' '.$m->get('house').' '.$m->get('number').', '.$t->get('name'));
}
return $this;
}
}
I could use afterLoad hook too, but I have decided to better save this concatenated value in database to minimize load time calculations.
Also when you use afterLoad hook you should be aware that it uses lazy load. That is, it loads only these model fields which are asked and not all of them.
For example, if you have grid with only columns street, house and name, then afterLoad will load only these three fields. Flat, territory_id and number will be empty no matter what. So desired functionality will not fully work that way.
Same applies to using addExpression + callback field.

in the previous version (4.1) the hooks are already in the framework and the functions just do nothing when called unles you override them in the model.
i assume this is the same in 4.2 that the beforeModify, beforeInsert and beforeDelete just need defining in the model and add whatever logic you need in there - this includes adding other models that might have calculated fields or php to do some calcs and dsql statements to populate.

Related

laravel Model returns null on calling another function from same model

I have a Product Model in which I have a attribute function which return certain data.
Normally if I call it with Product::with(['attributes'])->active()->paginate(config('app.rec_limit')); I get the output.
but I am not able to access that or any other function data in model if I do this,
protected $appends = ['product_attributes'];
public function getProductAttributesAttribute() {
return $this->attributes();
}
public function attributes() {
return $this->hasMany(ProductAttribute::class);
}
also if I pass a string in the getProductAttributesAttribute function I get that string as output.
What can be the reason here for an empty array as output?
Modify your code: return $this->attributes(); returns an eloquent object, not a collection, as if you still want to perform other queries. Change to return $this->attributes; which returns a collection
Note that using this means attributes are fetched for each Product Model, which can lead to performance issues later on.
The ideal way is to leave it as a model relationship, then call it only when you need it

Invoking cells directly from the controller

I am working with cakephp 3.1.7 and figuring out how to invoke view cells or retrieve cell data from the controller. I implemented basic cells with the help of cakephp docs and also http://josediazgonzalez.com/2014/03/20/view-cells/ document which is working fine. However, when I try to return cells directly from the controller I get the following error.
Error: Call to undefined method App\Controller\ProductsController::decorate()
This is what I have:
use Cake\View\Cell;
use Cake\ORM\TableRegistry;
class ProductupdateCell extends Cell
{
public function display($options = []){
if (!empty($options['displaylist'])) {
$this->set('productlist', $options['displaylist']);
return $this;
}else{
$category = $this->request->query['category'];
$this->loadModel('Products');
$query = $this->Products-> find()
-> where(['Products.category' => $category])
-> hydrate(false);
$productlist = $query->toArray();
$this->set('productlist',$productlist);
return $this;
}
}
}
In my controller,
<?php
class ProductsController extends Controller
{
use CellTrait;
public function view($id)
{
$products = $this->Products->findById($id);
$this->set('displaylist', $this->decorate('ProductupdateCell', $products));
}
}
Please correct me where I am going wrong. Is it efficient to use this to update my product list based on user input with ajax request? Can I selectively update the particular cell rendered in my view page? Is there any other method to update the cell directly. Please forgive me if this is a dumb question.
I am working with cakephp 3.1.7 and figuring out how to invoke view cells or retrieve cell data from the controller.
This is an architecturally wrong. They're supposed to be used from the view level.
If you want to have modular and abstracted controller logic either use the CRUD plugin. Or simply go for components. Components are packages of logic that are shared between controllers.
Error: Call to undefined method App\Controller\ProductsController::decorate()
There is no such method in the Controller, CellTrait nor the View class. I don't know from where you got that code, it's also not in the documentation of the cells.

Yii2 add condition in the last one of chain relations

Right now I have:
$products = Product::findAll([1,2,3,4]);
foreach ($products as $product){
$text = $product->part->type->texts;
}
This returns the related records from Texts table.
But I need to have only 1 record from it, and to do that I need to have one more condition in the last join type->texts, which is not defined in the model. It's dynamic session variable.
Is there any way to do this?
If you want modify the last relation query to have additional condition and return one record instead of many, simply change last relation call like so:
$text = $product->part->type->getTexts()->andWhere(...)->one();
Direct relation method call returns yii\db\ActiveQuery instance so you can modify conditions how you want.
If you want to use modified relation in more than just one place, create separate method for that:
/**
* #return yii\db\ActiveQuery
*/
public function getDynamicText()
{
// Insert link from texts relation similar as in hasMany() and additional condition in andWhere()
return $this->hasOne(...)->andWhere(...);
}
And then use it:
$text = $product->part->type->dynamicText;
In this case, scopes would be a handy solution, especially if you're going to use complicated conditions.
1. Start by creating a model that extends ActiveQuery with a method that will be used to add conditions to your query, for example active = 1:
namespace app\models;
use yii\db\ActiveQuery;
class TextQuery extends ActiveQuery
{
public function active($state = 1)
{
$this->andWhere(['active' => $state]); // or any other condition
return $this;
}
}
2. Override the find() method in your Text model:
public static function find()
{
return new \app\models\TextQuery(get_called_class());
}
3. Add a method in your Type model that retrieves your relational data via the newly made scope:
public function getActiveText()
{
return $this->hasMany(Text::className(), ['type_id' => 'id'])->active();
}
Finally, use it as follows:
$text = $product->part->type->activeText;
The docs are pretty clear on this, check 'em out.

First 'find' call does not return associated data. Second 'find' call does

The following is declared in model 'GradingPeriod':
class GradingPeriod extends AppModel {
public $belongsTo = array('AcademicYear' => array('className' => 'AcademicYear', 'foreignKey' => 'academic_year_id'));
...
public function getEnrolledSections(){
$this->recursive = 1;
debug($this->findById(21)); // Does **not** return AcademicYear
// model data when function is called
// from a different model.
debug($this->findById(21)); // **Does** return AcademicYear
// model data when function is called
// from a different model.
die();
}
}
When called from a controller or inside the GradingPeriod model, this works fine. The first 'find' call does return the GradingPeriod model's associated data (AcademicYear).
When called from a different model, the first 'find' call does not return the GradingPeriod model's associated data (AcademicYear). The second 'find' call does return the GradingPeriod model's associated data (AcademicYear).
class ReportCard extends AppModel {
public function callToGradingPeriod(){
$objGradingPeriod = ClassRegistry::init('GradingPeriod');
$objGradingPeriod->getEnrolledSections();
}
}
I have tried this with CakePHP 2.1.2 and 2.2.3 with the same results.
I know calling one model from another may be considered bad form, but why is this code behaving as it does? Thank you in advance for any assistance you can provide.
This is not really an answer as far as why it's working (or not) the way it is, but a suggestion for you and any future users trying something similar:
Don't EVER use recursive to get associated data. Set public $recursive = -1; in your AppModel and never look back. When you want related data, use CakePHP's Containable Behavior.
It might seem like recursive is your friend - or that it's "just easier", but I promise it will cause issues further down the road - either when you want more data, or just when you get more data in your database (memory issues/errors among other things). It's bad practice, and I believe they're even going to remove recursive all together in CakePHP 3+.
Trust me on this one - ditch recursive, and use contain() instead.

Bind a condition to a model in cakephp

<?php
class User extends AppModel {
var $name = 'User';
var $displayField = 'fname';
}
How can I only return users from this model that have a "standing" of "1"? I am not looking to do this from the controller but, from the model.
[Solution] In model
function beforeFind($queryData){
$queryData['conditions']['standing'] = 1;
return $queryData;
}
The easiest way to do this would be to put in some filtering conditions in your beforeFind callback. Modifying the $queryData variable and adding your restriction to the conditions key should do it.
From the manual entry - http://book.cakephp.org/1.3/en/view/1049/beforeFind
Called before any find-related operation. The $queryData passed to
this callback contains information about the current query:
conditions, fields, etc.
If you do not wish the find operation to begin (possibly based on a
decision relating to the $queryData options), return false. Otherwise,
return the possibly modified $queryData, or anything you want to get
passed to find and its counterparts.
You might use this callback to restrict find operations based on a
user’s role, or make caching decisions based on the current load.

Resources