After saving a new model / active record in Yii2, I'm scheduling a job on the filesystem. I do this in afterSave and I would like to update the record with the job id of the scheduler I get back.
But when I call $this->update() in the model after setting the correct property to the job id, no update is happening. Using update() in an afterSave() is probably a bad idea, but what would then be the right way to tackle this?
You can use updateAttributes($attributes)
This method is a shortcut to update() when data validation is not
needed and only a small set attributes need to be updated.
You may specify the attributes to be updated as name list or
name-value pairs. If the latter, the corresponding attribute values
will be modified accordingly. The method will then save the specified
attributes into database.
Note that this method will not perform data validation and will not
trigger events.
http://www.yiiframework.com/doc-2.0/yii-db-baseactiverecord.html#updateAttributes()-detail
I see 2 options there:
Create additional internal field in the model, something like private $task_sended = false; In afterUpdate set task_sended = true and check It
if (!$this->task_sended){
//send task to scheduler
$this->task_sended = true;
}
Use DAO command to update model
\Yii::$app->db->createCommand()->update(self::tableName(), $update, ['id'=>$this->id])->execute();
Related
imagine I have some doctrine Entity and I can have some records of this entity in the database which I dont want to be deleted, but I want them to be visible.
In general I can have entities, for which I have default records, which must stay there - must not be deleted, but must be visible.
Or for example, I want to have special User account only for CRON operations. I want this account to be visible in list of users, but it must not be deleted - obviously.
I was searching and best what I get was SoftDeletable https://github.com/Atlantic18/DoctrineExtensions/blob/v2.4.x/doc/softdeleteable.md It prevents fyzical/real deletion from DB, but also makes it unvisible on the Front of the app. It is good approach - make a column in the Entity's respective table column - 1/0 flag - which will mark what can not be deleted. I would also like it this way because it can be used as a Trait in multiple Entities. I think this would be good candidate for another extension in the above Atlantic18/DoctrineExtensions extension. If you think this is good idea (Doctrine filter) what is the best steps to do it?
The question is, is this the only way? Do you have a better solution? What is common way to solve this?
EDIT:
1. So, we know, that we need additional column in a database - it is easy to make a trait for it to make it reusable
But
2. To not have any additional code in each repository, how to accomplish the logic of "if column is tru, prevent delete" with help of Annotation? Like it is in SoftDeletable example above.
Thank you in advance.
You could do this down at the database level. Just create a table called for example protected_users with foreign key to users and set the key to ON DELETE RESTRICT. Create a record in this table for every user you don't want to delete. That way any attempt to delete the record will fail both in Doctrine as well as on db level (on any manual intervention in db). No edit to users entity itself is needed and it's protected even without Doctrine. Of course, you can make an entity for that protected_users table.
You can also create a method on User entity like isProtected() which will just check if related ProtectedUser entity exists.
You should have a look at the doctrine events with Symfony:
Step1: I create a ProtectedInterface interface with one method:
public function isDeletable(): boolean
Step2: I create a ProtectionTrait trait which create a new property. This isDeletable property is annotated with #ORM/Column. The trait implements the isDeletable(). It only is a getter.
If my entity could have some undeletable data, I update the class. My class will now implement my DeleteProtectedInterface and use my ProtectionTrait.
Step3: I create an exception which will be thrown each time someone try to delete an undeletable entity.
Step4: Here is the tips: I create a listener like the softdeletable. In this listener, I add a condition test when my entity implements the ProtectedInterface, I call the getter isDeleteable():
final class ProtectedDeletableSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $onFlushEventArgs): void
{
$entityManager = $onFlushEventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof ProtectedInterface && !$entity->isDeletable()) {
throw new EntityNotDeletableException();
}
}
}
}
I think that this code could be optimized, because it is called each time I delete an entity. On my application, users don't delete a lot of data. If you use the SoftDeletable component, you should replace it by a mix between this one and the original one to avoid a lot of test. As example, you could do this:
final class ProtectedSoftDeletableSubscriber implements EventSubscriber
{
public function onFlush(OnFlushEventArgs $onFlushEventArgs): void
{
$entityManager = $onFlushEventArgs->getEntityManager();
$unitOfWork = $entityManager->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof ProtectedInterface && !$entity->isDeletable()) {
throw new EntityNotDeletableException();
}
if (!$entity instance SoftDeletableInterface) {
return
}
//paste the code of the softdeletable subscriber
}
}
}
Well the best way to achieve this is to have one more column in the database for example boolean canBeDeleted and set it to true if the record must not be deleted. Then in the delete method in your repository you can check if the record that is passed to be deleted can be deleted and throw exception or handle the situation by other way. You can add this field to a trait and add it to any entity with just one line.
Soft delete is when you want to mark a record as deleted but you want it to stay in the database.
I want to check if certain columns in database are changed.
the update code in my controller goes like this:
$tCustomer = TCustomer::withTrashed()->findOrFail($id);
$tCustomer->update(request()->all());
How do I incorporate it with the ->isDirty() function?
I tried adding it after $tCustomer->update(request()->all()); but it always returns false:
$dirty = $tCustomer->getDirty('payment_method_id');
do I have to add isDirty() before or right after the update?
You can uuse fill() instead of update(), check for isDirty(), then save(). This way you can take advantage of the mass injectable fields.
$myModel->fill($arrayLikeinUpdate);
if ($myModel->isDirty()) {
// do something
}
$myModel->save();
You have to use observers, you can use updating() eloquent model event for before saving the model or updated() after saving model, you just have to add below code in your TCustomer model:
public static function boot(){
static::updated(function($tCustomer){
if($tCustomer->isDirty('field_name')){
//This code will run only after model save and field_name is updated, You can do whatever you want like triggering event etc.
}
}
static::updating(function($tCustomer){
if($tCustomer->isDirty('field_name')){
//This code will run only before saving model and field_name is updating, You can do whatever you want like triggering event etc.
}
}
isDirty returns a bool, so you'd use it with a conditional to check if a given models attributes have changed. Example:
// modify an attribute
$myModel->foo = 'some new value';
....
// do other stuff
...
// before the model has been saved
if ($myModel->isDirty()) {
// update model
$myModel->save();
}
So the check needs to be done before you update (save) the model.
Calling update saves the model with the given attributes in one call so you wouldn't use isDirty in that context.
I have a Resource Controller (with all the actions: index, create, store, show, edit, update and destroy) and I was wondering what is the best approach to edit a single field column?
Let's say we have a Users table with name, email, password and active (active is a tiny int 0 or 1).
In the users management page, there is a button to activate/deactivate users (makes a request to the server to update the "active" field for the selected user).
Should I create a new method updateStatus in the Controller or is there a way to handle this using the update method?
I don't want, by mistake, allow empty values in the name, email or password when updating the "active" column, so I need to keep the validation rules (in short, all fields are required), but this means when updating the "active" field, I need to pass all the user data in the request.
At this point I'm very confused and all help will be appreciated.
Thanks in advance!
When you send an instance from edit action to the form , all the data will be sent and you can edit one or more columns if you need .
For instance :
public function update(Request $request , $id) {
$data = YourModel::find($id);
$data->someColumn = $request->someColumn;
$data->save();
}
other fields that you didn't send any value for them will be saved as they were before . for this you can set the form like below :
{!! Form::model($yourInstance,['route'=>['someRoute.update','id'=>$yourInstance->id],'method'=>'PATCH',]) !!}
It sounds like you are new to Laravel, and some key concepts can be hard to grasp.
In my opinion the best way to do it would be via a Model class. This is slightly confused by the fact that Laravel has a built in Users model, so I'm going to use a different model as the example of how to update a db field.
php artisan make:model MyData
Will create a new empty model file for the MyData table in app/
The file will look like this:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MyData extends Model
{
//
}
Even though there's nothing in there, it now allows you do alter the database table using Eloquent.
In your controller add this to make sure the model is included:
use App\MyData as MyData;
The controller should have a method something like this if updating with user input from a form:
public function updateStatus(MyData $myData, Request $request){
$myData->where('id', $request->id)->update(['active' => $request->active]);
}
You could do the exact same thing like this:
public function updateStatus(Request $request){
$data = MyData::find($request->id);
$data->active = $request->active;
$data->save();
}
Both approaches make sense in different circumstances.
See https://laravel.com/docs/5.5/eloquent#updates
We are trying to detect the changes in Laravel related models at attribute level, as we have to keep audit trail of all the changes which are made via the application.
We can track the changes via isDirty method on the Eloquent model for single model that is not related to any other model, but there is no way that we can track the changes on the related eloquent models. isDirty doesn't work on related models attributes. Can some one please help us on this?
Update to original question:
Actually we are trying to track changes on the pivot table that has extra attributes as well defined on it. IsDirty method doesn't work on those extra attributes which are defined in the pivot table.
Thanks
As much I understand your question, It's can achieve through Model Event and some sort of extra code with current and relation model.
Laravel Model Events
If you dont want to use any additional stuff, you can just use the Laravel Model Events (that in fact Ardent is wrapping in the hooks). Look into the docs http://laravel.com/docs/5.1/eloquent#events
Eloquent models fire several events, allowing you to hook into various
points in the model's lifecycle using the following methods: creating,
created, updating, updated, saving, saved, deleting, deleted,
restoring, restored.
Whenever a new item is saved for the first time, the creating and
created events will fire. If an item is not new and the save method is
called, the updating / updated events will fire. In both cases, the
saving / saved events will fire.
If false is returned from the creating, updating, saving, or deleting
events, the action will be cancelled:
Finally, reffering to your question you can utilize the above approaches in numerous ways but most obviously you can combine it (or not) with the Eloquent Models' getDirty() api docs here method and getRelation() api docs here method
It will work for example with the saving event.
Model::saving(function($model){
foreach($model->getDirty() as $attribute => $value){
$original= $model->getOriginal($attribute);
echo "Changed";
}
$relations = $model->getRelations();
foreach($relations as $relation){
$relation_model = getRelation($relation);
foreach($relation_model->getDirty() as $attribute => $value){
$original= $relation_model->getOriginal($attribute);
echo "Relation Changed";
}
}
return true; //if false the model wont save!
});
Another Thought might help you. when you saving
save() will check if something in the model has changed. If it hasn't it won't run a db query.
Here's the relevant part of code in Illuminate\Database\Eloquent\Model#performUpdate:
protected function performUpdate(Builder $query, array $options = [])
{
$dirty = $this->getDirty();
if (count($dirty) > 0)
{
// runs update query
}
return true;
}
The getDirty() method simply compares the current attributes with a copy saved in original when the model is created. This is done in the syncOriginal() method:
public function __construct(array $attributes = array())
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
public function syncOriginal()
{
$this->original = $this->attributes;
return $this;
}
check model is dirty isDirty():
if($user->isDirty()){
// changes have been made
}
Or check certain attribute:
if($user->isDirty('price')){
// price has changed
}
I did not check this code but hopeful to use as your answer by thoughts, if you have any confusion to deal such requirement or something need to optimize or change please let me know.
I new in yii framework. i create an application in yii framework. i created model, controller, views using gii. After that i alter database table. I deleted 2 column and add 3 new columns. After that overwrite the model using the gii. But when i am trying to save into that table it show property(which was old column that I deleted) is not defined. Plz provide me a solution for this.
You need to define all columns in the validation rules() method in your model, have a look and make sure that you have defined a rule for every column in the table there, for example (if it's a string with max length 128):
public function rules()
{
return array(
...
array('myField', 'length', 'max'=>128),
...
);
}
See some info about validation rules.
Also, for forms if you're using CActiveForm widget and calling fields like so:
echo $form->labelEx($model,'myField');
echo $form->textField($model,'myField');
Then you'll need to make sure that a label is defined in the model too, in the attributeLabels() method, for example:
public function attributeLabels()
{
return array(
...
'myField'=>'My Field',
...
);
}
Lastly, if you want the field to be searchable, you'll need to add a statement to the search() method in the model, for example:
public function search()
{
...
$criteria->compare('myField',$this->myField);
...
}
Make sure you have all of those elements present and you shouldn't get the '* is not defined' error.
Also, if you're using schema caching in your main config file, you'll have to clear your cache before the app will see your new database structure.
Your changes should also be set at the Views since there are forms, widgets using the old properties !! (for this exact save issue, you will need to fix _form.php which is the partial responsible from your model Save & Update actions.
You can either do the same as you did with the model: (regenerate it using gii) or you can edit it manually (i recommend you get used to this since in the future you will have code you don't want to loose just because of altering a column name. simple Find & edit in most of the text editors will do the job).
May be you need to read a bit more about how MVC works in general & in Yii in special
This is because you are using schema-cache. Your table schema is cached in Yii. You need to flush AR cache. Either flush full schema cache or use
Yii::app()->db->schema->getTable('tablename', true); in start of your action. This will update model schema-cache.