Using PSR-7 UploadedFileInterface with zend-filter’s File and zend-validator’s File in zend-form instance
The latest zend-filter:^2.9 and zend-validator:^2.12 have full support for PSR-7 UploadedFileInterface handling. If we are using it with zend-form instance:
use Zend\Diactoros\StreamFactory; use Zend\Diactoros\UploadedFileFactory; use Zend\Filter\File\RenameUpload; use Zend\Validator\File\Size; // ... 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' => true, 'stream_factory' => new StreamFactory(), 'upload_file_factory' => new UploadedFileFactory(), ], ], ], 'validators' => [ [ 'name' => Size::class, 'options' => [ 'max' => '10MB', ], ], ], ], ]; } // ...
The only config that needed to be changed is in the filters: the stream_factory
and upload_file_factory
config with provide objects of class that implements Psr\Http\Message\StreamFactoryInterface
and Psr\Http\UploadedFileFactoryInterface
, which if we are using zend-diactoros
, we can use Zend\Diactoros\StreamFactory
and Zend\Diactoros\UploadedFileFactory
.
For Form set data, we can now do the following:
use Psr\Http\Message\ServerRequestInterface; // ... // $request is an object of class implements ServerRequestInterface $postAndFileData = \array_merge_recursive( $request->getParsedBody(), $request->getUploadedFiles() ); // ...
On get data, we have different result from normal Zend\Http\Phpenvironment\Request
. If we are using normal Zend\Http\Phpenvironment\Request
, we normally get the following data:
array (size=1) 'filename' => array (size=5) 'name' => string 'Untitled-1.png' (length=14) 'type' => string 'image/png' (length=9) 'size' => int 761087 'tmp_name' => string '/path/to/public/uploads/Untitled-1_5c51c49e1d0855_97287659.png' (length=103) 'error' => int 0
With the new parameters, the form data will be:
array (size=1) 'filename' => object(Zend\Diactoros\UploadedFile)[966] private 'clientFilename' => string 'Untitled-1.png' (length=14) private 'clientMediaType' => string 'image/png' (length=9) private 'error' => int 0 private 'file' => null private 'moved' => boolean false private 'size' => int 761087 private 'stream' => object(Zend\Diactoros\Stream)[967] protected 'resource' => resource(12, stream) protected 'stream' => string '/path/to/public/uploads/Untitled-1_5c51c49e1d0855_97287659.png' (length=103)
We can consume the file uploaded data from Zend\Diactoros\UploadedFile
‘s functions, or we want a array
data, we can loop the fields recursively and convert to array
data whenever the value is instanceof Psr\Http\Message\UploadedFileInterface
, eg:
use Psr\Http\Message\UploadedFileInterface; // ... $data = $form->getData(); \array_walk_recursive($data, function (& $value) { if ($value instanceof UploadedFileInterface) { $value = [ 'name' => $value->getClientFilename(), 'type' => $value->getClientMediaType(), 'size' => $value->getSize(), 'tmp_name' => $value->getStream()->getMetadata('uri'), 'error' => $value->getError(), ]; } }); // ...
The data are now completely an array so we are safe to use in another use case, eg: save to temporary session for File PRG use case.
Using zend-expressive-session-cache as PSR-6 session persistence adapter in Expressive 3
zend-expressive-session-cache is a PSR-6 session persistence adapter for zend-expressive-session that can be used in Expressive 3. In current post, I will provide the use case of it with apcu for cache adapter with zend-cache for cache item pool.
Let’s start with create new project from skeleton with the following command:
➜ composer create-project \ zendframework/zend-expressive-skeleton \ expressive3-cache-tutorial
After the skeleton installed, we can require the zendframework/zend-expressive-session-cache
and zend-cache
with the following command:
➜ cd expressive3-cache-tutorial ➜ composer require \ zendframework/zend-expressive-session-cache \ zendframework/zend-cache
Now, ensure config/config.php
has the following ConfigProvider
s registered:
<?php // config/config.php $aggregator = new ConfigAggregator([ // ... \Zend\Cache\ConfigProvider::class, \Zend\Expressive\Session\Cache\ConfigProvider::class, \Zend\Expressive\Session\ConfigProvider::class, // ... ], $cacheConfig['config_cache_path']);
For apcu
adapter, we need the apcu
extension to be installed, we can install via pecl
like the following:
➜ sudo pecl install apcu
After it installed, we need to set apc.use_request_time = 0
in php.ini like the following:
# your php.ini apc.use_request_time = 0
For Cache Item Pool service, we can create a factory for it, for example, like the following:
<?php // src/App/Cache/CacheItemPoolFactory.php declare(strict_types=1); namespace App\Cache; use Psr\Cache\CacheItemPoolInterface; use Psr\Container\ContainerInterface; use Zend\Cache\Psr\CacheItemPool\CacheItemPoolDecorator; use Zend\Cache\StorageFactory; final class CacheItemPoolFactory { public function __invoke(ContainerInterface $container) : CacheItemPoolInterface { $storage = StorageFactory::factory([ 'adapter' => [ 'name' => 'apcu', ], ]); return new CacheItemPoolDecorator($storage); } }
We can register then the cache pool service and make alias of SessionPersistenceInterface
with CacheSessionPersistence
:
<?php // config/autoload/dependencies.global.php use App\Cache\CacheItemPoolFactory; use Psr\Cache\CacheItemPoolInterface; use Zend\Expressive\Session\Cache\CacheSessionPersistence; use Zend\Expressive\Session\SessionPersistenceInterface; return [ // ... 'aliases' => [ SessionPersistenceInterface::class => CacheSessionPersistence::class, ], 'factories' => [ CacheItemPoolInterface::class => CacheItemPoolFactory::class, ], // ... ];
If we need a custom configuration for zend-expressive-session-cache
, we can define at config/autoload/zend-expressive.global.php
:
<?php // config/autoload/zend-expressive.global.php use Psr\Cache\CacheItemPoolInterface; return [ // ... 'zend-expressive-session-cache' => [ 'cache_item_pool_service' => CacheItemPoolInterface::class, 'cookie_name' => 'PHPSESSION', 'cookie_path' => '/', 'cache_limiter' => 'nocache', 'cache_expire' => 10800, 'last_modified' => null, ], // ... ];
To start the session, we can apply Zend\Expressive\Session\SessionMiddleware
into config/pipeline.php
before Zend\Expressive\Router\Middleware\RouteMiddleware
registration:
<?php use Zend\Expressive\Router\Middleware\RouteMiddleware; use Zend\Expressive\Session\SessionMiddleware; // ... $app->pipe(SessionMiddleware::class); $app->pipe(RouteMiddleware::class); // ...
The preparation done! Now, we can use it by consume it from via SessionMiddleware, for example, we need to generate “csrf” token when rendering the form, like I written at the CSRF usage in Expressive post, we can just use the session as is, and the cache will be used as session persistence:
<?php // src/Handler/LoginPageHandler.php use Zend\Expressive\Session\SessionInterface; use Zend\Expressive\Csrf\SessionCsrfGuard; use Zend\Expressive\Session\SessionMiddleware; use Zend\Expressive\Csrf\CsrfMiddleware; // ... private function getToken(SessionInterface $session, SessionCsrfGuard $guard) { if (! $session->has('__csrf')) { return $guard->generateToken(); } return $session->get('__csrf'); } public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { $guard = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE); $loginForm = new LoginForm($guard); $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $token = $this->getToken($session, $guard); // ... } // ...
That’s it!
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 ;).
Create Middleware for Post/Redirect/Get in Expressive 3
Yesterday, we already explore about CSRF usage in Expressive 3 using zend-expressive-csrf component, which I gave the sample inside a Login Page which utilize both GET and POST in single page, which we need to tweak the csrf regeneration to ensure next retry will use newly regenerated token. As I noted in the last paragraph, the better usage is by using PRG ( Post/Redirect/Get ) or delegate to separate middleware and redirect back to its page. Today, we will explore how to create Post/Redirect/Get Middleware for Expressive 3.
Requirements
If you already followed my 4 previous expressive posts, all requirements already applied.
The New Middleware
We are going to create a middleware for our application, for example, we name it PrgMiddleware
, placed at src/App/Middleware
. I will explain part by part.
First, we check whether the request method is POST, then save the POST data into session with new key, eg: ‘post_data’, then redirect to current page with status code = 303.
use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Session\SessionMiddleware; // ... $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($request->getMethod() === 'POST') { $session->set('post_data', $request->getParsedBody()); return new RedirectResponse($request->getUri(), 303); } // ...
On next flow, we can check if the session has ‘post_data’ key:
if ($session->has('post_data')) { $post = $session->get('post_data'); $session->unset('post_data'); $request = $request->withMethod('POST'); $request = $request->withParsedBody($post); } // ...
As in authentication process, we use Zend\Expressive\Authentication\Session\PhpSession::authenticate()
which require request method to be POST and use its parsed body to be used for authentication, we need to set method to POST with parsed body of saved session with key ‘post_data’ which immediately removed above.
Lastly, we return the $handler->handle($request) as whenever response it used:
return $handler->handle($request);
The complete middleware class can be as follows:
<?php // src/App/Middleware/PrgMiddleware.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\RedirectResponse; use Zend\Expressive\Session\SessionMiddleware; class PrgMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($request->getMethod() === 'POST') { $session->set('post_data', $request->getParsedBody()); return new RedirectResponse($request->getUri(), 303); } if ($session->has('post_data')) { $post = $session->get('post_data'); $session->unset('post_data'); $request = $request->withMethod('POST'); $request = $request->withParsedBody($post); } return $handler->handle($request); } }
PrgMiddleware Service Registration
We can register the PrgMiddleware
at src/App/ConfigProvider
under getDependencies()
function:
// src/App/ConfigProvider.php use Zend\ServiceManager\Factory\InvokableFactory; // ... public function getDependencies() : array { return [ 'factories' => [ // ... Middleware\PrgMiddleware::class => InvokableFactory::class, ], ]; }
Now, we can register it inside config/routes.php
for login route after csrf middleware registrations:
// config/routes.php $app->route('/login', [ // csrf \Zend\Expressive\Csrf\CsrfMiddleware::class, // prg middleware App\Middleware\PrgMiddleware::class, // login page App\Handler\LoginPageHandler::class, // authentication middleware \Zend\Expressive\Authentication\AuthenticationMiddleware::class, ], ['GET', 'POST'],'login');
To make clear “post_data” executed, place the App\Middleware\PrgMiddleware::class
at the very first in target redirect, eg: home route:
$app->get('/', [ // prg handling App\Middleware\PrgMiddleware::class, \Zend\Expressive\Authentication\AuthenticationMiddleware::class, App\Handler\HomePageHandler::class, ], 'home');
Using The Prg in LoginPageHandler
We can check if it is a PRG using the parsed body and use it to fill the form data:
// src/App/Handler/LoginPageHandler.php // ... public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $guard = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE); $loginForm = new LoginForm($guard); $prg = $request->getParsedBody(); if ($prg) { $loginForm->setData($prg); if ($loginForm->isValid()) { // ... } } }
By above, we just need to generate the token once. The complete LoginPageHandler
can be as follow:
<?php // src/App/Handler/LoginPageHandler.php declare(strict_types=1); namespace App\Handler; use App\Form\LoginForm; 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\Diactoros\Response\RedirectResponse; use Zend\Expressive\Csrf\CsrfMiddleware; use Zend\Expressive\Flash\FlashMessageMiddleware; use Zend\Expressive\Session\SessionMiddleware; use Zend\Expressive\Template\TemplateRendererInterface; class LoginPageHandler implements MiddlewareInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $guard = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE); $loginForm = new LoginForm($guard); $prg = $request->getParsedBody(); if ($prg) { $loginForm->setData($prg); if ($loginForm->isValid()) { $response = $handler->handle($request); $flashMessages = $request->getAttribute(FlashMessageMiddleware::FLASH_ATTRIBUTE); if ($response->getStatusCode() !== 302) { $flashMessages->flash('message', 'You are succesfully authenticated'); return new RedirectResponse('/'); } $flashMessages->flash('message', 'Login Failure, please try again'); return new RedirectResponse('/login'); } } $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $token = $guard->generateToken(); return new HtmlResponse( $this->template->render('app::login-page', [ 'form' => $loginForm, 'token' => $token, ]) ); } }
In view, we can just show the form:
<?php // templates/app/login-page.phtml $form->get('csrf')->setValue($token); $form->prepare(); echo $this->form($form);
Done 😉
Using zend-expressive-csrf with zend-form in Expressive 3
If you followed my post about authentication and authorization posts with Expressive 3, this time, I write another session related post for securing request, which uses zend-expressive-csrf with zend-form.
Setup
We already installed session and form components, so, we can just require the zend-expressive-csrf component via command:
$ composer require \ zendframework/zend-expressive-csrf:^1.0.0alpha1
On Login route, we register the csrf middleware before the LoginPageHandler
:
// config/routes.php $app->route('/login', [ // csrf middleware \Zend\Expressive\Csrf\CsrfMiddleware::class, App\Handler\LoginPageHandler::class, \Zend\Expressive\Authentication\AuthenticationMiddleware::class, ], ['GET', 'POST'],'login');
Csrf Validation in Form
We can inject the LoginForm
with Zend\Expressive\Csrf\SessionCsrfGuard
instance which next we use it to validate csrf token.
// src/App/Form/LoginForm.php use Zend\Expressive\Csrf\SessionCsrfGuard; // ... private $guard; public function __construct(SessionCsrfGuard $guard) { parent::__construct('login-form'); $this->guard = $guard; $this->init(); } // ...
In above __construct()
, I call init()
immediatelly on __construct()
to add form elements on form creation, as we are going to inject the Form with Zend\Expressive\Csrf\SessionCsrfGuard
instance on LoginPageHandler
which pulled from request object.
We then can add new element, for example, named: csrf
as hidden input, as follows:
// src/App/Form/LoginForm.php use Zend\Form\Element\Hidden; // ... public function init() { $this->add([ 'type' => Hidden::class, 'name' => 'csrf', ]); //... } // ...
To validate it, we register the csrf
validation token by Zend\Expressive\Csrf\SessionCsrfGuard
in getInputFilterSpecification()
:
// src/App/Form/LoginForm.php // ... public function getInputFilterSpecification() { return [ [ 'name' => 'csrf', 'required' => true, 'validators' => [ [ 'name' => 'callback', 'options' => [ 'callback' => function ($value) { return $this->guard->validateToken($value); }, 'messages' => [ 'callbackValue' => 'The form submitted did not originate from the expected site' ], ], ] ], ], // ... ]; }
Above, we supply a callback validator (you can create a special validator just for it) with call the Zend\Expressive\Csrf\SessionCsrfGuard::validateToken($value)
which returns true when valid, and false when invalid. On invalid token, we will get callbackValue
message key which we can customize its value.
The LoginPageHandler
As the Zend\Expressive\Csrf\SessionCsrfGuard
instance will be injected at the LoginPageHandler itself, we can remove the LoginForm
from LoginPageHandler
dependency, so, we just need to have TemplateRendererInterface
:
// src/Handler/LoginPageHandler.php // ... public function __construct(TemplateRendererInterface $template) { $this->template = $template; } // ...
The factory for it will remove LoginForm
dependency as well, so just the template:
// src/Handler/LoginPageFactory.php // ... public function __invoke(ContainerInterface $container) : MiddlewareInterface { $template = $container->get(TemplateRendererInterface::class); return new LoginPageHandler($template); }
As the token csrf is a one time token, and we use a single page for both GET (show form) and POST (authenticate), we can create a function to get generated token to be called before POST method check, and when authentication failure or the form is invalid to ensure next retry will use newly generated token:
// src/Handler/LoginPageHandler.php use Zend\Expressive\Session\SessionInterface; use Zend\Expressive\Csrf\SessionCsrfGuard; // ... private function getToken(SessionInterface $session, SessionCsrfGuard $guard) { if (! $session->has('__csrf')) { return $guard->generateToken(); } return $session->get('__csrf'); } // ...
Now, the LoginForm
and the token
can be created during process()
method:
// src/Handler/LoginPageHandler.php use Zend\Expressive\Session\SessionMiddleware; use Zend\Expressive\Csrf\CsrfMiddleware; use Zend\Expressive\Csrf\SessionCsrfGuard; // ... public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $guard = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE); $loginForm = new LoginForm($guard); $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); $token = $this->getToken($session, $guard); // ... }
On after form is valid check, the $token
need to be re-generated
:
// src/Handler/LoginPageHandler.php // ... public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { // ... if ($loginForm->isValid()) { // ... } // re-new token on failure login or not valid form $token = $this->getToken($session, $guard); }
The $token
will be consumed by view and can be set via template render:
// src/Handler/LoginPageHandler.php // ... public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { // ... return new HtmlResponse( $this->template->render('app::login-page', [ 'form' => $loginForm, 'error' => $error, 'token' => $token, ]) ); }
The View
Finally, we can set the csrf
element value in view via form object, as follows:
<?php // templates/app/login-page.phtml echo $error; $form->get('csrf')->setValue($token); $form->prepare(); echo $this->form($form);
All done, now, when the request is not using the session generated token, it will show form error:
The form submitted did not originate from the expected site
like the following screenshot:
Better Practice and Possible Refactor
For real application, it is better to use PRG or use another handler to handle it to be redirected back to the form when failure, so you don’t need to tweak the token regeneration as when form re-displayed again, it already in next request and we can just use the new token. I’ve written new post for create middleware for Post/Redirect/Get in Expressive 3 for it.
Using View Helper for Accessing zend-expressive-flash Messages in Expressive 3
If you already tried zend-expressive 3.0.0alpha3, and want to use flash message for it, you can use the new component: “zendframework/zend-expressive-flash”.
Setup
We need to require the component and its dependencies:
$ composer require zendframework/zend-expressive-session:^1.0.0 \ zendframework/zend-expressive-session-ext:^1.0.0 \ zendframework/zend-expressive-flash:^1.0.0
After it, we can ensure that the ConfigProvider
for above components registered at config/config.php
:
$aggregator = new ConfigAggregator([ // ... \Zend\Expressive\Session\ConfigProvider::class, \Zend\Expressive\Session\Ext\ConfigProvider::class, \Zend\Expressive\Flash\ConfigProvider::class, // ... ]);
To make the flash messages available in all pages, we need to register it in the pipeline before RouteMiddleware
piping:
use Zend\Expressive\Flash\FlashMessageMiddleware; use Zend\Expressive\Session\SessionMiddleware; // ... $app->pipe(SessionMiddleware::class); $app->pipe(FlashMessageMiddleware::class); $app->pipe(RouteMiddleware::class);
Get the Messages via View Helper
Most of the use cases, we are displaying the flash messages in the layout. If we are using zend-view
for the template engine, we can create view helper to access the flash messages. What we need is a Zend\Expressive\Flash\FlashMessages
instance with passes Zend\Expressive\Session\Session
which passes $_SESSION
into it if session status is active, as follows:
use Zend\Expressive\Session\Session; use Zend\Expressive\Flash\FlashMessages; // ... $session = session_status() === PHP_SESSION_ACTIVE ? $_SESSION : []; $flashMessages = FlashMessages::createFromSession(new Session($session));
The messages
is a private property “currentMessages” which currently no public function that returns it. We can wait for the PR for it to be merge or use it via ReflectionProperty
or Closure
which can be called by method:
$flashMessages->getFlashes();
Knowing that, we can create the view helper as follows:
<?php // src/App/View\Helper\Flash.php declare(strict_types=1); namespace App\View\Helper; use Zend\Expressive\Session\Session; use Zend\Expressive\Flash\FlashMessages; use Zend\View\Helper\AbstractHelper; class Flash extends AbstractHelper { public function __invoke() : array { $session = session_status() === PHP_SESSION_ACTIVE ? $_SESSION : []; $flashMessages = FlashMessages::createFromSession( new Session($session) ); return $flashMessages->getFlashes(); } }
and register it into view_helpers
config at src/App/ConfigProvider::__invoke()
:
// ... public function __invoke() : array { return [ 'dependencies' => $this->getDependencies(), 'templates' => $this->getTemplates(), // register additional view helpers 'view_helpers' => [ 'invokables' => [ 'flash' => View\Helper\Flash::class, ], ], ]; } // ...
Accessing Flash Messages in Layout
In layout, we can now access it via invoked flash
view helper with loop the messages to be displayed:
<?php if ($flashes = $this->flash()) :?> <ul> <?php foreach($flashes as $message) : ?> <li><?php echo $message; ?></li> <?php endforeach; ?> </ul> <?php endif; ?>
Give it a try
You can now create a flash messages in some page and access it in next request in the layout, check the documentation for its usage!
Create Authorization functionality in Expressive 3
So, yesterday, I already posted about Authentication part in “Create Login functionality in Expressive 3” post, so, it’s time for authorization part. If you didn’t read that, please read first.
We will use role
of user when deciding what access right of the user for accessed page. For example, we define role
field in users table with the following SQL:
expressive=# ALTER TABLE users ADD COLUMN role character varying(255) NOT NULL DEFAULT 'user'; ALTER TABLE
Ok, we have new column named role
with default value = ‘user’. So, we have existing data with role = ‘user’ :
expressive=# SELECT * FROM users; username | password | role ------------+--------------------------------------------------------------+------ samsonasik | $2a$06$uPvOqYT7fQFP5EYR2jzVrOefwU03GltjAHt.q8l1vWXmkTIbeBcHe | user
Let’s add another user with different role, eg: ‘admin’, as follows:
expressive=# INSERT INTO users(username, password, role) VALUES('admin', crypt('123456', gen_salt('bf')), 'admin'); INSERT 0 1 expressive=# SELECT * FROM users; username | password | role ------------+--------------------------------------------------------------+------- samsonasik | $2a$06$uPvOqYT7fQFP5EYR2jzVrOefwU03GltjAHt.q8l1vWXmkTIbeBcHe | user admin | $2a$06$0pLYG/GVQOL6v9tLmjBB..cvUIk0vBdcDM8aV373AVO3ve9MdSbom | admin (2 rows)
Perfect, now, we need to add sql_get_roles
config under [‘authentication’][‘pdo’] to get role from users table, we can add at our config/autoload/local.php
:
<?php // config/autoload/local.php return [ 'authentication' => [ 'pdo' => [ // ... 'sql_get_roles' => 'SELECT role FROM users WHERE username = :identity' ], // ... ], ];
When we login and var_dump the session data, we will get the following array value:
// var_dump($session->get(UserInterface::class)); array (size=2) 'username' => string 'samsonasik' (length=10) 'roles' => array (size=1) 0 => string 'user' (length=4)
Until here we are doing great!
To differentiate access page, let’s create a different page for admin
only, for example: AdminPageHandler
:
<?php declare(strict_types=1); namespace App\Handler; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Expressive\Template\TemplateRendererInterface; class AdminPageHandler implements RequestHandlerInterface { private $template; public function __construct(TemplateRendererInterface $template) { $this->template = $template; } public function handle(ServerRequestInterface $request) : ResponseInterface { return new HtmlResponse($this->template->render('app::admin-page', [])); } }
with factory as follows:
<?php declare(strict_types=1); namespace App\Handler; use Psr\Container\ContainerInterface; use Psr\Http\Server\RequestHandlerInterface; use Zend\Expressive\Template\TemplateRendererInterface; class AdminPageFactory { public function __invoke(ContainerInterface $container) : RequestHandlerInterface { $template = $container->get(TemplateRendererInterface::class); return new AdminPageHandler($template); } }
We can register the AdminPageHandler
middleware at App\ConfigProvider::getDependencies()
:
<?php class ConfigProvider { public function getDependencies() : array { return [ 'invokables' => [ /**/ ], 'factories' => [ // ... Handler\AdminPageHandler::class => Handler\AdminPageFactory::class, ], ]; } }
Then let’s define route for it, eg: ‘/admin’:
// config/routes.php $app->route('/admin', [ \Zend\Expressive\Authentication\AuthenticationMiddleware::class, App\Handler\AdminPageHandler::class, ], ['GET'], 'admin');
The view can just show it that it is currently at admin
page:
<?php // templates/app/admin-page.phtml ?> Admin Page
So, if we logged in as role = user, and access `/admin’ page, we still can see the page.
Let’s authorize it!
First, we can add components for it, for example, we are going to use ACL, we can install expressive component for it via command:
$ composer require \ zendframework/zend-expressive-authorization:^1.0 \ zendframework/zend-expressive-authorization-acl:^1.0
It will install the following components:
* zendframework/zend-expressive-authorization
* zendframework/zend-permissions-acl
* zendframework/zend-expressive-authorization-acl
After they installed, ensure our config/config.php
has registered the following ConfigProvider
classes:
<?php // config/config.php $aggregator = new ConfigAggregator([ // ... \Zend\Expressive\Authorization\Acl\ConfigProvider::class, \Zend\Expressive\Authorization\ConfigProvider::class, // ... ]);
Then, we can map Zend\Expressive\Authorization\AuthorizationInterface::class
to Zend\Expressive\Authorization\Acl\ZendAcl::class
at config/autoload/dependencies.global.php
under alias
to use the ZendAcl
service :
<?php // config/autoload/dependencies.global.php return [ 'dependencies' => [ 'aliases' => [ // ... Zend\Expressive\Authorization\AuthorizationInterface::class => Zend\Expressive\Authorization\Acl\ZendAcl::class ], ], ];
Roles, Resources, and Rights definitions
We can define roles, resources, and rights under [‘zend-expressive-authorization-acl’] config, for example, at config/autoload/zend-expressive.global.php
:
<?php // config/autoload/zend-expressive.global.php return [ // ... 'zend-expressive-authorization-acl' => [ 'roles' => [ 'guest' => [], 'user' => ['guest'], 'admin' => ['user'], ], 'resources' => [ 'home', 'admin', 'login', 'logout', ], 'allow' => [ 'guest' => [ 'login', ], 'user' => [ 'logout', 'home', ], 'admin' => [ 'admin', ], ], ], // ... ];
I’m going to mark non-logged user with role = “guest”, “user” role will inherit all guest rights, and “admin” role inherit all user rights, that mean, admin can access what user can access, but not opposite.
The resources are route names that registered at config/routes.php
.
Authorization Process
To get ‘roles’ value, we have Zend\Expressive\Authorization\AuthorizationMiddleware
that checks from request attribute named Zend\Expressive\Authentication\UserInterface::class
, we can define at config/pipeline.php
before $app->pipe(DispatchMiddleware::class); with continue pipe the Zend\Expressive\Authorization\AuthorizationMiddleware
after it, as follow:
// config/pipeline.php $app->pipe(new class implements Psr\Http\Server\MiddlewareInterface{ public function process( Psr\Http\Message\ServerRequestInterface $request, Psr\Http\Server\RequestHandlerInterface $handler ) : Psr\Http\Message\ResponseInterface { $session = $request->getAttribute( Zend\Expressive\Session\SessionMiddleware::SESSION_ATTRIBUTE ); // No Session data if (! $session->has(Zend\Expressive\Authentication\UserInterface::class)) { $user = ''; $roles = ['guest']; $request = $request->withAttribute( Zend\Expressive\Authentication\UserInterface::class, new Zend\Expressive\Authentication\DefaultUser( $user, $roles ) ); $response = $handler->handle($request); if ($request->getUri()->getPath() === '/login' || $response->getStatusCode() !== 403) { return $response; } return new Zend\Diactoros\Response\RedirectResponse('/login'); } // at /login page, redirect to authenticated page if ($request->getUri()->getPath() === '/login') { return new Zend\Diactoros\Response\RedirectResponse('/'); } // define roles from DB $sessionData = $session->get(Zend\Expressive\Authentication\UserInterface::class); $request = $request->withAttribute( Zend\Expressive\Authentication\UserInterface::class, new Zend\Expressive\Authentication\DefaultUser( $sessionData['username'], $sessionData['roles'] ) ); return $handler->handle($request); } }); $app->pipe(\Zend\Expressive\Authorization\AuthorizationMiddleware::class); $app->pipe(DispatchMiddleware::class);
When we logged as user, but want to access “admin” resource, eg: “/admin”, we will get “403 Forbidden” :
How about integration with “404” page?
With current setup, when we go to 404 page, eg: “/404”, we will get the “Resource ” not found”. To make it work with the NotFoundHandler, we need to inject NotFoundHandler to our authorization check. Let’s make above authorization check to a dedicated class with inject the NotFoundHandler, as follow:
<?php 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\RedirectResponse; use Zend\Expressive\Authentication\DefaultUser; use Zend\Expressive\Authentication\UserInterface; use Zend\Expressive\Handler\NotFoundHandler; use Zend\Expressive\Router\RouteResult; use Zend\Expressive\Session\SessionMiddleware; class AuthorizationMiddleware implements MiddlewareInterface { private $notFoundHandler; private $redirect; public function __construct(NotFoundHandler $notFoundHandler, string $redirect) { $this->notFoundHandler = $notFoundHandler; $this->redirect = $redirect; } public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { // 404 check early $routeResult = $request->getAttribute(RouteResult::class); if ($routeResult->isFailure()) { return $this->notFoundHandler->handle($request, $handler); } $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); // No Session data if (! $session->has(UserInterface::class)) { $user = ''; $roles = ['guest']; $request = $request->withAttribute( UserInterface::class, new DefaultUser( $user, $roles ) ); $response = $handler->handle($request); if ($request->getUri()->getPath() === $this->redirect || $response->getStatusCode() !== 403) { return $response; } return new RedirectResponse($this->redirect); } // at /login page, redirect to authenticated page if ($request->getUri()->getPath() === $this->redirect) { return new RedirectResponse('/'); } // define roles from DB $sessionData = $session->get(UserInterface::class); $request = $request->withAttribute( UserInterface::class, new DefaultUser( $sessionData['username'], $sessionData['roles'] ) ); return $handler->handle($request); } }
With above “$routeResult->isFailure()” check in line 30 above, we return 404 page early before check against authorization.
For service creation, we can create a AuthorizationMiddlewareFactory
for it:
<?php declare(strict_types=1); namespace App\Middleware; use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Zend\Expressive\Handler\NotFoundHandler; class AuthorizationMiddlewareFactory { public function __invoke(ContainerInterface $container) : MiddlewareInterface { $notFoundHandler = $container->get(NotFoundHandler::class); $redirect = $container->get('config')['authentication']['redirect']; return new AuthorizationMiddleware($notFoundHandler, $redirect); } }
Now, we can register as a service for the AuthorizationMiddleware
in the ConfigProvider class:
<?php class ConfigProvider { public function getDependencies() : array { return [ 'invokables' => [ /**/ ], 'factories' => [ // ... Middleware\AuthorizationMiddleware::class => Middleware\AuthorizationMiddlewareFactory::class, ], ]; } }
Now, in the pipeline, we have a cleaner, and with 404 handler integration:
$app->pipe(\Zend\Expressive\Session\SessionMiddleware::class); $app->pipe(RouteMiddleware::class); // ... $app->pipe(\App\Middleware\AuthorizationMiddleware::class); $app->pipe(\Zend\Expressive\Authorization\AuthorizationMiddleware::class); $app->pipe(DispatchMiddleware::class)
That’s it ;).
Create Login functionality in Expressive 3
Zend Expressive 3 is not released yet released, and expressive session related components are in active development ready to use. However, we already can give them a try.
Use case
For example, we need simple login functionalities:
- Login Form
- Authentication process, read from DB
- Save authenticated value to Session
Setup
First, we can install the Zend Expressive 3 skeleton with the following command:
$ composer create-project "zendframework/zend-expressive-skeleton:^3.0.0" expressive3
Important Notes:
When prompted with service container requirement, choose zend-servicemanager
When prompted with template engine requirement, choose zend-view
There are components that can be installed via command:
$ cd expressive3 $ composer require \ zendframework/zend-form:^2.11 \ zendframework/zend-i18n:^2.7 \ zendframework/zend-expressive-authentication:^1.0 \ zendframework/zend-expressive-authentication-session:^1.0 \ zendframework/zend-expressive-session:^1.0 \ zendframework/zend-expressive-session-ext:^1.0
After above components installed, ensure that your config/config.php
injected with ConfigProvider
like below:
<?php // config/config.php $aggregator = new ConfigAggregator([ // ... form requirements \Zend\I18n\ConfigProvider::class, \Zend\Form\ConfigProvider::class, \Zend\InputFilter\ConfigProvider::class, \Zend\Filter\ConfigProvider::class, \Zend\Hydrator\ConfigProvider::class, // ... // ... auth requirements \Zend\Expressive\Authentication\ConfigProvider::class, \Zend\Expressive\Authentication\Session\ConfigProvider::class, \Zend\Expressive\Session\ConfigProvider::class, \Zend\Expressive\Session\Ext\ConfigProvider::class, // ... ];
we can first setup database data, in this case, I tried with Postgresql:
$ createdb -Udeveloper expressive Password: $ psql -Udeveloper expressive Password for user developer: psql (10.1) Type "help" for help. expressive=# CREATE TABLE users(username character varying(255) PRIMARY KEY NOT NULL, password text NOT NULL); CREATE TABLE expressive=# CREATE EXTENSION pgcrypto; CREATE EXTENSION expressive=# INSERT INTO users(username, password) VALUES('samsonasik', crypt('123456', gen_salt('bf'))); INSERT 0 1
Above, I create database named “expressive”, create table named “users” with username and password field, insert sample data with pgcrypto extension for create hashed password of 123456 using blowfish.
Now, we can setup the authentication configuration at config/autoload/local.php
as follows:
<?php // config/autoload/local.php return [ 'authentication' => [ 'pdo' => [ 'dsn' => 'pgsql:host=localhost;port=5432;dbname=expressive;user=developer;password=xxxxx', 'table' => 'users', 'field' => [ 'identity' => 'username', 'password' => 'password', ], ], 'username' => 'username', 'password' => 'password', 'redirect' => '/login', ], ];
Then, we can map Zend\Expressive\Authentication\UserRepositoryInterface::class
to Zend\Expressive\Authentication\UserRepository\PdoDatabase::class
under alias
and register Zend\Expressive\Authentication\AuthenticationInterface::class
under factories
config at config/autoload/dependencies.global.php
:
<?php // config/autoload/dependencies.global.php return [ 'dependencies' => [ 'aliases' => [ // ... Zend\Expressive\Authentication\UserRepositoryInterface::class => Zend\Expressive\Authentication\UserRepository\PdoDatabase::class ], 'factories' => [ // ... Zend\Expressive\Authentication\AuthenticationInterface::class => Zend\Expressive\Authentication\Session\PhpSessionFactory::class, ], // ... ], ];
For Session operations, we need Zend\Expressive\Session\SessionMiddleware
middleware before routing middleware, so, in config/pipeline.php
, we call pipe on it before $app->pipe(RouteMiddleware::class);:
// ... $app->pipe(\Zend\Expressive\Session\SessionMiddleware::class); // Register the routing middleware in the middleware pipeline $app->pipe(RouteMiddleware::class); // ...
as example: we want to redirect non-logged user to /login
page, eg: at home page (/), we can register “home” routes config:
<?php // config/routes.php $app->route('/', [ \Zend\Expressive\Authentication\AuthenticationMiddleware::class, App\Handler\HomePageHandler::class, ], ['GET'], 'home');
Now run the php -S command:
$ php -S localhost:8080 -t public
When access ‘/’ page via localhost:8080, we should be redirected to /login
page which currently a 404 page, nice!
Login Page
First, we create a LoginForm
with username
and password
field like the following:
<?php // src/App/Form/LoginForm.php declare(strict_types=1); namespace App\Form; use Zend\Form\Element\Password; use Zend\Form\Element\Text; use Zend\Form\Form; use Zend\InputFilter\InputFilterProviderInterface; class LoginForm extends Form implements InputFilterProviderInterface { public function __construct() { parent::__construct('login-form'); } public function init() { $this->add([ 'type' => Text::class, 'name' => 'username', 'options' => [ 'label' => 'Username', ], ]); $this->add([ 'type' => Password::class, 'name' => 'password', 'options' => [ 'label' => 'Password', ], ]); $this->add([ 'name' => 'Login', 'type' => 'submit', 'attributes' => [ 'value' => 'Login', ], ]); } public function getInputFilterSpecification() { return [ [ 'name' => 'username', 'required' => true, 'filters' => [ ['name' => 'StripTags'], ['name' => 'StringTrim'], ], ], [ 'name' => 'password', 'required' => true, 'filters' => [ ['name' => 'StripTags'], ['name' => 'StringTrim'], ], ], ]; } }
We then can create a login page handler with inject it with login form with the following factory:
<?php // src/App/Handler/LoginPageFactory.php declare(strict_types=1); namespace App\Handler; use App\Form\LoginForm; use Psr\Http\Server\MiddlewareInterface; use Psr\Container\ContainerInterface; use Zend\Expressive\Template\TemplateRendererInterface; use Zend\Form\FormElementManager; class LoginPageFactory { public function __invoke(ContainerInterface $container) : MiddlewareInterface { $template = $container->get(TemplateRendererInterface::class); $loginForm = $container->get(FormElementManager::class) ->get(LoginForm::class); return new LoginPageHandler($template, $loginForm); } }
The LoginPageHandler
itself can be initialized with :
<?php // src/App/Handler/LoginPageHandler.php declare(strict_types=1); namespace App\Handler; use App\Form\LoginForm; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Diactoros\Response\HtmlResponse; use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Authentication\UserInterface; use Zend\Expressive\Session\SessionMiddleware; use Zend\Expressive\Template\TemplateRendererInterface; class LoginPageHandler implements MiddlewareInterface { private $template; private $loginForm; public function __construct( TemplateRendererInterface $template, LoginForm $loginForm ) { $this->template = $template; $this->loginForm = $loginForm; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($session->has(UserInterface::class)) { return new RedirectResponse('/'); } $error = ''; // handle authentication here Next return new HtmlResponse( $this->template->render('app::login-page', [ 'form' => $this->loginForm, 'error' => $error, ]) ); } }
Above, we redirect to ‘/’ page when there is a session data as it already authenticated check. We are going to add authentication process next.
The Login form can be as simple as the following:
<?php // templates/app/login-page.phtml echo $error; $form->prepare(); echo $this->form($form);
We can register the LoginPageHandler
at App\ConfigProvider::getDependencies()
config:
<?php // src/App/ConfigProvider.php class ConfigProvider { public function getDependencies() : array { return [ 'invokables' => [ /**/ ], 'factories' => [ // ... Handler\LoginPageHandler::class => Handler\LoginPageFactory::class, ], ]; } }
The routing can be registered as follows with add \Zend\Expressive\Authentication\AuthenticationMiddleware::class
for next middleware:
// config/routes.php // ... $app->route('/login', [ App\Handler\LoginPageHandler::class, // for authentication next handling \Zend\Expressive\Authentication\AuthenticationMiddleware::class, ], ['GET', 'POST'],'login');
Above, we allow ‘GET’ and ‘POST’ in same ‘/login’ page.
Authentication process
Time for authentication process, we utilize Zend\Expressive\Authentication\AuthenticationMiddleware
class that registered at the last entry at the /login
route, we can accomodate it after check of form is valid
<?php // src/App/Handler/LoginPageHandler.php class LoginPageHandler implements MiddlewareInterface { // ... public function __construct( TemplateRendererInterface $template, LoginForm $loginForm) { /* */ } public function process( ServerRequestInterface $request, RequestHandlerInterface $handler ) : ResponseInterface { // ....... $error = ''; if ($request->getMethod() === 'POST') { $this->loginForm->setData($request->getParsedBody()); if ($this->loginForm->isValid()) { $response = $handler->handle($request); if ($response->getStatusCode() !== 302) { return new RedirectResponse('/'); } $error = 'Login Failure, please try again'; } } // ... return new HtmlResponse( $this->template->render('app::login-page', [ 'form' => $this->loginForm, 'error' => $error, ]) ); }
We call handle($request)
for next Zend\Expressive\Authentication\AuthenticationMiddleware
with:
$response = $handler->handle($request); if ($response->getStatusCode() !== 302) { return new RedirectResponse('/'); }
When status code is not 301 302, it authenticated and session filled, we can then redirect to page that need to be authenticated to be access. Failure authentication default behaviour has 301 302 status code which we can set config “redirect” in “authentication” config, on above code, I just want to show it in the login form that the login failure, so I set the $error variable value to “Login Failure, please try again”, so when login failure, it will got the error like the following:
That’s it ;).
How about logout ? We can use clear()
method from SessionMiddleware::SESSION_ATTRIBUTE
attribute like the following:
use Zend\Expressive\Authentication\UserInterface; use Zend\Expressive\Session\SessionMiddleware; class LogoutPageHandler implements RequestHandlerInterface { public function handle(ServerRequestInterface $request) : ResponseInterface { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if ($session->has(UserInterface::class)) { $session->clear(); } // ... } }
How about authorization part? You can read my next post about create authorization functionality in zend expressive 3
ErrorHeroModule : a Hero for Your Zend Mvc and Expressive Application
After > 1 year work with 52 releases, I think it is time to show off. Even you have 100% test coverage, error may can still happen, that’s why ErrorHeroModule was born. ErrorHeroModule is a Hero for your Zend Mvc, and zend-expressive application to trap php errors and exception with configureable options.
The logging storage is mainly a database, then can continue “log” to your email when you want it.
Features
1. Save to DB with Db Writer Adapter
We can choose using Zend\Db
or Doctrine
via DoctrineORMModule
. The error log is recorded like below:
2. Log Exception (dispatch.error and render.error) and PHP Errors in all events process
This handle all Exceptions and Errors with support PHP 7 Error during MVC process or middleware flow.
3. Support excludes PHP E_* Error (eg: exclude E_USER_DEPRECATED) in config settings
This can be used when you have a functionality which has collection of E_* errors, and you need to keep the functionality to run.
4. Support excludes PHP Exception (eg: Exception class or classes that extends it) in config settings
This can be used when you have exceptions that you want to have special treatment.
5. Handle only once log error for same error per configured time range
This can be used when on some environment, eg: in production, we don’t want to get same error repeatly reported in some periodic time while we are fixing it.
6. Set default page (web access) or default message (console access) for error if configured ‘display_errors’ = 0
This can be used to set a “nice” page on web environment:
or content on console access:
7. Set default content when request is XMLHttpRequest via ‘ajax’ configuration
This can be used to set a default content when request is an XMLHttpRequest.
8. Provide request information ( http method, raw data, query data, files data, and cookie data )
This can be used to help reproduce the error.
9. Send Mail
This has options:
– many receivers to listed configured email
– with include $_FILES into attachments on upload error.
This can be used to help reproduce the error, with include uploaded data when error happen when we just submitted a form with upload process.
Support
This module support zend-mvc:^2.5 and zend-expressive:^1.1|^2.0 with php version ^5.6|^7.0. My plan is to drop php ^5.6 in version 2.
Limitations
There are some limitations right now and I want it to be implemented in next releases:
General functionality:
- Allow custom formatter when log to email, currently, it send Json format to email.
Current Json Formatter is really stable with the following format sample data:
{ "timestamp": "2017-12-20T15:23:00+07:00", "priority": 3, "priorityName": "ERR", "message": "a sample error preview", "extra": { "url": "http://app.dev/error-preview", "file": "/var/www/app/vendor/samsonasik/error-hero-module/src/Controller/ErrorPreviewController.php", "line": 11, "error_type": "Exception", "trace": "#0 /var/www/app/vendor/zendframework/zend-mvc/src/Controller/AbstractActionController.php(78): ErrorHeroModule\\Controller\\ErrorPreviewController->exceptionAction() #1 /var/www/app/vendor/zendframework/zend-eventmanager/src/EventManager.php(322): Zend\\Mvc\\Controller\\AbstractActionController->onDispatch(Object(Zend\\Mvc\\MvcEvent)) #2 /var/www/app/vendor/zendframework/zend-eventmanager/src/EventManager.php(179): Zend\\EventManager\\EventManager->triggerListeners(Object(Zend\\Mvc\\MvcEvent), Object(Closure)) #3 /var/www/app/vendor/zendframework/zend-mvc/src/Controller/AbstractController.php(106): Zend\\EventManager\\EventManager->triggerEventUntil(Object(Closure), Object(Zend\\Mvc\\MvcEvent)) #4 /var/www/app/vendor/zendframework/zend-mvc/src/DispatchListener.php(138): Zend\\Mvc\\Controller\\AbstractController->dispatch(Object(Zend\\Http\\PhpEnvironment\\Request), Object(Zend\\Http\\PhpEnvironment\\Response)) #5 /var/www/app/vendor/zendframework/zend-eventmanager/src/EventManager.php(322): Zend\\Mvc\\DispatchListener->onDispatch(Object(Zend\\Mvc\\MvcEvent)) #6 /var/www/app/vendor/zendframework/zend-eventmanager/src/EventManager.php(179): Zend\\EventManager\\EventManager->triggerListeners(Object(Zend\\Mvc\\MvcEvent), Object(Closure)) #7 /var/www/app/vendor/zendframework/zend-mvc/src/Application.php(332): Zend\\EventManager\\EventManager->triggerEventUntil(Object(Closure), Object(Zend\\Mvc\\MvcEvent)) #8 /var/www/app/public/index.php(53): Zend\\Mvc\\Application->run() #9 {main}", "request_data": { "query": [], "request_method": "GET", "body_data": [], "raw_data": "", "files_data": [], "cookie_data": { "ZS6SESSID": "pbihc9ts004oq4b5alg4tg91b6", "PHPSESSID": "bkd7jaj22z936vstc9l0xuc9sr2dqp4g", } } } }
The drawback with allow custom formatter is you maintain/keep an eye yourself for the formatter you provide!
Zend Mvc application:
- Trap exception and error when they happen at
Module::init()
.
Zend Expressive application:
Make support for non zend-servicemanager for container.(support zend-servicemanager, symfony, aura, auryn, and pimple for zend-expressive application at version 2.9.0)Make support for non zend-view for custom page template engine when error happen.(supported at version 2.1.0)
That’s it for now. If you see something can be improved, please contribute! Thank you for all users that using it.
leave a comment