Welcome to Abdul Malik Ikhsan's Blog

Replace Hard Dependency Class with New Simulation Class via “replace” and “classmap” in Composer Configuration

Posted in hack, php by samsonasik on October 15, 2018

If we are using 3rd party library that managed by composer, which has hard dependency that we don’t want to use, for example, at the following use case:

The one of the solutions for that is by using “replace” and “classmap” configuration in our composer.json. First, we need to prepare of the class to simulate the Logger class, for example, we have it in src/App/Apache/Logger.php:

<?php
// src/App/Apache/Logger.php

class Logger 
{
    function debug(...$args) {}
    function info(...$args) {}
    function trace(...$args) {}
    function warn(...$args) {}
    function error(...$args) {}
    function fatal(...$args) {}

    public static function configure(...$args) {}
    public static function getLogger() { return new self(); }
}

Yes, above class doesn’t do anything, for silent action when Logger::{themethod()} called in realexpayments/rxp-remote-php library classes.

Next, we can register it to our composer.json:

{
    "require": {
        // ...
        "realexpayments/rxp-remote-php": "^1.2"
        // ...
    },
    "replace": {
        "apache/log4php": "^2.3.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/App/",
        },
        "classmap": [
            "src/App/Apache/Logger.php"
        ]
    }
}   

In above configuration, the replace of “apache/log4php” doesn’t has replacement library in ‘require’ part will make the dependency removed entirely as we don’t want to use it anymore, and by the classmap configuration, we have new redefined of the Logger class as simulation of “apache/log4php” Logger class.

Last step, we can run:

➜ composer update

That’s it!

Using zend-expressive-session-cache as PSR-6 session persistence adapter in Expressive 3

Posted in expressive, Tutorial PHP, Zend Framework by samsonasik on October 14, 2018

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 ConfigProviders 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!

Using “no-api” key on Github Forked Repository in Composer repositories configuration for Travis Build

Posted in hack, php by samsonasik on October 7, 2018

So, you’re doing a pull request, you want the feature to be merged so bad to public repository, but for whatever reason, it is not merged yet. You can register your forked repository under composer “repositories” config:

{
    "require": {
        "awesome/library": "dev-my-fork-awesome-feature"
    },
    "repositories" : [
        {
            "type" : "vcs",
            "url" : "https://github.com/yourgithubuser/library"
        }
    ]
}

That will work for your local dev and your server, but unfortunatelly, that won’t work on Travis! We will get the following error:

Failed to clone the git@github.com:yourgithubuser/library.git repository, try running in interactive mode so that you can enter your GitHub credentials
                                                                                                                                                                                      
  [RuntimeException]                                                                                                                                                                  
  Failed to execute git clone --mirror 'git@github.com:yourgithubuser/library.git' '/home/travis/.composer/cache/vcs/git-github.com-yourgithubuser-library.git/'  

To make that work, we will need the “no-api” key in your composer.json under the per-repository inside “repositories”, as follow:

// ...
        {
            "type" : "vcs",
            "url" : "https://github.com/yourgithubuser/library",
            "no-api": true
        }
// ...

Now, our composer.json will look like this:

{
    "require": {
        "awesome/library": "dev-my-fork-awesome-feature"
    },
    "repositories" : [
        {
            "type" : "vcs",
            "url" : "https://github.com/yourgithubuser/library",
            "no-api": true
        }
    ]
}

That’s it!

Create API Service in CodeIgniter 4 with Request Filtering

Posted in CodeIgniter 4, Teknologi, Tutorial PHP by samsonasik on September 22, 2018

In CodeIgniter 4, there is a trait that specialized to be used for API, named CodeIgniter\API\ResponseTrait for that, that consist of collection of functionality to build a response. How about request filtering ? We will need a filter class that implements CodeIgniter\Filters\FilterInterface interface.

For example, we are going to create a /api/ping api service, which will returns time value, we can create controller as follow:

<?php
// application/Controllers/Api/Ping.php
declare(strict_types=1);

namespace App\Controllers\Api;

use function time;

use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;

final class Ping extends Controller
{
    use ResponseTrait;

	public function index(): ResponseInterface
	{
        return $this->respond(['ack' => time()]);
    }
}

Without filtering, we can call url and got result like the following based on Accept header, for example, for application/xml, it will get:

