Welcome to Abdul Malik Ikhsan's Blog

Using Model::paginate() for SQL Join in CodeIgniter 4

Posted in CodeIgniter 4, Tutorial PHP by samsonasik on March 29, 2020

From Model, in CodeIgniter 4, we can paginate existing query that we use in current table defined in $table property, eg in model, like the following:

<?php namespace App\Models;

use CodeIgniter\Model;

class ProductModel extends Model
{   
    /**
     * define properties table, returnType, allowedFields, validationRules, etc here
     */

    // ... 
	public function getPaginatedProductData(string $keyword = ''): array
	{
		if ($keyword)
		{
			$this->builder()
				 ->groupStart()
					 ->like('product_code', $keyword)
					 ->orLike('product_name', $keyword)
				 ->groupEnd();
		}

		return [
			'products'  => $this->paginate(),
			'pager'     => $this->pager,
		];
	}
    // ...

}

That’s for paginate rows in same table, how about in SQL Join? We can! For example, we have a use case to get product and price from the following table relation

that can be grabbed with join:

SELECT 
     `product`.`*`, 
     `price`.`price` 
         FROM 
             `product` 
         JOIN 
             `price` 
         ON 
             `product`.`id` = `price`.`product_id` 
         WHERE 
            `price`.`date` = DATE_FORMAT(NOW(),'%Y-%m-%d');

If we want an object representation with entity class, we can create an entity for that:

<?php namespace App\Entities;

use CodeIgniter\Entity;

class ProductWithPrice extends Entity
{
    protected $attributes = [
        'id'           => null,
        'product_code' => null,
        'product_name' => null,
        'price'        => null,
    ];
}

Now, in the model, we can query the join and then paginate:

<?php namespace App\Models;

use App\Entities\ProductWithPrice;
use CodeIgniter\Model;

class ProductModel extends Model
{   
    // ...
	public function getPaginatedProductWithPriceData()
	{
		$this->builder()
			 ->select(["{$this->table}.*", 'price.price'])
			 ->join('price', "{$this->table}.id = price.product_id")
			 ->where("price.date = DATE_FORMAT(NOW(),'%Y-%m-%d')");

		return [
			'productWithPrices'  => $this->asObject(ProductWithPrice::class)
                                         ->paginate(),
			'pager'              => $this->pager,
		];
	}
    // ...
}

That’s it, the paginate() function now will paginate the query join we have with the object entity for the result row.

How to Avoid –stderr When Running phpunit for Functional/Integration Testing

Posted in Teknologi, testing by samsonasik on March 3, 2020

When you do a Functional/Integration test with session and/or header relation. It will force you to use --stderr when running phpunit, or it will got error, eg: you’re testing that on logout when session exists as user, page will be redirected to login page with status code 302, and it got the following error:

$ vendor/bin/phpunit test/Integration/LogoutPageTest.php
PHPUnit 8.5.2 by Sebastian Bergmann and contributors.

Logout Page (AppTest\Integration\LogoutPage)
 ✘ Open logout page as auser redirect to login page
   ┐
   ├ Failed asserting that 500 matches expected 302.
   │
   ╵ /Users/samsonasik/www/mezzio-authentication-with-authorization/test/Integration/LogoutPageTest.php:36
   ┴

Time: 155 ms, Memory: 10.00 MB


FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

You can use --stderr option on running it:

$ vendor/bin/phpunit test/Integration/LogoutPageTest.php --stderr
PHPUnit 8.5.2 by Sebastian Bergmann and contributors.

Logout Page (AppTest\Integration\LogoutPage)
 √ Open logout page as auser redirect to login page

Time: 150 ms, Memory: 8.00 MB

OK (1 test, 2 assertions)

or define stderr=true in phpunit.xml configuration:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         testdox="true"
         stderr="true">

    <!-- testsuites, filter, etc config -->   

</phpunit>

Marking all test to be using stderr is a workaround, as not all tests actually require that, eg: unit test doesn’t need that. To avoid it, we can define @runTestsInSeparateProcesses and @preserveGlobalState disabled in the controller class that require that, so, the test class will be like the following:

<?php

declare(strict_types=1);

namespace AppTest\Integration;

use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Uri;
use Mezzio\Authentication\UserInterface;
use PHPUnit\Framework\TestCase;

/**
 * @runTestsInSeparateProcesses
 * @preserveGlobalState disabled
 */
class LogoutPageTest extends TestCase
{
    private $app;

    protected function setUp(): void
    {
        $this->app = AppFactory::create();
    }

    public function testOpenLogoutPageAsAuserRedirectToLoginPage()
    {
        $sessionData                    = [
            'username' => 'samsonasik',
            'roles'    => [
                'user',
            ],
        ];
        $_SESSION[UserInterface::class] = $sessionData;

        $uri           = new Uri('/logout');
        $serverRequest = new ServerRequest([], [], $uri);

        $response = $this->app->handle($serverRequest);
        $this->assertEquals(302, $response->getStatusCode());
        $this->assertEquals('/login', $response->getHeaderLine('Location'));
    }
}

That’s it!

Publish Test Coverage to Codecov from Github Actions

Posted in testing by samsonasik on February 26, 2020

Code Coverage

Github Actions is one of ways to run Continues Integration. For Coverage report, we can use Codecov to publish the coverage result after running and generating test coverage.

For example, you have a Github Repository. You can open https://codecov.io/login and choose “Github”:

After you logged in, you can choose repository, or directly access https://codecov.io/gh/{your github user}/{your github repo}, for example, I use “samsonasik” as user, and “mezzio-authentication-with-authorization” as repository name:

https://codecov.io/gh/samsonasik/mezzio-authentication-with-authorization

