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
before LoginPageHandler
:
// 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');
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() !== 301) { $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.0alpha1 \ zendframework/zend-expressive-session-ext:^0.1.2 \ zendframework/zend-expressive-flash:^1.0.0alpha1
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, as follows:
use Zend\Expressive\Session\Session; use Zend\Expressive\Flash\FlashMessages; // ... $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
.
If we are going to use Closure
, we can do:
$closure = Closure::bind(function (FlashMessages $flashMessages) { return $flashMessages->currentMessages; }, null, FlashMessages::class); $closure($flashMessages);
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 Closure; use Zend\Expressive\Session\Session; use Zend\Expressive\Flash\FlashMessages; use Zend\View\Helper\AbstractHelper; class Flash extends AbstractHelper { public function __invoke() : array { $flashMessages = FlashMessages::createFromSession( new Session($_SESSION) ); $closure = Closure::bind(function (FlashMessages $flashMessages) { return $flashMessages->currentMessages; }, null, FlashMessages::class); return $closure($flashMessages); } }
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\Diactoros\Response\RedirectResponse; use Zend\Expressive\Authentication\UserInterface; use Zend\Expressive\Session\SessionMiddleware; 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 { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if (! $session->has(UserInterface::class)) { return new RedirectResponse('/login'); } 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->get('/admin', [ App\Handler\AdminPageHandler::class, ], '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.0alpha1 \ zendframework/zend-expressive-authorization-acl:^0.1.2
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 [‘authorization’] config, for example, at config/autoload/zend-expressive.global.php
:
<?php // config/autoload/zend-expressive.global.php return [ // ... 'authorization' => [ '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
after $app->pipe(RouteMiddleware::class); and then pipe the Zend\Expressive\Authorization\AuthorizationMiddleware
after it, as follow:
// config/pipeline.php $app->pipe(RouteMiddleware::class); $app->pipe(new class implements Psr\Http\Server\MiddlewareInterface{ use Zend\Expressive\Authentication\UserRepository\UserTrait; 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 // - set roles as "guest" // - when status code !== 403 or page = /login, return response // - otherwise, redirect to login page if (! $session->has(UserInterface::class)) { $user = ''; $roles = ['guest']; $request = $request->withAttribute( UserInterface::class, $this->generateUser( $user, $roles ) ); $response = $handler->handle($request); if ($request->getUri()->getPath() === '/login' || $response->getStatusCode() !== 403 ) { return $response; } return new RedirectResponse('/login'); } // has session but 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, $this->generateUser( $sessionData['username'], $sessionData['roles'] ) ); return $handler->handle($request); } }); $app->pipe(\Zend\Expressive\Authorization\AuthorizationMiddleware::class);
By above, you can clean up $session->has() check in all pages.
Yes, you can move the middleware
new class
inside pipe() to dedicated class and register it as service to be called as its classname, use custom template, you name it.
When we logged as user, but want to access “admin” resource, eg: “/admin”, we will get “403 Forbidden” :
That’s it ;).
Create Login functionality in Expressive 3
Zend Expressive 3 is not released yet, and expressive session related components are in active development. 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.0alpha3" \ expressive-3.0.0alpha3
There are components that can be installed via command:
$ cd expressive-3.0.0alpha3 $ composer require \ zendframework/zend-form:^2.11 \ zendframework/zend-i18n:^2.7 \ zendframework/zend-expressive-authentication:^1.0.0alpha1 \ zendframework/zend-expressive-authentication-session:^0.3.0 \ zendframework/zend-expressive-session:^1.0.0alpha1 \ zendframework/zend-expressive-session-ext:^0.1.2
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', ], ];
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);:
<?php // config/pipeline.php // ... use Zend\Expressive\Session\SessionMiddleware; // ... $app->pipe(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->get('/', App\Handler\HomePageHandler::class, 'home');
and in HomePageHandler
, we can check:
<?php // src/App/Handler/HomePageHandler.php declare(strict_types=1); namespace App\Handler; // ... use Zend\Diactoros\Response\RedirectResponse; use Zend\Expressive\Authentication\UserInterface; use Zend\Expressive\Session\SessionMiddleware; // ... class HomePageHandler implements RequestHandlerInterface { // ... public function handle(ServerRequestInterface $request) : ResponseInterface { $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE); if (! $session->has(UserInterface::class)) { return new RedirectResponse('/login'); } // ... } }
When access ‘/’ page, 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 Interop\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() !== 301) { 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() !== 301) { return new RedirectResponse('/'); }
When status code is not 301, 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 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.
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.
Auto add _links property of HAL Resources into all api service in Apigility
If you want to have the _links property value to HAL Resource in apigility api service, for example:
{ "id": 1, "name": "Abdul Malik Ikhsan", "_links": { "self": { "href": "http://api.dev/user/1" } } }
you can do manually in every api service:
use ZF\ContentNegotiation\ViewModel; use ZF\Hal\Entity as HalEntity; use ZF\Hal\Link\Link; // ... public function userAction() { $halEntity = new HalEntity([ 'id' => 1, 'name' => 'Abdul Malik Ikhsan', ]); $link = $halEntity->getLinks(); $link->add(Link::factory( [ 'rel' => 'self', 'url' => $this->getRequest()->getUriString(), ] )); return new ViewModel([ 'payload' => $halEntity, ]); } // ...
You can eliminate that by apply via EventManager’s Shared Manager which attach to Zend\Mvc\Controller\AbstractActionController
on dispatch
event, like below:
namespace Application; use Zend\Mvc\Controller\AbstractActionController; use Zend\Mvc\MvcEvent; use ZF\Hal\Link\Link; use ZF\Hal\Plugin\Hal; class Module { public function onBootstrap(MvcEvent $e) { $app = $e->getApplication(); $sharedEvm = $app->getEventManager()->getSharedManager(); $sharedEvm->attach(AbstractActionController::class, 'dispatch', function($event) use ($sharedEvm) { $uri = $event->getRequest()->getUriString(); $sharedEvm->attach(Hal::class, 'renderEntity', function($event) use ($uri) { $event->getParam('entity') ->getLinks() ->add(Link::factory( [ 'rel' => 'self', 'url' => $uri, ] )); }); }, 100 ); } public function getConfig() { /* */ } }
On above code, we attach ZF\Hal\Plugin\Hal
on renderEntity
event which get the ZF\Hal\Entity
object from ZF\ContentNegotiation\ViewModel
payload property, and apply Link into it via ZF\Hal\Link\Link::factory()
.
Now, you can eliminate unneeded repetitive codes in all every api services.
Done 😉
Create ZF Client Authentication for Apigility Oauth with ApigilityConsumer
If you have Apigility as API builder in API side, and client app that consume it using Zend Framework 2/3 or ZF Expressive, you can create an authentication from the client application that call oauth in apigility side.
Zend\Authentication
has AbstractAdapter that you can extends to create custom adapter for its need. Let’s assume the applications are like the following diagram:
[CLIENT - A ZF Application] [API - An Apigility Application] | | AuthController ZF\MvcAuth\Authentication\OAuth2Adapter | | | authenticateAction() | | ------------------------------------> | | identity json | | <------------------------------------ |
On oauth result call, you may get the following result:
{ "access_token": "8e4b0e5ddc874a6f1500514ef530dbea3976ae77", "expires_in": 3600, "token_type": "Bearer", "scope": null, "refresh_token": "d19b79cd376924409c14ee46e5230617482fb169" }
The ApigilityConsumer
ApigilityConsumer is a ZF2/ZF3 Apigility Client module (can also be used in ZF Expressive) to consume Apigility API Services.
You can install by run composer command:
composer require samsonasik/apigility-consumer
For full configurations and features, you can read at its README, for this post’s need, you can do something like this:
<?php // config/autoload/apigility-consumer.local.php return [ 'apigility-consumer' => [ // your apigility host url 'api-host-url' => 'https://your.apigilty.api.host', // your apigility oauth setting 'oauth' => [ 'grant_type' => 'password', 'client_id' => 'your client id', 'client_secret' => 'your client secret', ], ], ];
and register the module into config/application.config.php
or config/modules.config.php
:
<?php // config/application.config.php or config/modules.config.php return [ 'ApigilityConsumer', // <-- register here 'Application', ],
Create Adapter
You need to extends Zend\Authentication\Adapter\AbstractAdapter
and implements Zend\Authentication\Adapter\AdapterInterface
. So, You can have the class:
<?php namespace Application\Adapter; use ApigilityConsumer\Service\ClientAuthService; use Zend\Authentication\Adapter\AbstractAdapter; use Zend\Authentication\Adapter\AdapterInterface; use Zend\Authentication\Result; class ApigilityAuthenticationAdapter extends AbstractAdapter implements AdapterInterface { /** * @var ClientAuthService */ private $clientAuthService; /** * @param ClientAuthService $clientAuthService */ public function __construct(ClientAuthService $clientAuthService) { $this->clientAuthService = $clientAuthService; } /** * @return Result */ public function authenticate() { $clientResult = $this->clientAuthService->callAPI( [ // your oauth registered route segment in apigility. 'api-route-segment' => '/oauth', 'form-data' => [ 'username' => $this->getIdentity(), 'password' => $this->getCredential(), ], 'form-request-method' => 'POST', ] ); if (! $clientResult->success) { return new Result(Result::FAILURE, null, $clientResult::$messages); } return new Result(RESULT::SUCCESS, $clientResult->data); } }
Your can now build a factory from it:
<?php namespace Application\Adapter; use ApigilityConsumer\Service\ClientAuthService; class ApigilityAuthenticationAdapterFactory { public function __invoke($container) { return new ApigilityAuthenticationAdapter( $container->get(ClientAuthService::class) ); } }
You can then register at service_manager
:
<?php // module/Application/config/module.config.php namespace Application; 'service_manager' => [ 'factories' => [ // ... Adapter\ApigilityAuthenticationAdapter::class => Adapter\ApigilityAuthenticationAdapterFactory::class, ], ],
For ZF Expressive, you can register under ‘dependencies’ key.
Set Adapter into AuthenticationService
You need to set authentication service’s adapter with defined adapter above with factory:
<?php namespace Application\Factory; use Application\Adapter\ApigilityAuthenticationAdapter; use Zend\Authentication\AuthenticationService; use Zend\Authentication\Storage\Session; class AuthenticationServiceFactory { public function __invoke($container) { $adapter = $container->get(ApigilityAuthenticationAdapter::class); return new AuthenticationService( new Session(), // or your own storage implementing Zend\Authentication\Storage\StorageInterface $adapter ); } }
You can then register also at service_manager
:
<?php // module/Application/config/module.config.php namespace Application; use Zend\Authentication\AuthenticationService; 'service_manager' => [ 'factories' => [ // ... AuthenticationService::class => Factory\AuthenticationServiceFactory::class, ], ],
For ZF Expressive, you can register under ‘dependencies’ key.
The AuthController::authenticate()
I assume that you already inject controler with login form, use “username” and “password” as field names, and fill the data, so, your AuthController::authenticate()
can be like the following:
<?php namespace Application\Controller; use Application\Form\LoginForm; use Zend\Authentication\AuthenticationService; class AuthController { public function __construct( AuthenticationService $authenticationService, LoginForm $loginForm, ) { /* ...*/ } public function authenticateAction() { /* * check request and form validity here */ $formData = $this->loginForm->getData(); $this->authenticationService->getAdapter() ->setIdentity($formData['username']) ->setCredential($formData['password']); $result = $this->authenticationService->authenticate(); if (!$result->isValid()) { /** * For security reason, you should not show user the reason of failure, * However, if it actually needed for specific purpose, you can pull by call: * * $result->getMessages(); * */ return $this->redirect()->toRoute('/auth'); } return $this->redirect()->toRoute('/account'); } }
For ZF Expressive, you can create routed Authentication middleware.
That’s it, you’re now have successfully created a client authentication for your ZF2/ZF3 or ZF Expressive application that consume Apigility oauth.
Testing Zend Expressive 2 with kahlan 3
Zend\Expressive ^2.0 has different default approach for piping and routing middleware which is programmatically way. In this post, I am going to show you how you can test Zend\Expressive ^2.0 application, with assumption, you use its skeleton with kahlan 3.
First, of course, install the Expressive ^2.0 skeleton, for example, install into directory named “expressive2”:
$ composer create-project zendframework/zend-expressive-skeleton:^2.0 expressive2 Installing zendframework/zend-expressive-skeleton (2.0.0) - Installing zendframework/zend-expressive-skeleton (2.0.0) Downloading: 100% Created project in expressive2 > ExpressiveInstaller\OptionalPackages::install Setting up optional packages Setup data and cache dir Removing installer development dependencies What type of installation would you like? [1] Minimal (no default middleware, templates, or assets; configuration only) [2] Flat (flat source code structure; default selection) [3] Modular (modular source code structure; recommended) Make your selection (2): 3
Now, install kahlan:
$ cd expressive2 $ composer require kahlan/kahlan:^3.1
We are going to need the $app
variable inside tests, for example, when testing functionality for each routed middlewares. To simplify and avoid repetitive code, we can register it into kahlan-config.php in root application:
// ./kahlan-config.php use Kahlan\Filter\Filter; use Zend\Expressive\Application; Filter::register('initialize app', function($chain) { $root = $this->suite(); ob_start(); $root->beforeAll(function ($var) { ob_start(); $var->app = $app = (require 'config/container.php')->get(Application::class); require 'config/pipeline.php'; require 'config/routes.php'; }); return $chain->next(); }); Filter::apply($this, 'run', 'initialize app');
By assign $app
into “$var->app” like above, the “$app” is accessible from entire tests via “$this->app”, so, we can write test like the following:
// ./src/App/spec/Action/HomePageActionSpec.php namespace AppSpec\Action; use Zend\Diactoros\ServerRequest; describe('HomePageAction', function () { describe('/', function () { it('contains welcome message', function () { $serverRequest = new ServerRequest([], [], '/', 'GET'); $this->app->run($serverRequest); $actual = ob_get_clean(); expect($actual)->toContain('Welcome to <span class="zf-green">zend-expressive</span>'); }); }); });
Now, let’s run the tests:
$ vendor/bin/kahlan --spec=src/App/spec/ _ _ /\ /\__ _| |__ | | __ _ _ __ / //_/ _` | '_ \| |/ _` | '_ \ / __ \ (_| | | | | | (_| | | | | \/ \/\__,_|_| |_|_|\__,_|_| | | The PHP Test Framework for Freedom, Truth and Justice. Working Directory: /Users/samsonasik/www/expressive2 . 1 / 1 (100%) Expectations : 1 Executed Specifications : 0 Pending, 0 Excluded, 0 Skipped Passed 1 of 1 PASS in 0.375 seconds (using 8Mo)
That’s it 😉
Functional Test for Zend\Expressive Routed Middleware with Kahlan ^3.0
You may tried do functional test Zend\Expressive Routed Middleware and end up with “Unable to emit response; headers already sent” error.
This can happen because of during run test, the Test framework itself already run fwrite()
or echo
to build test report, and make the headers_sent()
return true.
To handle that, we can use ob_start()
, but since the header is sent in the background, we need to place in both places:
- test bootstrap
- before each test
Seriously? Yes! That’s make sure we only get Diactoros response that we use in the buffer to be tested.
Preparation
As usual, we need require kahlan/kahlan:^3.0 in require-dev:
$ composer require --dev kahlan/kahlan:^3.0 --sort-packages
Set Kahlan’s Bootstrap and before each globally
In Kahlan, we can set tests bootstrap and what in all before each test with Kahlan\Filter\Filter
in kahlan-config.php
, so we can write:
<?php //kahlan-config.php use Kahlan\Filter\Filter; ob_start(); Filter::register('ob_start at each', function($chain) { $root = $this->suite(); $root->beforeEach(function () { ob_start(); }); return $chain->next(); }); Filter::apply($this, 'run', 'ob_start at each');
Write Spec and Run In Action
Now, if we use Expressive skeleton application, and for example, we need to test App\Action\PingAction
routed middleware, we can write spec in spec directory:
. ├── composer.json ├── config ├── data ├── kahlan-config.php ├── public ├── spec │  └── App │  └── Action │  ├── PingActionDispatchSpec.php ├── src │  └── App │  └── Action │  ├── PingAction.php
As the App\Ping\PingAction
is return Zend\Diactoros\Response\JsonResponse
which contains “ack” data with time()
method call:
return new JsonResponse(['ack' => time()]);
The spec can be the following:
<?php namespace AppSpec\Action; use Zend\Diactoros\ServerRequest; use Zend\Expressive\Application; describe('PingAction Dispatch', function () { beforeAll(function() { $container = require 'config/container.php'; $this->app = $container->get(Application::class); }); describe('/api/ping', function () { it('contains json "ack" data', function () { allow('time')->toBeCalled()->andReturn('1484291901'); $serverRequest = new ServerRequest([], [], '/api/ping', 'GET'); $this->app->run($serverRequest); $actual = ob_get_clean(); expect($actual)->toBe('{"ack":"1484291901"}'); }); }); });
The ob_start()
will automatically called during test bootstrap and before each test.
Now, we can run the test:
$ vendor/bin/kahlan --coverage=4 --src=src/App/Action/PingAction.php _ _ /\ /\__ _| |__ | | __ _ _ __ / //_/ _` | '_ \| |/ _` | '_ \ / __ \ (_| | | | | | (_| | | | | \/ \/\__,_|_| |_|_|\__,_|_| | | The PHP Test Framework for Freedom, Truth and Justice. Working Directory: /Users/samsonasik/www/expressive . 1 / 1 (100%) Expectations : 1 Executed Specifications : 0 Pending, 0 Excluded, 0 Skipped Passed 1 of 1 PASS in 0.210 seconds (using 7Mo) Coverage Summary ---------------- Lines % \ 1 / 1 100.00% └── App\ 1 / 1 100.00%    └── Action\ 1 / 1 100.00%       └── PingAction 1 / 1 100.00%          └── PingAction::__invoke() 1 / 1 100.00% Total: 100.00% (1/1) Coverage collected in 0.003 seconds (using an additionnal 0o)
Done 😉
Using ZF Component’s ConfigProvider in Expressive
If you already tried building Expressive application using modular approach with Expressive Config Manager, you can just use ZF Component’s services by consuming its config provider. If you didn’t use it, you need to first require the Expressive Config Manager in your Expressive application:
composer require mtymek/expressive-config-manager
When done, you can modify the config/config.php
like the following:
use Zend\Expressive\ConfigManager\ConfigManager; use Zend\Expressive\ConfigManager\PhpFileProvider; $configManager = new ConfigManager([ new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), ]); return new ArrayObject($configManager->getMergedConfig());
Now, for example, you want to consume Zend\Db
services, you can do:
1. Require it
composer require zendframework/zend-db
2. Register in config/config.php
use Zend\Expressive\ConfigManager\ConfigManager; use Zend\Expressive\ConfigManager\PhpFileProvider; $configManager = new ConfigManager([ new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), new Zend\Db\ConfigProvider() ]); return new ArrayObject($configManager->getMergedConfig());
By provide Zend\Db
Config Provider, you can now consume its service, let’s try define sample DB config:
// config/autoload/db.local.php return [ 'db' => [ 'username' => 'root', 'password' => '', 'driver' => 'Pdo', 'dsn' => 'mysql:dbname=test;host=localhost', 'driver_options' => [ PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'' ], ], ];
And now, you can consume \Zend\Db\Adapter\AdapterInterface::class
as service. Check the per-component’s ConfigProvider::getDependencyConfig()
for available services if you want to use them.
Done 😉
Start Using Middleware Approach with new zend-mvc
zend-mvc 2.7.0 is coming, beside of the forward compatibility with V3 components, there is new middleware listener that allow us to do Dispatching PSR-7 middleware. The middleware can be an invokable class with __invoke()
method like the following:
function __invoke($request, $response) { $response->getBody()->write('Hello World!'); return $response; }
Ok, let’s start try it, create new project:
$ composer create-project zendframework/skeleton-application:dev-master newzf
After composer create-project
done, as usual, you will get new project. zend-mvc 2.7.0 released today, so, You should get zend-mvc 2.7.0 already by run:
$ composer update
Now, We can create a middleware, for example, a HomeAction
middleware:
namespace Application\Middleware; class HomeAction { public function __invoke($request, $response) { $response->getBody()->write('Hello World!'); return $response; } } // module/Application/src/Application/Middleware/HomeAction.php
We then can replace the ‘home’ route:
namespace Application; // ... 'home' => [ 'type' => 'Literal', 'options' => [ 'route' => '/', 'defaults' => [ 'middleware' => Middleware\HomeAction::class, ], ], ], // ... // module/Application/config/module.config.php
As the Application\Middleware\HomeAction
is a service, then it need to be registered in service_manager
:
namespace Application; use Zend\ServiceManager\Factory\InvokableFactory; // ... 'service_manager' => [ 'factories' => [ Middleware\HomeAction::class => InvokableFactory::class, ], ] // ... // module/Application/config/module.config.php
Everything seems configured correctly, now, let’s start the server:
$ php -S localhost:8080 -t public
And open up in the browser: http://localhost:8080 ! So, the “Hello World!” will be shown!
Work with ViewModel
So, you now want to work with ViewModel with its layout, You can! Let’s do it. You can inject the Middleware with the Renderer and ViewManager.
use Zend\View\Renderer\PhpRenderer; use Zend\Mvc\View\Http\ViewManager; class HomeAction { // ... public function __construct( PhpRenderer $renderer, ViewManager $view ) { $this->renderer = $renderer; $this->view = $view; } // ... } // module/Application/src/Application/Middleware/HomeAction.php
To make it work, we can create factory for it:
namespace Application\Middleware; class HomeActionFactory { public function __invoke($container) { $viewRenderer = $container->get('ViewRenderer'); $viewManager = $container->get('ViewManager'); return new HomeAction($viewRenderer, $viewManager); } } // module/Application/src/Application/Middleware/HomeActionFactory.php
Based on the factory above, we then need to update the registered Application\Middleware\HomeAction
service:
namespace Application; // ... 'service_manager' => [ 'factories' => [ Middleware\HomeAction::class => Middleware\HomeActionFactory::class, ], ] // ... // module/Application/config/module.config.php
So, now, you can update the Middleware as follows:
use Zend\View\Renderer\PhpRenderer; use Zend\Mvc\View\Http\ViewManager; use Zend\View\Model\ViewModel; use Zend\Diactoros\Response\HtmlResponse; class HomeAction { // ... public function __invoke($request, $response) { $viewModel = new ViewModel(); $viewModel->setTemplate('application/index/index'); $layout = $this->view->getViewModel(); $layout->setVariable( 'content', $this->renderer->render($viewModel) ); return new HtmlResponse($this->renderer->render($layout)); } // ... } // module/Application/src/Application/Middleware/HomeAction.php
Done! 😉
References:
– https://gist.github.com/weierophinney/b9dbff92e4446f49e248
– https://github.com/weierophinney/2015-10-22-ZF3
Using Routed Middleware class as Controller with multi actions in Expressive
Note: this post is now part of Zend\Expressive cookbook.
If you are familiar with frameworks with provide controller with multi actions functionality, like in Zend Framework 1 and 2, you may want to apply it when you use Zend\Expressive microframework as well. Usually, we need to define 1 routed middleware, 1 __invoke() with 3 parameters ( request, response, next ). If we need another specifics usage, we can create another routed middleware classes, for example:
- AlbumPageIndex
- AlbumPageEdit
- AlbumPageAdd
What if we want to use only one middleware class which facilitate 3 pages above? We can with make request attribute with ‘action’ key via route config, and validate it in __invoke()
method with ReflectionMethod.
Let say, we have the following route config:
// ... 'routes' => [ [ 'name' => 'album', 'path' => '/album[/:action][/:id]', 'middleware' => Album\Action\AlbumPage::class, 'allowed_methods' => ['GET'], ], ], // ...
To avoid repetitive code for modifying __invoke()
method, we can create an AbstractPage, like the following:
namespace App\Action; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ReflectionMethod; abstract class AbstractPage { public function __invoke($request, $response, callable $next = null) { $action = $request->getAttribute('action', 'index') . 'Action'; if (method_exists($this, $action)) { $r = new ReflectionMethod($this, $action); $args = $r->getParameters(); if (count($args) === 3 && $args[0]->getType() == ServerRequestInterface::class && $args[1]->getType() == ResponseInterface::class && $args[2]->isCallable() && $args[2]->allowsNull() ) { return $this->$action($request, $response, $next); } } return $next($request, $response->withStatus(404), 'Page Not Found'); } }
In above abstract class with modified __invoke()
method, we check if the action attribute, which default is ‘index’ if not provided, have ‘Action’ suffix, and the the method is exists within the middleware class with 3 parameters with parameters with parameter 1 as ServerRequestInterface, parameter 2 as ResponseInterface, and parameter 3 is a callable and allows null, otherwise, it will response 404 page.
So, what we need to do in out routed middleware class is extends the AbstractPage we created:
namespace Album\Action; use App\Action\AbstractPage; use Zend\Diactoros\Response\HtmlResponse; use Zend\Expressive\Template; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class AlbumPage extends AbstractPage { protected $template; // you need to inject via factory public function __construct(Template\TemplateRendererInterface $template) { $this->template = $template; } public function indexAction( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { return new HtmlResponse($this->template->render('album::album-page')); } public function addAction( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { return new HtmlResponse($this->template->render('album::album-page-add')); } public function editAction( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { $id = $request->getAttribute('id'); if ($id === null) { throw new \InvalidArgumentException('id parameter must be provided'); } return new HtmlResponse( $this->template->render('album::album-page-edit', ['id' => $id]) ); } }
The rest is just create the view. Done 😉
Compose modular Expressive application with “ZF2 style” structure
Yes, there is a doc for compose modular application in Zend\Expressive. And No, I won’t debate about how non-shareable module shouldn’t be in other modules (only “root” module), which is on personal preference. In this post, I just want to show you how to compose modular application like the “ZF2” structure, with config, view, and class that require the config per-“module”, and that’s do-able!.
Let’s do it!:
1) Same as doc, requiring mtymek/expressive-config-manager
:
$ composer require mtymek/expressive-config-manager
2) Register your modules as namespace in composer autoload:
// composer.json // ... "autoload": { "psr-4": { "App\\": "src/App/", "Album\\": "src/Album/" } }, // ...
3) Run dump-autoload
$ composer dump-autoload
4) Let’s extract one by one per-module, “App” and “Album”
a. src/App/AppConfig.php
//src/App/AppConfig.php namespace App; class AppConfig { public function __invoke() { return include __DIR__ . '/config/expressive-config.php'; } }
b. src/Album/AlbumConfig.php
//src/Album/AlbumConfig.php namespace Album; class AlbumConfig { public function __invoke() { return include __DIR__ . '/config/expressive-config.php'; } }
c. src/App/config/expressive-config.php
You can move various arrays from config/autoload/routes.global.php
to src/App/config/expressive-config.php
:
return [ 'dependencies' => [ 'invokables' => [ App\Action\PingAction::class => App\Action\PingAction::class, ], 'factories' => [ App\Action\HomePageAction::class => App\Action\HomePageFactory::class, ], ], 'routes' => [ [ 'name' => 'home', 'path' => '/', 'middleware' => App\Action\HomePageAction::class, 'allowed_methods' => ['GET'], ], [ 'name' => 'api.ping', 'path' => '/api/ping', 'middleware' => App\Action\PingAction::class, 'allowed_methods' => ['GET'], ], ], ];
d. src/Album/config/expressive-config.php
Like App
config, you can define:
return [ 'dependencies' => [ 'factories' => [ Album\Action\AlbumPageAction::class => Album\Action\AlbumPageFactory::class, ], ], 'routes' => [ [ 'name' => 'album', 'path' => '/album', 'middleware' => Album\Action\AlbumPageAction::class, 'allowed_methods' => ['GET'], ], ], ];
5) Templates
As in the “App” and “Album” modules, at this case, we need template, we can define templates path in the config:
a. for App config
// src/App/config/expressive-config.php return [ // ... 'templates' => [ 'paths' => [ 'app' => [ __DIR__ . '/../templates/app'], ], ], ];
Note:
As it will redundant with “app” key registered in config/autoload/templates.global.php
, you need to remove “app” key under “paths” in config/autoload/templates.global.php
. Don’t forget to remove the “app” directory inside rootproject/templates to avoid confusion.
b. for Album config
// src/Album/config/expressive-config.php return [ // ... 'templates' => [ 'paths' => [ 'album' => [ __DIR__ . '/../templates/album'], ], ], ];
Now, you can have the following structure:
6) Register the “modules” in config/config.php
:
use Zend\Expressive\ConfigManager\ConfigManager; use Zend\Expressive\ConfigManager\PhpFileProvider; $configManager = new ConfigManager([ App\AppConfig::class, Album\AlbumConfig::class, // ... new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), ]); return new ArrayObject($configManager->getMergedConfig());
It’s done! You can continue with writing middlewares! Tired to follow the steps? Just clone and install from here: https://github.com/samsonasik/learn-expressive-modular
1 comment