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
116 comments