On very first, we need to activate Webhook by open https://codecov.io/gh/gh/{your github user}/{your github repo}/settings, for example:

https://codecov.io/gh/samsonasik/mezzio-authentication-with-authorization/settings

Then, we click “Create new webhook” under Github Webhook:

After it done, we can copy “Repository Upload Token”:

by click “Copy” after then token, and back to Github, and save to Secrets section under Your Github Repository Settings with click “Add a new secret”, with eg: named: CODECOV_TOKEN, fill the value with your copied token, and click “Add secret” to save it to be like as follow:

The preparation is done. Now, time to add the github workflow, eg: “.github/workflows/ci-build.yml” at your repository, eg for php package/project and use phpunit, the workflow can be like the following:

name: "ci build"

on:
  pull_request:
  push:
    branches:
      - "master"

jobs:
  build:
    name: PHP ${{ matrix.php-versions }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php-versions: ['7.2', '7.3', '7.4']
    steps:
      - name: Setup PHP Action
        uses: shivammathur/setup-php@1.8.2
        with:
          extensions: intl
          php-version: "${{ matrix.php-versions }}"
          coverage: pcov
      - name: Checkout
        uses: actions/checkout@v2
      - name: "Validate composer.json and composer.lock"
        run: "composer validate"
      - name: "Install dependencies"
        run: "composer install --prefer-dist --no-progress --no-suggest && composer development-enable"
      - name: "Run test suite"
        run: "vendor/bin/phpunit --coverage-clover=coverage.xml"
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          file: ./coverage.xml
          flags: tests
          name: codecov-umbrella
          yml: ./codecov.yml
          fail_ci_if_error: true

That’s it. To trigger the Continues Integration, we can push a commit to the repository.

When its succeed (ci build green), we can then display coverage badge, eg at README.md like the following:

[![Code Coverage](https://codecov.io/gh/{your github user}/{your github repo}/branch/master/graph/badge.svg)](https://codecov.io/gh/{your github user}/{your github repo})

that will show:

Code Coverage

Create isGranted View Helper for Mezzio Application with mezzio-authorization-acl component

Posted in Mezzio by samsonasik on February 15, 2020

This post is inspired by @KiwiJuicer question at Laminas’s Slack, about how to verify granted access on resource in the layout or view part, eg:

<?php if ($this->isGranted('admin.add')) : ?>
    <button name="add">Add</button>
<?php endif; ?>

OR

<?php if ($this->isGranted('admin.edit', ['id' => 1])) : ?>
    <button name="edit">Edit</button>
<?php endif; ?>

We can use Mezzio\Authorization\LaminasAcl service, but it requires a Psr\Http\Message\ServerRequestInterface instance to be passed at 2nd parameter as signature:

public function isGranted(string $role, ServerRequestInterface $request) : bool
{

}

We can pull the role from session, but how about Psr\Http\Message\ServerRequestInterface instance? Well, we don’t need middleware to fill that! We can create a RouteResult from Mezzio\Router\LaminasRouter service with utilize Mezzio\LaminasView\UrlHelper to get path from route name as resource name, with use Laminas\Diactoros\ServerRequestFactory to create Laminas\Diactoros\ServerRequest instance, so, the view helper can be as follow:

<?php

// src/App/View/Helper/IsGranted.php

declare(strict_types=1);

namespace App\View\Helper;

use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use Laminas\View\Helper\AbstractHelper;
use Mezzio\Authorization\Acl\LaminasAcl;
use Mezzio\LaminasView\UrlHelper;
use Mezzio\Router\LaminasRouter;
use Mezzio\Router\RouteResult;

class IsGranted extends AbstractHelper
{
    private $acl;
    private $getRole;
    private $url;
    private $router;

    public function __construct(
        LaminasAcl    $acl,
        GetRole       $getRole, 
        UrlHelper     $url, 
        LaminasRouter $router
    ) {
        $this->acl     = $acl;
        $this->getRole = $getRole;
        $this->url     = $url;
        $this->router  = $router;
    }

    public function __invoke(
        string $resource,
        array $routeParams = [], 
        array $queryParams = []
    ): bool {
        $request = ServerRequestFactory::fromGlobals();
        $request = $request->withUri(
            new Uri(($this->url)($resource, $routeParams, $queryParams))
        );

        $request = $request->withAttribute(
            RouteResult::class,
            $this->router->match($request)
        );

        return $this->acl->isGranted(($this->getRole)(), $request);
    }
}

Above, I assume that you already have another view helper or service to get role, eg, named App\View\Helper\GetRole so the factory for the isGranted view helper can be as follow:

<?php

// src/App/View/Helper/IsGrantedFactory.php

declare(strict_types=1);

namespace App\View\Helper;

use Laminas\View\HelperPluginManager;
use Mezzio\Authorization\Acl\LaminasAcl;
use Mezzio\Router\LaminasRouter;
use Psr\Container\ContainerInterface;

class IsGrantedFactory
{
    public function __invoke(ContainerInterface $container): IsGranted
    {
        $acl                 = $container->get(LaminasAcl::class);
        $helperPluginManager = $container->get(HelperPluginManager::class);
        $getRole             = $helperPluginManager->get('getRole');
        $url                 = $helperPluginManager->get('url');
        $router              = $container->get(LaminasRouter::class);

        return new IsGranted($acl, $getRole, $url, $router);
    }
}

We can register the view helper to App\ConfigProvider class:

<?php

// src/App/ConfigProvider.php

declare(strict_types=1);

namespace App;

class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            'dependencies' => $this->getDependencies(),
            'templates'    => $this->getTemplates(),
            'view_helpers' => [
                'invokables' => [
                    'getRole' => View\Helper\GetRole::class,
                ],
                'factories'  => [
                    'isGranted' => View\Helper\IsGrantedFactory::class,
                ],
            ],
        ];
    }

    public function getDependencies(): array
    { /* */ }

    public function getTemplates(): array
    { /* */ }
}

