Create Middleware for File Post/Redirect/Get in Expressive 3
Previously, I wrote a post about Post/Redirect/Get in Expressive 3 which specifically handle POST parsed body. How about File upload? Let’s do it!
In this post, I will do with Zend\Form
component.
Let’s start by install a fresh skeleton:
$ composer create-project \ zendframework/zend-expressive-skeleton \ expressive-fileprg-tutorial
To save temporary the form data and its invalid input error messages during redirect, we can use session, so, we can require session components:
$ cd expressive-fileprg-tutorial $ composer require \ zendframework/zend-expressive-session:^1.0.0 \ zendframework/zend-expressive-session-ext:^1.1.1
After above components installed, ensure that your config/config.php injected with ConfigProvider like below:
<?php // config/config.php $aggregator = new ConfigAggregator([ // session requirements \Zend\Expressive\Session\Ext\ConfigProvider::class, \Zend\Expressive\Session\ConfigProvider::class, // ... ]);
The session middleware from zend-expressive-session
component need to be registered in our config/pipeline.php
before RouteMiddleware
:
// config/pipeline.php // ... use Zend\Expressive\Session\SessionMiddleware; // ... $app->pipe(SessionMiddleware::class); $app->pipe(RouteMiddleware::class);
Next, we are going to require a form dependencies for it with a command:
$ composer require \ zendframework/zend-form:^2.11 \ zendframework/zend-i18n:^2.7
After above components installed, ensure that your config/config.php have sessions and form ConfigProviders:
<?php // config/config.php $aggregator = new ConfigAggregator([ // session requirements \Zend\Expressive\Session\Ext\ConfigProvider::class, \Zend\Expressive\Session\ConfigProvider::class, // ... // form requirements \Zend\I18n\ConfigProvider::class, \Zend\Form\ConfigProvider::class, \Zend\InputFilter\ConfigProvider::class, \Zend\Filter\ConfigProvider::class, \Zend\Hydrator\ConfigProvider::class, ]);
The New Middleware
We are going to create a middleware for our application, for example, we name it FilePrgMiddleware
, placed at src/App/Middleware. I will explain part by part.
Unlike the normal PRG
middleware in previous post, the FilePrgMiddleware
need to bring the Zend\Form\Form
object to be filtered and validated, so, it will be applied in the routes after the Page handler registration.
First, we check whether the request method is POST and has the media type is “multipart/form-data”, then applied POST and FILES data into the form instance. We can save form post and file data, a form filtered/validated data (“form->getData()”), and its form error messages into session. Next, we redirect to current page with status code = 303.
As I explored, the easiest way to work with zend-form
for post and files data is by using zend-psr7bridge
for it like below:
use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Session\SessionMiddleware; use Zend\Psr7Bridge\Psr7ServerRequest; $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $zendRequest = Psr7ServerRequest::toZend($request); $zendRequestHeaders = $zendRequest->getHeaders(); $isMultiPart = $zendRequestHeaders->has('Content-type') ? $zendRequestHeaders->get('Content-type')->getMediaType() === 'multipart/form-data' : false; if ($request->getMethod() === 'POST' && $isMultiPart === true) { $postAndFileData = \array_merge_recursive( $zendRequest->getPost()->toArray(), $zendRequest->getFiles()->toArray() ); $session->set('post_and_file_data', $postAndFileData); $form->setData($postAndFileData); if ($form->isValid()) { $session->set('form_data', $form->getData()); } if ($messages = $form->getMessages()) { $session->set('form_errors', $messages); } return new RedirectResponse($request->getUri(), 303); }
On next flow, we can check if the session has “post_and_file_data” key, set form error messages when session has “form_errors” key, and return Response.
if ($session->has('post_and_file_data')) { $form->setData($session->get('post_and_file_data')); $session->unset('post_and_file_data'); if ($session->has('form_errors')) { $form->setMessages($session->get('form_errors')); $session->unset('form_errors'); } return new Response(); }
Above, we didn’t use the “form_data” session as the form data may be used in the page handler, so, we can handle it with keep the “form_data” as “form->getData()” result once by return Response above, and remove it when it already hit in next refresh, so next flow can be:
if ($session->has('form_data')) { $session->unset('form_data'); } return new Response();
The complete middleware class can be as follow:
<?php // src/App/Middleware/FilePrgMiddleware.php declare(strict_types=1); namespace App\Middleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Diactoros\Response; use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Session\SessionMiddleware; use Zend\Psr7Bridge\Psr7ServerRequest; class FilePrgMiddleware implements MiddlewareInterface { public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $zendRequest = Psr7ServerRequest::toZend($request); $zendRequestHeaders = $zendRequest->getHeaders(); $isMultiPart = $zendRequestHeaders->has('Content-type') ? $zendRequestHeaders->get('Content-type')->getMediaType() === 'multipart/form-data' : false; $form = $request->getAttribute('form'); if ($request->getMethod() === 'POST' && $isMultiPart === true) { $postAndFileData = \array_merge_recursive( $zendRequest->getPost()->toArray(), $zendRequest->getFiles()->toArray() ); $session->set('post_and_file_data', $postAndFileData); $form->setData($postAndFileData); if ($form->isValid()) { $session->set('form_data', $form->getData()); } if ($messages = $form->getMessages()) { $session->set('form_errors', $messages); } return new RedirectResponse($request->getUri(), 303); } if ($session->has('post_and_file_data')) { $form->setData($session->get('post_and_file_data')); $session->unset('post_and_file_data'); if ($session->has('form_errors')) { $form->setMessages($session->get('form_errors')); $session->unset('form_errors'); } return new Response(); } if ($session->has('form_data')) { $session->unset('form_data'); } return new Response(); } }
FilePrgMiddleware Service Registration
We can register the FilePrgMiddleware at src/App/ConfigProvider under getDependencies() function:
// src/App/ConfigProvider.php use Zend\ServiceManager\Factory\InvokableFactory; // ... public function getDependencies() : array { return [ 'factories' => [ // ... Middleware\FilePrgMiddleware::class => InvokableFactory::class, ], ]; }
The Upload Form and Its Page
Time for usage. First, we can create an upload form, for example like below:
<?php namespace App\Form; use Zend\Form\Element\File; use Zend\Form\Element\Submit; use Zend\Form\Form; use Zend\InputFilter\InputFilterProviderInterface; use Zend\Validator\File\MimeType; use Zend\Validator\File\Size; use Zend\Filter\File\RenameUpload; class UploadForm extends Form implements InputFilterProviderInterface { public function __construct() { parent::__construct('upload-form'); $this->init(); } public function init() { $this->add([ 'type' => File::class, 'name' => 'filename', 'options' => [ 'label' => 'File upload', ], ]); $this->add([ 'name' => 'submit', 'type' => Submit::class, 'attributes' => [ 'value' => 'Submit', ], ]); } public function getInputFilterSpecification() { return [ [ 'name' => 'filename', 'required' => true, 'filters' => [ [ 'name' => RenameUpload::class, 'options' => [ 'target' => \getcwd() . '/public/uploads', 'use_upload_extension' => true, 'use_upload_name' => true, 'overwrite' => true, 'randomize' => false, ], ], ], 'validators' => [ [ 'name' => Size::class, 'options' => [ 'max' => '10MB', ], ], [ 'name' => MimeType::class, 'options' => [ 'mimeType' => [ 'image/jpg', 'image/jpeg', 'image/png', ], ] ] ], ], ]; } }
To ensure the file upload correctly, I applied filter RenameUpload
to move it to public/uploads
directory. We can create an uploads directory by run command:
$ mkdir -p public/uploads && chmod 755 public/uploads
Next, we can create an Upload Page Handler :
<?php declare(strict_types=1); namespace App\Handler; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Expressive\Template; class UploadPageHandler implements MiddlewareInterface { private $template; public function __construct(Template\TemplateRendererInterface $template) { $this->template = $template; } public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { $data = []; return new HtmlResponse($this->template->render('app::upload-page', $data)); } }
We need template in templates/app/upload-page.phtml
for it. For view, we can create a file:
$ touch templates/app/upload-page.phtml
and write with the form helper:
<?php // templates/app/upload-page.phtml echo $this->form($form);
As above UploadPageHandler require an TemplateRendererInterface
service, we need factory for it, as follow:
<?php declare(strict_types=1); namespace App\Handler; use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Zend\Expressive\Template\TemplateRendererInterface; class UploadPageHandlerFactory { public function __invoke(ContainerInterface $container) : MiddlewareInterface { $template = $container->get(TemplateRendererInterface::class); return new UploadPageHandler($template); } }
We can register the UploadPageHandler at src/App/ConfigProvider under getDependencies() function:
// src/App/ConfigProvider.php use Zend\ServiceManager\Factory\InvokableFactory; // ... public function getDependencies() : array { return [ 'factories' => [ // ... Handler\UploadPageHandler::class => Handler\UploadPageHandlerFactory::class, // ... ], ]; }
To make it accessible, we need to register its routing, for example, at config/routes.php
:
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { // ... $app->route( '/upload', [ App\Handler\UploadPageHandler::class, App\Middleware\FilePrgMiddleware::class, ], [ 'GET', 'POST' ], 'upload' ); // ... };
So, we will get the following display when access “/upload” page:
Using The FilePrg in UploadPageHandler
We can set the Zend\Form\Form
instance into request attribute and call it handler, on form is valid, we can apply after its response check not a RedirectResponse
.
use App\Form\UploadForm; use Zend\Expressive\Session\SessionMiddleware; // ... public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { $form = new UploadForm(); $request = $request->withAttribute('form', $form); $response = $handler->handle($request); if ($response instanceof RedirectResponse) { return $response; } $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($session->has('form_data')) { $formData = $session->get('form_data'); // we can returns RedirectResponse to "success upload" page, // process form data, // set view variable to allow display that the upload success, // etc } $data = ['form' => $form]; return new HtmlResponse($this->template->render('app::upload-page', $data)); } // ...
When we get invalid input, we will get the following form page like below:
When we refresh it, it will normal refresh and won’t call a duplicate form submission.
That’s it ;).
leave a comment