➜  ~ curl -i -H "Accept: application/xml" http://localhost:8080/api/ping
HTTP/1.1 200 OK
Host: localhost:8080
Date: Sat, 22 Sep 2018 01:11:30 -0500
Connection: close
X-Powered-By: PHP/7.2.9
Cache-control: no-store, max-age=0, no-cache
Content-Type: application/xml; charset=UTF-8
Debugbar-Time: 1537596690
Debugbar-Link: http://localhost:8080/index.php/?debugbar_time=1537596690

<?xml version="1.0"?>
<response><ack>1537596690</ack></response>

and when it changed to application/json, it will get:

➜  ~ curl -i -H "Accept: application/json" http://localhost:8080/api/ping
HTTP/1.1 200 OK
Host: localhost:8080
Date: Sat, 22 Sep 2018 01:11:53 -0500
Connection: close
X-Powered-By: PHP/7.2.9
Cache-control: no-store, max-age=0, no-cache
Content-Type: application/json; charset=UTF-8
Debugbar-Time: 1537596713
Debugbar-Link: http://localhost:8080/index.php/?debugbar_time=1537596713

{
    "ack": 1537596713
}

Let’s try create filter it to only allow the “POST” request! We can create filter as follow:

<?php
// application/Filters/PostRequestOnly.php
declare(strict_types=1);

namespace App\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;

final class PostRequestOnly implements FilterInterface
{
	public function before(RequestInterface $request)
	{
        if ($request->getMethod() === 'post') {
            return;
        }

        return Services::response()
            ->setStatusCode(ResponseInterface::HTTP_METHOD_NOT_ALLOWED);
    }

	public function after(RequestInterface $request, ResponseInterface $response)
	{
	}
}

In above PostRequestOnly filter, we are allowing request only for “POST”, other request methods will get response with “Method Not Allowed” (405). To make it work, we need register it into Config\Filters::$aliases class under application directory, and ensure it applied into Config\Filters::$filters to register specific uri for the filter, as follow:

<?php
// application/Config/Filters.php

// ...
use App\Filters\PostRequestOnly;

class Filters extends BaseConfig
{
	public $aliases = [
        // ...
        'postRequestOnly' => PostRequestOnly::class,
    ];

    // ...

	public $filters = [
        // ...
        'postRequestOnly' => [
            'before' => [
                'api/ping',
            ],
        ],
    ];
}

That’s it! Now, when we try to see different result with GET and POST, it will get the following response:

GET: Method Not Allowed 405

➜  ~ curl -i -H "Accept: application/json" -X GET http://localhost:8080/api/ping
HTTP/1.1 405 Method Not Allowed
Host: localhost:8080
Date: Sat, 22 Sep 2018 13:12:22 +0700
Connection: close
X-Powered-By: PHP/7.2.9
Cache-control: no-store, max-age=0, no-cache
Content-Type: text/html; charset=UTF-8

POST: Got Response OK 200

➜  ~ curl -i -H "Accept: application/json" -X POST http://localhost:8080/api/ping
HTTP/1.1 200 OK
Host: localhost:8080
Date: Sat, 22 Sep 2018 01:12:54 -0500
Connection: close
X-Powered-By: PHP/7.2.9
Cache-control: no-store, max-age=0, no-cache
Content-Type: application/json; charset=UTF-8
Debugbar-Time: 1537596774
Debugbar-Link: http://localhost:8080/index.php/?debugbar_time=1537596774

{
    "ack": 1537596774
}

Upload File with Validation in CodeIgniter 4

Posted in CodeIgniter 4, Tutorial PHP by samsonasik on September 5, 2018

In CodeIgniter 4, there is CodeIgniter\Validation\FileRules that can be used out of the box from validate() from controllers that we can using it on upload process.

For example, we have an upload form page with flow as follow:

  1. Display form at /form page with “avatar” file field
  2. Process form at /form/process with validations:
    • is uploaded
    • is mime in image/jpg,image/jpeg,image/gif,image/png
    • the max size is less or equal than 4MB
  3. On valid upload, we move uploaded file to writable/uploads directory.

Before we start, please ensure that you’re using latest CodeIgniter4 develop branch by clone it:

$ git clone https://github.com/bcit-ci/CodeIgniter4.git

If you already have it, ensure to pull latest develop branch via command:

$ cd CodeIgniter4
$ git checkout develop
$ git remote add upstream https://github.com/bcit-ci/CodeIgniter4.git
$ git pull upstream develop

Most of the time, the updated code in the CodeIgniter4 repository will be core framework (system) and its tests, so, if we need to be always up to date framework, we can create new branch based on develop branch, for example:

