Welcome to Abdul Malik Ikhsan's Blog

Testing CodeIgniter 4 application with kahlan ^3.0

Posted in CodeIgniter 4, Tutorial PHP by samsonasik on January 14, 2017

This is another post series about CodeIgniter and Kahlan, but now, it will uses CodeIgniter 4. CodeIgniter 4 requires some constants and required classes and functions to be included during application bootstrap to make front controller works.
For example, we need to do functional test App\Controllers\Home controller, this is the kahlan-config.php we need to have:

<?php

use CodeIgniter\Services;
use Config\App;
use Config\Autoload;

define('ENVIRONMENT', 'testing');
define('BASEPATH',    'system'      . DIRECTORY_SEPARATOR);
define('APPPATH',     'application' . DIRECTORY_SEPARATOR);
define('WRITEPATH',   'writable'    . DIRECTORY_SEPARATOR);
define('CI_DEBUG',    1);

require BASEPATH . 'Autoloader/Autoloader.php';
require APPPATH  . 'Config/Constants.php';
require APPPATH  . 'Config/Autoload.php';
require APPPATH  . 'Config/Services.php';

class_alias('Config\Services', 'CodeIgniter\Services');

$loader = Services::autoloader();
$loader->initialize(new Autoload());
$loader->register();

require BASEPATH . 'Common.php';
Services::exceptions(new App(), true)->initialize();

We can then place the spec under spec/ directory:

.
├── DCO.txt
├── README.md
├── application
│   ├── Controllers
│   │   └── Home.php
├── spec
│   └── Controllers
│       └── HomeDispatchSpec.php

We need to utilize CodeIgniter\CodeIgniter instance to run the test:

$codeIgniter = new CodeIgniter(memory_get_usage(), microtime(true),new App());
$codeIgniter->run();

So, we can place class instantiation in tests beforeAll(), and apply $_SESSION['argv'] and $_SESSION['argc'] to assign URI routing data:

$_SERVER['argv'] = [
    __FILE__,
    '/', // path
];
$_SERVER['argc'] = 2;

ob_start();
$this->codeIgniter->run();
$actual = ob_get_clean();

expect($actual)->toContain('Welcome to CodeIgniter');

Here is the complete tests:

<?php

namespace AppSpec\Controllers;

use Config\App;
use CodeIgniter\CodeIgniter;

describe('Home Dispatch', function () {

    beforeAll(function () {

        $this->codeIgniter = new CodeIgniter(
            memory_get_usage(),
            microtime(true),
            new App()
        );

    });

    describe('/', function () {

        it('contains "welcome" greeting', function () {

            $_SERVER['argv'] = [
                __FILE__,
                '/', // path
            ];
            $_SERVER['argc'] = 2;

            ob_start();
            $this->codeIgniter->run();
            $actual = ob_get_clean();

            expect($actual)->toContain('Welcome to CodeIgniter');

        });

    });

});

Time to run test:

$ vendor/bin/kahlan --coverage=4 --src=application/Controllers/Home.php
            _     _
  /\ /\__ _| |__ | | __ _ _ __
 / //_/ _` | '_ \| |/ _` | '_ \