Now, we can check use “$this->isGranted($resource)” or “$this->isGranted($resource, $routeParams, $queryParams)” check in the layout or view.

This source code example can be found at samsonasik/mezzio-authentication-with-authorization repository that you can try yourself 😉

Install PHP 7.4 in macOS Sierra with Homebrew

Posted in Tutorial PHP by samsonasik on December 1, 2019

So, you’re still using macOS Sierra because of old mac hardware or whatever reason. You can still using Homebrew, while you will get the following warning:

Warning: You are using macOS 10.12.
We (and Apple) do not provide support for this old version.
You will encounter build failures with some formulae.
Please create pull requests instead of asking for help on Homebrew's GitHub,
Discourse, Twitter or IRC. You are responsible for resolving any issues you
experience while you are running this old version.

Read above warning carefully before continue, as you will responsible yourself if experiencing issues.

First, if you are still want to try it, what you need to do is verify that you have latest Xcode 9.2 that support macOS Sierra:

$ /usr/bin/xcodebuild -version

Xcode 9.2
Build version 9C40b

If you’re still using older version, you can first remove the Application/Xcode directory and download manually (yes, manually) as you cannot update via App Store from the following URI:

https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_9.2/Xcode_9.2.xip

That’s about 5GB file. After downloaded, you can extract to /Application that will be about 10GB contents, if you’re using old mac hardware, it may take a while. After it extracted, you need to accept its license by run command:

$ sudo xcodebuild -license accept

If everything is correct, you can update and upgrade Homebrew with commands:

$ brew update
$ brew upgrade

When done, you can verify that some “probably” dependencies needs install/update with run brew doctor until it only show 1 warning, which is outdated operating system:

$ brew doctor

Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!

Warning: You are using macOS 10.12.
We (and Apple) do not provide support for this old version.
You will encounter build failures with some formulae.
Please create pull requests instead of asking for help on Homebrew's GitHub,
Discourse, Twitter or IRC. You are responsible for resolving any issues you
experience while you are running this old version.

Now, we can follow to install php 7.4 for it at https://getgrav.org/blog/macos-catalina-apache-multiple-php-versions which the example is for macOS Catalina, that the steps can be used in macOS Sierra.

On PHP Installation section steps, run install php 7.4 command via Homebrew:

$ brew install php@7.4

And that’s it, if everything is correct, now you have PHP 7.4.

Bonus

You can add Xdebug extension with clone latest xdebug from its repository at https://github.com/xdebug/xdebug :

$ git clone https://github.com/xdebug/xdebug
$ cd xdebug
$ git checkout 2.8.1
$ phpize
$ ./configure --enable-xdebug
$ sudo make
$ sudo make install

After it, add the following line to /usr/local/etc/php/7.4/php.ini

zend_extension="/usr/local/Cellar/php/7.4.0/pecl/20190902/xdebug.so"

zend_extension pointed to real location of xdebug.so installed above.

The alternative can be via pecl command:

$ sudo pecl install xdebug

If everying correct, you will get PHP 7.4 with Xdebug 2.8.1 like the following php -v command output:

Tagged with: ,

Create templated 404 page in Slim 4 with CallableResolver

Posted in Slim 4, Tutorial PHP by samsonasik on November 8, 2019

In Slim 4 Skeleton, the 404 response handled in App\Application\Handlers\HttpErrorHandler::respond() which check against Slim\Exception\HttpNotFoundException. We can create a templated 404 page for it with utilize Slim\CallableResolver via callableResolver property which can resolve the callable handler. I assume that we are using Twig template engine, and already has setup of Twig service like in my previous post.

We can create a not found handler like the following in src/Application/Handlers/NotFoundHandler.php

<?php
// src/Application/Handlers/NotFoundHandler.php
declare(strict_types=1);

namespace App\Application\Handlers;

use Psr\Http\Message\ResponseInterface;
use Slim\Views\Twig;

use function compact;

class NotFoundHandler
{
    private $view;

    public function __construct(Twig $view)
    {
        $this->view = $view;
    }

    public function __invoke(
        ResponseInterface $response,
        string            $message
    ): ResponseInterface {
        return $this->view->render($response, '404.html.twig', compact('message'));
    }
}

We can create a view based on it like the following at templates/404.html.twig:

