From 2b1dde8ac6d0e40a8d124da7a386f77dccc6015e Mon Sep 17 00:00:00 2001 From: George Steel Date: Sun, 12 Jan 2025 19:35:46 +0000 Subject: [PATCH] Prepare docs for version 3 Signed-off-by: George Steel --- .../usage-in-a-laminas-mvc-application.md | 0 .../usage-in-a-mezzio-application.md | 0 .../cookbook/enhanced-type-inference.md | 0 .../cookbook/input-filter-in-forms.md | 0 docs/book/{ => v2}/file-input.md | 0 docs/book/{ => v2}/installation.md | 0 docs/book/{ => v2}/intro.md | 0 docs/book/{ => v2}/optional-input-filters.md | 0 docs/book/{ => v2}/specs.md | 0 docs/book/{ => v2}/unfiltered-data.md | 0 .../usage-in-a-laminas-mvc-application.md | 114 ++++++++ .../usage-in-a-mezzio-application.md | 178 +++++++++++++ .../v3/cookbook/enhanced-type-inference.md | 72 +++++ .../book/v3/cookbook/input-filter-in-forms.md | 143 ++++++++++ docs/book/v3/file-input.md | 141 ++++++++++ docs/book/v3/installation.md | 5 + docs/book/v3/intro.md | 251 ++++++++++++++++++ docs/book/v3/optional-input-filters.md | 94 +++++++ docs/book/v3/specs.md | 96 +++++++ docs/book/v3/unfiltered-data.md | 49 ++++ mkdocs.yml | 58 +++- 21 files changed, 1188 insertions(+), 13 deletions(-) rename docs/book/{ => v2}/application-integration/usage-in-a-laminas-mvc-application.md (100%) rename docs/book/{ => v2}/application-integration/usage-in-a-mezzio-application.md (100%) rename docs/book/{ => v2}/cookbook/enhanced-type-inference.md (100%) rename docs/book/{ => v2}/cookbook/input-filter-in-forms.md (100%) rename docs/book/{ => v2}/file-input.md (100%) rename docs/book/{ => v2}/installation.md (100%) rename docs/book/{ => v2}/intro.md (100%) rename docs/book/{ => v2}/optional-input-filters.md (100%) rename docs/book/{ => v2}/specs.md (100%) rename docs/book/{ => v2}/unfiltered-data.md (100%) create mode 100644 docs/book/v3/application-integration/usage-in-a-laminas-mvc-application.md create mode 100644 docs/book/v3/application-integration/usage-in-a-mezzio-application.md create mode 100644 docs/book/v3/cookbook/enhanced-type-inference.md create mode 100644 docs/book/v3/cookbook/input-filter-in-forms.md create mode 100644 docs/book/v3/file-input.md create mode 100644 docs/book/v3/installation.md create mode 100644 docs/book/v3/intro.md create mode 100644 docs/book/v3/optional-input-filters.md create mode 100644 docs/book/v3/specs.md create mode 100644 docs/book/v3/unfiltered-data.md diff --git a/docs/book/application-integration/usage-in-a-laminas-mvc-application.md b/docs/book/v2/application-integration/usage-in-a-laminas-mvc-application.md similarity index 100% rename from docs/book/application-integration/usage-in-a-laminas-mvc-application.md rename to docs/book/v2/application-integration/usage-in-a-laminas-mvc-application.md diff --git a/docs/book/application-integration/usage-in-a-mezzio-application.md b/docs/book/v2/application-integration/usage-in-a-mezzio-application.md similarity index 100% rename from docs/book/application-integration/usage-in-a-mezzio-application.md rename to docs/book/v2/application-integration/usage-in-a-mezzio-application.md diff --git a/docs/book/cookbook/enhanced-type-inference.md b/docs/book/v2/cookbook/enhanced-type-inference.md similarity index 100% rename from docs/book/cookbook/enhanced-type-inference.md rename to docs/book/v2/cookbook/enhanced-type-inference.md diff --git a/docs/book/cookbook/input-filter-in-forms.md b/docs/book/v2/cookbook/input-filter-in-forms.md similarity index 100% rename from docs/book/cookbook/input-filter-in-forms.md rename to docs/book/v2/cookbook/input-filter-in-forms.md diff --git a/docs/book/file-input.md b/docs/book/v2/file-input.md similarity index 100% rename from docs/book/file-input.md rename to docs/book/v2/file-input.md diff --git a/docs/book/installation.md b/docs/book/v2/installation.md similarity index 100% rename from docs/book/installation.md rename to docs/book/v2/installation.md diff --git a/docs/book/intro.md b/docs/book/v2/intro.md similarity index 100% rename from docs/book/intro.md rename to docs/book/v2/intro.md diff --git a/docs/book/optional-input-filters.md b/docs/book/v2/optional-input-filters.md similarity index 100% rename from docs/book/optional-input-filters.md rename to docs/book/v2/optional-input-filters.md diff --git a/docs/book/specs.md b/docs/book/v2/specs.md similarity index 100% rename from docs/book/specs.md rename to docs/book/v2/specs.md diff --git a/docs/book/unfiltered-data.md b/docs/book/v2/unfiltered-data.md similarity index 100% rename from docs/book/unfiltered-data.md rename to docs/book/v2/unfiltered-data.md diff --git a/docs/book/v3/application-integration/usage-in-a-laminas-mvc-application.md b/docs/book/v3/application-integration/usage-in-a-laminas-mvc-application.md new file mode 100644 index 00000000..f492bebb --- /dev/null +++ b/docs/book/v3/application-integration/usage-in-a-laminas-mvc-application.md @@ -0,0 +1,114 @@ +# Usage in a laminas-mvc Application + +The following example shows _one_ potential use case of laminas-inputfilter within a laminas-mvc based application. +The example uses a module, a controller and the input filter plugin manager. + +The example is based on the [tutorial application](https://docs.laminas.dev/tutorials/getting-started/overview/) which builds an album inventory system. + +Before starting, make sure laminas-inputfilter is [installed and configured](../installation.md). + +## Create Input Filter + +[Create an input filter as separate class](../intro.md) using the `init` method, e.g. `module/Album/src/InputFilter/QueryInputFilter.php`: + +```php +namespace Album\InputFilter; + +use Laminas\Filter\ToInt; +use Laminas\I18n\Validator\IsInt; +use Laminas\InputFilter\InputFilter; + +final class QueryInputFilter extends InputFilter +{ + public function init(): void + { + // Page + $this->add( + [ + 'name' => 'page', + 'allow_empty' => true, + 'validators' => [ + [ + 'name' => IsInt::class, + ], + ], + 'filters' => [ + [ + 'name' => ToInt::class, + ], + ], + 'fallback_value' => 1, + ] + ); + + // … + } +} +``` + +## Create Controller + +[Create a controller class](https://docs.laminas.dev/laminas-mvc/quick-start/#create-a-controller) and inject the input filter plugin manager via the constructor, e.g. `module/Album/Controller/AlbumController.php`: + +```php +namespace Album\Controller; + +use Album\InputFilter\QueryInputFilter; +use Laminas\InputFilter\InputFilterPluginManager; +use Laminas\Mvc\Controller\AbstractActionController; + +use function assert; + +final class AlbumController extends AbstractActionController +{ + public function __construct( + public readonly InputFilterPluginManager $inputFilterPluginManager + ) {} + + public function indexAction() + { + $inputFilter = $this->inputFilterPluginManager->get(QueryInputFilter::class); + assert($inputFilter instanceof QueryInputFilter); + + $inputFilter->setData($this->getRequest()->getQuery()); + $inputFilter->isValid(); + $filteredParams = $inputFilter->getValues(); + + // … + } +} +``` + +> INFO: **Instantiating the Input Filter** +> +> The input filter plugin manager (`Laminas\InputFilter\InputFilterPluginManager`) is used instead of directly instantiating the input filter to ensure to get the filter and validator plugin managers injected. +> This allows usage of any filters and validators registered with their respective plugin managers. +> +> Additionally, the input filter plugin manager calls the `init` method _after_ instantiating the input filter, ensuring all dependencies are fully injected first. + +## Register Input Filter and Controller + +If no separate factory is required for the input filter, then the input filter plugin manager will be instantiating the input filter class without prior registration. Otherwise, the input filter must be registered. + +To [register the controller](https://docs.laminas.dev/laminas-mvc/quick-start/#create-a-route) for the application, extend the configuration of the module. +Add the following lines to the module configuration file, e.g. `module/Album/config/module.config.php`: + + +

+namespace Album;
+
+use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;
+
+return [
+    'controllers' => [
+        'factories' => [
+            // Add this line
+            Controller\AlbumController::class => ReflectionBasedAbstractFactory::class,
+        ],
+    ],
+    // …
+];
+
+ + +The example uses the [reflection factory from laminas-servicemanager](https://docs.laminas.dev/laminas-servicemanager/reflection-abstract-factory/) to resolve the constructor dependencies for the controller class. diff --git a/docs/book/v3/application-integration/usage-in-a-mezzio-application.md b/docs/book/v3/application-integration/usage-in-a-mezzio-application.md new file mode 100644 index 00000000..9aa642f0 --- /dev/null +++ b/docs/book/v3/application-integration/usage-in-a-mezzio-application.md @@ -0,0 +1,178 @@ +# Usage in a mezzio Application + +The following example shows _one_ potential use case of laminas-inputfilter within +a mezzio based application. The example uses a module, config provider +configuration, laminas-servicemanager as dependency injection container, the +laminas-inputfilter plugin manager and a request handler. + +Before starting, make sure laminas-inputfilter is [installed and configured](../installation.md). + +## Create Input Filter + +Create an input filter as separate class, e.g. +`src/Album/InputFilter/QueryInputFilter.php`: + +```php +namespace Album\InputFilter; + +use Laminas\Filter\ToInt; +use Laminas\I18n\Validator\IsInt; +use Laminas\InputFilter\InputFilter; + +class QueryInputFilter extends InputFilter +{ + public function init() + { + // Page + $this->add( + [ + 'name' => 'page', + 'allow_empty' => true, + 'validators' => [ + [ + 'name' => IsInt::class, + ], + ], + 'filters' => [ + [ + 'name' => ToInt::class, + ], + ], + 'fallback_value' => 1, + ] + ); + + // … + } +} +``` + +## Using Input Filter + +### Create Handler + +Using the input filter in a request handler, e.g. +`src/Album/Handler/ListHandler.php`: + +```php +namespace Album\Handler; + +use Album\InputFilter\QueryInputFilter; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Psr\Http\Message\ResponseInterface; +use Laminas\InputFilter\InputFilterInterface; +use Mezzio\Template\TemplateRendererInterface; + +class ListHandler implements RequestHandlerInterface +{ + /** @var InputFilterInterface */ + private $inputFilter; + + /** @var TemplateRendererInterface */ + private $renderer; + + public function __construct( + InputFilterInterface $inputFilter, + TemplateRendererInterface $renderer + ) { + $this->inputFilter = $inputFilter; + $this->renderer = $renderer; + } + + public function handle(ServerRequestInterface $request) : ResponseInterface + { + $this->inputFilter->setData($request->getQueryParams()); + $this->inputFilter->isValid(); + $filteredParams = $this->inputFilter->getValues(); + + // … + + return new HtmlResponse($this->renderer->render( + 'album::list', + [] + )); + } +} +``` + +### Create Factory for Handler + +Fetch the `QueryInputFilter` from the input filter plugin manager in a factory, +e.g. `src/Album/Handler/ListHandlerFactory.php`: + +```php +namespace Album\Handler; + +use Album\InputFilter\QueryInputFilter; +use Psr\Container\ContainerInterface; +use Laminas\InputFilter\InputFilterPluginManager; + +class ListHandlerFactory +{ + public function __invoke(ContainerInterface $container) + { + /** @var InputFilterPluginManager $pluginManager */ + $pluginManager = $container->get(InputFilterPluginManager::class); + $inputFilter = $pluginManager->get(QueryInputFilter::class); + + return new ListHandler( + $inputFilter, + $container->get(TemplateRendererInterface::class) + ); + } +} +``` + +> ### Instantiating the InputFilter +> +> The `InputFilterPluginManager` is used instead of directly instantiating the +> input filter to ensure to get the filter and validator plugin managers +> injected. This allows usage of any filters and validators registered with +> their respective plugin managers. +> +> Additionally the `InputFilterPluginManager` calls the `init` method _after_ +> instantiating the input filter, ensuring all dependencies are fully injected +> first. + +## Register Input Filter and Handler + +Extend the configuration provider of the module to register the input filter and +the request handler, e.g. `src/Album/ConfigProvider.php`: + +```php +namespace Album; + +class ConfigProvider +{ + public function __invoke() : array + { + return [ + 'dependencies' => $this->getDependencies(), + 'input_filters' => $this->getInputFilters(), // <-- Add this line + ]; + } + + public function getDependencies() : array + { + return [ + 'factories' => [ + Handler\ListHandler::class => Handler\ListHandlerFactory::class, // <-- Add this line + // … + ], + ]; + } + + // Add the following method + public function getInputFilters() : array + { + return [ + 'factories' => [ + InputFilter\QueryInputFilter::class => InvokableFactory::class, + ], + ]; + } + + // … +} +``` diff --git a/docs/book/v3/cookbook/enhanced-type-inference.md b/docs/book/v3/cookbook/enhanced-type-inference.md new file mode 100644 index 00000000..c2a811cb --- /dev/null +++ b/docs/book/v3/cookbook/enhanced-type-inference.md @@ -0,0 +1,72 @@ +# Using Laminas InputFilter with Static Analysis Tools + +It can be tedious to assert that the array returned by a given input filter contains the keys and value types that you would expect before using those values in your domain. +If you use static analysis tools such as Psalm or PHPStan, `InputFilterInterface` defines a generic template that can be used to refine the types you receive from the `getValues()` method. + +```php +, + * } + * @extends InputFilter + */ +final class SomeInputFilter extends InputFilter +{ + public function init(): void + { + $this->add([ + 'name' => 'anInteger', + 'required' => true, + 'filters' => [ + ['name' => ToNull::class], + ['name' => ToInt::class], + ], + 'validators' => [ + ['name' => NotEmpty::class], + ['name' => IsInt::class], + [ + 'name' => GreaterThan::class, + 'options' => [ + 'min' => 1, + ], + ], + ], + ]); + } +} +``` + +With the above input filter specification, one can guarantee that, if the input payload is deemed valid, then you will receive an array with the expected shape from `InputFilter::getValues()`, therefore, your static analysis tooling will not complain when you pass that value directly to something that expects a `positive-int`, for example: + +```php +/** + * @param positive-int $value + * @return positive-int + */ +function addTo5(int $value): int +{ + return $value + 5; +} + +$filter = new SomeInputFilter(); +$filter->setData(['anInteger' => '123']); +assert($filter->isValid()); + +$result = addTo5($filter->getValues()['anInteger']); +``` + +## Further reading + +- [Psalm documentation on array shapes](https://psalm.dev/docs/annotating_code/type_syntax/array_types/#array-shapes) +- [Psalm documentation on type aliases](https://psalm.dev/docs/annotating_code/type_syntax/utility_types/#type-aliases) +- [PHPStan documentation on array shapes](https://phpstan.org/writing-php-code/phpdoc-types#array-shapes) +- [PHPStan documentation on type aliases](https://phpstan.org/writing-php-code/phpdoc-types#local-type-aliases) diff --git a/docs/book/v3/cookbook/input-filter-in-forms.md b/docs/book/v3/cookbook/input-filter-in-forms.md new file mode 100644 index 00000000..8f3fbb09 --- /dev/null +++ b/docs/book/v3/cookbook/input-filter-in-forms.md @@ -0,0 +1,143 @@ +# Using Input Filters in Forms of laminas-form + +The following examples show _two_ potential use cases of laminas-inputfilter within [laminas-form](https://docs.laminas.dev/laminas-form/). + +## Define the Input Filter in a Form + +An input filter can be [defined directly in a form class](https://docs.laminas.dev/laminas-form/v3/quick-start/#hinting-to-the-input-filter) itself, using `Laminas\InputFilter\InputFilterProviderInterface`. +This interface provides one method (`getInputFilterSpecification()`) that is used by a form to create an input filter. + +[Create a form as a separate class](https://docs.laminas.dev/laminas-form/v3/quick-start/#factory-backed-form-extension), define the [`init` method](https://docs.laminas.dev/laminas-form/v3/advanced/#initialization), implement the interface `Laminas\InputFilter\InputFilterProviderInterface`, and define its inputs via a configuration array; as an example, consider the following definition in a file found at `module/Album/src/Form/AlbumForm.php`: + + +

+namespace Album\Form;
+
+use Laminas\Filter\StringTrim;
+use Laminas\Filter\StripTags;
+use Laminas\Form\Element\Text;
+use Laminas\Form\Form;
+use Laminas\InputFilter\InputFilterProviderInterface;
+use Laminas\Validator\StringLength;
+
+final class AlbumForm extends Form implements InputFilterProviderInterface
+{
+    public function init(): void
+    {
+        // Add form elements
+        $this->add([
+            'name'    => 'title',
+            'type'    => Text::class,
+            'options' => [
+                'label' => 'Title',
+            ],
+        ]);
+
+        // …
+    }
+
+    public function getInputFilterSpecification(): array
+    {
+        return [
+            // Add inputs
+            [
+                'name'    => 'title',
+                'filters' => [
+                    ['name' => StripTags::class],
+                    ['name' => StringTrim::class],
+                ],
+                'validators' => [
+                    [
+                        'name'    => StringLength::class,
+                        'options' => [
+                            'min' => 1,
+                            'max' => 100,
+                        ],
+                    ],
+                ],
+            ],
+            // …
+        ];
+    }
+}
+
+ + +## Adding an Input Filter Defined as a Separate Class to a Form + +### Create Input Filter + +[Create an input filter as a separate class](../intro.md), e.g. `module/Album/src/InputFilter/AlbumInputFilter.php`: + +```php +namespace Album\InputFilter; + +use Laminas\Filter\StringTrim; +use Laminas\Filter\StripTags; +use Laminas\Validator\StringLength; +use Laminas\InputFilter\InputFilter; + +final class AlbumInputFilter extends InputFilter +{ + public function init(): void + { + // Add inputs + $this->add( + [ + 'name' => 'title', + 'filters' => [ + ['name' => StripTags::class], + ['name' => StringTrim::class], + ], + 'validators' => [ + [ + 'name' => StringLength::class, + 'options' => [ + 'min' => 1, + 'max' => 100, + ], + ], + ], + ] + ); + + // … + } +} +``` + +### Create Form and Add Input Filter + +An input filter can be added directly in a form class itself, using the class name of the input filter or whatever name the input filter is registered under. + +Create a form as a separate class, define its `init` method, and set the input filter via the `setInputFilterByName()` method of `Laminas\Form\Form`, e.g. `module/Album/src/Form/AlbumForm.php`: + + +

+namespace Album\Form;
+
+use Album\InputFilter\AlbumInputFilter;
+use Laminas\Form\Element\Text;
+use Laminas\Form\Form;
+
+final class AlbumForm extends Form
+{
+    public function init(): void
+    {
+        // Set the input filter
+        $this->setInputFilterByName(AlbumInputFilter::class);
+
+        // Add form elements
+        $this->add([
+            'name'    => 'title',
+            'type'    => Text::class,
+            'options' => [
+                'label' => 'Title',
+            ],
+        ]);
+
+        // …
+    }
+}
+
+ diff --git a/docs/book/v3/file-input.md b/docs/book/v3/file-input.md new file mode 100644 index 00000000..b962dd50 --- /dev/null +++ b/docs/book/v3/file-input.md @@ -0,0 +1,141 @@ +# File Upload Input + +The `Laminas\InputFilter\FileInput` class is a special `Input` type for uploaded +files found in the `$_FILES` array. + +While `FileInput` uses the same interface as `Input`, it differs in a few ways: + +1. It expects the raw value to be in a normalized `$_FILES` array format. See + the [PSR-7 Uploaded files](http://www.php-fig.org/psr/psr-7/#uploaded-files) + chapter for details on how to accomplish this. + [Diactoros](https://docs.laminas.dev/laminas-diactoros/) and + [laminas-http](https://docs.laminas.dev/laminas-http/) can do this for you. + + Alternately, you may provide an array of PSR-7 uploaded file instances. + +2. The validators are run **before** the filters (which is the opposite behavior + of `Input`). This is so that any validation can be run + prior to any filters that may rename/move/modify the file; we should not do + those operations if the file upload was invalid! + +3. Instead of adding a `NotEmpty` validator, it will (by default) automatically + add a `Laminas\Validator\File\UploadFile` validator. + +The biggest thing to be concerned about is that if you are using a `` element in your form, you will need to use the `FileInput` +**instead of** `Input` or else you will encounter issues. + +## Basic Usage + +Usage of `FileInput` is essentially the same as `Input`: + +```php +use Laminas\Http\PhpEnvironment\Request; +use Laminas\Filter; +use Laminas\InputFilter\InputFilter; +use Laminas\InputFilter\Input; +use Laminas\InputFilter\FileInput; +use Laminas\Validator; + +// Description text input +$description = new Input('description'); // Standard Input type + +// Filters are run first w/ Input +$description + ->getFilterChain() + ->attach(new Filter\StringTrim()); + +// Validators are run second w/ Input +$description + ->getValidatorChain() + ->attach(new Validator\StringLength(['max' => 140])); + +// File upload input +$file = new FileInput('file'); // Special File Input type + +// Validators are run first w/ FileInput +$file + ->getValidatorChain() + ->attach(new Validator\File\UploadFile()); + +// Filters are run second w/ FileInput +$file + ->getFilterChain() + ->attach(new Filter\File\RenameUpload([ + 'target' => './data/tmpuploads/file', + 'randomize' => true, + ])); + +// Merge $_POST and $_FILES data together +$request = new Request(); +$postData = array_merge_recursive( + $request->getPost()->toArray(), + $request->getFiles()->toArray() +); + +$inputFilter = new InputFilter(); +$inputFilter + ->add($description) + ->add($file) + ->setData($postData); + +if ($inputFilter->isValid()) { // FileInput validators are run, but not the filters... + echo "The form is valid\n"; + $data = $inputFilter->getValues(); // This is when the FileInput filters are run. +} else { + echo "The form is not valid\n"; + foreach ($inputFilter->getInvalidInput() as $error) { + print_r ($error->getMessages()); + } +} +``` + +## PSR-7 Support + +> Available since version 2.9.0 + +You may also pass an array of uploaded files from a [PSR-7 ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#serverrequestinterface). + +```php +use Psr\Http\Message\ServerRequestInterface; +use Laminas\Filter; +use Laminas\InputFilter\InputFilter; +use Laminas\InputFilter\FileInput; +use Laminas\Validator; + +// File upload input +$file = new FileInput('file'); +$file + ->getValidatorChain() + ->attach(new Validator\File\UploadFile()); +$file + ->getFilterChain() + ->attach(new Filter\File\RenameUpload([ + 'target' => './data/tmpuploads/file', + 'randomize' => true, + ])); + +// Merge form and uploaded file data together +// Unlike the previous example, we get the form data from `getParsedBody()`, and +// the uploaded file data from `getUploadedFiles()`. +// @var ServerRequestInterface $request +$postData = array_merge_recursive( + $request->getParsedBody(), + $request->getUploadedFiles() +); + +$inputFilter = new InputFilter(); +$inputFilter + ->add($file) + ->setData($postData); + +if ($inputFilter->isValid()) { // FileInput validators are run, but not the filters... + echo "The form is valid\n"; + $data = $inputFilter->getValues(); // This is when the FileInput filters are run. +} else { + echo "The form is not valid\n"; + foreach ($inputFilter->getInvalidInput() as $error) { + print_r ($error->getMessages()); + } +} +``` diff --git a/docs/book/v3/installation.md b/docs/book/v3/installation.md new file mode 100644 index 00000000..be1198c9 --- /dev/null +++ b/docs/book/v3/installation.md @@ -0,0 +1,5 @@ +# This Is Only a Placeholder + +The content of this page can be found under: + +https://github.com/laminas/documentation-theme/blob/master/theme/pages/installation.html diff --git a/docs/book/v3/intro.md b/docs/book/v3/intro.md new file mode 100644 index 00000000..73f305b7 --- /dev/null +++ b/docs/book/v3/intro.md @@ -0,0 +1,251 @@ +# Introduction + +laminas-inputfilter can be used to filter and validate generic sets of input data. +For instance, you could use it to filter `$_GET` or `$_POST` values, CLI +arguments, etc. + +To pass input data to the `InputFilter`, use the `setData()` method; the data +must be specified using an associative array. Below is an example on how to +validate the data coming from a form using the POST method. + +```php +use Laminas\InputFilter\InputFilter; +use Laminas\InputFilter\Input; +use Laminas\Validator; + +$email = new Input('email'); +$email->getValidatorChain() + ->attach(new Validator\EmailAddress()); + +$password = new Input('password'); +$password->getValidatorChain() + ->attach(new Validator\StringLength(8)); + +$inputFilter = new InputFilter(); +$inputFilter->add($email); +$inputFilter->add($password); +$inputFilter->setData($_POST); + +if ($inputFilter->isValid()) { + echo "The form is valid\n"; +} else { + echo "The form is not valid\n"; + foreach ($inputFilter->getInvalidInput() as $error) { + print_r($error->getMessages()); + } +} +``` + +In this example, we validated the email and password values. The email must be a +valid address and the password must be composed of at least 8 characters. If the +input data are not valid, we report the list of invalid input using the +`getInvalidInput()` method. + +You can add validators to each input using the `attach()` method for each +validator chain. It is also possible to specify a "validation group", a subset +of the data to be validated; this may be done using the `setValidationGroup()` +method. You can specify the list of the input names as an array or as individual +parameters. + +```php +// As individual parameters +$inputFilter->setValidationGroup('email', 'password'); + +// or as an array of names +$inputFilter->setValidationGroup(['email', 'password']); +``` + +You can validate and/or filter the data using the `InputFilter`. To filter data, +use the `getFilterChain()` method of individual `Input` instances, and attach +filters to the returned filter chain. Below is an example that uses filtering +without validation. + +```php +use Laminas\InputFilter\Input; +use Laminas\InputFilter\InputFilter; + +$input = new Input('foo'); +$input->getFilterChain() + ->attachByName('stringtrim') + ->attachByName('alpha'); + +$inputFilter = new InputFilter(); +$inputFilter->add($input); +$inputFilter->setData([ + 'foo' => ' Bar3 ', +]); + +echo "Before:\n"; +echo $inputFilter->getRawValue('foo') . "\n"; // the output is ' Bar3 ' +echo "After:\n"; +echo $inputFilter->getValue('foo') . "\n"; // the output is 'Bar' +``` + +The `getValue()` method returns the filtered value of the 'foo' input, while +`getRawValue()` returns the original value of the input. + +We also provide `Laminas\InputFilter\Factory` to allow initialization of the +`InputFilter` based on a configuration array or `Traversable` object. Below is +an example where we create a password input value with the same constraints +proposed before (a string with at least 8 characters): + +```php +use Laminas\InputFilter\Factory; +use Laminas\Validator\NotEmpty; +use Laminas\Validator\StringLength; + +$factory = new Factory(); +$inputFilter = $factory->createInputFilter([ + 'password' => [ + 'name' => 'password', + 'required' => true, + 'validators' => [ + [ + 'name' => NotEmpty::class, + ], + [ + 'name' => StringLength::class, + 'options' => [ + 'min' => 8 + ], + ], + ], + ], +]); + +$inputFilter->setData($_POST); +echo $inputFilter->isValid() ? 'Valid form' : 'Invalid form'; +``` + +The factory may be used to create not only `Input` instances, but also nested +`InputFilter`s, allowing you to create validation and filtering rules for +hierarchical data sets. + +Finally, the default `InputFilter` implementation is backed by a `Factory`. This +means that when calling `add()`, you can provide a specification that the +`Factory` understands, and it will create the appropriate object. You may +create either `Input` or `InputFilter` objects in this fashion. + +```php +use Laminas\InputFilter\InputFilter; +use Laminas\Validator\NotEmpty; +use Laminas\Validator\StringLength; + +$filter = new InputFilter(); + +// Adding a single input +$filter->add([ + 'name' => 'username', + 'required' => true, + 'validators' => [ + [ + 'name' => NotEmpty::class, + ], + [ + 'name' => StringLength::class, + 'options' => [ + 'min' => 5 + ], + ], + ], +]); + +// Adding another input filter what also contains a single input. Merging both. +$filter->add([ + 'type' => 'Laminas\InputFilter\InputFilter', + 'password' => [ + 'name' => 'password', + 'required' => true, + 'validators' => [ + [ + 'name' => NotEmpty::class, + ], + [ + 'name' => StringLength::class, + 'options' => [ + 'min' => 8 + ], + ], + ], + ], +]); +``` + +The `merge()` method may be used on an `InputFilterInterface` instance in order +to add two or more filters to each other, effectively allowing you to create +chains of filters. This is especially useful in object hierarchies that define a +generic set of validation rules on the base object and build on these to create +more specific rules. + +In the example below, an `InputFilter` is built creating name and email +properties, allowing them to be re-used elsewhere. When the `isValid()` method +is called on the object, all of the merged filters are run against the calling +object in order to validate the internal properties based on our compound set of +filters. + +```php +use Laminas\InputFilter\InputFilter; + +/** + * Filter to ensure a name property is set and > 8 characters + */ +class NameInputFilter extends InputFilter +{ + /** Filter body goes here **/ +} + +/** + * Filter to ensure an email property is set and > 8 characters and is valid + */ +class EmailInputFilter extends InputFilter +{ + /** Filter body goes here **/ +} + +class SimplePerson +{ + /** Member variables omitted for brevity **/ + + /** @var InputFilter */ + protected $inputFilter; + + /** + * Retrieve input filter + * + * @return InputFilter + */ + public function getInputFilter() + { + if (! $this->inputFilter) { + // Create a new input filter + $this->inputFilter = new InputFilter(); + + // Merge our inputFilter in for the email property + $this->inputFilter->merge(new EmailInputFilter()); + + // Merge our inputFilter in for the name property + $this->inputFilter->merge(new NameInputFilter()); + } + + return $this->inputFilter; + } + + /** + * Set input filter + * + * @param InputFilterInterface $inputFilter + * @return SimplePerson + */ + public function setInputFilter(InputFilterInterface $inputFilter) + { + $this->inputFilter = $inputFilter; + + return $this; + } +} +``` + +Also see + +- [laminas-filter](https://docs.laminas.dev/laminas-filter/) +- [laminas-validator](https://docs.laminas.dev/laminas-validator/) diff --git a/docs/book/v3/optional-input-filters.md b/docs/book/v3/optional-input-filters.md new file mode 100644 index 00000000..b8bed4b4 --- /dev/null +++ b/docs/book/v3/optional-input-filters.md @@ -0,0 +1,94 @@ +# Optional Input Filters + +> Available since version 2.8.0 + +Normally, input filters are _required_, which means that if you compose them as +a subset of another input filter (e.g., to validate a subset of a larger set of +data), and no data is provided for that item, or an empty set of data is +provided, then the input filter will consider the data invalid. + +If you want to allow a set of data to be empty, you can use +`Laminas\InputFilter\OptionalInputFilter`. + +To illustrate this, let's consider a form where a user provides profile +information. The user can provide an optional "title" and a required "email", +and _optionally_ details about a project they lead, which will include the +project title and a URL, both of which are required if present. + +First, let's create an `OptionalInputFilter` instance for the project data: + +```php +$projectFilter = new OptionalInputFilter(); +$projectFilter->add([ + 'name' => 'project_name', + 'required' => true, +]); +$projectFilter->add([ + 'name' => 'url', + 'required' => true, + 'validators' => [ + ['type' => 'uri'], + ], +]); +``` + +Now, we'll create our primary input filter: + +```php +$profileFilter = new InputFilter(); +$profileFilter->add([ + 'name' => 'title', + 'required' => false, +]); +$profileFilter->add([ + 'name' => 'email', + 'required' => true, + 'validators' => [ + ['type' => 'EmailAddress'], + ], +]); + +// And, finally, compose our project sub-filter: +$profileFilter->add($projectFilter, 'project'); +``` + +With this defined, we can now validate the following sets of data, presented in +JSON for readability: + +- Just profile information: + ```json + { + "email": "user@example.com", + "title": "Software Developer" + } + ``` + +- `null` project provided: + ```json + { + "email": "user@example.com", + "title": "Software Developer", + "project": null + } + ``` + +- Empty project provided: + ```json + { + "email": "user@example.com", + "title": "Software Developer", + "project": {} + } + ``` + +- Valid project provided: + ```json + { + "email": "user@example.com", + "title": "Software Developer", + "project": { + "project_name": "laminas-inputfilter", + "url": "https://github.com/laminas-inputfilter" + } + } + ``` diff --git a/docs/book/v3/specs.md b/docs/book/v3/specs.md new file mode 100644 index 00000000..9c6a6642 --- /dev/null +++ b/docs/book/v3/specs.md @@ -0,0 +1,96 @@ +# Specifications + +`Laminas\InputFilter` allows configuration-driven creation of input filters via +`Laminas\InputFilter\InputFilterAbstractServiceFactory`. This abstract factory is +responsible for creating and returning an appropriate input filter given named +configuration under the top-level configuration key `input_filter_specs`. + +It is registered with `Laminas\InputFilter\InputFilterPluginManager`, allowing you +to pull the input filter via that plugin manager. A side effect is that forms +pulled from `Laminas\Form\FormElementManager` can use these named input filters. + +## Setup + +When using laminas-mvc version 2 releases, this functionality is disabled by +default. To enable it, you must add the +`Laminas\InputFilter\InputFilterAbstractServiceFactory` abstract factory to the +`Laminas\InputFilter\InputFilterPluginManager` configuration, which is under the +`input_filters` configuration key. + +```php +return [ + 'input_filters' => [ + 'abstract_factories' => [ + 'Laminas\InputFilter\InputFilterAbstractServiceFactory', + ], + ], +]; +``` + +For [Mezzio](https://docs.mezzio.dev/mezzio/) when using +the configuration manager, and for laminas-mvc v3 releases, the functionality is +enabled by default, assuming you are using the +[component installer](https://docs.laminas.dev/laminas-component-installer/). + +## Example + +In the following code, we define configuration for an input filter named `foobar`: + +```php +return [ + 'input_filter_specs' => [ + 'foobar' => [ + [ + 'name' => 'name', + 'required' => true, + 'filters' => [ + [ + 'name' => 'Laminas\Filter\StringTrim', + 'options' => [], + ], + ], + 'validators' => [], + 'description' => 'Hello to name', + 'allow_empty' => false, + 'continue_if_empty' => false, + ], + ], + ], +]; +``` + +When creating a controller, we might then pull the `InputFilterPluginManager`, and +retrieve the `foobar` input filter we've defined in order to inject it: + +```php +use Laminas\InputFilter\InputFilterPluginManager; +use Laminas\ServiceManager\FactoryInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; + +class MyValidatingControllerFactory implements FactoryInterface +{ + public function createService(ServiceLocatorInterface $controllers) + { + // Retrieve the application service manager + $services = $controllers->getServiceLocator(); + + // Retrieve the InputFilterManager + $filters = $services->get(InputFilterPluginManager::class); + + // Instantiate the controller and pass it the foobar input filter + return new MyValidatingController($filters->get('foobar')); + } +} +``` + +And you can use it as you already did with other input filters: + +```php +$inputFilter->setData([ + 'name' => 'test', +]); + +if (! $inputFilter->isValid()) { + echo 'Data invalid'; +} +``` diff --git a/docs/book/v3/unfiltered-data.md b/docs/book/v3/unfiltered-data.md new file mode 100644 index 00000000..ce921311 --- /dev/null +++ b/docs/book/v3/unfiltered-data.md @@ -0,0 +1,49 @@ +# Unfiltered Data + +> Available since version 2.10.0 + +On input filters, there are several methods for retrieving the data: + +- `getValues()` will return all known values after filtering them. +- `getRawValues()` will return all known values with no filtering applied. +- `getUnknown()` returns the set of all unknown values (values with no + corresponding input or input filter). + +At times, particularly when working with collections, you may want access to the +complete set of original data provided to the input filter. This can be +accomplished by merging the sets returned by `getRawValues()` and +`getUnknown()` when working with normal input filters, but this approach breaks +down when working with collections. + +Version 2.10.0 introduces a new interface, `Laminas\InputFilter\UnfilteredDataInterface`, +for dealing with this situation. `Laminas\InputFilter\BaseInputFilter`, which +forms the parent class for all shipped input filter implementations, implements +the interface, which consists of the following methods: + +```php +interface UnfilteredDataInterface +{ + /** + * @return array|object + */ + public function getUnfilteredData() + { + return $this->unfilteredData; + } + + /** + * @param array|object $data + * @return $this + */ + public function setUnfilteredData($data) + { + $this->unfilteredData = $data; + return $this; + } +} +``` + +The `setUnfilteredData()` method is called by `setData()` with the full `$data` +provided to that method, ensuring that `getUnfilteredData()` will always provide +the original data with which the input filter was initialized, with no filtering +applied. diff --git a/mkdocs.yml b/mkdocs.yml index 18ce1acd..5d3d8045 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,19 +2,34 @@ docs_dir: docs/book site_dir: docs/html nav: - Home: index.md - - Introduction: intro.md - - Installation: installation.md - - Reference: - - Specifications: specs.md - - "File Upload Input": file-input.md - - "Optional Input Filters": optional-input-filters.md - - "Unfiltered Data": unfiltered-data.md - - "Application Integration": - - "Usage in a laminas-mvc application": application-integration/usage-in-a-laminas-mvc-application.md - - "Usage in a mezzio application": application-integration/usage-in-a-mezzio-application.md - - Cookbook: - - "Using Input Filters in Forms of laminas-form": cookbook/input-filter-in-forms.md - - "Using Static Analysis tools with laminas-inputfilter": cookbook/enhanced-type-inference.md + - v2: + - Introduction: v2/intro.md + - Installation: v2/installation.md + - Reference: + - Specifications: v2/specs.md + - "File Upload Input": v2/file-input.md + - "Optional Input Filters": v2/optional-input-filters.md + - "Unfiltered Data": v2/unfiltered-data.md + - "Application Integration": + - "Usage in a laminas-mvc application": v2/application-integration/usage-in-a-laminas-mvc-application.md + - "Usage in a mezzio application": v2/application-integration/usage-in-a-mezzio-application.md + - Cookbook: + - "Using Input Filters in Forms of laminas-form": v2/cookbook/input-filter-in-forms.md + - "Using Static Analysis tools with laminas-inputfilter": v2/cookbook/enhanced-type-inference.md + - v3: + - Introduction: v3/intro.md + - Installation: v3/installation.md + - Reference: + - Specifications: v3/specs.md + - "File Upload Input": v3/file-input.md + - "Optional Input Filters": v3/optional-input-filters.md + - "Unfiltered Data": v3/unfiltered-data.md + - "Application Integration": + - "Usage in a laminas-mvc application": v3/application-integration/usage-in-a-laminas-mvc-application.md + - "Usage in a mezzio application": v3/application-integration/usage-in-a-mezzio-application.md + - Cookbook: + - "Using Input Filters in Forms of laminas-form": v3/cookbook/input-filter-in-forms.md + - "Using Static Analysis tools with laminas-inputfilter": v3/cookbook/enhanced-type-inference.md site_name: laminas-inputfilter site_description: 'Normalize and validate input sets from the web, APIs, the CLI, and more, including files.' repo_url: 'https://github.com/laminas/laminas-inputfilter' @@ -23,3 +38,20 @@ extra: installation: config_provider_class: 'Laminas\InputFilter\ConfigProvider' module_class: 'Laminas\InputFilter\Module' + current_version: v3 + versions: + - v2 + - v3 +plugins: + - redirects: + redirect_maps: + intro.md: v3/intro.md + installation.md: v3/installation.md + specs.md: v3/specs.md + file-input.md: v3/file-input.md + optional-input-filters.md: v3/optional-input-filters.md + unfiltered-data.md: v3/unfiltered-data.md + application-integration/usage-in-a-laminas-mvc-application.md: v3/application-integration/usage-in-a-laminas-mvc-application.md + application-integration/usage-in-a-mezzio-application.md: v3/application-integration/usage-in-a-mezzio-application.md + cookbook/input-filter-in-forms.md: v3/cookbook/input-filter-in-forms.md + cookbook/enhanced-type-inference.md: v3/cookbook/enhanced-type-inference.md