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.

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"
  }
}

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!

Using PSR-7 UploadedFileInterface with zend-filter’s File and zend-validator’s File in zend-form instance

Posted in expressive, Tutorial PHP, Zend Framework by samsonasik on February 3, 2019

The latest zend-filter:^2.9 and zend-validator:^2.12 have full support for PSR-7 UploadedFileInterface handling. If we are using it with zend-form instance:

use Zend\Diactoros\StreamFactory;
use Zend\Diactoros\UploadedFileFactory;
use Zend\Filter\File\RenameUpload;
use Zend\Validator\File\Size;

// ...
    public function getInputFilterSpecification()
    {
        return [
            [
                'name' => 'filename',
                'required' => true,
                'filters' => [
                    [
                        'name' => RenameUpload::class,
                        'options' => [
                            'target'               => \getcwd() . '/public/uploads',
                            'use_upload_extension' => true,
                            'use_upload_name'      => true,
                            'overwrite'            => true,
                            'randomize'            => true,

                            'stream_factory'       => new StreamFactory(),
                            'upload_file_factory'  => new UploadedFileFactory(),
                        ],
                    ],
                ],
                'validators' => [
                    [
                        'name' => Size::class,
                        'options' => [
                            'max' => '10MB',
                        ],
                    ],
                ],
            ],
        ];
    }
// ...

The only config that needed to be changed is in the filters: the stream_factory and upload_file_factory config with provide objects of class that implements Psr\Http\Message\StreamFactoryInterface and Psr\Http\UploadedFileFactoryInterface, which if we are using zend-diactoros, we can use Zend\Diactoros\StreamFactory and Zend\Diactoros\UploadedFileFactory.

For Form set data, we can now do the following:

use Psr\Http\Message\ServerRequestInterface;
// ...
    // $request is an object of class implements ServerRequestInterface
    $postAndFileData = \array_merge_recursive(
        $request->getParsedBody(),
        $request->getUploadedFiles()
    );
// ...

On get data, we have different result from normal Zend\Http\Phpenvironment\Request. If we are using normal Zend\Http\Phpenvironment\Request, we normally get the following data:

array (size=1)
  'filename' => 
    array (size=5)
      'name' => string 'Untitled-1.png' (length=14)
      'type' => string 'image/png' (length=9)
      'size' => int 761087
      'tmp_name' => string '/path/to/public/uploads/Untitled-1_5c51c49e1d0855_97287659.png' (length=103)
      'error' => int 0

With the new parameters, the form data will be:

array (size=1)
  'filename' => 
    object(Zend\Diactoros\UploadedFile)[966]
      private 'clientFilename' => string 'Untitled-1.png' (length=14)
      private 'clientMediaType' => string 'image/png' (length=9)
      private 'error' => int 0
      private 'file' => null
      private 'moved' => boolean false
      private 'size' => int 761087
      private 'stream' => 
        object(Zend\Diactoros\Stream)[967]
          protected 'resource' => resource(12, stream)
          protected 'stream' => string '/path/to/public/uploads/Untitled-1_5c51c49e1d0855_97287659.png' (length=103)

We can consume the file uploaded data from Zend\Diactoros\UploadedFile‘s functions, or we want a array data, we can loop the fields recursively and convert to array data whenever the value is instanceof Psr\Http\Message\UploadedFileInterface, eg:

use Psr\Http\Message\UploadedFileInterface;

// ...
$data = $form->getData();

\array_walk_recursive($data, function (& $value) {
    if ($value instanceof UploadedFileInterface) {
        $value = [
            'name'     => $value->getClientFilename(),
            'type'     => $value->getClientMediaType(),
            'size'     => $value->getSize(),
            'tmp_name' => $value->getStream()->getMetadata('uri'),
            'error'    => $value->getError(),
        ];
    }
});
// ...

The data are now completely an array so we are safe to use in another use case, eg: save to temporary session for File PRG use case.

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

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

zend-expressive-session-cache is a PSR-6 session persistence adapter for zend-expressive-session that can be used in Expressive 3. In current post, I will provide the use case of it with apcu for cache adapter with zend-cache for cache item pool.

Let’s start with create new project from skeleton with the following command:

➜ composer create-project \
    zendframework/zend-expressive-skeleton \
    expressive3-cache-tutorial

After the skeleton installed, we can require the zendframework/zend-expressive-session-cache and zend-cache with the following command:

➜ cd expressive3-cache-tutorial
➜ composer require \
    zendframework/zend-expressive-session-cache \
    zendframework/zend-cache

Now, ensure config/config.php has the following ConfigProviders registered:

<?php
// config/config.php
$aggregator = new ConfigAggregator([
    // ...
    \Zend\Cache\ConfigProvider::class,
    \Zend\Expressive\Session\Cache\ConfigProvider::class,
    \Zend\Expressive\Session\ConfigProvider::class,
    // ...
], $cacheConfig['config_cache_path']);

