I want to execute my custom code after or before every $model->save() in Yii2.
I want to perform this globally like using components, etc.
I want to create a user activity log to store how many times a user insert or update any rows in database table, so for this I want to run some code when ever data inserted or update in tables.
Any help or suggestion will appreciated.
As #patryk mentioned ActiveRecord has beforeSave and afterSave methods.
I use something like the following to store a created date for new records (and updated date when existing records are updated). The code in the example is, of course, trivial but it allows you to use any arbitrary code you need, see the layout and how to split code for 'new' records and existing.
This overridden method can be added to any model class which extends ActiveRecord to allow the parent beforeSave to be called correctly also.
/**
* #inheritdoc
*/
public function beforeSave($insert)
{
if ($insert) {
// This is a new instance of modelClass, run your 'insert' code here.
$this->created_date = time();
}
// Anything else will be run any time a model is saved.
$this->updated_date = time();
return parent::beforeSave($insert);
}
edited to add:
if the code to be run is the same for each model you could create a trait and use the trait in each model to allow you to change the behaviour in one place. Or create a custom ActiveRecord class to override the beforeSave method for each subclass.
Create new class(MyActiveRecord) which extends \yii\db\ActiveRecord
Use extends MyActiveRecord to all your project models
Ex:
class MyActiveRecord extends \yii\db\ActiveRecord
{
public function afterSave($insert, $changedAttributes){
//This will called after every model saved
return parent::beforeSave($insert,$changedAttributes);
}
}
In your project other models
class Customer extends app\models\MyActiveRecord
{
}
Yii2 ActiveRecord class has beforeSave and afterSave methods. https://github.com/yiisoft/yii2/blob/master/framework/db/BaseActiveRecord.php#L926
But maybe it would be better to do such operation on database triggers?
Related
I have a package media library by spatie. I need to get table name of the model.
I know that I can do this:
public function getPath(Media $media) {
$name = (new $media->model())->getTable()
}
But this creates a new query. I don't need to create an extra query on database. In table media, I have a column a model_type, where records can be like this: App\ModelName. Maybe I can get names of the model without a query?
There is an answer in laravel framework github:
https://github.com/laravel/framework/issues/1436 .
So it seems you will need to extend Media model.
Example from github
class BaseModel extends Eloquent {
public static function getTableName()
{
return with(new static)->getTable();
}
}
class User extends BaseModel {
}
User::getTableName();
I don't think "new model()" created a query on the database, it just spawns a new object instance of the model class. I don't know the library by heart, but given that it's a Spatie library, it probably functions very similar like Eloquent does, which has the same behaviour.
These are my classes
class DB{
// returns the instance as it should - all system works well
public function create(){
// creates a row in the db....
}
}
class User{
// does all it does....
}
I have an autoloader that loads the classes and the system works well
$user = new User();
$user->create(....)
I want to use the method create from the DB while instantiating User class, how can I do so?
the thing that I want to prevent is not having to need to create a function "update" in every class which needs to be updated
For example:
Class user - update "users table"
class kids - update "kids table"
instead
// from DB class
update($table_name, $field)
The autoloader is responsible for finding the and including the files that contain the class definitions so you don't have to explicitly call include 'db.php'; etc. , but it isn't going to be able to do what you're trying to do here.
If the create() method is defined in the DB class, there's no reason for it to be accessible through an instance of a different class like User, and unless you specifically do something to allow it, the autoloader won't help with that.
The only way for you to access the DB::create() method from an instance of User is for User to extend DB.
class User extends DB { ...
This means that all User objects will also be instances of DB and have access to all of the public and protected methods of DB, not just create().
It looks like this might be what you want. If that's the case, it would be helpful for you to read up on Object Inheritance.
Well i don't know how to format the title of this post in very clear way, but here's my question:
Say i have
Posts::find('1);
Photos:find('1');
... and so on, every mode db request
now by default i can access db columns, for instance the id: through model->id
$Photos = Photos::find('1')->first();
echo $Photos->id; // will return 1
what i want is that i need all those kind of requests to add a custom field automatically like hashed_id, which is not in the database, which in return will make all models have a hashed_id as well, i know i can add that field to database and then grab it but i need it for different reasons/implementations
i did create a BaseModel and every Model will extend that BaseModel, so Photos extends BaseModel, BaseModel extends Model... and all that etc etc.
but i need some kind of constructor, upon retrieving data to process the data automatically without having to add -let's say- a hash_id() after retrieving the data.
something like, onAfterGet(), onReady()....sort of commands.
i hope my question is clear.
Thanks.
What you're looking for is an Accessor. Accesors can be used to add custom attributes to the model. Combine this with the $appends property and you have exactly what you need. The $appends property adds the custom accessor in every result.
You can do this by creating a base model like you've stated in the question or by using traits. I'll show you an example on how to achieve this using a base model.
Let's create base model called BaseModel. All other models that need this custom attribute will extend this.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BaseModel extends Model
{
protected $appends = ['hashed_id'];
public function getHashedIdAttribute()
{
return some_hash_function($this->id);
}
}
We have a Image model which extends our BaseModel.
<?php
namespace App;
class Image extends BaseModel
{
}
Now every result from the Image model will have the hashed_id field added by default.
Accesor documenation https://laravel.com/docs/5.4/eloquent-mutators#defining-an-accessor
If I understand you right, all you need to do is to define mutator, for example:
<?php
class Photo extends Model
{
/* ... model implementation ... */
public function getHashedIdAttribute()
{
return md5($this->id);
}
}
Then you can access property like it was in database:
echo Photo::find(5)->hashed_id;
I have a load of Models. Users can comment on some of the model records. For example, a user can comment on a Notice model record, or a user can comment on a Calendar Model record.
I've created a trait called Commentable. This contains all the methods needed to retrieve comments based on a model, to add / delete comments, create the comment create form and so on.
Whenever I want a model to be commentable, all I need to do is use that trait within the model.
Because that comment is a polymorphic relationship with the model record, I can't do an onCascade = delete migration.
Whenever the parent model is deleted (such as a notice or calendar item), then I want all associated records to be deleted as well, but I'd prefer not to have to rely on the developer writing the deleteRelatedComments() method call into an overridden delete function.
I thought I would create a service provider that listened for any model deletion event, check to see if that model was commentable, and delete any associated comments, but the event doesn't fire correctly.
This is my service provider code:
Model::deleting(function (Model $record) {
if(in_array('App\Libraries\Traits\Commentable', class_uses($record))) {
$record->deleteRelatedComments();
}
return true;
});
As you can see, all it does is check the deleted model to see if it uses the Commentable trait. If it does, it calls the deleteRelatedComments() method.
My Question
Is there any way to automatically delete related polymorphic related content on the deletion of it's parent record:
If I delete a Notice record, is there any way to delete any associated polymorphic Comment records?
I have previously created a service provider to listen for any Model deletes using Model Events as specified at http://laravel.com/docs/5.1/eloquent#events
However, this doesn't work if you're trying to listen to the Model class instead of a specific class such as Notice.
Instead, when you delete each model, a eloquent.deleting event is fired. I can then manually create a listener within the EventServiceProvider to listen for any model being deleted, and perform my checks accordingly, but without having the rely on the user overloading the delete() method and manually deleting the polymorphic relations.
Answer
If you want to delete polymorphic content automatically when you delete a model, make sure you either implement an interface or use a trait in the model.
For example, I want to delete all polymorphic comments when I delete the parent model record (If I delete a Notice record delete all polymorphic comments).
I created a Commentable trait which I use in any model (but you can just as easily use an empty interface on your model).
Then, in your EventServiceProvider.php alter the boot() method accordingly:
public function boot(DispatcherContract $events)
{
parent::boot($events);
/**
* If you use a trait in your model:
*/
$events->listen('eloquent.deleting*', function ($record) {
if (in_array('App\Libraries\Traits\Commentable', class_uses($record))) {
$record->deleteRelatedComments();
}
});
/**
* Or if you use an interface:
*/
$events->listen('eloquent.deleting*', function ($record) {
if ($record instanceof SomeInterface) {
$record->deleteSomePolymorphicRelation();
{
});
}
Use case:
Commentable Trait
namespace App\Libraries\Traits;
trait Commentable
{
public function comments()
{
return $this->morphMany(Comment::class, 'content');
}
public function deleteRelatedComments()
{
$this->comments()->delete();
}
}
Commentable Model
class Notice extends Model
{
use Commentable;
/** ... **/
/**
* NOTE: You do not need to overload the delete() method
*/
}
EventServiceProvider.php
public function boot(DispatcherContract $events)
{
parent::boot($events);
$events->listen('eloquent.deleting*', function ($record) {
if (in_array('App\Libraries\Traits\Commentable', class_uses($record))) {
$record->deleteRelatedComments();
}
});
}
Now, the developer creating a new content type that wants it to be commentable doesn't need to worry about creating the relations, managing the relation methods, or even deleting the polymorphic contents by overloading delete(). To make a model entirely commentable, they just need to use the Commentable trait and the framework handles everything.
This method can be used to automatically delete any polymorphic record, as long as the parent model implements some interface or uses a related trait.
i'm working with Agile Toolkit
i got a Model_Product
class Model_Product extends Model_Table {
public $table="product";
function init(){
parent::init();
$this->addField('name')->mandatory(true);
$this->addField('price')->mandatory(true)->type('money');
$this->addField('user_id')->refModel('Model_User')
->defaultValue($this->api->auth->get('id'));
//$this->hasOne('User',null,'email'); => send me an error message
}
}
and Model_User
class Model_User extends Model_Table {
public $table="user";
function init(){
parent::init();
$this->addField('first_name')->mandatory('Prénom nécesssaire');
$this->addField('last_name')->mandatory('Nom nécesssaire');
$this->addField('email')->mandatory('entrez un email valide');
$this->addField('nationality')->mandatory('nécessaire')->enum(array('FR','EN','US'));
$this->addField('birthday')->defaultValue(date('Y-m-d'))->type('date');
$this->addField('is_admin')->type('boolean');
$this->hasMany('Product','user_id');
}
I want to list on a User page all the products from one User
$q=$this->api->db->dsql();
$q->table('product')->where('product.user_id',$this->api->auth->model['id']);
$tab->add('GRID')->setModel($q);
Some way, I get it wrong because I get an error no mater how I try to filter my Model.
If you're not using newest ATK4 version from Github then you should grab it and stay up-to-date.
You should do like this:
1) In Model_Product create hasOne reference and not refModel (it's deprecated).
// adding 'user_id' parameter is not needed, it'll be calculated anyway
// but many developers add it anyway to clear thing up a bit.
$this->hasOne('User','user_id')->defaultValue($this->api->auth->get('id'));
2) Model_User is OK.
Just some side-notes about it:
I don't think you should make birthday = today() by default.
It's quite unbelievable that child at his first day in this world will use computer :)
is_admin should be mandatory + defaultValue(false) - by default user is not admin.
3) How to list all all products from current user.
// model of current user
$model = $this->add('Model_User')
->load($this->api->auth->get('id'));
// add grid
$page->add('Grid')
// reference Product model with condition already set
->setModel($model->ref('Product'));
and that's it.
Maybe even better and safer way is to define new model class for logged in user:
class Model_Myself extends Model_User {
function init(){
parent::init();
$this->addCondition('id', $this->api->auth->get('id'));
$this->loadAny(); // I'm not sure do we have to explicitly load it here
}
}
and then create grid like this
// model of products of current user
$prod_model = $this->add('Model_Myself')->ref('Product');
// add grid
$page->add('Grid')->setModel($prod_model);