{# templates/404.html.twig #}

{% extends "layout.html.twig" %}
{% block title '404 - '~parent() %}

{% block body %}

{{ message }}

{% endblock %}

Now, in App\Application\Handlers\HttpErrorHandler::respond(), we can check when $exception instanceof HttpNotFoundException to make a self called resolved NotFoundHandler.

// src/Application/Handlers/HttpErrorHandler.php
// ...
    protected function respond(): Response
    {
        // ...
        if ($exception instanceof HttpNotFoundException) {
            $response = $this->responseFactory->createResponse($statusCode);
            return ($this->callableResolver->resolve(NotFoundHandler::class)(
                $response,
                $exception->getMessage()
            ));
        }
        // ...
    }
// ...

So, when the Slim\Exception\HttpNotFoundException thrown, it will shown the 404 page with brought the message passed into it like the following:

Bonus

We can create a handling against Request as well, eg: show 404 templated page only when request doesn’t has Accept: application/json or X-Requested-With:XmlHttpRequest header, so, we can modify like the following:

// src/Application/Handlers/HttpErrorHandler.php
// ...
    protected function respond(): Response
    {
        // ...
        if ($exception instanceof HttpNotFoundException) {            
            $isAppJsonAccept  = $this->request->getHeaderLine('Accept')           === 'application/json';
            $isXmlHttpRequest = $this->request->getHeaderLine('X-Requested-With') === 'XmlHttpRequest';

            if (! $isAppJsonAccept && ! $isXmlHttpRequest) {
                $response = $this->responseFactory->createResponse($statusCode);
                return ($this->callableResolver->resolve(NotFoundHandler::class)(
                    $response,
                    $exception->getMessage()
                ));
            }

            // already return early, no need else
            $error->setType(ActionError::RESOURCE_NOT_FOUND);
        }
        // ...
    }
// ...

So, for example, when called via ajax, it will show like the following:

Create “Abstract Factory” service in PHP-DI with RequestedEntry and Wildcards

Posted in Slim 4, Tutorial PHP by samsonasik on November 6, 2019

If you’re familiar with Zend Framework’s servicemanager, you may already used the abstract factory which acts as limbo when service not registered. When the pattern checked is matched, it will try to create service based on it, automatically.

We can do that in PHP-DI as well. For example, we are experimenting with my previous post on DDD in slim 4 framework:

├───Application
├───Domain
│   ├───DomainException
│   │       DomainException.php
│   │       DomainRecordNotFoundException.php
│   │
│   ├───Post
│   │       Post.php
│   │       PostNotFoundException.php
│   │       PostRepository.php
│
└───Infrastructure
    └───Persistence
        ├───Post
        │       ZendDbPostRepository.php

We are going to create service with pattern “App\Domain\Post\PostRepository” from App\Infrastructure\Persistence namespace with as an object from “App\Infrastructure\Persistence\Post\ZendDbPostRepository”. In PHP-DI, we can combine both RequestedEntry and Wildcards definitions, the service definition will be like the following:

<?php
declare(strict_types=1);

use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\Hydrator\ObjectPropertyHydrator;
use DI\Factory\RequestedEntry;

return function (ContainerBuilder $containerBuilder) {

    $containerBuilder->addDefinitions([

        'App\Domain\*\*Repository' => function (RequestedEntry $entry, ContainerInterface $c) {

            // get entity class name, 
            // eg: "Post" by service named "App\Domain\Post\PostRepository" 
            preg_match(
                '/(?<=App\\\\Domain\\\\)([A-Z][a-z]{1,})(?=\\\\\1Repository)/',
                $entry->getName(),
                $matches
            );
            $entity          = current($matches);
            $fullEntityClass = 'App\Domain' . str_repeat('\\' . $entity, 2);
            $fullRepoClass   = 'App\Infrastructure\Persistence' . '\\' . $entity . '\ZendDb' . $entity . 'Repository';

            $tableGateway = new TableGateway(
                $fullEntityClass::TABLE,
                $c->get(AdapterInterface::class),
                null,
                new HydratingResultSet(new ObjectPropertyHydrator(), new $fullEntityClass)
            );

            return new $fullRepoClass($tableGateway);
        },

    ]);
};

That’s it!

Tagged with:

Apply Twig Extension via Middleware in Slim 4

Posted in Slim 4 by samsonasik on October 28, 2019

If we use Slim 4, we can use Twig for template engine, and there is slim/twig-view for that. However, when dealing with extension, especially routing and uri related stuff, we can’t use a way like in Slim 3, at least, by default, at least, what I know right now. There is an alternative way to add twig extension, which is via Middleware!

First, I assume that the Twig service already registered in app/dependencies.php:

<?php
// app/dependencies.php
declare(strict_types=1);

use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use Slim\Views\Twig;

return function (ContainerBuilder $containerBuilder) {
    $containerBuilder->addDefinitions([
        // ...
        Twig::class => function (ContainerInterface $c) {
            return new Twig(__DIR__ . '/../templates', [
                'cache' => __DIR__ . '/../var/cache',
                'auto_reload' => true
            ]);
        },
        // ...
    ]);
};

Now, we need to add extension, which consume Slim\Routing\RouteCollector and Slim\Psr7\Uri instance. Now, we can create middleware on the fly in app/middleware.php, eg: via anonymous class that implements Psr\Http\Server\MiddlewareInterface.

<?php
// app/middleware.php
declare(strict_types=1);

// ...
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\App;
use Slim\Routing\RouteCollector;
use Slim\Views\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
// ...

return function (App $app) {

    // ...
    $container = $app->getContainer();
    $twig      = $container->get(Twig::class);
    $router    = $app->getRouteCollector();

    $app->add(new class ($twig, $router) implements MiddlewareInterface {

        private $twig;
        private $router;

        public function __construct(Twig $twig, RouteCollector $router)
        {
            $this->twig   = $twig;
            $this->router = $router;
        }

        public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
        {
           $uri = $request->getUri();
           $this->twig->addExtension(new class ($this->router, $uri) extends AbstractExtension {

                private $router;
                private $uri;

                public function __construct(RouteCollector $router, UriInterface $uri)
                {
                    $this->router = $router;
                    $this->uri    = $uri;
                }

                public function getFunctions()
                {
                    return [
                        new TwigFunction('base_path', function () : string {
                            return $this->router->getBasePath();
                        }),
                        new TwigFunction('full_url_for', function (string $routeName, array $data = [], array $queryParams = []) : string {
                            return $this->router->getRouteParser()->fullUrlFor($this->uri, $routeName, $data, $queryParams);
                        }),
                    ];
                }

           });
           return $handler->handle($request);
        }
    });
    // ...
};

In its __construct, we inject with Slim\Views\Twig and Slim\Routing\RouteCollector instance. In its process() function, we call Slim\Views\Twig::addExtension() function to add extension, which we can create another anonymous class that extends Twig\Extension\AbstractExtension, injected with Slim\Routing\RouteCollector and Slim\Psr7\Uri (which pulled from object instance of Psr\Http\Message\ServerRequestInterface), which we can add getFunctions() method to add twig functions.

That’s it, in above example, I added base_path and full_url_for function which now can be used in twig view, eg:

    {# display base path url #}
    {{ base_path() }}

    {# display full url for route with name "postpagedetail" with parameter id = 1 and query parameter action = view  #}
    {{ full_url_for('postpagedetail', { 'id': 1 }, { 'action': 'view' }) }}

You are feeling have too much code in app/middleware.php ? Of course, you can create a separate class for it!

Using DDD architecture with zend-db in Slim 4

Posted in Slim 4 by samsonasik on October 27, 2019

Slim 4 provides a skeleton that uses DDD directory style. By this, we can implement DDD architecture easier. Let’s try with zend-db for it. In this post, I assume that you already uses Twig as view template engine like in previous post.

Requirements

First, we need to require zend-db via composer:

$ composer require zendframework/zend-db \
    zendframework/zend-hydrator \
    --sort-packages

We are going to make pages to display post table record(s) with the following data:

# create and use database
create database slim;
use slim;

# create table
create table post(id int not null primary key auto_increment, title varchar(50) not null, content text not null);

# insert data
insert into post(title, content) values('first post', 'first post content');
insert into post(title, content) values('second post', 'second post content');

Database Configuration

<?php
// app/settings.php
// ...
return function (ContainerBuilder $containerBuilder) {
    $containerBuilder->addDefinitions([
            // ...
            'db' => [
                'username' => 'root',
                'password' => '',
                'driver'   => 'pdo_mysql',
                'database' => 'slim',
                'host'     => 'localhost',
                'driver_options' => [
                    \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'',
                ],
            ],
            // ...
        ],
    ]);
};

We can register database configuration at app/settings.php, for example, use MySQL database as configured above.

DB Service

We are using service named Zend\Db\Adapter\AdapterInterface, so we can register as follow in app/dependencies.php:

<?php
// app/dependencies.php
declare(strict_types=1);

use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterInterface;

return function (ContainerBuilder $containerBuilder) {
    $containerBuilder->addDefinitions([
        // ...
        AdapterInterface::class => function (ContainerInterface $c) {
            return new Adapter($c->get('settings')['db']);
        },
        // ...
    ]);
};

Domain

a. Post class:

<?php
// src/Domain/Post/Post.php
declare(strict_types=1);

namespace App\Domain\Post;

final class Post
{
    public $id;
    public $title;
    public $content;

    public const TABLE = 'post';

    public function __construct(Post $post = null)
    {
        if ($post instanceof Post) {
            $this->id      = +$post->id;
            $this->title   = ucwords($post->title);
            $this->content = ucfirst($post->content);
        }
    }
}

b. PostRepository interface:

<?php
// src/Domain/Post/PostRepository.php
declare(strict_types=1);

namespace App\Domain\Post;

interface PostRepository
{
    public function findAll(): array;
    public function findPostOfId(int $id): Post;
}

c. PostNotFoundException class:

<?php
// src/Domain/Post/PostNotFoundException.php
declare(strict_types=1);

namespace App\Domain\Post;

use App\Domain\DomainException\DomainRecordNotFoundException;

class PostNotFoundException extends DomainRecordNotFoundException
{
    public $message = 'The post you requested does not exist.';
}

Infrastructure

We can create a repository that implements App\Domain\Post\PostRepository, eg: named ZendDbPostRepository:

<?php
// src/Infrastructure/Persitence/Post/ZendDbPostRepository.php
declare(strict_types=1);

namespace App\Infrastructure\Persistence\Post;

use function compact;

use App\Domain\Post\Post;
use App\Domain\Post\PostNotFoundException;
use App\Domain\Post\PostRepository;
use Zend\Db\TableGateway\AbstractTableGateway;

class ZendDbPostRepository implements PostRepository
{
    private $tableGateway;

    public function __construct(AbstractTableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    public function findAll(): array
    {
        $results = $this->tableGateway->select();
        $posts   = [];

        foreach ($results as $result) {
            $posts[] = new Post($result);
        }

        return $posts;
    }

    public function findPostOfId(int $id): Post
    {
        $current = $this->tableGateway->select(compact('id'))->current();
        if (! $current) {
            throw new PostNotFoundException();
        }

        return new Post($current);
    }
}

The Repository class above needs to be registered to app/repositories.php:

<?php
// app/repositories.php
declare(strict_types=1);

use App\Domain\Post\Post;
use App\Domain\Post\PostRepository;
use App\Infrastructure\Persistence\Post\ZendDbPostRepository;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\Hydrator\ObjectPropertyHydrator;

return function (ContainerBuilder $containerBuilder) {
    $containerBuilder->addDefinitions([
        // ...
        PostRepository::class => function (ContainerInterface $c) {
            $tableGateway = new TableGateway(
                Post::TABLE,
                $c->get(AdapterInterface::class),
                null,
                new HydratingResultSet(new ObjectPropertyHydrator(), new Post())
            );

            return new ZendDbPostRepository($tableGateway);
        },
        // ...
    ]);
};

Application

For application, we can create 2 classes to display all posts and display post by id.

a. All posts via PostPage class

<?php
// src/Application/Page/PostPage.php

declare(strict_types=1);

namespace App\Application\Page;

use App\Domain\Post\PostRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
use Slim\Views\Twig;

class PostPage implements RequestHandlerInterface
{
    private $view;
    private $postRepository;

    public function __construct(Twig $view, PostRepository $postRepository)
    {
        $this->view           = $view;
        $this->postRepository = $postRepository;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return $this->view->render(
            new Response(),
            'page/post/index.html.twig',
            [
                'posts' => $this->postRepository->findAll(),
            ]
        );
    }
}

b. Post By Id via PostPageDetail class

<?php
// src/Application/Page/PostPageDetail.php

declare(strict_types=1);

namespace App\Application\Page;

use App\Domain\DomainException\DomainRecordNotFoundException;
use App\Domain\Post\PostRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Exception\HttpNotFoundException;
use Slim\Psr7\Response;
use Slim\Views\Twig;

class PostPageDetail implements RequestHandlerInterface
{
    private $view;
    private $postRepository;

    public function __construct(Twig $view, PostRepository $postRepository)
    {
        $this->view           = $view;
        $this->postRepository = $postRepository;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $id = +$request->getAttribute('id');
        try {
            $post = $this->postRepository->findPostOfId($id);
        } catch (DomainRecordNotFoundException $e) {
            throw new HttpNotFoundException($request, $e->getMessage());
        }

        return $this->view->render(
            new Response(),
            'page/post/detail.html.twig',
            [
                'post' => $post,
            ]
        );
    }
}

Routing

Now, time to register routing:

<?php
// app/routes.php
declare(strict_types=1);

use App\Application\Page\PostPage;
use App\Application\Page\PostPageDetail;
use Slim\App;
use Slim\Handlers\Strategies\RequestHandler;

return function (App $app) {
    $routeCollector = $app->getRouteCollector();
    $routeCollector->setDefaultInvocationStrategy(
        new RequestHandler(true)
    );

    $app->get('/posts', PostPage::class);
    $app->get('/posts/{id:[0-9]+}', PostPageDetail::class);
};

View

a. templates/page/post/index.html.twig:

{# templates/page/post/index.html.twig #}
{% extends "layout.html.twig" %}

{% block body %}

    {% for post in posts %}
        <h3> {{ post.title }} </h3>

        <p> {{ post.content }} </p>
    {% endfor %}

{% endblock %}

b. templates/page/post/detail.html.twig:

{# templates/page/post/detail.html.twig #}
{% extends "layout.html.twig" %}

{% block body %}

    <h3> {{ post.title }} </h3>
    <p>  {{ post.content }} </p>

{% endblock %}

Run PHP Server

php -S localhost:8080 -t public

Now, we can see the pages, http://localhost:8080/posts for all posts:

and http://localhost:8080/posts/1 for detail post:

Tagged with:

Using routed PSR-15 RequestHandlerInterface in Slim 4

Posted in Slim 4 by samsonasik on September 2, 2019

Slim 4 already support PSR-15, and we can use class implements Psr\Http\Server\RequestHandlerInterface that produce response and register to the routes. First, we can install Slim-Skeleton to get started:

$ composer create-project slim/slim-skeleton

After it, for example, we have the following AboutPage class:

<?php
// src/Application/Page/AboutPage.php
declare(strict_types=1);

namespace App\Application\Page;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;

class AboutPage implements RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $response = new Response();
        $response->getBody()->write('about page');

        return $response;
    }
}

We can register above to routes configuration in app/routes.php as follow:

<?php
// app/routes.php
declare(strict_types=1);

use App\Application\Page\AboutPage;
use Slim\App;

return function (App $app) {
    // ...
    $app->get('/about', AboutPage::class);
};

To test, we can run PHP Development Server:

$ php -S localhost:8080 -t public

Then open it in the web browser as http://localhost:8080/about :

How about injecting service(s) to it?

We can supply the service(s) into its __construct and it will automatically injected with the service(s), eg: we need to inject service named Psr\Log\LoggerInterface which its service definition already registered in app/dependencies.php:

<?php
// src/Application/Page/AboutPage.php
// ...
use Psr\Log\LoggerInterface;
// ...
class AboutPage implements RequestHandlerInterface
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    // ...
}

How about add template?

For example, we want to use slim/twig-view package for it, we can first require it:

$ composer require slim/twig-view

After it, we can create cache and templates directories for it:

# create var/cache directory
$ mkdir -p var/cache

# ignore cached data from git repo (in case use git as version control) 
$ touch var/cache/.gitignore && (echo '*' && echo '!.gitignore') > var/cache/.gitignore

# create templates directories
$ mkdir -p templates/page

By above directory structure, we can register Slim\Views\Twig service in app/dependencies.php as follow:

<?php
// app/dependencies.php
// ...
use Slim\Views\Twig;

return function (ContainerBuilder $containerBuilder) {
    $containerBuilder->addDefinitions([
        // ...
        Twig::class => function (ContainerInterface $c) {
            return new Twig(__DIR__ . '/../templates', [
                'cache'       => __DIR__ . '/../var/cache',
                'auto_reload' => true
            ]);
        },
        // ...
    ]);
};

After it, we can prepare the templates with the following structure:

├───templates
│   │   layout.html.twig
│   └───page
│           about.html.twig

Next, apply templates/layout.html.twig:

<!-- templates/layout.html.twig -->
<!DOCTYPE html>
<html>
    <head>
        <title>{% block title %}Slim App{% endblock %}</title>
    </head>
    <body>
        {% block body %}{% endblock %}
    </body>
</html>

Next, apply templates/page/about.html.twig:

<!-- templates/page/about.html.twig -->
{% extends "layout.html.twig" %}

{% block body %}

About Page.

{% endblock %}

Finally, in the AboutPage class, we can inject Slim\Views\Twig instance and use it to render the twig view as instance of Slim\Psr7\Response:

<?php
// src/Application/Page/AboutPage.php
declare(strict_types=1);

namespace App\Application\Page;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
use Slim\Views\Twig;

class AboutPage implements RequestHandlerInterface
{
    private $view;

    public function __construct(Twig $view)
    {
        $this->view = $view;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return $this->view->render(new Response(), 'page/about.html.twig');
    }
}

How about getting request attribute value?

To do this, we need to enable it in the routing by apply Slim\Handlers\Strategies\RequestHandler as its router invocation strategy, so, for example, we need to pass optional “id” parameter, we can do in app/routes:

<?php
// app/routes.php
declare(strict_types=1);

use App\Application\Page\AboutPage;
use Slim\App;
use Slim\Handlers\Strategies\RequestHandler;

return function (App $app) {
    // ...
    $app->get('/about[/{id:[0-9]+}]', AboutPage::class)
        ->setInvocationStrategy(new RequestHandler(true));
};

We can also set default invocation strategy by apply it before register the route(s) that the pages implements RequestHandlerInterface:

<?php
// app/routes.php
declare(strict_types=1);

use App\Application\Page\AboutPage;
use Slim\App;
use Slim\Handlers\Strategies\RequestHandler;

return function (App $app) {
    $routeCollector = $app->getRouteCollector();
    // ... before setDefaultInvocationStrategy placement,
    // ... it will compatible with RequestResponse strategy    
  
    $routeCollector->setDefaultInvocationStrategy(new RequestHandler(true));
    // ... after setDefaultInvocationStrategy placement,
    // ... it will compatible only with selected default invocable strategy

    $app->get('/about[/{id:[0-9]+}]', AboutPage::class);
};

So, from now on, we can get attribute in the handle() method and passed parameter to the view:

<?php
// src/Application/Page/AboutPage.php
declare(strict_types=1);

namespace App\Application\Page;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
use Slim\Views\Twig;
use Psr\Log\LoggerInterface;

class AboutPage implements RequestHandlerInterface
{
    private $view;

    public function __construct(Twig $view)
    {
        $this->view = $view;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $id = $request->getAttribute('id');

        return $this->view->render(
            new Response(),
            'page/about.html.twig',
            [
                'id' => $id
            ]
        );
    }
}

So, in the view, we can do:

<!-- templates/page/about.html.twig -->
{% extends "layout.html.twig" %}

{% block body %}

About Page with id {{ id }}

{% endblock %}

and when open http://localhost:8080/about/1 , we can get:

That’s it!

Tagged with: , , , , ,

Immediate remove cookie data in zend-mvc application

Posted in Tutorial PHP, Zend Framework, Zend Framework 2, Zend Framework 3 by samsonasik on August 8, 2019

In zend-mvc application, we can utilize Zend\Http\Header\SetCookie header to set cookie to be added to response header. For example, we have a setAction() to set it, we can do:

use Zend\Http\Header\SetCookie;
// ...
    public function setAction()
    {
        $request  = $this->getRequest();
        $response = $this->getResponse();

        $response->getHeaders()->addHeader(new SetCookie(
            // name
            'test',

            // value
            'abc',
            
            // expires
            \time() + (60 * 60 * 24 * 365),
            
            // path
            '/',
            
            // domain
            null,
            
            // secure
            $request->getUri()->getScheme() === 'https',
            
            // httponly
            true
        ));

        return $response;
    }
// ...

Above, we set cookie with name “test” with value “abc”, with expires 1 year. To remove it completely before 1 year hit, we can pass 8th parameter as maxAge to be 0, so, for example, we have a removeAction to remove it, we can do:

use Zend\Http\Header\SetCookie;
// ...
    public function removeAction()
    {
        $request  = $this->getRequest();
        $response = $this->getResponse();

        $cookie   = $request->getCookie();
        if (! isset($cookie['test'])) {
            // already removed, return response early
            return $response;
        }

        $response->getHeaders()->addHeader(new SetCookie(
            'test',
            null, // no need to set value
            null, // no need to set expires
            '/',
            null,
            $request->getUri()->getScheme() === 'https',
            true,
            0    // set maxAge to 0 make "test" cookie gone
        ));

        return $response;
    }
// ...

That’s it!

Using Buffer for Resultset::rewind() after Resultset::next() called in Zend\Db

Posted in hack, Tutorial PHP, Zend Framework 3 by samsonasik on August 5, 2019

In Zend\Db, there is Resultset which can be used as result of db records as instanceof PHP Iterator. For example, we have the following table structure:

CREATE TABLE test (
    id serial NOT NULL PRIMARY KEY,
    name character varying(255)
);

and we have the following data:

To build resultset, we can use the following code:

include './vendor/autoload.php';

use Zend\Db\Adapter\Adapter;
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\ResultSet\ResultSet;

$adapter = new Adapter([
    'username' => 'developer',
    'password' => '123456',
    'driver'   => 'pdo_pgsql',
    'database' => 'learn',
    'host'     => 'localhost',
]);

$resultSetPrototype = new ResultSet(
    null,
    new ArrayObject([], ArrayObject::ARRAY_AS_PROPS)
);

$tableGateway = new TableGateway(
    'test',
    $adapter,
    null,
    $resultSetPrototype
);

$select    = $tableGateway->getSql()->select();
$resultSet = $tableGateway->selectWith($select);

On getting the data, we can go to specific record position by using next(), for example: we want to get the 2nd record of selected data, we can use the following code:

$resultSet->current();
$resultSet->next();

var_dump($resultSet->current());

and we will get the following data:

class ArrayObject#16 (1) {
  private $storage =>
  array(2) {
    'id' =>
    int(2)
    'name' =>
    string(6) "test 2"
  }
}

However, when we need to back to first position, we can’t just use rewind() as follow:

$resultSet->rewind();
var_dump($resultSet->current());

Above code will result wrong data, which is a next record data:

class ArrayObject#16 (1) {
  private $storage =>
  array(2) {
    'id' =>
    int(3)
    'name' =>
    string(6) "test 3"
  }
}