$ git checkout -b tutorial-upload

Then, we can regularly rebase against develop :

$ git checkout develop && git pull upstream develop
$ git checkout tutorial-upload && git rebase develop

Enough with introduction, let’s start with code!

1. Display Form

<?php
// application/Views/form.php
$validationErrors = $this->config->plugins['validation_errors'];

helper('form');
echo form_open(
    'form/process',
    [
        'enctype' => 'multipart/form-data'
    ]
);

echo form_input('avatar', '', '', 'file');
echo $validationErrors(['field' => 'avatar']);

echo form_submit('Send', 'Send');
echo form_close();

Above, we display form with form helpers, and display validation errors by get it in validation_errors key in plugins.

To make it a form page, we can create a Form controller as follow:

<?php
// application/Controllers/Form.php
namespace App\Controllers;

use CodeIgniter\Controller;

class Form extends Controller
{
    public function index()
    {
        return view('form');
    }
}

To see the page, let’s fire a command:

$ php spark serve

The form page is now displayed at http://localhost:8080/form or http://localhost:8080/form/index like below:

2. Upload validation

We are going to use “form/process” page for file upload validation, so, we can create an process() function at the Form controller:

class Form extends Controller
{
    // ...
    public function process()
    {
        // verify if request method is POST
        if ($this->request->getMethod() !== 'post') {
            return redirect('index');
        }

        // validation
        $validated = $this->validate([
            'avatar' => [
                'uploaded[avatar]',
                'mime_in[avatar,image/jpg,image/jpeg,image/gif,image/png]',
                'max_size[avatar,4096]',
            ],
        ]);

        if ($validated) {
            // Business!
        }

        return redirect('index')->withInput();
    }
}

On above process() function, the validate() call supply the rules of avatar field needed, which are uploaded, mime_in, and max_size which we can found as functions at CodeIgniter\Validation\FileRules. When it is valid, it will returns true. We redirect back to /form/index with bring validation errors that saved in session flash with key _ci_validation_errors to be allowed to be displayed once in the next request like below:

3. Move uploaded file to writable/uploads directory

We have a WRITEPATH constant that refers to writable directory, and we can use its “$request->getFile(‘avatar’)->move()” to it, so when we have validated, we can do:

if ($validated) {
    $avatar = $this->request->getFile('avatar');
    $avatar->move(WRITEPATH . 'uploads');
}

That’s it! The complete Form controller class can be as follow:

<?php
// application/Controllers/Form.php
namespace App\Controllers;

use CodeIgniter\Controller;

class Form extends Controller
{
    public function index()
    {
        return view('form');
    }

    public function process()
    {
        if ($this->request->getMethod() !== 'post') {
            return redirect('index');
        }

        $validated = $this->validate([
            'avatar' => [
                'uploaded[avatar]',
                'mime_in[avatar,image/jpg,image/jpeg,image/gif,image/png]',
                'max_size[avatar,4096]',
            ],
        ]);

        if ($validated) {
            $avatar = $this->request->getFile('avatar');
            $avatar->move(WRITEPATH . 'uploads');
        }

        return redirect('index')->withInput();
    }
}

Create Middleware for File Post/Redirect/Get in Expressive 3

Posted in expressive, Teknologi, Tutorial PHP, Zend Framework by samsonasik on July 31, 2018

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 ;).

Tagged with: ,

Create Middleware for Post/Redirect/Get in Expressive 3

Posted in expressive, Zend Framework by samsonasik on February 13, 2018

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() !== 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

Posted in expressive, Tutorial PHP, Zend Framework by samsonasik on February 13, 2018

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

Posted in expressive, Zend Framework by samsonasik on February 11, 2018

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!

Introduce IsDeprecated: PHP7+ Helper for E_USER_DEPRECATED and E_DEPRECATED Detection

Posted in hack, php, Teknologi by samsonasik on January 18, 2018

Before I continue, allow me to give you my suggestions:

Live with deprecated function is bad, you may can’t upgrade to newer PHP version or newer library as the function that was deprecated already removed in next major/minor version.

Use same environment or at least same major and minor version between local dev and production environment when possible, your life will be easier.

When reality is not always what you want

You may found a a reality when you work at different version of PHP version or library that rely on specific function which already deprecated in some environment, the IsDeprecated may be solution at that time that can verify E_USER_DEPRECATED or E_DEPRECATED trigger error. It utilize jeremeamia/FunctionParser for user defined function check, and zendframework/zend-stdlib’s ErrorHandler for E_DEPRECATED function check.

