Skip to content

Commit

Permalink
Document breaking changes to AbstractValidator
Browse files Browse the repository at this point in the history
Signed-off-by: George Steel <george@net-glue.co.uk>
  • Loading branch information
gsteel committed Jul 25, 2024
1 parent 3bd50cd commit bdc6e7e
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 0 deletions.
151 changes: 151 additions & 0 deletions docs/book/v3/migration/refactoring-legacy-validators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Refactoring Legacy Validators

This document is intended to show an example of refactoring custom validators to remove runtime mutation of options and move option determination to the constructor of your validator.

## The Old Validator

The following custom validator is our starting point which relies on now removed methods and behaviour from the `AbstractValidator` in the 2.x series of releases.

```php
namespace My;

use Laminas\Validator\AbstractValidator;

class MuppetNameValidator extends AbstractValidator {
public const KNOWN_MUPPETS = [
'Kermit',
'Miss Piggy',
'Fozzie Bear',
'Gonzo the Great',
'Scooter',
'Animal',
'Beaker',
];

public const ERR_NOT_STRING = 'notString';
public const ERR_NOT_ALLOWED = 'notAllowed';

protected array $messageTemplates = [
self::ERR_NOT_STRING => 'Please provide a string value',
self::ERR_NOT_ALLOWED => '"%value%" is not an allowed muppet name',
];

public function setAllowedMuppets(array $muppets): void {
$this->options['allowed_muppets'] = [];
foreach ($muppets as $muppet) {
$this->addMuppet($muppet);
}
}

public function addMuppet(string $muppet): void
{
$this->options['allowed_muppets'][] = $muppet;
}

public function setCaseSensitive(bool $caseSensitive): void
{
$this->options['case_sensitive'] = $caseSensitive;
}

public function isValid(mixed $value): bool {
if (! is_string($value)) {
$this->error(self::ERR_NOT_STRING);

return false;
}

$list = $this->options['allowed_muppets'];
if (! $this->options['case_sensitive']) {
$list = array_map('strtolower', $list);
$value = strtolower($value);
}

if (! in_array($value, $list, true)) {
$this->error(self::ERR_NOT_ALLOWED);

return false;
}

return true;
}
}
```

Given an array of options such as `['allowed_muppets' => ['Miss Piggy'], 'caseSensitive' => false]`, previously, the `AbstractValidator` would have "magically" called the setter methods `setAllowedMuppets` and `setCaseSensitive`. The same would be true if you provided these options to the removed `AbstractValidator::setOptions()` method.

Additionally, with the class above, there is nothing to stop you from creating the validator in an invalid state with:

```php
$validator = new MuppetNameValidator();
$validator->isValid('Kermit');
// false, because the list of allowed muppets has not been initialised
```

## The Refactored Validator

```php
final readonly class MuppetNameValidator extends AbstractValidator {
public const KNOWN_MUPPETS = [
'Kermit',
'Miss Piggy',
'Fozzie Bear',
'Gonzo the Great',
'Scooter',
'Animal',
'Beaker',
];

public const ERR_NOT_STRING = 'notString';
public const ERR_NOT_ALLOWED = 'notAllowed';

protected array $messageTemplates = [
self::ERR_NOT_STRING => 'Please provide a string value',
self::ERR_NOT_ALLOWED => '"%value%" is not an allowed muppet name',
];

private array $allowed;
private bool $caseSensitive;

/**
* @param array{
* allowed_muppets: list<non-empty-string>,
* case_sensitive: bool,
* } $options
*/
public function __construct(array $options)
{
$this->allowed = $options['allowed_muppets'] ?? self::KNOWN_MUPPETS;
$this->caseSensitive = $options['case_sensitive'] ?? true;

// Pass options such as the translator, overridden error messages, etc
// to the parent AbstractValidator
parent::__construct($options);
}

public function isValid(mixed $value): bool {
if (! is_string($value)) {
$this->error(self::ERR_NOT_STRING);

return false;
}

$list = $this->allowed;
if (! $this->caseSensitive) {
$list = array_map('strtolower', $list);
$value = strtolower($value);
}

if (! in_array($value, $list, true)) {
$this->error(self::ERR_NOT_ALLOWED);

return false;
}

return true;
}
}
```

With the refactored validator, our options are clearly and obviously declared as class properties, and cannot be changed once they have been set.

There are fewer methods to test; In your test case you can easily set up data providers with varying options to thoroughly test that your validator behaves in the expected way.
27 changes: 27 additions & 0 deletions docs/book/v3/migration/v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@

In order to reduce maintenance burden and to help users to favour [composition over inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance), all classes, where possible have been marked as final.

### `AbstractValidator` Breaking Changes

There have been a number of significant, breaking changes to the `AbstractValidator` which _all_ shipped validators inherit from.

The following methods have been removed affecting all the shipped validators in addition to [individual changes](#changes-to-individual-validators) to those validators:

- `getOption`
- `getOptions`
- `setOptions`
- `getMessageVariables`
- `getMessageTemplates`
- `setMessages`
- `setValueObscured`
- `isValueObscured`
- `getDefaultTranslator`
- `hasDefaultTranslator`
- `getDefaultTranslatorTextDomain`
- `getMessageLength`

#### Validator Options

It is now necessary to pass all options to the validator constructor.
This means that you cannot create a validator instance in an invalid state.
It also makes it impossible to change the option values after the validator has been instantiated.

Removal of the various option "getters" and "setters" are likely to cause a number of breaking changes to inheritors of `AbstractValidator` _(i.e. custom validators you may have written)_ so we have provided an [example refactoring](refactoring-legacy-validators.md) to illustrate the necessary changes.

### Validator Plugin Manager

#### Removal of legacy Zend aliases
Expand Down

0 comments on commit bdc6e7e

Please sign in to comment.