To make that work, we need to use buffer() method early after result set created, so, the code will need to be:

$select    = $tableGateway->getSql()->select();
$resultSet = $tableGateway->selectWith($select);

$resultSet->buffer();

$resultSet->current();  // ensure hit 1st record first 
$resultSet->next();     // next position

var_dump($resultSet->current()); // get 2nd record 

$resultSet->rewind(); // back to position 0
var_dump($resultSet->current());  // get 1st record again 

That will show the correct data:

# second record by call of next()
class ArrayObject#16 (1) {
  private $storage =>
  array(2) {
    'id' =>
    int(2)
    'name' =>
    string(6) "test 2"
  }
}

# first record after call of rewind() 
class ArrayObject#16 (1) {
  private $storage =>
  array(2) {
    'id' =>
    int(1)
    'name' =>
    string(6) "test 1"
  }
}

Call private/protected method without ReflectionMethod in PHP

Posted in php by samsonasik on July 21, 2019

For whatever reason, you may need to call private/protected method from an object. For example, we have a Foo class with bar method with private modifier like below:

class Foo
{
    private function bar()
    {
        echo 'hit';
    }
}

We need to call bar() method from an instance of Foo class. If you are familiar with ReflectionMethod, you may do this:

$r = new ReflectionMethod($foo = new Foo(), 'bar');
$r->setAccessible(true);
$r->invoke($foo);