When you found the passed function is deprecated, you can use alternative function.

Installation

This helper can be installed via composer:

composer require samsonasik/is-deprecated

This helper have features:

1. Detect E_USER_DEPRECATED

  • At independent function
  • At function inside class

You can use IsDeprecated\isDeprecatedUser function with signature:

/**
 * @param  string|array $function the "functionName" or ["ClassName" or object, "functionName"] or "ClassName::functionName"
 * @throws InvalidArgumentException when trigger_error found but the error is not E_USER_DEPRECATED
 * @throws InvalidArgumentException when trigger_error and E_USER_DEPRECATED found but misplaced
 * @return bool
 */
function isDeprecatedUser($function): bool

Note: when trigger_error E_USER_DEPRECATED inside condition, you need to use actual call with signature:

/**
 * @param  callable $function callable function
 * @return bool
 */
function isDeprecatedWithActualCall(callable $function)

1a. Independent Function

The usage is like the following:

use function IsDeprecated\isDeprecatedUser;

function foo()
{
    trigger_error('this method has been deprecated.', E_USER_DEPRECATED);
}

if (isDeprecatedUser('foo')) {
    // apply alternative/new function to call...
} else {
    foo();
}

1.b Function Inside Class

The usage is like the following:

use function IsDeprecated\isDeprecatedUser;

class AClass
{
    public function foo()
    {
        trigger_error('this method has been deprecated.', E_USER_DEPRECATED);
    }

    // check inside with $this
    public function execute()
    {
        if (isDeprecatedUser([$this, 'foo'])) {
             // apply alternative/new function to call...
             return;
        }

        $this->foo();
    }
}

// you may call after instantiation
$object = new \AClass();
if (isDeprecatedUser([$object, 'foo'])) {
    // apply alternative/new function to call...
} else {
    $object->foo();
}

// or
if (isDeprecatedUser(['AClass', 'foo'])) {
    // apply alternative/new function to call...
} else {
    (new \AClass())->foo();
}

2. Detect E_DEPRECATED

E_DEPRECATED can be triggered on Core PHP function call.

You can use IsDeprecated\isDeprecatedCore function with signature:

/**
 * @param  callable $function callable function
 * @return bool
 */
function isDeprecatedCore(callable $function): bool

The usage is like the following:

use function IsDeprecated\isDeprecatedCore;

$function = function () {
    mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
};

if (isDeprecatedCore($function)) {
    // alternative function, eg: openssl ...
} else {
    $function();
}

Limitation

For Core PHP Functions or user function with condition (T_IF or T_SWITCH token), the function passed actually need to be called. It ensure that we don’t get error during call deprecated function, and we can use alternative function if the isDeprecatedCore() returns true with call of isDeprecatedWithActualCall.

You want to use it? You can check my repository https://github.com/samsonasik/IsDeprecated

That’s it ;).

Create Authorization functionality in Expressive 3

Posted in expressive, Zend Framework by samsonasik on January 13, 2018

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

Posted in expressive, Zend Framework by samsonasik on January 12, 2018

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:

  1. Login Form
  2. Authentication process, read from DB
  3. 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

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:^0.4.0 \
     zendframework/zend-expressive-authentication-session:^0.4.1 \
     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([
    // ... 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 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

Functional Test Symfony 4 with Kahlan 4

Posted in Symfony 4, testing by samsonasik on January 3, 2018

Yes, there is a bundle for it, but currently not fully work well with kahlan 4 yet. However, we can still use kahlan 4 for it. The simplest way is define Symfony 4 skeleton bootstrap in kahlan config, and use its property at specs, for example, we configure config at kahlan-config.php as follows:

<?php // kahlan-config.php 

use App\Kernel;
use Kahlan\Filter\Filters;
use Symfony\Component\HttpFoundation\Request;

Filters::apply($this, 'bootstrap', function($next) {

    require __DIR__ . '/vendor/autoload.php';

    $root = $this->suite()->root();
    $root->beforeAll(function () {
        $this->request = Request::createFromGlobals();
        $this->kernel  = new Kernel('test', false);
    });

    return $next();

});

Above settings are minimal, if you need more setup, you can define there. If you didn’t require kahlan/kahlan:^4.0, you can require via composer:

$ composer require --dev kahlan/kahlan:^4.0

Give a try

Let’s try testing a famous /lucky/number from LuckyController. We have the following controller:

<?php // src/Controller/LuckyController.php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;

class LuckyController extends Controller
{
    /**
     * @Route("/lucky/number", name="lucky_number")
     */
    public function number()
    {
        $number = mt_rand(0, 100);

        return $this->render('lucky/number.html.twig', [
            'number' => $number,
        ]);
    }
}

And our twig file is:

{# templates/lucky/number.html.twig #}

<h1>Your lucky number is {{ number }}</h1>

We can place test under spec directory at root directory, for its test, we can create a spec/Controller directory:

kahlan.config.php
├── spec
│   └── Controller

Now, we can create the test as follows with make request to the ‘/lucky/number’ page and get its response. We can use toMatchEcho matcher provided with regex to get match random number of mt_rand(0, 100) that printed inside a response html content:

<?php // spec/Controller/LuckyControllerSpec.php

namespace App\Spec\Controller;

describe('LuckyController', function () {

    describe('/lucky/number', function () {

        it('shows lucky number', function () {

            $request = $this->request->create('/lucky/number', 'GET');
            $response = $this->kernel->handle($request);

            expect(function () use ($response) {
                $response->send();
            })->toMatchEcho(
                "#Your lucky number is ([0-9]|[1-8][0-9]|9[0-9]|100)#"
            );

        });

    });

});

Time to run it with command:

$ vendor/bin/kahlan 

We will get the success output:

That’s it 😉

ErrorHeroModule : a Hero for Your Zend Mvc and Expressive Application

Posted in expressive, Teknologi, Zend Framework, Zend Framework 2, Zend Framework 3 by samsonasik on December 20, 2017

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.

Apigility: Using zf-oauth2’s refresh_token_lifetime to create client’s remember me functionality

Posted in Tutorial PHP, Zend Framework 2, Zend Framework 3 by samsonasik on August 13, 2017

If you’re building client based application which require oauth authentication to apigility application which uses time based expire access token, you may want to create a remember me functionality in client.

PHP Configuration

The very first required is set the client and the server side has same timezone, eg:

# your php.ini
date.timezone = "Asia/Jakarta"

This is ensure that you have actually same time for both client and api server.

DB data requirements

If you already setup the Oauth2 DB, you need to insert/update the client to support “password” and “refresh_token”, that use both means use space-separated value eg:

INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `grant_types`, `scope`, `user_id`) VALUES
(
    'test',
    '$2y$10$vbuy12RNSTJ.LHDdivegwu9dqkxh8h6OS4VoIX64HQGngAqUfcSJe',
    '/oauth/receivecode',
    'password refresh_token',
    NULL,
    NULL
);

Above sql insert data to oauth_clients table with client_id valued “test” with bcrypted client_secret “test”.

You can also insert a users data, for example:

INSERT INTO `oauth_users` (`username`, `password`) VALUES
(
    'test',
    '$2y$10$vbuy12RNSTJ.LHDdivegwu9dqkxh8h6OS4VoIX64HQGngAqUfcSJe'
),

Above sql insert data to oauth_users table with username valued “test” with bcrypted password “test”.

ZF-Oauth2 configuration

In apigility side, we can configure the “zf-oauth2” config, for example, as follows:

// config/autoload/global.php
return [
    // ...
    'zf-oauth2' => [
        'access_lifetime' => 1800,
        'options' => [
            'refresh_token_lifetime' => 604800,
            'always_issue_new_refresh_token' => true,
        ],
    ],
];

The configuration above means we can have an access token lifetime in 1800 seconds, and we can re-issue the new token lifetime with existing “refresh_token” as far as the time range is not > 604800 seconds ( 1 week ). For example, we authenticate with data:

{
    "grant_type": "password",
    "username": "test",
    "password": "test",
    "client_id": "test",
    "client_secret" : "test"
}

have authenticated tokens data like the following:

{
  "access_token": "8e4b0e5ddc874a6f1500514ef530dbea3976ae77",
  "expires_in": 1800,
  "token_type": "Bearer",
  "scope": null,
  "refresh_token": "d19b79cd376924409c14ee46e5230617482fb169"
}

The “refresh_token” is the key here.

The client application

I assume you’re using Zend Framework 2/3 application for client side, which we can use Zend\Authentication\AuthenticationService service. We can build custom Auth storage for it, eg:

namespace Application\Storage;

use Zend\Authentication\Storage;

class AuthStorage extends Storage\Session
{
    public function __construct()
    {
        parent::__construct('app_client');

        $sessionConfigOptions = [
            'use_cookies'     => true,
            'cookie_httponly' => true,
            'gc_maxlifetime'  => 1800,
            'cookie_lifetime' => 1800,
        ];
        $this->getSessionManager()->getConfig()
                                  ->setOptions($sessionConfigOptions);
    }

    public function rememberMe($time)
    {
        $this->getSessionManager()->rememberMe($time);
    }

    public function clear()
    {
        $this->getSessionManager()->forgetMe();
        parent::clear();
    }

    public function getSessionManager()
    {
        return $this->session->getManager();
    }
}

You can now create factory to build the AuthenticationService service with the authstorage like I blog posted at Create ZF Client Authentication for Apigility Oauth with ApigilityConsumer post.

On authentication part, eg: AuthenticationController, you can do:

$result = $this->authenticationService->authenticate();

if ($result->isValid()) {
    $storage = $this->authenticationService->getStorage();

    // save next "expires" time to session
    $storage->write(
        $storage->read() +
        [
            // it is better to use
            // api service to get the `oauth_access_tokens` real expires
            'expires' => date('Y-m-d H:i:s', time() + $read['expires_in'])
        ]
    );

    // for example, you have "rememberme" checkbox
    if (($rememberme = $request->getPost('rememberme')) == 1 ) {

        $storage->rememberMe(604800);

        $read = $storage->read();
        $storage->write(
            compact('rememberme') +
            $read +
            [
                // it is better to use
                // api service to get the `oauth_refresh_tokens` real expires
                'refreshExpires' => date('Y-m-d H:i:s', time() + 604800)
            ]
        );
    }
    // ...
}

We are going to use “expires” as immediate check session lifetime hit the expires, and “refreshExpires” to check when it stil be able to re-issue new token.

In bootstrap, for example, in Application\Module::onBootstrap() you can verify it to re-issue the token when access lifetime has hit.

namespace Application;

use Zend\Authentication\AuthenticationService;
use Zend\Mvc\MvcEvent;

class Module
{
    // ...
    public function onBootstrap(MvcEvent $e)
    {
        $services = $e->getApplication()->getServiceManager();
        $storage  = $services->get(AuthenticationService::class)->getStorage();
        $read     = $storage->read();

        if (isset($read['access_token'])) {

            $timeFirst   = strtotime($read['expires']);
            $currentTime = date('Y-m-d H:i:s');
            $timeSecond  = strtotime($currentTime);
            $counter     = $timeFirst - $timeSecond;

            if (! empty($rememberme = $read['rememberme'])) {

                $storage->getSessionManager()
                        ->getConfig()
                        ->setStorageOption('gc_maxlifetime', 604800)
                        ->setStorageOption('cookie_lifetime', 604800);
                
                if ($counter < 0 && $currentTime < $read['refreshExpires']) {

                    // API CALL to apigility oauth uri
                    // with grant_types = "refresh_token" and uses
                    // refresh_token as the key to re-issue new token
                    //
                    //    {
                    //        "grant_type"    : "refresh_token",
                    //        "refresh_token" : $read['refresh_token']
                    //        "client_id"     : "test",
                    //        "client_secret" : "test"
                    //    }
                    //
                    $storage->write(
                        [
                            // it is better to use
                            // api service to get the `oauth_access_tokens` real expires
                            'expires' => date('Y-m-d H:i:s', time() + $read['expires_in']),

                            // api service to get the `oauth_refresh_tokens` real expires
                            'refreshExpires' => date('Y-m-d H:i:s', time() + 604800),
                        ] +
                        compact('rememberme') +
                        [
                            "access_token": "<new access token based on oauth call>",
                            "expires_in": <new expire_in based on oauth call>,
                            "token_type": "<new token_type based on oauth call>",
                            "refresh_token": "<new refresh_token token based on oauth call>"
                        ] +
                        $read
                    );

                    $read = $storage->read();
                }
            }

            if ($currentTime > $read['expires'])) {
                // force clean up session
                $storage->clear();
            }

        }

    }
    // ...
}

Note

As commented in the codes sample above, in your real life application, it is beter to use real token expires instead of adding current time with expire_in time or manual fill refresh token lifetime. Do more automation yourself!

If you use Zend Framework 2/3 or Zend Expressive, you can try ApigilityConsumer for client module to consume api services. Enjoy 😉