For apcu adapter, we need the apcu extension to be installed, we can install via pecl like the following:

➜ sudo pecl install apcu

After it installed, we need to set apc.use_request_time = 0 in php.ini like the following:

# your php.ini
apc.use_request_time = 0

For Cache Item Pool service, we can create a factory for it, for example, like the following:

<?php
// src/App/Cache/CacheItemPoolFactory.php

declare(strict_types=1);

namespace App\Cache;

use Psr\Cache\CacheItemPoolInterface;
use Psr\Container\ContainerInterface;
use Zend\Cache\Psr\CacheItemPool\CacheItemPoolDecorator;
use Zend\Cache\StorageFactory;

final class CacheItemPoolFactory
{
    public function __invoke(ContainerInterface $container) : CacheItemPoolInterface
    {
        $storage = StorageFactory::factory([
            'adapter' => [
                'name'    => 'apcu',
            ],
        ]);

        return new CacheItemPoolDecorator($storage);
    }
}

We can register then the cache pool service and make alias of SessionPersistenceInterface with CacheSessionPersistence:

<?php
// config/autoload/dependencies.global.php
use App\Cache\CacheItemPoolFactory;
use Psr\Cache\CacheItemPoolInterface;
use Zend\Expressive\Session\Cache\CacheSessionPersistence;
use Zend\Expressive\Session\SessionPersistenceInterface;

return [
    // ...
    'aliases' => [
        SessionPersistenceInterface::class => CacheSessionPersistence::class,
    ],
    'factories'  => [
        CacheItemPoolInterface::class => CacheItemPoolFactory::class,
    ],
    // ...
];

If we need a custom configuration for zend-expressive-session-cache, we can define at config/autoload/zend-expressive.global.php:

<?php
// config/autoload/zend-expressive.global.php
use Psr\Cache\CacheItemPoolInterface;

return [
    // ...
    'zend-expressive-session-cache' => [
        'cache_item_pool_service' => CacheItemPoolInterface::class,
        'cookie_name'             => 'PHPSESSION',
        'cookie_path'             => '/',
        'cache_limiter'           => 'nocache',
        'cache_expire'            => 10800,
        'last_modified'           => null,
    ],
    // ...
];

To start the session, we can apply Zend\Expressive\Session\SessionMiddleware into config/pipeline.php before Zend\Expressive\Router\Middleware\RouteMiddleware registration:

<?php
use Zend\Expressive\Router\Middleware\RouteMiddleware;
use Zend\Expressive\Session\SessionMiddleware;

// ...
    $app->pipe(SessionMiddleware::class);
    $app->pipe(RouteMiddleware::class);
// ...

The preparation done! Now, we can use it by consume it from via SessionMiddleware, for example, we need to generate “csrf” token when rendering the form, like I written at the CSRF usage in Expressive post, we can just use the session as is, and the cache will be used as session persistence:

<?php
// src/Handler/LoginPageHandler.php
use Zend\Expressive\Session\SessionInterface;
use Zend\Expressive\Csrf\SessionCsrfGuard;
use Zend\Expressive\Session\SessionMiddleware;
use Zend\Expressive\Csrf\CsrfMiddleware;

    // ...
    private function getToken(SessionInterface $session, SessionCsrfGuard $guard)
    {
        if (! $session->has('__csrf')) {
            return $guard->generateToken();
        }

        return $session->get('__csrf');
    }

    public function process(
         ServerRequestInterface $request,
         RequestHandlerInterface $handler
    ) : ResponseInterface
    {
        $guard     = $request->getAttribute(CsrfMiddleware::GUARD_ATTRIBUTE);
        $loginForm = new LoginForm($guard);

        $session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
        $token   = $this->getToken($session, $guard);

        // ...
    }
    // ...

That’s it!

Create API Service in CodeIgniter 4 with Request Filtering

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

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

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

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

namespace App\Controllers\Api;

use function time;

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

final class Ping extends Controller
{
    use ResponseTrait;

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

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

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

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

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

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

{
    "ack": 1537596713
}

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

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

namespace App\Filters;

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

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

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

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

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

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

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

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

    // ...

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

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

GET: Method Not Allowed 405

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

POST: Got Response OK 200

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

{
    "ack": 1537596774
}

Upload File with Validation in CodeIgniter 4

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

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

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

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

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

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

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

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

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

$ git checkout -b tutorial-upload

Then, we can regularly rebase against develop :

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

Enough with introduction, let’s start with code!

1. Display Form

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

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

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

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

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

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

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

use CodeIgniter\Controller;

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

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

$ php spark serve

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

2. Upload validation

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

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

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

        if ($validated) {
            // Business!
        }

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

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

3. Move uploaded file to writable/uploads directory

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

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

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

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

use CodeIgniter\Controller;

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

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

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

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

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