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

APIv4 - When running validateValues(), fire an event #20184

Closed
wants to merge 7 commits into from

Conversation

totten
Copy link
Member

@totten totten commented Apr 28, 2021

Overview

This patch introduces an internal event, civi.api4.validate, which can be used to introduce more fine-tuned/per-entity validation rules.

Before

To validate an entity in APIv4, one would need to override each method (MyEntity::create(), MyEntity::save()), then override the relevant class and override the validateValues() method.

After

One may listen to civi.api4.validate and add extra validation rules. Here's an example adapted from the docblock:

Civi::dispatcher()->addListener('civi.api4.validate', function(ValidateValuesEvent $e) {
  if ($e->getEntity() !== 'Foozball') return;
  foreach ($e->getRecords() as $r => $record) {
    if (strtotime($record['start_time']) < CRM_Utils_Time::time()) {
      $e->addError($r, 'start_time', 'past', ts('Foozball can only be scheduled in the future. Foozball revisionism is not permitted.'));
    }
    if ($record['length'] * $record['width'] * $record['height'] > VOLUME_LIMIT) {
      $e->addError($r, ['length', 'width', 'height'], 'excessive_volume', ts('The record is too big.'));
    }
  }
});

Technical Details

As with hook_civicrm_pre and hook_civicrm_post or Drupal form hooks, this actually dispatches synonymous events (civi.api4.validate and civi.api4.validate::ENTITY_NAME). For typical/simple cases, this allows to avoid the problem of systemic over-subscription (wherein every listener as to receive every event and then do some conditionals).

There is fairly light test coverage directly in this patch. There is more extensive (but implicit) coverage in a follow-up branch (master-strings). Specifically:

  • The Strings entity has a validation listener.
  • The StringsTest has several cases where it attempts to send bad data - and the data is rejected based on a validation hook.

@civibot
Copy link

civibot bot commented Apr 28, 2021

(Standard links)

@civibot civibot bot added the master label Apr 28, 2021
@totten
Copy link
Member Author

totten commented Apr 28, 2021

ping @colemanw

@colemanw
Copy link
Member

Interesting. There's some overlap with #20170 or maybe it's synergy.

@colemanw
Copy link
Member

One thing I noticed right away is that this doesn't pass the name of the action to the event, it's assumed to be "create or maybe update". But those two aren't the same. In the Save action you have to tease them apart like this: 20af3ca#diff-ccb444858b7cfcd34c34786bd76003cdccad4c53fbadef2b89df5ba7a94e82f2R107

I think we're going to want to fire this event from the delete action too.

@totten totten force-pushed the master-api4-validate branch from 4d54b8c to 9eec5aa Compare May 18, 2021 09:03
@totten
Copy link
Member Author

totten commented May 18, 2021

@colemanw A few updates here:

  • Provide the name of the $action as part of the event (per suggestion)
  • Change the code-style to resemble PreEvent/PostEvent more (ie with respect to how event fields are arranged and how the sub-events are dispatched).
  • Rebase for good measure

I didn't expand the scope of validateValues to include the delete action... I'm not quite seeing how that would make sense. A delete doesn't have any $values to validate. (I suppose there are some old values in the DB... but if those were invalid to begin with, then it would be silly to require them to become valid before permitting deletion...)

Perhaps that suggestion was spurred by the idea of combining validateValues and checkAccess into one event? I think we could aim for similar style in the events - but now I'm leaning strongly towards "keep them separate". (1) delete is a good example where an action needs one event (checkAccess) but not the other event (validateValues). (2) The payload is different. checkAccess builds a bool $granted, whereas validateValues builds an array $errors.

Comment on lines 108 to 125
$e = new ValidateValuesEvent($this->getEntityName(), 'save', $this->records);
\Civi::dispatcher()->dispatch('civi.api4.validate', $e);
if (!empty($e->errors)) {
throw new \API_Exception(ts('Found %1 error(s) in submitted %2 record(s) of type "%3": %4', [
1 => count($e->errors),
2 => count(array_unique(array_column($e->errors, 'record'))),
3 => $this->getEntityName(),
4 => implode(', ', array_column($e->errors, 'message')),
]));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole block ought to be inside a foreach loop as the event is supposed to fire once per record not for an array of records. Also I think the second param (action name) shouldn't be 'save' but should be 'create' or 'update' depending on the presence of $idField like 20af3ca#diff-ccb444858b7cfcd34c34786bd76003cdccad4c53fbadef2b89df5ba7a94e82f2R107

@colemanw
Copy link
Member

@totten this looks great. I think it's almost merge-ready. See my one comment on the save action. Also, the event appears to be missing from the update action?

@totten totten force-pushed the master-api4-validate branch from 9eec5aa to 4acb9c2 Compare May 27, 2021 23:35
@totten
Copy link
Member Author

totten commented May 27, 2021

@colemanw Revised it to support the update operation. Additionally, the new ValidateValuesTest does a bunch of assertions to ensure that $e->records (etal) are well-defined across different scenarios (eg create vs save vs update).

@totten
Copy link
Member Author

totten commented May 27, 2021

Also, I took a stab at refactoring checkRequiredFields() to be a service that participates in civi.api4.validate - but several implementation bits were hard to get hold because they're baked into the action-class-hierarchy ($this->idField, $this->evaluateCondition(), $this->entityFields()) and I got a bit put-off by the scope-creep. There is a partial refactoring in 61aa344 (which at least puts the 'check-required-fields' stuff into a distinct file), but I'm really tired of having this PR hanging out there, so that's just living in a side-branch.

(EDIT) Which is a long-winded way of saying... I think there's some opportunity for making it easier to use things like idField and entityFields() from a listener... just not right now...

@colemanw
Copy link
Member

@totten I think the problem is

PHP Fatal error:  Declaration of api\v4\Entity\ValidateValuesTest::setUp() must be compatible with PHPUnit\Framework\TestCase::setUp(): void in /home/jenkins/bknix-dfl/build/core-20184-2ttjf/web/sites/all/modules/civicrm/tests/phpunit/api/v4/Entity/ValidateValuesTest.php on line 31

@totten totten force-pushed the master-api4-validate branch from 4acb9c2 to 052490b Compare May 28, 2021 01:30
@totten
Copy link
Member Author

totten commented May 28, 2021

Good eye @colemanw. Fixed/squashed.

@colemanw
Copy link
Member

@totten we could trade reviews for #20170

totten added 6 commits May 27, 2021 21:10
The original form (`getBatchRecords()`) still returns an array of items.

The alternate form (`getBatchAction()`) returns an API call for fetching the
batchs - but this API call may be further refined (e.g.  selecting different
fields or data-pages).
@totten totten force-pushed the master-api4-validate branch from 052490b to fdea27f Compare May 28, 2021 04:13
@totten totten force-pushed the master-api4-validate branch from fdea27f to 9e63589 Compare May 28, 2021 06:58
@totten
Copy link
Member Author

totten commented May 28, 2021

Fixed another quirky test issue (data-leak - needed to tweak tearDown()).

@colemanw Sure, I'll take a look when I get up.

@totten
Copy link
Member Author

totten commented Jun 10, 2021

Replaced by #20533.

@totten totten closed this Jun 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants