Welcome to Abdul Malik Ikhsan's Blog

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 😉