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

MLSS-2212 & PREF-309 | Search Criteria Validators #265

Open
wants to merge 11 commits into
base: 8.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions BuphaloTemplates/Prefab5/PrimaryActorName/Map/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Repository implements RepositoryInterface
use PrimaryActorName\Map\Builder\Factory\AwareTrait;
use Prefab5\Doctrine\DBAL\Connection\Decorator\Repository\AwareTrait;
use Prefab5\SearchCriteria\Doctrine\DBAL\Query\QueryBuilder\Builder\Factory\AwareTrait;
use Prefab5\SearchCriteria\Validator\Builder\Factory\AwareTrait;

protected $connection;

Expand All @@ -33,6 +34,10 @@ public function createBuilder() : Map\BuilderInterface

public function get(Prefab5\SearchCriteriaInterface $searchCriteria) : MapInterface
{
if ($this->hasValidatorBuilderFactory()) {
$this->getValidatorBuilderFactory()->create()->build()->validate($searchCriteria);
}

$queryBuilderBuilder = $this->getSearchCriteriaDoctrineDBALQueryQueryBuilderBuilderFactory()->create();
$queryBuilderBuilder->setSearchCriteria($searchCriteria);
$queryBuilder = $queryBuilderBuilder->build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ services:
- [setDoctrineDBALConnectionDecoratorRepository, ['@PREFAB_PLACEHOLDER_VENDOR\PREFAB_PLACEHOLDER_PRODUCT\Prefab5\Doctrine\DBAL\Connection\Decorator\RepositoryInterface']]
- [setPrimaryActorNameMapBuilderFactory, ['@Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Map\Builder\FactoryInterface']]
- [setSearchCriteriaDoctrineDBALQueryQueryBuilderBuilderFactory, ['@PREFAB_PLACEHOLDER_VENDOR\PREFAB_PLACEHOLDER_PRODUCT\Prefab5\SearchCriteria\Doctrine\DBAL\Query\QueryBuilder\Builder\FactoryInterface']]
- [setValidatorBuilderFactory, ['@Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Map\Repository\Validator\Builder\FactoryInterface']]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Map\Repository\Validator\BuilderInterface:
class: PREFAB_PLACEHOLDER_VENDOR\PREFAB_PLACEHOLDER_PRODUCT\Prefab5\SearchCriteria\Validator\Builder
public: false
shared: false
calls:
- [setValidatorFactory, ['@PREFAB_PLACEHOLDER_VENDOR\PREFAB_PLACEHOLDER_PRODUCT\Prefab5\SearchCriteria\Validator\FactoryInterface']]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Map\Repository\Validator\Builder\FactoryInterface:
class: PREFAB_PLACEHOLDER_VENDOR\PREFAB_PLACEHOLDER_PRODUCT\Prefab5\SearchCriteria\Validator\Builder\Factory
Comment on lines +2 to +3
Copy link
Contributor

Choose a reason for hiding this comment

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

potential blocker: This looks like every repo Validator Builder Factory will have their own service but use the same class. Is that intended?

Copy link
Contributor Author

@arielallon arielallon Aug 23, 2021

Choose a reason for hiding this comment

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

from synchronous review:

pro:
having prefab build per primary actors versions of this could avoid the covariance/contravariance problem and might be more in line with how prefab operates otherwise.

cons:
might make it harder to have shared validators?

public: false
shared: true
calls:
- [setValidatorBuilder, ['@Neighborhoods\BuphaloTemplateTree\PrimaryActorName\Map\Repository\Validator\BuilderInterface']]
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"laminas/laminas-stdlib": "^3.1",
"neighborhoods/buphalo": "^1.0",
"neighborhoods/datadog-component": "^1.1",
"neighborhoods/dependency-injection-container-builder": "^1.0"
"neighborhoods/dependency-injection-container-builder": "^1.0",
"neighborhoods/exception-component": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.4",
Expand Down
276 changes: 276 additions & 0 deletions docs/SearchCriteriaValidators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# Search Criteria Validators

Search Criteria Validators allow you to validate the *semantics* of the search criteria provided in a request.

## Adding Search Criteria Validators to your HTTP Handlers

### Per Application

1. Upgrade your composer dependency of Prefab to a version that includes Search Criteria Validators. //@todo include minimum version here
2. Ensure that you have resolved any discrepancies in overrides in your project's `/src/` directory
for those called out in the [Modifications in `./Prefab/`](#modifications-in----prefab5--) section below.

### Per Repository
1. If overridden in `/src/`, update your `Map/Repository.service.yml` and `Map/Reposiory.php` files to include the modifications to the fabbed version of those files.
(See the [Modifications to Actor Templates](#modifications-to-actor-templates) section below.
2. Move the `<PrimaryActorName>/Map/Repository/Validator/Builder.service.yml` from `/fab/` to `/src/`.
As you create Validator Decorators, you will add them to the stack of decorators in this file.
They are called by the runtime from the bottom of the list to the top.
The below example includes the one Validator Decorator created in [Per Decorator](#per-decorator).

> `PrimaryActorName/Map/Repository/Validator/Builder.service.yml`
>```yml
>services:
> VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\BuilderInterface:
> class: VendorName\ProductName\Prefab5\SearchCriteria\Validator\Builder
> public: false
> shared: false
> calls:
> - [ setValidatorFactory, ['@VendorName\ProductName\Prefab5\SearchCriteria\Validator\FactoryInterface' ] ]
> - [ addFactory, [ '@VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\CustomDecorator\FactoryInterface']]
>```

### Per Decorator
These files will all be in the `PrimaryActorName` tree. The examples below are all in `PrimaryActorName/Map/Repository/Validator/`.

#### Custom Decorator Interface and Service Container

> `CustomDecoratorInterface.php`
>```php
><?php
>declare(strict_types=1);
>
>namespace VendorName\ProductName\PrimaryActorName\Map\Repository\Validator;
>
>use VendorName\ProductName\Prefab5\SearchCriteria\Validator\DecoratorInterface;
>
>interface CustomDecoratorInterface extends DecoratorInterface
>{
>}
>```
> `CustomDecorator.service.yml`
>```yml
>services:
> VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\CustomDecoratorInterface:
> class: VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\CustomDecorator
> public: false
> shared: false
>```
#### Custom Decorator

> `CustomDecorator.php`
>```php
><?php
>declare(strict_types=1);
>
>namespace VendorName\ProductName\PrimaryActorName\Map\Repository\Validator;
>
>use VendorName\ProductName\PrimaryActorNameInterface;
>use VendorName\ProductName\Prefab5\HTTP\SearchCriteriaBuilderException;
>use VendorName\ProductName\Prefab5\SearchCriteria\ValidationException;
>use VendorName\ProductName\Prefab5\SearchCriteria\ValidatorInterface;
>use VendorName\ProductName\Prefab5\SearchCriteriaInterface;
>use VendorName\ProductName\Prefab5\SearchCriteria\Validator;
>
>final class CustomDecorator implements CustomDecoratorInterface
>{
> use Validator\AwareTrait;
>
> private const FIELD_INDEXES = [
> [
> PrimaryActorNameInterface::PROP_ID,
> ],
> [
> PrimaryActorNameInterface::PROP_NAME,
> PrimaryActorNameInterface::PROP_CREATED_AT,
> ],
> ];
>
> public function validate(SearchCriteriaInterface $searchCriteria): ValidatorInterface
> {
> $filterFields = $this->extractFiltersFieldListFromSearchCriteria($searchCriteria);
> $sortOrderFields = $this->extractSortOrdersFieldListFromSearchCriteria($searchCriteria);
> $this->validateAtLeastOneIndexedFieldPresent($filterFields);
> if (!empty($sortOrderFields)) {
> $this->validateAtLeastOneIndexedFieldPresent($sortOrderFields);
> }
>
> if ($this->hasValidator()) {
> $this->getValidator()->validate($searchCriteria);
> }
>
> return $this;
> }
>
> private function validateAtLeastOneIndexedFieldPresent(array $searchCriteriaFields): void
> {
> foreach (self::FIELD_INDEXES as $indexFields) {
> if (count(array_diff($indexFields, $searchCriteriaFields)) === 0) {
> return;
> }
> }
> $errorMessage = sprintf('Invalid Search Criteria. No indexed fields present.');
> $previousException = new SearchCriteriaBuilderException($errorMessage);
> throw (new ValidationException())->setCode('422')->setPrevious($previousException);
> }
>
> private function extractFiltersFieldListFromSearchCriteria(SearchCriteriaInterface $searchCriteria) : array
> {
> $fieldList = [];
>
> foreach ($searchCriteria->getFilters() as $filter) {
> $fieldList[] = $filter->getField();
> }
>
> return $fieldList;
> }
>
> private function extractSortOrdersFieldListFromSearchCriteria(SearchCriteriaInterface $searchCriteria) : array
> {
> $fieldList = [];
>
> foreach ($searchCriteria->getSortOrders() as $sortOrder) {
> $fieldList[] = $sortOrder->getField();
> }
>
> return $fieldList;
> }
>}
>```
#### Supporting Actors

> `CustomDecorator.buphalo.v1.fabrication.yml`
> ```yml
> actors:
> <PrimaryActorName>.service.yml:
> template: PrimaryActorName.service.yml
> <PrimaryActorName>/Factory.service.yml:
> template: PrimaryActorName/Factory.service.yml
> ```
>
> `CustomDecorator/FactoryInterface.php`
>```php
><?php
>declare(strict_types=1);
>
>namespace VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\CustomDecorator;
>
>use VendorName\ProductName\HTTP1\PropertyIdentity\Map\Repository\Validator\CustomDecoratorInterface;
>use VendorName\ProductName\Prefab5\SearchCriteria\Validator\DecoratorInterface;
>use VendorName\ProductName\Prefab5\SearchCriteria\Validator\Decorator\FactoryInterface as PrefabValidatorDecoratorFactoryInterface;
>
>interface FactoryInterface extends PrefabValidatorDecoratorFactoryInterface
>{
> public function create(): DecoratorInterface;
>}
>```
>
> `CustomDecorator/Factory.php`
>```php
><?php
>declare(strict_types=1);
>
>namespace VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\CustomDecorator;
>
>use VendorName\ProductName\PrimaryActorName\Map\Repository\Validator\CustomDecoratorInterface;
>use VendorName\ProductName\Prefab5\SearchCriteria\Validator\DecoratorInterface;
>
>class Factory implements FactoryInterface
>{
> use AwareTrait;
>
> public function create(): DecoratorInterface
> {
> return clone $this->getPrimaryActorNameMapRepositoryValidatorCustomDecorator();
> }
>}
>```
> `CustomDecorator/AwareTrait.php`
>```php
><?php
>declare(strict_types=1);
>
>namespace VendorName\ProductName\HTTP1\PropertyIdentity\Map\Repository\Validator\CustomDecorator;
>
>use VendorName\ProductName\HTTP1\PropertyIdentity\Map\Repository\Validator\CustomDecoratorInterface;
>use VendorName\ProductName\Prefab5\SearchCriteria\Validator\DecoratorInterface;
>
>trait AwareTrait
>{
> protected $PrimaryActorNameMapRepositoryValidatorCustomDecorator;
>
> public function setPrimaryActorNameMapRepositoryValidatorCustomDecorator(DecoratorInterface $CustomDecorator): self
> {
> if ($this->hasPrimaryActorNameMapRepositoryValidatorCustomDecorator()) {
> throw new \LogicException('PrimaryActorNameMapRepositoryValidatorCustomDecorator is already set.');
> }
>
> $this->PrimaryActorNameMapRepositoryValidatorCustomDecorator = $CustomDecorator;
>
> return $this;
> }
>
> protected function getPrimaryActorNameMapRepositoryValidatorCustomDecorator(): DecoratorInterface
> {
> if (!$this->hasPrimaryActorNameMapRepositoryValidatorCustomDecorator()) {
> throw new \LogicException('PrimaryActorNameMapRepositoryValidatorCustomDecorator is not set.');
> }
>
> return $this->PrimaryActorNameMapRepositoryValidatorCustomDecorator;
> }
>
> protected function hasPrimaryActorNameMapRepositoryValidatorCustomDecorator(): bool
> {
> return isset($this->PrimaryActorNameMapRepositoryValidatorCustomDecorator);
> }
>
> protected function unsetPrimaryActorNameMapRepositoryValidatorCustomDecorator(): self
> {
> if (!$this->hasPrimaryActorNameMapRepositoryValidatorCustomDecorator()) {
> throw new \LogicException('PrimaryActorNameMapRepositoryValidatorCustomDecorator is not set.');
> }
> unset($this->PrimaryActorNameMapRepositoryValidatorCustomDecorator);
>
> return $this;
> }
>}
>```


## Recommendations

- Create a dedicated Validator Decorator for a single logical/semantic validation need.
You can and should stack multiple Validator Decorators with distinct logical/semantic validations purposes on a given Repository.
- Be sure to carefully consider the behavior of each decorator as it relates to the procession down the stack of decorators.
A runtime analysis diagram and meeting is highly recommended.
Generally, if a decorator is _certain_ that the request is valid without needing further validation down the stack, it may return immediately.
Similarly, if a decorator is _certain_ that the request is not valid, it may throw an exception immediately.
If the decorator is _not certain_ that other decorators down the stack may have more insight, it should call the next decorator in the stack.
- The last custom decorator in the stack (the first one in the list of `addDecorator` calls in the `Builder.service.yml`) should _not_ call `$this->getValidator()->validate()`.
The default Validator is designed to act as a failsafe to indicate that no custom decorators were implemented for a handler.


## Modified Classes

The following classes have been modified in Prefab as part of the implementation of Search Criteria Validators.
If you have overrides of these in your project's `/src/` directory, you will need to resolve any discrepancies.

Note that in addition to those detailed below, other files were added,
but should not need reconciliation with existing files as they are net-new.

### Modifications in `./Prefab5/`

- `Prefab5\HTTP` has been refactored a little, though there are no changes to its API.
- `Prefab5\HTTPSearchCriteriaBuilderException` has updated the vendor of a `use` statement to reflect where the package migrated to.

### Modifications to Actor Templates

- `PrimaryActorName\Map\Repository` and associated `Repository.service.yml` have been updated to set and use the `Validator\Builder\Factory`.

## Breaking Changes

In version 8.x, implementing Search Criteria Validators is optional and isn't enforced by Prefab.
In a future major version update of Prefab, they will be required.
Specifically, in a future major version update for Prefab,
1. the `try`/`catch` will be removed from `Prefab5\SearchCriteria\Validator::validate()`, requiring that at least one custom Validator Decorator be present per Map/Repository, and
2. the `if ($this->hasValidatorBuilderFactory())` check will be removed from `PrimaryActorName\Map\Repository`.
Loading