There is another way. It is by using invoked of Closure::bindTo() like below:

(function ($foo) {
    $foo->bar();
})->bindTo($foo = new Foo(), Foo::class)($foo);

That’s it!

Tagged with: , ,

Run test and coverage report with ant build in Windows

Posted in Teknologi, testing by samsonasik on July 9, 2019

So, you want to run test and coverage report commands in Windows environment. For example, you want to run kahlan command for test, then make coverage report by istanbul and istanbul-merge.

Based on the ant documentation on exec part, it noted as follow:

The task delegates to Runtime.exec which in turn apparently calls ::CreateProcess. It is the latter Win32 function that defines the exact semantics of the call. In particular, if you do not put a file extension on the executable, only .EXE files are looked for, not .COM, .CMD or other file types listed in the environment variable PATHEXT. That is only used by the shell.
Note that .bat files cannot in general by executed directly. One normally needs to execute the command shell executable cmd using the /c switch.

If you are familiar with ant command, usually, you can create a build.xml file as default build configuration. You can create another build file that you can mention it when run the ant command, eg: build-windows.xml:

$ ant -buildfile build-windows.xml

Your build-windows.xml then can use “cmd” as executable property with /c /path/to/executable-file as argument. The file will like below:

<?xml version="1.0" encoding="UTF-8"?>
<project name="Your Application name" default="build">

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

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

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

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

            <!-- other modules to be tested here -->

    </target>

    <target name="coverage-report"
            description="Run coverage report generation">
            
            <!-- merge coverages for many modules test -->
            <exec executable="cmd" failonerror="true" taskname="istanbul merge">
                <arg line="/c istanbul-merge --out coverage.json coverage/*.json"/>
            </exec>
            
            <!-- make report -->
            <exec executable="cmd" failonerror="true" taskname="istanbul report">
                <arg line="/c istanbul report"/>
            </exec>

    </target>

</project>

With above, when you run the command, if it succeed, it will likely as follow:

$ ant -buildfile build-windows.xml
Buildfile: D:\app\build-windows.xml

kahlan:
   [kahlan]             _     _
   [kahlan]   /\ /\__ _| |__ | | __ _ _ __
   [kahlan]  / //_/ _` | '_ \| |/ _` | '_ \
   [kahlan] / __ \ (_| | | | | | (_| | | | |
   [kahlan] \/  \/\__,_|_| |_|_|\__,_|_| | |
   [kahlan]
   [kahlan] The PHP Test Framework for Freedom, Truth and Justice.
   [kahlan]
   [kahlan] src directory  : D:\app\module\Application\src
   [kahlan] spec directory : D:\app\module\Application\spec
   [kahlan]
   [kahlan] ......................................................  11 / 11 (100%)
   [kahlan]
   [kahlan]
   [kahlan]
   [kahlan] Expectations   : 11 Executed
   [kahlan] Specifications : 0 Pending, 0 Excluded, 0 Skipped
   [kahlan]
   [kahlan] Passed 11 of 11 PASS in 1.831 seconds (using 8Mo)
   [kahlan]
   [kahlan] Coverage Summary
   [kahlan] ----------------
   [kahlan]
   [kahlan] Total: 100.00% (25/25)
   [kahlan]
   [kahlan] Coverage collected in 0.035 seconds

coverage-report:
[istanbul report] Done

build:

BUILD SUCCESSFUL
Total time: 4 seconds

That’s it!

References:

Programmatically add route in zend-mvc application

Posted in Tutorial PHP, Zend Framework 3 by samsonasik on June 16, 2019

Yes, we usually setup routing via configuration in our module.config.php in our modules. However, in very specific condition, we may need to programmatically setting it. There is a ‘Router’ service for that, it represent Zend\Router\Http\TreeRouteStack instance and we can use addRoute() method against it.

For example, we need to setting it in Module::onBootstrap(), so we can code like the following:

<?php
namespace Application;

use Zend\Mvc\MvcEvent;
use Zend\Router\Http\Literal;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        // if ( ... condition to be handled programmatically ... ) :

            $services = $e->getApplication()->getServiceManager();
            $router   = $services->get('Router');

            $route = Literal::factory([
                'route' => '/foo',
                'defaults' => [
                    'controller' => Controller\FooController::class,
                    'action'     => 'index',
                ],
            ]);
            $router->addRoute('foo', $route);

        // end if    
    }

    public function getConfig() { /* ... */ }
}

That’s it!