Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fire model events after event methods to facilitate extensibility. #145

Closed
wants to merge 4 commits into from

Conversation

mjauvin
Copy link
Member

@mjauvin mjauvin commented Apr 4, 2023

This is a proposal to fire the model events (before/after create/delete/save/update) after the corresponding model event method has been called in order to allow overriding the behavior of the model event methods using events.

TODO:

  • apply to Halcyon/Model
  • should this apply to SoftDelete trait ?
  • should this apply to Validation trait ?

@mjauvin mjauvin added this to the v1.2.2 milestone Apr 4, 2023
@mjauvin mjauvin self-assigned this Apr 4, 2023
@mjauvin mjauvin changed the title Fire model events after event method to facilitate extensibility. Fire model events after event methods to facilitate extensibility. Apr 4, 2023
@LukeTowers LukeTowers modified the milestones: v1.2.2, v1.2.3 Apr 20, 2023
@mjauvin mjauvin removed the request for review from LukeTowers April 21, 2023 02:57
@LukeTowers LukeTowers modified the milestones: v1.2.3, v1.2.4 Jul 7, 2023
@mjauvin
Copy link
Member Author

mjauvin commented Jul 28, 2023

Any feedback on this one guys?

@LukeTowers
Copy link
Member

@mjauvin could you provide a usage example?

@mjauvin
Copy link
Member Author

mjauvin commented Aug 2, 2023

@LukeTowers one example is in your EasyForms plugin, for the Confirmation model, you have the beforeSave() method like this:

    public function beforeSave()
    {
        // Prevent default confirmations from being disabled or renamed
        if ($this->is_default) {
            $this->is_active = true;
            $this->name = 'Default Confirmation';
        }
    }

If for some reason you want to extend this model dynamically by using the model.beforeSave event, you cannot change is_active for example since the model method runs AFTER the event gets called.

This is just an example, but the idea is that currently, the default model functionality cannot be changed dynamically with model events if the original model has a corresponding method defined which overrides the changes you want to make.

@LukeTowers
Copy link
Member

If we're going to make this change it should be applied to everything in the core that fires the model events using this pattern.

@mjauvin
Copy link
Member Author

mjauvin commented Aug 5, 2023

If we're going to make this change it should be applied to everything in the core that fires the model events using this pattern.

Done!

@LukeTowers
Copy link
Member

@mjauvin after thinking about this more I think we're still stuck with the same problem. Now if someone wants to prevent something happening in beforeSave or beforeValidate for example they aren't able to do so.

I think the only solution would be to force the model methods to fire as listeners with a default priority on the events, i.e.

if ($model->methodExists('beforeSave')) {
    $model->bindEvent('model.beforeSave', fn => $model->beforeSave()); // or whatever the syntax is
}

Any thoughts on this @bennothommo or @jaxwilko?

@bennothommo
Copy link
Member

bennothommo commented Aug 6, 2023

I still have the same thoughts as before to be honest - I've always felt that the internal "developer intended" model methods should have the last say on anything, over any externally driven functionality through events.

I know it goes (somewhat) against our implied total extensibility, but it gives some agency to the plugin (or core feature) to have at least some control over its own functionality.

EDIT: However... I think it would be palatable to copy DOM events, in that an external event listener can prevent the default functionality, so perhaps we can keep the original order of the calls (prior to @mjauvin's PR) but maybe allow some way to "prevent default" - even if it's just allowing false to be returned and using that to skip the model's callback method.

I realise this goes against my first thoughts 😂. I guess I'm just trying to find a middle ground.

@wintercms wintercms deleted a comment from what-the-diff bot Aug 6, 2023
@mjauvin
Copy link
Member Author

mjauvin commented Aug 6, 2023

Ok, I see good points on each side, not sure what to think anymore...

@jaxwilko
Copy link
Member

jaxwilko commented Aug 6, 2023

I'm kinda with @bennothommo's edit, overriding the default would be nice, but could also introduce a load of different problems. What I see being an issue could be replacing a 3rd party plugin's beforeSave to manipulate data but then the 3rd party pushing an update with extra logic in beforeSave than now causes issues when the method is being overwritten.

Is it worth writing the test cases for what we would expect as plugin devs and then working out how it should work from that?

@LukeTowers
Copy link
Member

What I see being an issue could be replacing a 3rd party plugin's beforeSave to manipulate data but then the 3rd party pushing an update with extra logic in beforeSave than now causes issues when the method is being overwritten.

My expectations as both the author of the original plugin and the author of the extending plugin is that it is solely the responsibility of the extending plugin to maintain compatibility with the original and to appropriately lock down its own requirements if such a concern arises.

At this point in time I've personally not had a need for any changes to be made to the way the events / methods play with each other currently, but I think placing them on the same level and giving the control to the listener via the event priority system to attach either before or after the methods run would give the most flexibility.

However, as always, I do not like making changes for changes sake and I agree with @jaxwilko that we'd need to see some more real world examples of the need to change what we already have or just test cases for the desired functionality in general.

@mjauvin
Copy link
Member Author

mjauvin commented Aug 6, 2023

I would go with @LukeTowers' proposed solution with the events, I thinks it gives maximum flexibility without breaking anything like changing the order might...

@LukeTowers
Copy link
Member

@bennothommo @jaxwilko any final thoughts?

@jaxwilko
Copy link
Member

jaxwilko commented Aug 7, 2023

Nope, I did encounter something that might be relevant, trying to prevent file deletion in a afterDelete (File model), but it's not that important for my use case atm

@mjauvin
Copy link
Member Author

mjauvin commented Aug 7, 2023

Nope, I did encounter something that might be relevant, trying to prevent file deletion in a afterDelete (File model), but it's not that important for my use case atm

@jaxwilko You can use the deleting() method on the model to do this:

YourModel::deleting(function ($model) {
    return false;
}); 

@mjauvin
Copy link
Member Author

mjauvin commented Aug 7, 2023

See #150 for the solution proposed by @LukeTowers

I tested it and it works very well, preserving default behavior.

@LukeTowers
Copy link
Member

Closing in favour of #150

@LukeTowers LukeTowers closed this Aug 8, 2023
@LukeTowers LukeTowers deleted the model-events-last branch August 8, 2023 04:59
LukeTowers pushed a commit that referenced this pull request Aug 13, 2023
Replaces #145

This preserves current behaviour: an event listener defined on the model with default priority (0) will get called first, then the model method will get called (through an event listener with default priority).

If an event listener uses a LOWER priority (e.g. -1), the model method will get called first (because its priority is 0 by default)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants