diff --git a/docs/book/v3/migration/composing-final-validators.md b/docs/book/v3/migration/composing-final-validators.md new file mode 100644 index 00000000..f699e9c7 --- /dev/null +++ b/docs/book/v3/migration/composing-final-validators.md @@ -0,0 +1,125 @@ +# Composing `final` Validators + +In version 3.0, nearly all validators have been marked as `final`. + +This document aims to provide guidance on composing validators to achieve the same results as inheritance may have. + +Consider the following custom validator. It ensures the value given is a valid email address and that it is an email address from `gmail.com` + +```php +namespace My; + +use Laminas\Validator\EmailAddress; + +class GMailOnly extends EmailAddress +{ + public const NOT_GMAIL = 'notGmail'; + + protected $messageTemplates = [ + self::INVALID => "Invalid type given. String expected", + self::INVALID_FORMAT => "The input is not a valid email address. Use the basic format local-part@hostname", + self::INVALID_HOSTNAME => "'%hostname%' is not a valid hostname for the email address", + self::INVALID_MX_RECORD => "'%hostname%' does not appear to have any valid MX or A records for the email address", + self::INVALID_SEGMENT => "'%hostname%' is not in a routable network segment. The email address should not be resolved from public network", + self::DOT_ATOM => "'%localPart%' can not be matched against dot-atom format", + self::QUOTED_STRING => "'%localPart%' can not be matched against quoted-string format", + self::INVALID_LOCAL_PART => "'%localPart%' is not a valid local part for the email address", + self::LENGTH_EXCEEDED => "The input exceeds the allowed length", + // And the one new constant introduced: + self::NOT_GMAIL => 'Please use a gmail address', + ]; + + public function isValid(mixed $value) : bool + { + if (! parent::isValid($value)) { + return false; + } + + if (! preg_match('/@gmail\.com$/', $value)) { + $this->error(self::NOT_GMAIL); + + return false; + } + + return true; + } +} +``` + +A better approach could be to use a validator chain: + +```php +use Laminas\Validator\EmailAddress; +use Laminas\Validator\Regex; +use Laminas\Validator\ValidatorChain; + +$chain = new ValidatorChain(); +$chain->attachByName(EmailAddress::class); +$chain->attachByName(Regex::class, [ + 'pattern' => '/@gmail\.com$/', + 'messages' => [ + Regex::NOT_MATCH => 'Please use a gmail.com address', + ], +]); +``` + +Or, to compose the email validator into a concrete class: + +```php +namespace My; + +use Laminas\Validator\AbstractValidator; +use Laminas\Validator\EmailAddress; + +final class GMailOnly extends AbstractValidator +{ + public const NOT_GMAIL = 'notGmail'; + public const INVALID = 'invalid'; + + protected $messageTemplates = [ + self::INVALID => 'Please provide a valid email address', + self::NOT_GMAIL => 'Please use a gmail address', + ]; + + public function __construct( + private readonly EmailAddress $emailValidator + ) { + } + + public function isValid(mixed $value) : bool + { + if (! $this->emailValidator->isValid($value)) { + $this->error(self::INVALID); + + return false; + } + + if (strtoupper($value) !== $value) { + $this->error(self::NOT_GMAIL); + + return false; + } + + return true; + } +} +``` + +In the latter case you would need to define factory for your validator which for this contrived example would seem like overkill, but for more real-world use cases a factory is likely employed already: + +```php +use Laminas\Validator\EmailAddress; +use Laminas\Validator\ValidatorPluginManager; +use Psr\Container\ContainerInterface; + +final class GMailOnlyFactory { + public function __invoke(ContainerInterface $container, string $name, array $options = []): GMailOnly + { + $pluginManager = $container->get(ValidatorPluginManager::class); + + return new GmailOnly( + $pluginManager->build(EmailAddress::class, $options), + ); + } +} +``` diff --git a/docs/book/v3/migration/refactoring-legacy-validators.md b/docs/book/v3/migration/refactoring-legacy-validators.md new file mode 100644 index 00000000..85c7ac49 --- /dev/null +++ b/docs/book/v3/migration/refactoring-legacy-validators.md @@ -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, + * 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. diff --git a/docs/book/v3/migration/v2-to-v3.md b/docs/book/v3/migration/v2-to-v3.md new file mode 100644 index 00000000..cb147aec --- /dev/null +++ b/docs/book/v3/migration/v2-to-v3.md @@ -0,0 +1,712 @@ +# Migration from Version 2 to 3 + +## Changed Behaviour & Signature Changes + +### Final Classes + +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. + +As a best practice, even if a validator is not marked as final in this library, it is not advisable to extend from it; it is likely that in future major releases inheritance will become prohibited. + +We have prepared a short guide on [refactoring to composition here](composing-final-validators.md). + +### Strict Types and Native Type Hints Throughout + +All classes now use native parameter types, return types and property types. + +### `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. + +Of particular note, is that all "magic" has been removed. Previously, passing an options such as `my_option` to a validator constructor would result in `AbstractValidator` calling the `setMyOption` method if it existed. This functionality has been removed, and it is the responsibility of validator implementations to deal with option values during construction. + +#### Translator Interface Type has Changed + +In the 2.x series of `laminas-validator` there was a translator interface and implementation `Laminas\Validator\Translator\TranslatorInterface`. +`AbstractValidator` expected an instance of this interface to its `setTranslator` method _(Defined in `Laminas\Validator\Translator\TranslatorAwareInterface`)_. + +`AbstractValidator` and `Laminas\Validator\Translator\TranslatorAwareInterface` now type hint on `Laminas\Translator\TranslatorInterface`. The `laminas-translator` library defines only this interface and nothing more, furthermore, the translator supplied by `Laminas\I18n` implements this interface so there is no longer any need for multiple implementations, or validator specific implementations of the translator. Users can now pass the translator shipped by `Laminas\I18n` directly. + +### Breaking Changes to `ValidatorChain` + +All methods that previously returned `$this` now return `void`. You will need to refactor code such as: + +```php +// Code making use of fluent return values: +$validatorChain->attach(new StringLength()) + ->attach(new NotEmpty()) + ->attach(new Digits()); + +// Should be refactored to: +$validatorChain->attach(new StringLength()); +$validatorChain->attach(new NotEmpty()); +$validatorChain->attach(new Digits()); +``` + +The following methods have been removed: + +- `addValidator` _(replaced with `attach`)_ +- `addByName` _(replaced with `attachByName`)_ + +### Validator Plugin Manager + +#### Removal of legacy Zend aliases + +All aliases that referenced the equivalent, legacy "Zend" validators have been removed. This means that an exception will be thrown if you attempt to retrieve a validator using one of these aliases such as `Zend\Validator\NotEmpty::class`. + +You will need to either update your codebase to use known aliases such as `Laminas\Validator\NotEmpty::class`, or re-implement the aliases in your configuration. + +#### Removal of Service Manager v2 canonical FQCNs + +There are a number of aliases left over from early versions of Service Manager where each validator would be aliased by a lowercase, normalized string such as `laminasvalidatoremail` to represent `Laminas\Validator\Email::class`. All of these aliases have been removed. + +#### Removal of Laminas\i18n aliases and factories + +The [`laminas-i18n`](https://docs.laminas.dev/laminas-i18n/validators/introduction/) component ships a number of validators that historically have been pre-configured from within this component. These aliases and factory entries have been removed. + +[Removal of the aliases](https://github.com/laminas/laminas-validator/commit/5bbfe8baeba48f3b77c909a8d6aa930c1d2897b7) here is unlikely to cause any issues, providing you have enabled the `ConfigProvider` or `Module` from `laminas-i18n` in your application. + +### Required Options at Construction Time + +A number of validators now require options during construction now that runtime mutation of validator settings is no longer possible _(Described in ['General Changes'](#general-changes) below)_. + +Validators that require options will now throw an exception when the relevant option is not provided. For example, the [`Regex` validator](#laminasvalidatorregex) requires a `pattern` option. + +The affected validators are: + +- `Laminas\Validator\Barcode` +- `Laminas\Validator\Bitwise` +- `Laminas\Validator\Callback` +- `Laminas\Validator\DateComparison` +- `Laminas\Validator\Explode` +- `Laminas\Validator\InArray` +- `Laminas\Validator\IsInstanceOf` +- `Laminas\Validator\NumberComparison` +- `Laminas\Validator\Regex` +- `Laminas\Validator\File\ExcludeExtension` +- `Laminas\Validator\File\ExcludeMimeType` +- `Laminas\Validator\File\Extension` +- `Laminas\Validator\File\FilesSize` +- `Laminas\Validator\File\Hash` +- `Laminas\Validator\File\ImageSize` +- `Laminas\Validator\File\MimeType` +- `Laminas\Validator\File\Size` +- `Laminas\Validator\File\WordCount` + +## Changes to Individual Validators + +### General Changes + +#### Removal of "Getters" and "Setters" + +In general, most validators no longer have "getters" and "setters" for options and configuration. + +Taking the `Regex` validator as an example, in the 2.x series, it was possible to create a regex validator and then configure it _after_ it had been constructed. +This allows the creation of validator instances with an invalid state, or configuration. + +Removing getters and setters forces us to provide valid configuration of the validator instance at construction time, vastly reducing the API surface and closing off potential sources of bugs. + +#### Consistent Construction via an Array of Options + +In the 2.x series, _most_ validators accepted a range of constructor arguments, for example, a single options array, an `ArrayAccess` or `Traversable` and frequently variadic arguments of the most important configuration parameters. + +Generally speaking, validators now only accept associative arrays with improved documentation of exactly which options are available. + +### `Laminas\Validator\Barcode` + +The following methods have been removed: + +- `getAdapter` +- `setAdapter` +- `getChecksum` +- `useChecksum` + +Behaviour changes: + +- The constructor now only accepts an associative array of documented options. +- The `adapter` option can now be a FQCN - previously it had to be an instance, or an unqualified class name. + +#### Significant Changes to Adapters + +Inheritance has changed for all the shipped barcode adapters. None of the adapters extend from the **now removed** `AbstractAdapter` and instead, all adapters implement the methods expected by `Laminas\Validator\Barcode\AdapterInterface`. The interface itself now only specifies 4 methods: + +- `hasValidLength` +- `hasValidCharacters` +- `hasValidChecksum` +- `getLength` + +The documentation on [writing custom adapters](../validators/barcode.md#writing-custom-adapters) has been updated to reflect these changes. + +### `Laminas\Validator\Bitwise` + +The following methods have been removed: + +- `setControl` +- `getControl` +- `setOperator` +- `getOperator` +- `setStrict` +- `getStrict` + +Behaviour changes: + +- The constructor now only accepts an associative array of documented options +- Validation will now fail if the input is not `interger-ish` i.e. `int` or `int-string` + +### `Laminas\Validator\Callback` + +The following methods have been removed: + +- `setCallback` +- `getCallback` +- `setCallbackOptions` +- `getCallbackOptions` + +A new option `bind` has been added that will bind the given callback to the scope of the validator so that you can manipulate error messages from within the callback itself. + +The [arguments provided to the user supplied callable](../validators/callback.md#validation-context-argument) no longer change depending on whether the `$context` parameter is provided to the validator. Callable will now always be called with the signature `callable(mixed $value, array $context, ...$userDefinedParameters)`. + +The [documentation](../validators/callback.md#callbacks-and-scope) has been updated with the relevant details. + +### `Laminas\Validator\CreditCard` + +The following methods have been removed: + +- `getType` +- `setType` +- `addType` +- `getService` +- `setService` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/credit-card.md). +- If you provide a callable for further validation of the credit card number, the expected signature of the callable has changed and should now be `callable(mixed $value, array $context, array $acceptedCardTypes)`. Please [consult the documentation](../validators/credit-card.md#validation-using-apis) for further details. + +### `Laminas\Validator\Date` + +The following methods have been removed: + +- `getFormat` +- `setFormat` +- `isStrict` +- `setStrict` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/date.md). + +### `Laminas\Validator\DateStep` + +The following methods have been removed: + +- `getFormat` +- `setFormat` +- `isStrict` +- `setStrict` +- `getBaseValue` +- `setBaseValue` +- `getStep` +- `setStep` +- `getTimezone` +- `setTimezone` + +Behaviour changes: + +- The constructor now only accepts an associative array. +- The default format has changed to use `DateTimeInterface::ATOM` instead of the deprecated `DateTimeInterface::ISO8601` + +### `Laminas\Validator\Digits` + +This validator no longer uses the Digits filter from `laminas/laminas-filter`, so its static filter property has been removed. This change is unlikely to cause any problems unless for some reason you have extended this class. + +### `Laminas\Validator\EmailAddress` + +The following methods have been removed: + +- `getHostnameValidator` +- `setHostnameValidator` +- `getAllow` +- `setAllow` +- `isMxSupported` +- `getMxCheck` +- `useMxCheck` +- `getDeepMxCheck` +- `useDeepMxCheck` +- `getDomainCheck` +- `useDomainCheck` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/email-address.md). +- If you use custom `Hostname` validators to restrict valid host types, it is worth [reading the documentation](../validators/email-address.md#controlling-hostname-validation-options) about how the Email Address validator interacts with the Hostname validator with regard to option priority for the `allow` option. + +### `Laminas\Validator\Explode` + +The following methods have been removed: + +- `setValidator` +- `getValidator` +- `setValueDelimiter` +- `getValueDelimiter` +- `setValidatorPluginManager` +- `getValidatorPluginManager` +- `setBreakOnFirstFailure` +- `isBreakOnFirstFailure` + +Behaviour changes: + +- Non-string input will now cause a validation failure +- The composed validator can now be specified as a FQCN +- The constructor now only accepts an associative array +- Error messages match the same format as other validators, i.e. `array` + +### `Laminas\Validator\Hostname` + +The following methods have been removed: + +- `getIpValidator` +- `setIpValidator` +- `getAllow` +- `setAllow` +- `getIdnCheck` +- `useIdnCheck` +- `getTldCheck` +- `useTldCheck` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/hostname.md). + +### `Laminas\Validator\Iban` + +The following methods have been removed: + +- `getCountryCode` +- `setCountryCode` +- `allowNonSepa` +- `setAllowNonSepa` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/iban.md). + +### `Laminas\Validator\Identical` + +The following methods have been removed: + +- `getToken` +- `setToken` +- `getStrict` +- `setStrict` +- `getLiteral` +- `setLiteral` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/identical.md). + +### `Laminas\Validator\InArray` + +The following methods have been removed: + +- `getHaystack` +- `setHaystack` +- `getStrict` +- `setStrict` +- `getRecursive` +- `setRecursive` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/in-array.md). + +### `Laminas\Validator\Ip` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/ip.md). + +### `Laminas\Validator\Isbn` + +The following methods have been removed: + +- `setSeparator` +- `getSeparator` +- `setType` +- `getType` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/isbn.md). +- The `separator` option has been removed. Instead of requiring users to provide the expected separator, all valid separators are now stripped from the input prior to validation. With the default option for auto-detection of ISBN-10 and ISBN-13 formats, the validator is greatly simplified at the point of use. +- Previously, the classes `Laminas\Validator\Isbn\Isbn10` and `Laminas\Validator\Isbn\Isbn13` were used to validate each format. The code in these classes is now inlined inside the validator so these classes have been removed. + +### `Laminas\Validator\IsCountable` + +The following methods have been removed: + +- `getCount` +- `setCount` +- `getMin` +- `setMin` +- `getMax` +- `setMax` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/is-countable.md). + +### `Laminas\Validator\IsInstanceOf` + +The following methods have been removed: + +- `getClassName` +- `setClassName` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/isinstanceof.md). + +### `Laminas\Validator\IsJsonString` + +The following methods have been removed: + +- `setAllow` +- `setMaxDepth` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/is-json-string.md). + +### `Laminas\Validator\NotEmpty` + +The following methods have been removed: + +- `getType` +- `setType` +- `getDefaultType` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/not-empty.md). + +### `Laminas\Validator\Regex` + +The following methods have been removed: + +- `setPattern` +- `getPattern` + +Behaviour changes: + +- Non string input will now fail validation. Previously, scalars would be cast to string before pattern validation leading to possible bugs, for example, floating point numbers could be cast to scientific notation. +- Now the pattern is a required option in the constructor, an invalid pattern will cause an exception during `__construct` instead of during validation. +- The single constructor argument must now be either an associative array of options, or the regex pattern as a string. + +### `Laminas\Validator\Step` + +The following methods have been removed: + +- `setBaseValue` +- `getBaseValue` +- `setStep` +- `getStep` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/step.md). + +### `Laminas\Validator\StringLength` + +The following methods have been removed: + +- `setMin` +- `getMin` +- `setMax` +- `getMax` +- `getStringWrapper` +- `setStringWrapper` +- `getEncoding` +- `setEncoding` +- `getLength` +- `setLength` + +Behaviour changes: + +- The constructor now only accepts an associative array of options. +- Malformed multibyte input is now handled more consistently: In the event that any of the string wrappers cannot reliably detect the length of a string, an exception will be thrown. + +### `Laminas\Validator\Timezone` + +The following methods have been removed + +- `setType` + +Behaviour changes: + +- The constructor now only accepts an associative array of documented options +- The `type` option can now only be one of the type constants declared on the class, i.e. `Timezone::ABBREVIATION`, `Timezone::LOCATION`, or `Timezone::ALL` +- When validating timezone abbreviations, the check is now case-insensitive, so `CET` will pass validation when previously it did not. + +### `Laminas\Validator\Uri` + +The following methods have been removed: + +- `setUriHandler` +- `getUriHandler` +- `setAllowAbsolute` +- `getAllowAbsolute` +- `setAllowRelative` +- `getAllowRelative` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/uri.md). + +### `Laminas\Validator\File\Count` + +The following methods have been removed: + +- `getMin` +- `setMin` +- `getMax` +- `setMax` +- `addFile` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/count.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed + +### `Laminas\Validator\File\ExcludeExtension` and `Laminas\Validator\File\Extension` + +`ExcludeExtension` no longer inherits from the `Extension` validator. + +The following methods have been removed: + +- `getCase` +- `setCase` +- `getExtension` +- `setExtension` +- `addExtension` +- `getAllowNonExistentFile` +- `setAllowNonExistentFile` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/extension.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed +- An additional validation failure condition has been added for situations where the input cannot be recognised as either a file path or some type of upload. + +### `Laminas\Validator\File\ExcludeMimeType`, `Laminas\Validator\File\MimeType`, `Laminas\Validator\File\IsCompressed` and `Laminas\Validator\File\IsImage` + +The following methods have been removed: + +- `getMagicFile` +- `setMagicFile` +- `disableMagicFile` +- `isMagicFileDisabled` +- `getHeaderCheck` +- `enableHeaderCheck` +- `getMimeType` +- `setMimeType` +- `addMimeType` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/mime-type.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed +- The options `enableHeaderCheck`, `disableMagicFile` and `magicFile` have been removed. A custom magic file is now no longer accepted or used, instead the magic file bundled with PHP is used instead. + +### `Laminas\Validator\File\Size` and `Laminas\Validator\File\FilesSize` + +`FilesSize` no longer inherits from the `Size` validator. + +The following methods have been removed: + +- `useByteString` +- `getByteString` +- `getMin` +- `setMin` +- `getMax` +- `setMax` +- `getSize` +- `setSize` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/size.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed + +### `Laminas\Validator\File\Hash` + +The following methods have been removed: + +- `getHash` +- `setHash` +- `addHash` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/hash.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed +- Also see information about the [removal of inheritors](#removal-of-laminasvalidatorfilehash-inheritors) + +### `Laminas\Validator\File\Exists` and `Laminas\Validator\File\NotExists` + +The following methods have been removed: + +- `getDirectory` +- `setDirectory` +- `addDirectory` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/exists.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed +- `NotExists` no longer inherits from `Exists` + +### `Laminas\Validator\File\ImageSize` + +The following methods have been removed: + +- `getMinWidth` +- `setMinWidth` +- `getMaxWidth` +- `setMaxWidth` +- `getMinHeight` +- `setMinHeight` +- `getMaxHeight` +- `setMaxHeight` +- `getImageMin` +- `setImageMin` +- `getImageMax` +- `setImageMax` +- `getImageWidth` +- `setImageWidth` +- `getImageHeight` +- `setImageHeight` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/image-size.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed + +### `Laminas\Validator\File\WordCount` + +The following methods have been removed: + +- `getMin` +- `setMin` +- `getMax` +- `setMax` + +Behaviour changes: + +- The constructor now only accepts an associative array of [documented options](../validators/file/word-count.md) +- Compatibility with the legacy `Laminas\File\Transfer` api has been removed + +## Removed Features + +### `Laminas\Csrf` Validator Removal + +This validator was the only shipped validator with a hard dependency on the [`laminas-session`](https://docs.laminas.dev/laminas-session/) component. It has now been removed from this component and re-implemented, in a different namespace, but with the same functionality in `laminas-session`. + +In order to transition to the new validator, all that should be required is to ensure that you have listed `laminas-session` as a composer dependency and replace references to `Laminas\Validator\Csrf::class` with `Laminas\Session\Validator\Csrf::class`. + +### `Laminas\Db` Validator Removal + +The deprecated "Db" validators that shipped in version 2.0 have been removed. The removed classes are: + +- `Laminas\Validator\Db\AbstractDb` +- `Laminas\Validator\Db\NoRecordExists` +- `Laminas\Validator\Db\RecordExists` + +### Removal of `Between`, `LessThan` and `GreaterThan` Validators + +These validators could theoretically, and indeed were used to perform comparisons on `DateTime` instances, numbers and arbitrary strings. +Whilst these validators worked well for numbers, they worked less well for other data types. + +In order to reduce ambiguity, these validators have been replaced by [`NumberComparison`](../validators/number-comparison.md) and [`DateComparison`](../validators/date-comparison.md). + +Taking `LessThan` as an example replacement target: + +```php +$validator = new Laminas\Validator\LessThan([ + 'max' => 10, + 'inclusive' => true, +]); +``` + +Would become: + +```php +$validator = new Laminas\Validator\NumberComparison([ + 'max' => 10, + 'inclusiveMax' => true, +]); +``` + +### Removal of `Laminas\Validator\File\Upload` + +The deprecated `Upload` validator was only capable of validating the `$_FILES` super global, providing the entire `$_FILES` array was provided, at runtime, prior to validation. The validator expected a key such as `my-upload` corresponding to a posted form element and also relied on the legacy and deprecated `Laminas\File\Transfer` api. + +We suggest that you look at the [`UploadFile`](../validators/file/upload-file.md) validator instead. + +### Removal of `Laminas\Validator\File\Hash` inheritors + +The following classes have been removed: + +- `Laminas\Validator\File\Crc32` +- `Laminas\Validator\File\Md5` +- `Laminas\Validator\File\Sha1` + +These inheritors of the `Hash` validator were unnecessary. Simply construct an instance of the `Hash` validator with the algorithm that you require, for example: + +```php +$hash = new \Laminas\Validator\File\Hash([ + 'hash' => 'SomeExpectedSha256Hash', + 'algorithm' => 'sha256', +]); + +$hash->isValid('/path/to/file.md'); +``` + +The algorithms available are dictated by your installation of PHP and can be determined with [`hash_algos()`](https://www.php.net/manual/function.hash-algos.php) + +### Migration to `Laminas\Translator` + +As described under [changes to `AbstractValidator`](#translator-interface-type-has-changed), the following classes no longer exist: + +- `Laminas\Validator\Translator\DummyTranslator` +- `Laminas\Validator\Translator\Translator` +- `Laminas\Validator\Translator\TranslatorFactory` +- `Laminas\Validator\Translator\TranslatorInterface` + +### Removal of Module Manager Support + +[Module Manager](https://docs.laminas.dev/laminas-modulemanager/) support has been removed along with the interface `Laminas\Validator\ValidatorProviderInterface` diff --git a/docs/book/v3/validators/hostname.md b/docs/book/v3/validators/hostname.md index bfc3e3f5..af4d3f69 100644 --- a/docs/book/v3/validators/hostname.md +++ b/docs/book/v3/validators/hostname.md @@ -11,10 +11,10 @@ The following options are supported for `Laminas\Validator\Hostname`: - `allow`: Defines the sort of hostname which is allowed to be used. [See below](#validating-different-types-of-hostnames) for details. -- `idn`: Defines if IDN domains are allowed or not. This option defaults to +- `useIdnCheck`: Defines if IDN domains are allowed or not. This option defaults to `true`. - `ipValidator`: Allows defining an [IP validator](ip.md) with custom configuration -- `tld`: Defines if TLDs are validated. This option defaults to `true`. +- `useTldCheck`: Defines if TLDs are validated. This option defaults to `true`. ## Basic Usage diff --git a/docs/book/v3/writing-validators.md b/docs/book/v3/writing-validators.md index f01ff32e..1ee1e351 100644 --- a/docs/book/v3/writing-validators.md +++ b/docs/book/v3/writing-validators.md @@ -43,10 +43,10 @@ use Laminas\Validator\AbstractValidator; final class Float extends AbstractValidator { - const FLOAT = 'float'; + public const ERR_NOT_FLOAT = 'float'; protected array $messageTemplates = [ - self::FLOAT => "'%value%' is not a floating point value", + self::ERR_NOT_FLOAT => "'%value%' is not a floating point value", ]; public function isValid(mixed $value): bool @@ -54,7 +54,7 @@ final class Float extends AbstractValidator $this->setValue($value); if (! is_float($value)) { - $this->error(self::FLOAT); + $this->error(self::ERR_NOT_FLOAT); return false; } @@ -92,11 +92,17 @@ namespace MyValid; use Laminas\Validator\AbstractValidator; +/** + * @psalm-type Options = array{ + * minimum: positive-int, + * maximum: positive-int, + * } + */ final class NumericBetween extends AbstractValidator { - const MSG_NUMERIC = 'msgNumeric'; - const MSG_MINIMUM = 'msgMinimum'; - const MSG_MAXIMUM = 'msgMaximum'; + public const ERR_NOT_NUMERIC = 'msgNumeric'; + public const ERR_NOT_MINIMUM = 'msgMinimum'; + public const ERR_NOT_MAXIMUM = 'msgMaximum'; protected readonly $minimum; protected readonly $maximum; @@ -106,15 +112,18 @@ final class NumericBetween extends AbstractValidator 'max' => 'maximum', ]; - protected $messageTemplates = [ - self::MSG_NUMERIC => "'%value%' is not numeric", - self::MSG_MINIMUM => "'%value%' must be at least '%min%'", - self::MSG_MAXIMUM => "'%value%' must be no more than '%max%'", + protected array $messageTemplates = [ + self::ERR_NOT_NUMERIC => "'%value%' is not numeric", + self::ERR_NOT_MINIMUM => "'%value%' must be at least '%min%'", + self::ERR_NOT_MAXIMUM => "'%value%' must be no more than '%max%'", ]; - public function __construct(int $min, int $max) { - $this->minimum = $min; - $this->maximum = $max; + /** @param Options $options */ + public function __construct(array $options) { + $this->minimum = $options['minimum']; + $this->maximum = $options['maximum']; + + parent::__construct($options); } public function isValid(mixed $value): bool @@ -122,17 +131,17 @@ final class NumericBetween extends AbstractValidator $this->setValue($value); if (! is_numeric($value)) { - $this->error(self::MSG_NUMERIC); + $this->error(self::ERR_NOT_NUMERIC); return false; } if ($value < $this->minimum) { - $this->error(self::MSG_MINIMUM); + $this->error(self::ERR_NOT_MINIMUM); return false; } if ($value > $this->maximum) { - $this->error(self::MSG_MAXIMUM); + $this->error(self::ERR_NOT_MAXIMUM); return false; } @@ -176,16 +185,16 @@ use Laminas\Validator\AbstractValidator; final class PasswordStrength extends AbstractValidator { - const LENGTH = 'length'; - const UPPER = 'upper'; - const LOWER = 'lower'; - const DIGIT = 'digit'; - - protected $messageTemplates = [ - self::LENGTH => "'%value%' must be at least 8 characters in length", - self::UPPER => "'%value%' must contain at least one uppercase letter", - self::LOWER => "'%value%' must contain at least one lowercase letter", - self::DIGIT => "'%value%' must contain at least one digit character", + public const ERR_LENGTH = 'length'; + public const ERR_UPPER = 'upper'; + public const ERR_LOWER = 'lower'; + public const ERR_DIGIT = 'digit'; + + protected array $messageTemplates = [ + self::ERR_LENGTH => "'%value%' must be at least 8 characters in length", + self::ERR_UPPER => "'%value%' must contain at least one uppercase letter", + self::ERR_LOWER => "'%value%' must contain at least one lowercase letter", + self::ERR_DIGIT => "'%value%' must contain at least one digit character", ]; public function isValid(mixed $value): bool @@ -195,22 +204,22 @@ final class PasswordStrength extends AbstractValidator $isValid = true; if (strlen($value) < 8) { - $this->error(self::LENGTH); + $this->error(self::ERR_LENGTH); $isValid = false; } if (! preg_match('/[A-Z]/', $value)) { - $this->error(self::UPPER); + $this->error(self::ERR_UPPER); $isValid = false; } if (! preg_match('/[a-z]/', $value)) { - $this->error(self::LOWER); + $this->error(self::ERR_LOWER); $isValid = false; } if (! preg_match('/\d/', $value)) { - $this->error(self::DIGIT); + $this->error(self::ERR_DIGIT); $isValid = false; } @@ -239,3 +248,77 @@ public function isValid(mixed $value, ?array $context = null): bool // ... validation logic } ``` + +## Best Practices When Inheriting From `AbstractValidator` + +### Constructor Signature + +Define your constructor to accept a normal associative array of options with a signature such as: + +```php +public function __construct(array $options) { /** ... **/} +``` + +Additionally, call `parent::__construct($options)` within the constructor. + +The reason for this is to ensure that when users provide the `messages` option, the array of error messages override the defaults you have defined in the class. + +`AbstractValidator` also accepts: + +- The `translator` option which can be a specific implementation of `Laminas\Translator\TranslatorInterface` +- The `translatorEnabled` option which is a boolean indicating whether translation should be enabled or not +- The `translatorTextDomain` option - A string defining the text domain the translator should use +- The `valueObscured` option - A boolean that indicates whether the validated value should be replaced with '****' when interpolated into error messages + +The additional benefit of defining your constructor to accept an array of options is improved compatibility with the `ValidatorPluginManager`. +The plugin manager always creates a new instance, providing options to the constructor, meaning fewer specialised factories to write. + +When your validator has runtime dependencies on services, consider allowing an options array in the constructor so that the `AbstractValidator` options can be provided if required: + +```php +namespace MyValid; + +use Laminas\Validator\AbstractValidator; +use Psr\Container\ContainerInterface; + +final class FlightNumber extends AbstractValidator { + + public const ERR_INVALID_FLIGHT_NUMBER = 'invalidFlightNumber'; + + public function __construct(private readonly FlightNumberValidationService $service, array $options = []) { + parent::__construct($options); + } + + public function isValid(mixed $value): bool + { + if (! is_string($value)) { + $this->error(self::ERR_INVALID_FLIGHT_NUMBER); + + return false; + } + + if (! $this->service->isValidFlightNumber($value)) { + $this->error(self::ERR_INVALID_FLIGHT_NUMBER); + + return false; + } + + return true; + } +} + +final class FlightNumberFactory +{ + public function __invoke(ContainerInterface $container, string $name, array $options = []): FlightNumber + { + return new FlightNumber( + $container->get(FlightNumberValidationService::class), + $options, + ); + } +} +``` + +### Set Option Values Once and Make Them `readonly` + +By resolving validator options in the constructor to `private readonly` properties, and removing methods such as `getMyOption` and `setMyOption` you are forced to test how your validator behaviour varies based on its options, and, you can be sure that options simply cannot change once the validator has been constructed. diff --git a/mkdocs.yml b/mkdocs.yml index c0826a75..8e5390a4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,10 @@ nav: - Size: v3/validators/file/size.md - UploadFile: v3/validators/file/upload-file.md - WordCount: v3/validators/file/word-count.md + - Migration: + - "Migration from Version 2 to 3": v3/migration/v2-to-v3.md + - "Refactoring Legacy Validators": v3/migration/refactoring-legacy-validators.md + - "Composing Final Validators": v3/migration/composing-final-validators.md - v2: - Introduction: v2/intro.md - Installation: v2/installation.md