Where to put beforeRules logic, when rules aren't being checked

I have an application with some complex interactions between fields, such as always requiring that property x be set to the same as y when z is true. There are also rules that require that x >= y. I’ve handled this to date by doing pre-processing in beforeRules: if ($entity->z) { $entity->x = $entity->y; } This works well, and the related rule will always pass.

Now, I’ve got a new requirement to be able to accept data from third-party sources. The data from them may not pass our validation rules, yet it must be saved anyway; the next time someone works on the record (which must happen in order for it to progress through the system’s workflow), the rules will be applied and they’ll need to resolve the discrepancy. This is also easily handled, by just passing 'checkRules' => false to the save call when receiving the third-party data.

The problem is, when I skip the rules, that also skips the beforeRules callback. But I still need for that code to run and set x when z is true. I’m looking for a way to make sure that this happens.

Thought #1: Move the business logic to beforeSave instead of beforeRules. But now if the entity is edited such that y > x but also z = true, the rule will fail and prevent the save from proceeding, so that’s a non-starter.

Thought #2: Add a buildRules callback on the affected tables, which can skip adding rules based on options passed to save. This seems like a good option, as it could skip only some rules and leave others in place. However, neither the buildRules callback nor the rulesChecker function have the options array passed to them, so implementing this would seem to require overloading both the checkRules and rulesChecker functions from the RulesAwareTrait to get the options to a useful place.

Thought #3: Change the implementation of every relevant rule (of which there are many) so that it can be skipped based on an option passed to the save function. This would probably work, but feels extremely invasive.

Thought #4: Always manually call beforeRules first when saving with 'checkRules' => false. This works, but relies on all developers knowing (and remembering) that this is required.

Any options that I haven’t considered here? For now, I think I’m going with #4, but maybe make a PR for Cake to pass the options array into the rule generator functions to support this sort of functionality in the future.

Drawing from vague memory:

Isn’t there a way to control the order of rules (or the order of event handlers), and a way to stop propagation of events?

If so, there would be the possibility of rearranging things. I guess your 3 party rules would have to be could be the first rule-handler/event?

Don Drake

Here are the two event system features that I was thinking about.

https://book.cakephp.org/4/en/core-libraries/events.html#establishing-priorities

https://book.cakephp.org/4/en/core-libraries/events.html#stopping-events

Rules don’t get passed the event, so no way for one rule to cause the rest to get skipped. Any one returning false would cause the save to fail.

It’s possible to change the order of handlers for a particular event, but no way to change the order of the events themselves.

So, unless I’m missing something in your suggestions, this won’t get me there.

No. Not missing anything.

How about:

I’m guessing the 3rd party save is on a particular Table class that is only used in this case? If so, can you override the Table::save() call there to:

  • manually trigger the beforeRules event
  • manually set checkRules = false
  • call the parent::save()

?

It’s tables that are used regularly by other functions too; the third-party data import transmogrifies their structure into ours. But I could add a “saveRegardless” type function that does what you suggest for these situations, that’s not a bad idea. (It’s not a great solution, IMO, but sometimes “not bad” is the best we can hope for. :slight_smile:)

I’m with you on ‘not bad’.

can you:

  • Write a sub-class of ThirdParty table
  • Let all your old code use oldThirdParty table as normal
  • Make your new 3rd party problem calls use the newThirdParty table (with the save override)

The advantage I imagine is

  • everyone uses the familiar save() call
  • The controller dedicated to accepting the foreign calls could install the new sub-class table
  • All other code/controllers keep using the old table class

The success of this strategy really depends on how focused the intake of the problem 3rd party data is on a single controller.

This has been a fun problem to consider. Especially since I’m not on the hook for a working solution.

1 Like

What I’m trying to develop is an OO solution to your option #4, which was proposed as a bit of procedure in existing code.

Finding a single place to compose in a Class that does the right thing while sticking to the familiar interface Table->save($entity) would represent a ‘best’ solution.

Having a new saveRegardless call is ok. It would wrap up all current and future implementation details in one method. But you’d still have to know and remember to use it. That was the legitimate concern with option #4

Maybe I’m getting your problem wrong, but did you think about using different validation-sets?

So you can e.g. simply accept the 3rd-party records at first and when they are edited/updated later, you let a more hardened validationset take effect…?

That only applies to validation, not rules. I think the closest approximation to what you’re talking about that works with rules is #3 from my original post.