CakePHP Model Callback, specifically beforeDelete - cakephp

I'm trying to execute some logic before deleting a field. I have some models that are dependent on the model being deleted, and I want to make sure that image files related to those dependent models are also deleted, but I'm a bit confused on how the model callbacks work.
I know that I define the before Delete function in the model class, but How do I access the data in the current model or dependent models being deleted?
function beforeDelete() {
}
I'm just a little confused as how to use these callbacks, and I haven't seen any great documentation out there.
Edit:
After adding this to the parent model, it seems to always return false.
function beforeDelete() {
if ($this->DependentModel->find('count', array('conditions' => array('DependentModel.parent_id' => $this->id))) == 1){
return true;
} else{
return false;
}
}
Should be obvious what I'm trying to do here. If there is one entry of the dependent model present in the table, return true and continue the deletion. I made sure that there is in fact one table entry that is dependent on the object being deleted. When I execute the delete action it always returns false. What's going on here?

When using callbacks, you can refer to the API for the class you are extending to check the parameters it accepts. Your implementation should accept, at minimum, the same parameters as the methods you are overriding.
For example, Model::beforeDelete is implemented like so:
/**
* Called before every deletion operation.
*
* #param boolean $cascade If true records that depend on this record will also be deleted
* #return boolean True if the operation should continue, false if it should abort
* #link http://book.cakephp.org/2.0/en/models/callback-methods.html#beforedelete
*/
public function beforeDelete($cascade = true) {
return true;
}
And also, ModelBehavior::beforeDelete is implemented like this (ie. when making a behavior):
/**
* Before delete is called before any delete occurs on the attached model, but after the model's
* beforeDelete is called. Returning false from a beforeDelete will abort the delete.
*
* #param Model $model Model using this behavior
* #param boolean $cascade If true records that depend on this record will also be deleted
* #return mixed False if the operation should abort. Any other result will continue.
*/
public function beforeDelete(Model $model, $cascade = true) {
return true;
}
Next, it is useful to know that when saving to a model and passing in the controller's data (ie. $this->data in the controller) that data is set to the model (ie. $this->data in the model). [This happens during Model::save(), currently on line 1225.]
In the case of the first example you can access the model using $this and in the second example you can access the model using $model (as $this would be the behavior in that context). So to get at the data, you want to use $this->data or $model->data. You can also access that model's related models using chaining (ie. $this->RelatedModel or $model->RelatedModel).
As the docblock comments state, $cascade should let you know if this is a cascading delete that is happening (true by default) in case your code needs to take different actions when this is or isn't the case; and your implementation of the method should return false if you want to abort the save operation (otherwise, return true when you are done).
There is a Media plugin for CakePHP which implements this exact functionality that can be used as a reference.

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

How to use beforeSave in CakePHP 3? $event, $entity and $options must be always filled?

I'm inside "PostsTable.php" I'm trying to get form data to treat image files.
In CakePHP 2, I used to do:
public function beforeSave($options = array())
{
if(!empty($this->data['Post']['picture']['name'])...
Someone could explain this in Cake 3:
beforeSave
Cake\ORM\Table::beforeSave(Event $event, Entity $entity, ArrayObject $options)
?
ADDED
I try this snippet of code to see if I'm able to save this field on database just as a test but it seems beforeSave is being ignored:
public function beforeSave($options)
{
if(!empty($entity->pic1['name']))
{
$entity->pic1 = 'jus a test';
}
Thanks
Start with the function definition.
Cake\ORM\Table::beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
Since CakePHP is calling the function automatically, this is how it is being called, so build your function identically to the function definition:
// In PostsTable.php
public function beforeSave($event, $entity, $options) {
}
If you aren't sure what data is being sent, use CakePHP's debug() function:
debug($event); debug($entity); debug($options);
Once you find your data in $entity use it to do what you want to do to your data:
if (!empty($entity->picture['name'])) { ...
Here is an example that I used:
use Cake\Event\Event;
public function beforeSave(Event $event)
{
$entity = $event->getData('entity');
if(!empty($entity->picture['name']){
// your action here
}
}
Let me read the manual for you:
The Model.beforeSave event is fired before each entity is saved. Stopping this event will abort the save operation. When the event is stopped the result of the event will be returned.
Also:
Model.beforeSave: Will be triggered just before the list of fields to be persisted is calculated. It receives both the entity and the options as arguments. The options array is passed as an ArrayObject, so any changes in it will be reflected in every listener and remembered at the end of the event so it can be used for the rest of the save operation. Returning false in any of the listeners will abort the saving process. If the event is stopped using the event API, the event object's result property will be returned. This can be useful when having your own saving strategy implemented inside a listener.
To do whatever you did before in Cake2 you can simply modify $entity because entities have replaced the Model::$data property.
if(!empty($entity->picture['name'])
If you don't know how events work read about them. Also the migration guide is a must read. It lists everything that has changed.

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