/ __ \ (_| | | | | | (_| | | | |
\/  \/\__,_|_| |_|_|\__,_|_| | |

The PHP Test Framework for Freedom, Truth and Justice.

Working Directory: /Users/samsonasik/www/CodeIgniter4

.                                                                   1 / 1 (100%)



Expectations   : 1 Executed
Specifications : 0 Pending, 0 Excluded, 0 Skipped

Passed 1 of 1 PASS in 0.120 seconds (using 4Mo)

Coverage Summary
----------------
                               Lines           %

 \                             1 / 1     100.00%
└── App\                       1 / 1     100.00%
   └── Controllers\            1 / 1     100.00%
      └── Home                 1 / 1     100.00%
         └── Home::index()     1 / 1     100.00%

Total: 100.00% (1/1)

Coverage collected in 0.003 seconds (using an additionnal 16Ko)

Done 😉

Tagged with: ,

Functional Test for Zend\Expressive Routed Middleware with Kahlan ^3.0

Posted in testing, Tutorial PHP, Zend Framework by samsonasik on January 13, 2017

You may tried do functional test Zend\Expressive Routed Middleware and end up with “Unable to emit response; headers already sent” error.
This can happen because of during run test, the Test framework itself already run fwrite() or echo to build test report, and make the headers_sent() return true.

To handle that, we can use ob_start(), but since the header is sent in the background, we need to place in both places:

  • test bootstrap
  • during tests

Seriously? Yes! That’s make sure we only get Diactoros response that we use in the buffer to be tested.

Preparation

As usual, we need require kahlan/kahlan:^3.0 in require-dev:

$ composer require --dev kahlan/kahlan:^3.0 --sort-packages

Set Kahlan’s Bootstrap

In Kahlan, we can set tests bootstrap in kahlan-config.php, so we can write:

<?php
// kahlan-config.php
ob_start();

Write Spec and Run In Action

Now, if we use Expressive skeleton application, and for example, we need to test App\Action\PingAction routed middleware, we can write spec in spec directory:

.
├── composer.json
├── config
├── data
├── kahlan-config.php
├── public
├── spec
│   └── App
│       └── Action
│           ├── PingActionDispatchSpec.php
├── src
│   └── App
│       └── Action
│           ├── PingAction.php

As the App\Ping\PingAction is return Zend\Diactoros\Response\JsonResponse which contains “ack” data with time() method call:

return new JsonResponse(['ack' => time()]);

The spec can be the following:

<?php
namespace AppSpec\Action;

use Zend\Diactoros\ServerRequest;
use Zend\Expressive\Application;

describe('PingAction Dispatch', function () {

    beforeAll(function() {
        $container = require 'config/container.php';
        $this->app = $container->get(Application::class);
    });

    describe('/api/ping', function () {

        it('contains json "ack" data', function () {

            ob_start();

            allow('time')->toBeCalled()->andReturn('1484291901');

            $serverRequest = new ServerRequest([], [], '/api/ping', 'GET');
            $this->app->run($serverRequest);

            $actual = ob_get_clean();
            expect($actual)->toBe('{"ack":"1484291901"}');

        });

    });

});

As highlighted above, the ob_start() is placed during test also.

Now, we can run the test:

$ vendor/bin/kahlan --coverage=4 --src=src/App/Action/PingAction.php 
            _     _
  /\ /\__ _| |__ | | __ _ _ __
 / //_/ _` | '_ \| |/ _` | '_ \
/ __ \ (_| | | | | | (_| | | | |
\/  \/\__,_|_| |_|_|\__,_|_| | |

The PHP Test Framework for Freedom, Truth and Justice.

Working Directory: /Users/samsonasik/www/expressive

.                                                                   1 / 1 (100%)



Expectations   : 1 Executed
Specifications : 0 Pending, 0 Excluded, 0 Skipped

Passed 1 of 1 PASS in 0.210 seconds (using 7Mo)

Coverage Summary
----------------
                                        Lines           %

 \                                      1 / 1     100.00%
└── App\                                1 / 1     100.00%
   └── Action\                          1 / 1     100.00%
      └── PingAction                    1 / 1     100.00%
         └── PingAction::__invoke()     1 / 1     100.00%

Total: 100.00% (1/1)

Coverage collected in 0.003 seconds (using an additionnal 0o)

Done 😉

Setup Java9 Early Access in Mac OS X

Posted in Tutorial Java by samsonasik on January 7, 2017

I have a chance to try latest Java9 development build 151, and I am very glad to have it successfully installed in my system. I am using OS X “El Capitan” (you may have latest: Mac OS X “Sierra” which I think will not be different for a way to install it). Here is the steps I did to get it works:

  • Download the .dmg file from https://jdk9.java.net/download/ , select “Mac OS X” on “JDK” column (300+ MB file)
  • Install it
  • Edit ~/.bash_profile and add the following line (if not exists)
#JAVA
export JAVA_HOME=$(/usr/libexec/java_home)
export PATH=$JAVA_HOME/bin:$PATH
  • Apply ~/.bash_profile setting that has been modified by run command
$ source ~/.bash_profile

That’s it! Now, I can see it works:

java9-command-os-x

Merge multiple coverages for Kahlan with istanbul merge

Posted in testing, Tutorial PHP by samsonasik on December 24, 2016

As you may already knew, you can generate HTML coverage report with kahlan with the following command:

$ ./bin/kahlan --src=path/to/src --spec=path/to/spec --istanbul="coverage.json"
$ istanbul report

For multiple src path, with different specs location, as there are multiple coverages, we need to merge them. For example, we have the following application structure:

.
└── module
    ├── A
    │   ├── spec
    │   │   └── ASpec.php
    │   └── src
    │       └── A.php
    └── B
        ├── spec
        │   └── BSpec.php
        └── src
            └── B.php

Prepare dependencies

1. Install the following tools:

2. Install kahlan/kahlan:^3.0

$ composer require kahlan/kahlan:^3.0 --dev --sort-packages

Here is the sample of composer.json:

// composer.json
{
    "name": "samsonasik/kahlan-demo",
    "type": "project",
    "require-dev": {
        "kahlan/kahlan": "^3.0"
    },

    "autoload": {
        "psr-4": {
            "A\\": "module/A/src",
            "B\\": "module/B/src"
        }
    },

    "autoload-dev": {
        "psr-4": {
            "ASpec\\": "module/A/spec",
            "BSpec\\": "module/B/spec"
        }
    },

    "license": "MIT",
    "authors": [
        {
            "name": "Abdul Malik Ikhsan",
            "email": "samsonasik@gmail.com"
        }
    ],
    "config": {
        "bin-dir": "bin"
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

Write specs

We can write specs under module/{FOLDER}/spec.

Define Tasks on build.xml

We need to register multiple commands in build.xml

  • run bin/kahlan for module/A/src which set coverage target to coverage/coverge-A.json
  • run bin/kahlan for module/B/src which set coverage target to coverage/coverge-B.json
  • run instanbul-merge that merge coverage/coverage*.json to coverage.json
  • run instanbul report

The build.xml can be like the following:

<?xml version="1.0" encoding="UTF-8"?>
<project name="kahlan-demo app" default="build">

    <property name="toolsdir" value="${basedir}/bin/"/>
    <property name="moduledir" value="${basedir}/module/"/>

    <target name="build"
            depends="kahlan,coverage-report"
            description=""/>

    <target name="kahlan"
            description="Run kahlan">

            <!-- A -->
            <exec executable="${toolsdir}kahlan" failonerror="true" taskname="kahlan">
                <arg
                    line="--spec=${moduledir}A/spec/
                    --src=${moduledir}A/src
                    --istanbul=coverage/coverage-A.json
                    "/>
            </exec>
            <!-- A -->

            <!-- B -->
            <exec executable="${toolsdir}kahlan" failonerror="true" taskname="kahlan">
                <arg
                    line="--spec=${moduledir}B/spec/
                    --src=${moduledir}B/src
                    --istanbul=coverage/coverage-B.json
                    "/>
            </exec>
            <!-- B -->

    </target>

    <target name="coverage-report"
            description="Run coverage report generation">

            <!-- merging coverage under coverage/ to coverage.json -->
            <exec executable="istanbul-merge" failonerror="true" taskname="istanbul merge">
                <arg line="--out coverage.json coverage/*.json"/>
            </exec>

            <!-- generate report with use of merged coverages to coverage.json  -->
            <exec executable="istanbul" failonerror="true" taskname="istanbul report">
                <arg line="report"/>
            </exec>

    </target>

</project>

Run tasks

We can run ant command and we will get the following output:

$ ant
Buildfile: /Users/samsonasik/www/kahlan-demo/build.xml

kahlan:
   [kahlan]             _     _
   [kahlan]   /\ /\__ _| |__ | | __ _ _ __
   [kahlan]  / //_/ _` | '_ \| |/ _` | '_ \
   [kahlan] / __ \ (_| | | | | | (_| | | | |
   [kahlan] \/  \/\__,_|_| |_|_|\__,_|_| | |
   [kahlan]
   [kahlan] The PHP Test Framework for Freedom, Truth and Justice.
   [kahlan]
   [kahlan] Working Directory: /Users/samsonasik/www/kahlan-demo
   [kahlan]
   [kahlan] .                                                                   1 / 1 (100%)
   [kahlan]
   [kahlan]
   [kahlan]
   [kahlan] Expectations   : 1 Executed
   [kahlan] Specifications : 0 Pending, 0 Excluded, 0 Skipped
   [kahlan]
   [kahlan] Passed 1 of 1 PASS in 0.074 seconds (using 2Mo)
   [kahlan]
   [kahlan] Coverage Summary
   [kahlan] ----------------
   [kahlan]
   [kahlan] Total: 100.00% (1/1)
   [kahlan]
   [kahlan] Coverage collected in 0.002 seconds (using an additionnal 70Ko)
   [kahlan]
   [kahlan]
   [kahlan]             _     _
   [kahlan]   /\ /\__ _| |__ | | __ _ _ __
   [kahlan]  / //_/ _` | '_ \| |/ _` | '_ \
   [kahlan] / __ \ (_| | | | | | (_| | | | |
   [kahlan] \/  \/\__,_|_| |_|_|\__,_|_| | |
   [kahlan]
   [kahlan] The PHP Test Framework for Freedom, Truth and Justice.
   [kahlan]
   [kahlan] Working Directory: /Users/samsonasik/www/kahlan-demo
   [kahlan]
   [kahlan] .                                                                   1 / 1 (100%)
   [kahlan]
   [kahlan]
   [kahlan]
   [kahlan] Expectations   : 1 Executed
   [kahlan] Specifications : 0 Pending, 0 Excluded, 0 Skipped
   [kahlan]
   [kahlan] Passed 1 of 1 PASS in 0.045 seconds (using 2Mo)
   [kahlan]
   [kahlan] Coverage Summary
   [kahlan] ----------------
   [kahlan]
   [kahlan] Total: 100.00% (1/1)
   [kahlan]
   [kahlan] Coverage collected in 0.001 seconds (using an additionnal 70Ko)
   [kahlan]
   [kahlan]

coverage-report:
[istanbul report] Done

build:

BUILD SUCCESSFUL
Total time: 2 seconds

Now, we have successfully gotten merged coverage results with open coverage/lcov-report/index.html:

kahlan-multiple-cov-merge-result

Using Layout in CodeIgniter 4

Posted in CodeIgniter 4, Tutorial PHP by samsonasik on November 25, 2016

CodeIgniter 4 is not ready for production use, but we can already play with it. We can apply layout support to avoid repetitive header/footer/sidebar html code in our application’s views by provide helper and autoload it in the hook.

Ok, let’s start, first, we can clone a CI4 project by run command:

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

We can use PHP-Development server launcher by go to CodeIgniter4 directory and run command:

php serve.php 

and we will get the page:
ci4
The view of “Home::index()” of controller is placed at application/Views/welcome_message.php:

application/
├── Config
├── Controllers
│   └── Home.php
├── Views
│   ├── errors
│   ├── form.php
│   └── welcome_message.php

with code called in the controller like the following:

<?php namespace App\Controllers;

use CodeIgniter\Controller;

class Home extends Controller
{
    public function index()
    {
        return view('welcome_message');  
    }
}

For example, we need to move the header and footer to the separate file, named application/Views/layout.php:

application/
├── Config
├── Controllers
│   └── Home.php
├── Views
│   ├── errors
│   ├── form.php
│   ├── layout.php
│   └── welcome_message.php

So, Let’s prepare the layout:

<html>
   <head></head> <!-- move code from welcome_message.php's html <head> to here -->
   <body>
        <!-- move code from welcome_message.php's style to here, 
             or use separate file for css and apply to <head>
        -->
	
        <div class="wrap">
            <?php echo $content; ?>
        </div>
   </body>
</html>

Now, we can write a helper to wrap it, for example, named render helper. We can create a file named application/Helpers/render_helper.php:

application/
├── Config
├── Controllers
├── Helpers
│   └── render_helper.php
├── Views

Our render helper can have function() for render view with layout functionality, which we get the content of view, and then apply to the layout:

<?php

if ( ! function_exists('render'))
{
    function render(string $name, array $data = [], array $options = [])
    {
        return view(
            'layout',
            [
                'content' => view($name, $data, $options),
            ],
            $options
        );
    }
}

If most of the controllers will use the render helper, we can then autoload it in the Hooks:

application/
├── Config
│   ├── Hooks.php
├── Controllers
├── Helpers
├── Views

with post_controller_constructor hook point:

<?php namespace Config;

use CodeIgniter\Hooks\Hooks;

Hooks::on('post_controller_constructor', function() {
    helper('render');
});

And now, we are ready to use the render() function in the controller:

<?php namespace App\Controllers;

use CodeIgniter\Controller;

class Home extends Controller
{
    public function index()
    {
        return render('welcome_message');  
    }
}

Done 😉

Unit and Functional testing Zend Framework 3 Controller with Kahlan 3.0

Posted in testing, Tutorial PHP, Zend Framework 2 by samsonasik on October 24, 2016

This post will cover unit and functional testing ZF3 Controller with Kahlan 3.0. For example, you have a ZF3 Skeleton application with an IndexController like the following:

namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        return new ViewModel();
    }
}

As usual, we need to require kahlan/kahlan:^3.0 via composer command:

composer require --dev kahlan/kahlan:^3.0 --sort-packages

You can then write the spec. Let’s write our spec inside module/Application/spec like the following structure:

module/Application/
├── config
├── spec
│   ├── Controller
│   │   ├── IndexControllerDispatchSpec.php
│   │   └── IndexControllerSpec.php
├── src
│   ├── Controller
│   │   ├── IndexController.php

if we are only have the 1 module, named Application module, we can define the spec and src path via kahlan-config.php like the following:

// ./kahlan.config.php
$commandLine = $this->commandLine();
$commandLine->option('spec', 'default', 'module/Application/spec');
$commandLine->option('src', 'default', 'module/Application/src');

Or for multi-modules, we can run parallel command that specify --spec and --src in command like the following:

vendor/bin/kahlan --spec=module/Application/spec --src=module/Application/src

in each iteration. If you’re using ant, you can write a build.xml for tasks definition:

<?xml version="1.0" encoding="UTF-8"?>
<project name="My Website" default="build">
    
    <!-- executable files directory definition -->
    <property name="toolsdir" value="${basedir}/vendor/bin/"/>
    <!-- module directory definition --> 
    <property name="moduledir" value="${basedir}/module/"/>

    <target name="build"
            depends="kahlan"
            description=""/>

    <target name="kahlan"
            description="Run kahlan">
        
        <parallel>    
        
            <!-- Application -->    
            <exec executable="${toolsdir}kahlan" failonerror="true" taskname="kahlan">
                <arg 
                    line="-spec=${moduledir}Application/spec/ 
                    --src=${moduledir}Application/src"/>
            </exec>
            <!-- Application -->
            
            <!-- other modules run test definition go here --> 
        </parallel>
        
    </target>

</project>

Unit testing

Let’s write the unit testing inside spec/Controller/IndexControllerSpec.php:

namespace ApplicationSpec\Controller;

use Application\Controller\IndexController;
use Zend\View\Model\ViewModel;

describe('IndexController', function () {
    
    given('controller', function () {
        
        return new IndexController();
    
    });
    
    describe('->indexAction()', function() {
        
        it('instance of ViewModel', function() {
            
            $actual = $this->controller->indexAction();
            expect($actual)->toBeAnInstanceOf(ViewModel::class);
            
        });
        
    });
    
});

That’s enough for IndexController::indexAction() unit test, nothing complex logic we need to accomodate as it only return the ViewModel instance, so we just need to check if return values is instance of ViewModel.

Functional Testing

Now, we need to make sure if the dispatch response of IndexController::indexAction() by open ‘/’ url that shown by user is the expected result, that show a welcome page, let’s do with spec/Controller/IndexControllerDispatchSpec.php:

namespace ApplicationSpec\Controller;

use Zend\Console\Console;
use Zend\Mvc\Application;

describe('IndexController Dispatch', function () {
    
    // setup the Application
    beforeAll(function () {
        
        Console::overrideIsConsole(false);
        $appConfig = include __DIR__ . '/../../../../config/application.config.php';
        $this->application = Application::init($appConfig);

        $events = $this->application->getEventManager();
        $this->application->getServiceManager()
                          ->get('SendResponseListener')
                          ->detach($events);

    });
    
    // dispatch '/' page tests
    describe('/', function() {
        
        it('contains welcome page', function() {
            
            $request     = $this->application->getRequest();
            
            $request->setMethod('GET');
            $request->setUri('/'); 
            
            // run app with '/' url
            $app =  $this->application->run();
            
            // expect actual response is contain
            // a welcome page
            expect(
                $app->getResponse()->toString()
            )->toContain('<h1>Welcome to <span class="zf-green">Zend Framework</span></h1>');
            
        });
        
    });
    
});

That’s it 😉

Testing CodeIgniter application with Kahlan 3.0

Posted in testing, Tutorial PHP by samsonasik on October 19, 2016

Really? Yes, it is testable with kahlan – The PHP Test Framework for Freedom, Truth, and Justice -. Let’s give a try, I am using CodeIgniter 3.1.0 for this example. You can download CodeIgniter from codeigniter.com . For example, we are going to test its Welcome controller.

Setup:
a. require kahlan/kahlan

composer require --dev kahlan/kahlan:^3.0

b. setup minimal autoloading in kahlan-config.php in root CodeIgniter project with Kahlan\Filter\Filter::register() to register its autoloader:

<?php
// ./kahlan.config.php
use Kahlan\Filter\Filter;

define('CI_VERSION', '3.1.0');
define('ENVIRONMENT', 'development');
define('APPPATH', 'application/');
define('VIEWPATH', 'application/views/');
define('BASEPATH', 'system/');

require_once BASEPATH . 'core/Common.php';
function &get_instance()
{
    return CI_Controller::get_instance();
}

Filter::register('ci.autoloader', function($chain) {
    $this->autoloader()->addClassMap([
        // core
        'CI_Controller' =>  BASEPATH . 'core/Controller.php',
        
        // controllers
        'Welcome' => APPPATH . 'controllers/Welcome.php',
    ]);
    return $chain->next();
});
Filter::apply($this, 'namespaces', 'ci.autoloader');

c. Define the spec, we can create spec/controllers directory for placing controller spec:

application/
spec/
└── controllers
    └── WelcomeSpec.php
system/
kahlan-config.php

d. Write the spec:

<?php

describe('Welcome', function () {
    
    describe('->index()', function () {
        
        it('contains welcome message', function() {
            
            $controller = new Welcome();
            
            ob_start();
            $controller->index();
            $actual = ob_get_clean();
            
            expect($actual)->toContain('Welcome to CodeIgniter!');
            
        });
        
    });
    
});

e. run the kahlan command

vendor/bin/kahlan  --coverage=4 --src=application/

and you will get the following output:
kahlan-ci-output

What If we load model into controller ? How to test ?
We can also, For example, you have a model named Welcome_model which check what passed name that will be used in controller:

<?php
// application/models/Welcome_model.php
class Welcome_model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
    }
    
    public function greeting($name)
    {
        if (trim($name) === '') {
            return 'Hello Guest';
        }
        
        return 'Hello ' . $name;
    }
}

As we need to check uri segment, we need to register new route in application/config/routes.php:

$route['welcome/:name'] = 'welcome/index';

And now, we load in controller:

<?php
// application/controllers/Welcome.php

class Welcome extends CI_Controller
{	
    public function __construct()
    {
         parent::__construct();
		
	    $this->load->model('Welcome_model', 'welcome');
    }
	
    public function index()
    {
	    $greeting = $this->welcome->greeting($this->uri->segment(3));	
	    $this->load->view('welcome_message', ['greeting' => $greeting]);
    }
}

On view ( application/views/welcome_message.php ), we modify the greeting:

<?php // application/views/welcome_message.php ?>
<h1><?php echo $greeting; ?>, Welcome to CodeIgniter!</h1>

At this case, we need a CI_URI::segment() and Welcome_model::greeting() to be stubbed in the spec, so, we need to modify our kahlan-config.php to register CI_URI, CI_Model and its Welcome_model classes:

<?php
// ./kahlan-config.php
use Kahlan\Filter\Filter;

define('CI_VERSION', '3.1.0');
define('ENVIRONMENT', 'development');
define('APPPATH', 'application/');
define('VIEWPATH', 'application/views/');
define('BASEPATH', 'system/');

require_once BASEPATH . 'core/Common.php';
function &get_instance()
{
    return CI_Controller::get_instance();
}

Filter::register('ci.autoloader', function($chain) {
    $this->autoloader()->addClassMap([
        // core
        'CI_Controller' =>  BASEPATH . 'core/Controller.php',
        'CI_URI' =>  BASEPATH . 'core/URI.php',
        'CI_Model' => BASEPATH . 'core/Model.php',
        
        // controllers
        'Welcome' => APPPATH . 'controllers/Welcome.php',
        
        // models
        'Welcome_model' => APPPATH . 'models/Welcome_model.php',
    ]);
    return $chain->next();
});
Filter::apply($this, 'namespaces', 'ci.autoloader');

Then, here is the spec we will need to have:

<?php

use Kahlan\Plugin\Double;

describe('Welcome', function () {
     
    describe('->index()', function () {
         
        it('contains welcome message to specific passed name parameter', function() {
            
            define('UTF8_ENABLED', TRUE); // used by CI_Uri
            
            allow('is_cli')->toBeCalled()->andReturn(false); // to disable _parse_argv call
            
            //  stubs CI_Uri::segment() 
            $uri = Double::instance(['extends' => 'CI_URI']);
            allow($uri)->toReceive('segment')->with(3)->andReturn('samsonasik');
            
            // stubs Welcome_model::greeting()              
            $welcome_model = Double::instance(['extends' => 'Welcome_model']);
            allow($welcome_model)->toReceive('greeting')
                                 ->with('samsonasik')
                                 ->andReturn('Hello samsonasik');
                         
            $controller = new Welcome();
            $controller->uri = $uri;
            $controller->welcome = $welcome_model;
             
            ob_start();
            $controller->index();
            $actual = ob_get_clean();
             
            expect($actual)->toContain('Hello samsonasik, Welcome to CodeIgniter!');
             
        });
         
    });
     
});

As we are stubbing Welcome_model::greeting(), here is the expected output that will be shown on run test:
kahlan-ci-output-with-model-load-in-controller-test.
If we want to make Welcome_model::greeting() coverable, we can create a new spec for testing real Welcome_model::greeting() call.

You wanna grab full sample? I created a repository for it so you can try: https://github.com/samsonasik/ci_310_with_kahlan 😉

Done 😉

Apigility: Create custom Authentication for Oauth2 with service delegators

Posted in Tutorial PHP, Zend Framework by samsonasik on August 21, 2016

apigility logo Custom authentication in apigility is do-able with service delegators. We need to wrap ZF\MvcAuth\Authentication\DefaultAuthenticationListener::class in decorator. For example, we want to use ZF\OAuth2\Adapter\PdoAdapter but want to modify checkUserCredentials($username, $password) to include is_active check. Let’s do it!

  1. Setup Apigility Authentication with Oauth2
  2. With in assumption, we have the following config:
    return [
    // ... config/autoload/local.php
        'zf-oauth2' => [
          'db' => [
              'driver' => 'PDO_Mysql',
              'username' => 'root',
              'password' => '',
              'dsn' => 'mysql:host=localhost;dbname=app_oauth',
          ],
        ],
    // ...
    ];
    

    We can then modify config/autoload/zf-mvc-auth-oauth2-override.global.php as follows:

    // config/autoload/zf-mvc-auth-oauth2-override.global.php
    return [
        'service_manager' => [
            'factories' => [
                'ZF\OAuth2\Service\OAuth2Server' 
                    => 'Application\MvcAuth\NamedOAuth2ServerFactory',
            ],
        ],
    ];
    
  3. Define our own NamedOAuth2ServerFactory to use our own OAuth2ServerFactory for OAuth2\Server instance creation
    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    
    class NamedOAuth2ServerFactory
    {
        /**
         * @param ContainerInterface $container
         *
         * @return callable
         */
        public function __invoke(ContainerInterface $container)
        {
            $config = $container->get('config');
    
            $oauth2Config = isset($config['zf-oauth2']) ? $config['zf-oauth2'] : [];
            $mvcAuthConfig = isset($config['zf-mvc-auth']['authentication']['adapters'])
                ? $config['zf-mvc-auth']['authentication']['adapters']
                : [];
    
            $servers = (object) ['application' => null, 'api' => []];
    
            return function ($type = null) use (
               $oauth2Config, $mvcAuthConfig, $container, $servers
            ) {
                foreach ($mvcAuthConfig as $name => $adapterConfig) {
                    if (!isset($adapterConfig['storage']['route'])) {
                        // Not a zf-oauth2 config
                        continue;
                    }
    
                    if ($type !== $adapterConfig['storage']['route']) {
                        continue;
                    }
    
                    // Found!
                    return $servers->api[$type] = OAuth2ServerFactory::factory(
                        $adapterConfig['storage'],
                        $container
                    );
                }
            };
        }
    }
    
  4. Create our Application\MvcAuth\OAuth2ServerFactory based on \ZF\MvcAuth\Factory\OAuth2ServerFactory
    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    use OAuth2\GrantType\AuthorizationCode;
    use OAuth2\GrantType\ClientCredentials;
    use OAuth2\GrantType\RefreshToken;
    use OAuth2\GrantType\UserCredentials;
    use OAuth2\GrantType\JwtBearer;
    use OAuth2\Server as OAuth2Server;
    
    final class OAuth2ServerFactory
    {
        private function __construct()
        {
        }
    
        public static function factory(array $config, ContainerInterface $container)
        {
            $allConfig = $container->get('config');
            $oauth2Config = isset($allConfig['zf-oauth2']) ? $allConfig['zf-oauth2'] : [];
            $options = self::marshalOptions($oauth2Config);
    
            $oauth2Server = new OAuth2Server(
                $container->get(\ZF\OAuth2\Adapter\PdoAdapter::class),
                $options
            );
    
            return self::injectGrantTypes($oauth2Server, $oauth2Config['grant_types'], $options);
        }
    
       private static function marshalOptions(array $config)
       { 
           // same as \ZF\MvcAuth\Factory\OAuth2ServerFactory::marshalOptions()
       }
       
        private static function injectGrantTypes(
           OAuth2Server $server,
           array $availableGrantTypes,
           array $options
       ) {
          // same as \ZF\MvcAuth\Factory\OAuth2ServerFactory::injectGrantTypes()
       }
    }
    
  5. As we want custom PdoAdapter, we need to map \ZF\OAuth2\Adapter\PdoAdapter::class to our PdoAdapter, for example: Application\MvcAuth\PdoAdapter:
    namespace Application\MvcAuth;
    
    use Zend\Crypt\Bcrypt;
    use ZF\OAuth2\Adapter\PdoAdapter as BasePdoAdapter;
    
    class PdoAdapter extends BasePdoAdapter
    {
        public function checkUserCredentials($username, $password)
        {
            $stmt = $this->db->prepare(
                'SELECT * from oauth_users where username = :username and is_active = 1'
            );
            $stmt->execute(compact('username'));
            $result = $stmt->fetch();
    
            if ($result === null) {
                return false;
            }
    
            // bcrypt verify
            return $this->verifyHash($password, $result['password']);
        }
    }
    
  6. For our Application\MvcAuth\PdoAdapter, we need to define factory for it:
    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    use ZF\OAuth2\Factory\PdoAdapterFactory as BasePdoAdapterFactory;
    
    class PdoAdapterFactory extends BasePdoAdapterFactory
    {
        public function __invoke(ContainerInterface $container)
        {
            $config = $container->get('config');
    
            return new PdoAdapter([
                'dsn' => $config['zf-oauth2']['db']['dsn'],
                'username' => $config['zf-oauth2']['db']['username'],
                'password' => $config['zf-oauth2']['db']['password'],
                'options' => [],
            ], []);
        }
    }
    
  7. Register the adapter into service manager into config/autoload/global.php
    // config/autoload/global.php
    return [
    // ... 
        'service_manager' => [
            'factories' => [
                \ZF\OAuth2\Adapter\PdoAdapter::class => 
                     \Application\MvcAuth\PdoAdapterFactory::class,
            ],
        ],
    // ...
    ];
    

  8. Time to attach the \ZF\OAuth2\Adapter\PdoAdapter into our delegated service ZF\MvcAuth\Authentication\DefaultAuthenticationListener via delegator factory

    namespace Application\MvcAuth;
    
    use Interop\Container\ContainerInterface;
    use OAuth2\Server as OAuth2Server;
    use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
    use ZF\MvcAuth\Authentication\OAuth2Adapter;
    
    class AuthenticationListenerDelegatorFactory implements DelegatorFactoryInterface
    {
        public function __invoke(
           ContainerInterface $container,
           $name,
           callable $callback,
           array $options = null
       ) {
            $listener = call_user_func($callback);
            $listener->attach(
                new OAuth2Adapter(
                    new Oauth2Server(
                        $container->get(\ZF\OAuth2\Adapter\PdoAdapter::class),
                        ['Bearer']
                    )
                )
            );
    
            return $listener;
        }
    }
    

  9. Last one! Register our AuthenticationListenerDelegatorFactory into service delegators:

    // config/autoload/global.php
    return [
    // ... 
        'service_manager' => [
            'delegators' => [
                \ZF\MvcAuth\Authentication\DefaultAuthenticationListener::class => [
                    \Application\MvcAuth\AuthenticationListenerDelegatorFactory::class
                ],
            ],
        ],
    // ...
    ];
    

Done 😉

Using Middleware in CakePHP “3.next”

Posted in CakePHP by samsonasik on May 29, 2016

Middleware support will be come in CakePHP 3.3.0, but you can try it now, there is a branch for it named “3.next”! If you want to start with composer create-project, you can run with the following command:

composer create-project --prefer-dist cakephp/app <dir-target> dev-3.next

By specifying “dev-3.next” after directory target, you will pull the 3.next branch of “cakephp/app”. Once it done, you will now have an Application class inside src/ that bootstrap the application and also setup the middleware your application will use:

namespace App;

class Application extends BaseApplication
{
    public function bootstrap(){ /** **/}

    public function middleware($middleware)
    {
        $middleware->push(new ErrorHandlerMiddleware());
        $middleware->push(new AssetMiddleware());
        $middleware->push(new RoutingMiddleware());

        return $middleware;
    }
}

Now, let say you want to add your own middleware, named “Authorization” middleware, let’s create it:

namespace App\Middleware;

use Cake\Network\Session;
use Cake\Core\Configure;
use Zend\Diactoros\Response\RedirectResponse;

class Authorization
{
    public function __invoke($request, $response, $next)
    {
        $session             =  Session::create(Configure::read('Session'));
        $checkHasUserSession = $session->check('user');

        $path = $request->getUri()->getPath();
        if ($path === '/admin' && ! $checkHasUserSession) {
            return new RedirectResponse('/auth');
        }

        return $next($request, $response);
    }
}

The “Authorization” middleware we just created now needs to be registered via middleware->push:

namespace App;

use App\Middleware\Authorization;

class Application extends BaseApplication
{
    public function middleware($middleware)
    {
        $middleware->push(new ErrorHandlerMiddleware());
        $middleware->push(new AssetMiddleware());
        $middleware->push(new RoutingMiddleware());
        
        //add the Authorization middleware
        $middleware->push(new Authorization());

        return $middleware;
    }
}

Done 😉

References:
1. http://www.slideshare.net/markstory/future-of-http-in-cakephp

Tagged with: ,

Using ZF Component’s ConfigProvider in Expressive

Posted in Tutorial PHP, Zend Framework by samsonasik on May 24, 2016

If you already tried building Expressive application using modular approach with Expressive Config Manager, you can just use ZF Component’s services by consuming its config provider. If you didn’t use it, you need to first require the Expressive Config Manager in your Expressive application:

composer require mtymek/expressive-config-manager

When done, you can modify the config/config.php like the following:

use Zend\Expressive\ConfigManager\ConfigManager;
use Zend\Expressive\ConfigManager\PhpFileProvider;

$configManager = new ConfigManager([
    new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
]);

return new ArrayObject($configManager->getMergedConfig());

Now, for example, you want to consume Zend\Db services, you can do:

1. Require it

 composer require zendframework/zend-db

2. Register in config/config.php

use Zend\Expressive\ConfigManager\ConfigManager;
use Zend\Expressive\ConfigManager\PhpFileProvider;

$configManager = new ConfigManager([
    new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
    new Zend\Db\ConfigProvider()
]);

return new ArrayObject($configManager->getMergedConfig());

By provide Zend\Db Config Provider, you can now consume its service, let’s try define sample DB config:

// config/autoload/db.local.php
return [
    'db' => [
        'username' => 'root',
        'password' => '',

        'driver' => 'Pdo',
        'dsn' => 'mysql:dbname=test;host=localhost',
        'driver_options' => [
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ],
    ],
];

And now, you can consume \Zend\Db\Adapter\AdapterInterface::class as service. Check the per-component’s ConfigProvider::getDependencyConfig() for available services if you want to use them.

Done 😉

Zend Framework 2 : Using Component as Module

Posted in Tutorial PHP, Zend Framework 2 by samsonasik on May 17, 2016

If you use latest zendframework by requiring “zendframework/zendframework”: “^2.5”, You will now get various zendframework components tagged as ~2.7 or ~2.8 as per-component has own life based on each evolution. Interestingly, many components that has services, now act as module. You can check the components on zendframework blog post. In the components mentioned in the post, they will have Module class that uses ConfigProvider class when registering services. If you don’t get the Module class yet inside the components mentioned, you need to run:

composer update

Usage
We can consume the components as modules, for example, in config/application.config.php, you can define:

return [
    'modules' => [
        'Application',
        'Zend\Cache'
    ],
    //  other configs here 
];

If you check the services registered in Zend\Cache, you will get Zend\Cache\Service\StorageCacheAbstractServiceFactory::class that registered in abstract_factories, that allow us to configure cache services via array with ‘caches’ config as limbo when no services that has name inside the ‘caches’ array registered yet. So, for example, we have configured ‘caches’ config:

// config/autoload/global.php
return [
    // ...
    'caches' => [
        'cache_file' => [
            'adapter' => 'filesystem',
            'options' => [
                'ttl' => 7200,
                'cache_dir' => './data/cache',
            ],
        ],
    ],
];

We have cache_file as service name that utilize filesystem cache adapter as config above. We then can just consume the cache_file via service manager:

// assumption: $services is a Zend\ServiceManager\ServiceManager 
// that pulled during Mvc workflow
$services->get('cache_file')->setItem('foo', 'fooValue');
echo $services->get('cache_file')->getItem('foo');

Done 😉

Start Using Middleware Approach with new zend-mvc

Posted in Tutorial PHP, Zend Framework, Zend Framework 2 by samsonasik on March 1, 2016

zend-mvc 2.7.0 is coming, beside of the forward compatibility with V3 components, there is new middleware listener that allow us to do Dispatching PSR-7 middleware. The middleware can be an invokable class with __invoke() method like the following:

function __invoke($request, $response)
{
    $response->getBody()->write('Hello World!');

    return $response;
}

Ok, let’s start try it, create new project:

$ composer create-project zendframework/skeleton-application:dev-master newzf

After composer create-project done, as usual, you will get new project. zend-mvc 2.7.0 released today, so, You should get zend-mvc 2.7.0 already by run:

$ composer update

Now, We can create a middleware, for example, a HomeAction middleware:

namespace Application\Middleware;

class HomeAction
{
    public function __invoke($request, $response)
    {
        $response->getBody()->write('Hello World!');
        return $response;
    }
}
// module/Application/src/Application/Middleware/HomeAction.php

We then can replace the ‘home’ route:

namespace Application;

// ...
    'home' => [
        'type' => 'Literal',
        'options' => [
            'route' => '/',
            'defaults' => [
                'middleware' => Middleware\HomeAction::class,
            ],
        ],
    ],
// ...
// module/Application/config/module.config.php

As the Application\Middleware\HomeAction is a service, then it need to be registered in service_manager:

namespace Application;

use Zend\ServiceManager\Factory\InvokableFactory;

// ...
    'service_manager' => [
        'factories' => [
            Middleware\HomeAction::class => InvokableFactory::class,
        ],
    ]
// ...
// module/Application/config/module.config.php

Everything seems configured correctly, now, let’s start the server:

$ php -S localhost:8080 -t public

And open up in the browser: http://localhost:8080 ! So, the “Hello World!” will be shown!

Work with ViewModel

So, you now want to work with ViewModel with its layout, You can! Let’s do it. You can inject the Middleware with the Renderer and ViewManager.

use Zend\View\Renderer\PhpRenderer;
use Zend\Mvc\View\Http\ViewManager;

class HomeAction
{
    // ...
    public function __construct(
        PhpRenderer $renderer,
        ViewManager $view
    ) {
        $this->renderer = $renderer;
        $this->view = $view;
    }
    // ...
}
// module/Application/src/Application/Middleware/HomeAction.php

To make it work, we can create factory for it:

namespace Application\Middleware;

class HomeActionFactory
{
    public function __invoke($container)
    {
        $viewRenderer = $container->get('ViewRenderer');
        $viewManager  = $container->get('ViewManager');

        return new HomeAction($viewRenderer, $viewManager);
    }
}
// module/Application/src/Application/Middleware/HomeActionFactory.php

Based on the factory above, we then need to update the registered Application\Middleware\HomeAction service:

namespace Application;

// ...
    'service_manager' => [
        'factories' => [
            Middleware\HomeAction::class => Middleware\HomeActionFactory::class,
        ],
    ]
// ...
// module/Application/config/module.config.php

So, now, you can update the Middleware as follows:

use Zend\View\Renderer\PhpRenderer;
use Zend\Mvc\View\Http\ViewManager;
use Zend\View\Model\ViewModel;
use Zend\Diactoros\Response\HtmlResponse;

class HomeAction
{
    // ...
    public function __invoke($request, $response)
    {
        $viewModel = new ViewModel();
        $viewModel->setTemplate('application/index/index');
        
        $layout = $this->view->getViewModel();
        $layout->setVariable(
            'content',
            $this->renderer->render($viewModel)
        );

        return new HtmlResponse($this->renderer->render($layout));
    }
    // ...
}
// module/Application/src/Application/Middleware/HomeAction.php

Done! 😉

References:
https://gist.github.com/weierophinney/b9dbff92e4446f49e248
https://github.com/weierophinney/2015-10-22-ZF3

Testing Hard Dependency with AspectMock

Posted in Teknologi, Tutorial PHP by samsonasik on January 28, 2016

This is another testing legacy application post. Don’t tell your client/other people to refactor, if your job is only to make tests for it, as at some situations, there are reasons to not refactor. You may have situation to test hard dependency that impossible to be mocked and stubbed. There is a library named AspectMock for it, that you can use in PHPUnit, for example.

So, to have it, you can require via composer:

composer require "codeception/aspect-mock:^0.5.5" --dev

For example, you have the following class:

namespace App;

class MyController
{
    public function save()
    {
        $user = new User();
        if (! $user->save()) {
            echo 'not saved';
            return;
        }

        echo 'saved';
    }
}

That bad, huh! Ok, let’s deal to tests it even you don’t really like it. First, setup your phpunit.xml to have ‘backupGlobals=”false”‘ config:

<?xml version="1.0" encoding="UTF-8"?>

<phpunit
    colors="true"
    backupGlobals="false"
    bootstrap="bootstrap.php">
    <testsuites>
        <testsuite name="AppTest">
            <directory suffix=".php">./test</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist addUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./src</directory>
        </whitelist>
    </filter>
</phpunit>

Now, based on config above, you can create bootstrap.php:

include 'vendor/autoload.php';

use AspectMock\Kernel;

$kernel = Kernel::getInstance();
$kernel->init([
    'debug' => true,
    'cacheDir' => __DIR__ . '/data/cache',
    'includePaths' => [__DIR__.'/src'],
]);

Assumption: You have ‘./data/cache’ for saving cache and ‘src/’ for your source code directory, if you use your own autoloader, you can add:

// ...
$kernel->loadFile('YourAutoloader.php');

as the AspectMock documentation mentioned.

Now, time to write the tests:

  1. Preparation
    namespace AppTest;
    
    use PHPUnit_Framework_TestCase;
    use App\MyController;
    use AspectMock\Test as test;
    
    class MyControllerTest extends PHPUnit_Framework_TestCase
    {
        private $myController;
    
        protected function setUp()
        {
            $this->myController = new MyController;
        }
    
        protected function tearDown()
        {
            test::clean(); // remove all registered test doubles
        }
    }
    

  2. write the test cases

class MyControllerTest extends PHPUnit_Framework_TestCase
{
    // ...
    public function provideSave()
    {
        return [
            [true, 'saved'],
            [false, 'not saved'],
        ];
    }

    /**
     * @dataProvider provideSave
     */
    public function testSave($succeed, $echoed)
    {
        // mock
        $userMock = test::double('App\User', ['save' => $succeed]);

        ob_start();
        $this->myController->save();
        $content = ob_get_clean();
        $this->assertEquals($echoed, $content);

        // stub
        $userMock->verifyInvoked('save');
    }
    // ...
}

Done 😉

references:
https://github.com/Codeception/AspectMock
https://twitter.com/grmpyprogrammer/status/642847787713884160
https://littlehart.net/atthekeyboard/2014/12/14/stop-telling-me-to-refactor/

Monkey Patch PHP Quit Statement with Patchwork

Posted in Tutorial PHP by samsonasik on January 18, 2016

If your job is make tests for legacy app that has exit() or die(); everywhere, and you don’t have privilege to refactor them, make tests for them may be hard as the test aborted when the quit statements executed in the tests. To test them, we need to redefine the user-defined functions and methods at runtime, and there is a lib for that, it is named Patchwork.

We can run command:

$ composer require antecedent/patchwork:*

to get the Patchwork dependency.

Let’s see how it can work, let say, we have a class:

namespace App;

class MyClass
{
    public function foo($arg)
    {
        if ($arg === 1) {
            return true;
        }

        exit('app exit.');
    }
}

Seeing the MyClass::foo, we can only tests if the $arg is equal then 1, otherwise, we need to redefine it, and there is a Patchwork\replace() method for it by call like the following:

replace(MyClass::class. '::foo', function($arg) {
    if ($arg === 1) {
        pass();
    }
    return 'app exit.';
});

The pass() method will call original method functionality if $arg === 1 as that not return quit statement, otherwise we redefine to return string with value = ‘app exit’.

We can define in our unit test like this MyClassTest class:

namespace AppTest;

use PHPUnit_Framework_TestCase;
use App\MyClass;
use function Patchwork\pass;
use function Patchwork\replace;
use function Patchwork\undoAll;

class MyClassTest extends PHPUnit_Framework_TestCase
{
    private $myclass;

    protected function setUp()
    {
        replace(MyClass::class. '::foo', function($arg) {
            if ($arg === 1) {
                pass();
            }
            return 'app exit.';
        });

        $this->myclass = new MyClass;
    }

    protected function tearDown()
    {
        undoAll();
    }
}

We can call the replace it in setUp(), and to undo, we can call undoAll() in tearDown(). And now, we can add the tests into MyClassTest tests :

// ...

    public function provideFoo()
    {
        return [
            [1, true],
            [0, 'app exit.'],
        ];
    }

    /**
     * @dataProvider provideFoo
     */
    public function testFoo($arg, $result)
    {
        $this->assertSame($result, $this->myclass->foo($arg));
    }

// ...

References to read:
http://kahlan.readthedocs.org/en/latest/monkey-patching/
http://antecedent.github.io/patchwork/
http://afilina.com/testing-methods-that-make-static-calls/

Using Routed Middleware class as Controller with multi actions in Expressive

Posted in Tutorial PHP, Zend Framework by samsonasik on January 3, 2016

Note: this post is now part of Zend\Expressive cookbook.

multi-action-1-middleware
If you are familiar with frameworks with provide controller with multi actions functionality, like in Zend Framework 1 and 2, you may want to apply it when you use Zend\Expressive microframework as well. Usually, we need to define 1 routed middleware, 1 __invoke() with 3 parameters ( request, response, next ). If we need another specifics usage, we can create another routed middleware classes, for example:

  1. AlbumPageIndex
  2. AlbumPageEdit
  3. AlbumPageAdd

What if we want to use only one middleware class which facilitate 3 pages above? We can with make request attribute with ‘action’ key via route config, and validate it in __invoke() method with ReflectionMethod.

Let say, we have the following route config:

// ...
    'routes' => [
        [
            'name' => 'album',
            'path' => '/album[/:action][/:id]',
            'middleware' => Album\Action\AlbumPage::class,
            'allowed_methods' => ['GET'],
        ],
    ],
// ...

To avoid repetitive code for modifying __invoke() method, we can create an AbstractPage, like the following:

namespace App\Action;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionMethod;

abstract class AbstractPage
{
    public function __invoke($request, $response, callable $next = null)
    {
        $action = $request->getAttribute('action', 'index') . 'Action';

        if (method_exists($this, $action)) {
            $r = new ReflectionMethod($this, $action);
            $args = $r->getParameters();

            if (count($args) === 3
                && $args[0]->getType() == ServerRequestInterface::class
                && $args[1]->getType() == ResponseInterface::class
                && $args[2]->isCallable()
                && $args[2]->allowsNull()
            ) {
                return $this->$action($request, $response, $next);
            }
        }

        return $next($request, $response->withStatus(404), 'Page Not Found');
    }
}

In above abstract class with modified __invoke() method, we check if the action attribute, which default is ‘index’ if not provided, have ‘Action’ suffix, and the the method is exists within the middleware class with 3 parameters with parameters with parameter 1 as ServerRequestInterface, parameter 2 as ResponseInterface, and parameter 3 is a callable and allows null, otherwise, it will response 404 page.

So, what we need to do in out routed middleware class is extends the AbstractPage we created:

namespace Album\Action;

use App\Action\AbstractPage;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Expressive\Template;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class AlbumPage extends AbstractPage
{
    protected $template;    
    // you need to inject via factory 
    public function __construct(Template\TemplateRendererInterface $template)
    { $this->template = $template; }

    public function indexAction(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next = null
    ) {
        return new HtmlResponse($this->template->render('album::album-page'));
    }

    public function addAction(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next = null
    ) {
        return new HtmlResponse($this->template->render('album::album-page-add'));
    }

    public function editAction(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next = null
    ) {
        $id = $request->getAttribute('id');
        if ($id === null) {
            throw new \InvalidArgumentException('id parameter must be provided');
        }

        return new HtmlResponse(
            $this->template->render('album::album-page-edit', ['id' => $id])
        );
    }
}

The rest is just create the view. Done 😉

Tagged with: