Welcome to Abdul Malik Ikhsan's Blog

Handle Promise.allSettled() resolved and each rejected Promise

Posted in Javascript by samsonasik on April 25, 2020

This is a Javascript post! As you probably already knew, that Promise.all() is all or never. How about run all the things even there is/are a rejected promise(s)? There is Promise.allSettled() for that.

The issue is about catching the error for specific promise(s). You CANNOT do this:

await Promise.allSettled(
        [
            promise2,
            promise1,
            promise3
        ]
    )
    .then((results) => results.forEach((result) => {
        // ...
    }))
    .catch(
        // ...
    )
    .finally(
        // ...
    );

As above will only works for Promise.all(), while the resolved promises won’t executed even only there is one promise that rejected.

What you need to do is loop the results, and you will get the result like the following:

On loop, you can check if it has status == ‘rejected’, that a way you know that the promise rejected. The result will always has ‘reason’ key when it rejected, even if the reject value is undefined. So, the code can be:

await Promise.allSettled(
        [
            promise2,
            promise1,
            promise3
        ]
    )   
    .then(
        (results) => {
            for (let result of results) {
                if (result.status == 'rejected') {
                    // handle rejected promise here
                    // eg: make console.log()...
                    console.log('error : ' + result.reason);

                    // continue to next iteration
                    continue;
                }

                // handle fulfilled promise here
                // eg: append in the div
                document.getElementsByClassName('load')[0].innerHTML += result.value + '<br />';
            }
        }
    )
    .finally(
        // ...
    );

That’s it!

References:

Tagged with: ,

Connecting to non-public PostgreSQL schema with CodeIgniter 4

Posted in CodeIgniter 4, Tutorial PHP by samsonasik on April 21, 2020

If you are building application with CodeIgniter 4 using PostgreSQL database, you will have to use ‘public’ default schema. How about if you want to use a different schema for specific needs? For example, you have the the product table that placed in “inventory” schema like the following:

To be able to make operation against “inventory” schema, we need to update schema property of Database Connection class. If we use Model class, eg: ProductModel class, we can override __construct() method and update the schema value, like the following:

<?php

namespace App\Models;

use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Model;
use CodeIgniter\Validation\ValidationInterface;

class ProductModel extends Model
{
    protected $table      = 'product';
    protected $returnType = 'array';

    public function __construct(ConnectionInterface &$db = null, ValidationInterface $validation = null)
    {
        parent::__construct($db, $validation);

        $this->db->schema = 'inventory';
    }
}

So, whenever we call:

use App\Models\ProductModel;

// ...
$model = model(ProductModel::class);
$model->findAll();

We will find all product table records in ‘inventory’ schema on first priority, if table not found in the ‘inventory’ schema, it will fallback to ‘public’.

That’s it.

Tagged with: ,

Using preInsert event for generating UUID with laminas-db

Posted in Laminas, Tutorial PHP by samsonasik on April 17, 2020

If you want to do something before insert data into database table, for example: generate id as UUID binary, you can do with preInsert event. For example, you have the following album table structure:

DROP TABLE IF EXISTS `album`;

CREATE TABLE `album` (
  `id` binary(16) NOT NULL COMMENT 'uuid binary',
  `artist` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

ALTER TABLE `album`
  ADD PRIMARY KEY (`id`);

Now, to generate the UUID data, you can use ramsey/uuid, you can require:

➜  composer require ramsey/uuid

Now, time for the action!

Note, this is just a quick example to show you how it works. You can borrow your design architecture you love in real implementation.

For example, in laminas-mvc-skeleton application, we inject the IndexController with db adapter via factory:

namespace Application\Controller;

use Laminas\Db\Adapter\AdapterInterface;

class IndexControllerFactory
{
    public function __invoke($c)
    {
        return new IndexController($c->get(AdapterInterface::class));
    }
}

Now, we can update the module/Application/config/module.config.php controller factory:

// ...
    'controllers' => [
        'factories' => [
            Controller\IndexController::class => Controller\IndexControllerFactory::class,
        ],
    ],
// ...

In our IndexController __construct, we can use the db adapter to create the TableGateway instance featuring EventFeature:

<?php

declare(strict_types=1);

namespace Application\Controller;

use Laminas\Db\Adapter\AdapterInterface;
use Laminas\Db\TableGateway\Feature\EventFeature;
use Laminas\Db\TableGateway\Feature\EventFeature\TableGatewayEvent;
use Laminas\Db\TableGateway\Feature\EventFeatureEventsInterface;
use Laminas\Db\TableGateway\TableGateway;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
use Ramsey\Uuid\Uuid;

class IndexController extends AbstractActionController
{
    private $albumTableGateway;

    public function __construct(AdapterInterface $adapter)
    {
        $events =  $this->getEventManager();
        $events->attach(EventFeatureEventsInterface::EVENT_PRE_INSERT, function (TableGatewayEvent $event) {
            $insert     = $event->getParam('insert');
            $insert->id = Uuid::uuid4()->getBytes();
        });

        $this->albumTableGateway = new TableGateway('album', $adapter, new EventFeature($events));
    }
}

Above, with EventFeatureEventsInterface::EVENT_PRE_INSERT, we update the insert id with the binary value generated.

Let’s check with index action for insert:

// ...
    public function indexAction()
    {
        $this->albumTableGateway->insert([
            'artist' => 'Sheila on 7',
            'title'  => 'Pejantan Tangguh',
        ]);

        return new ViewModel();
    }
// ..

Ok, when open the index page, we will have the album table inserted with id generated in preInsert event.

mysql> SELECT LOWER(
    ->         CONCAT(SUBSTR(HEX(id), 1, 8)
    ->         , '-' 
    ->         , SUBSTR(HEX(id), 9, 4)
    ->         , '-'
    ->         , SUBSTR(HEX(id), 13, 4)
    ->         , '-'
    ->         , SUBSTR(HEX(id), 17, 4)
    ->         , '-'
    ->         , SUBSTR(HEX(id), 21))
    ->     ) as id, 
    ->     artist, 
    ->     title 
    ->     FROM album;
+--------------------------------------+-------------+------------------+
| id                                   | artist      | title            |
+--------------------------------------+-------------+------------------+
| 551a8518-cdd2-4f3a-968d-a45a4b232b5e | Sheila on 7 | Pejantan Tangguh |
+--------------------------------------+-------------+------------------+
1 row in set (0.00 sec)

For complete event list, you can read the documentation https://docs.laminas.dev/laminas-db/table-gateway/#tablegateway-lifecycle-events

Using laminas-cli to Consume Symfony Console Command in Mezzio Application

Posted in Laminas, Mezzio, Symfony 4 by samsonasik on April 17, 2020


So, you want to use Symfony Console Command in Mezzio Application? You can! There is laminas-cli for that. While it still in development, you already can give it a try. First, I assume that you already installed the mezzio application. Next, you can set minimum-stability and prefer-stable config in your composer.json:

➜  composer config minimum-stability dev
➜  composer config prefer-stable true

By above command, you can ensure that you can install the non-stable dependency, while prefer stable version if found. Next, you can require the laminas-cli via command:

➜  composer require laminas/laminas-cli

After installed, let’s create our first command: “HelloWorld command”, like the following:

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

use function sprintf;

final class HelloWorld extends Command
{
    protected function configure()
    {
        $this
            ->addArgument('message', InputArgument::REQUIRED, 'Greeting Message');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $message = $input->getArgument('message');
        $output->writeln(sprintf('<info>Hello to world: %s<info>! ', $message));

        return 0;
    }
}

Greet! Now, time to register it to our App\ConfigProvider class:

<?php

declare(strict_types=1);

namespace App;

class ConfigProvider
{
    public function __invoke(): array
    {
        return [
            // ...
            'laminas-cli'  => $this->getCliConfig(),
            // ...
        ];
    }

    public function getCliConfig(): array
    {
        return [
            'commands' => [
                // ...
                'app:hello-world' => Command\HelloWorld::class,
                // ...
            ],
        ];
    }

    public function getDependencies(): array
    {
        return [
            'invokables'         => [
                // ...
                Command\HelloWorld::class  => Command\HelloWorld::class,
                // ...
            ],
        ];
    }

    // ...
}

First, in getDependencies(), we register the command, if the command has dependency, you need to provide factory for it. And then, in __invoke() method, we register the commands, which you can move the commands definition in separate method. That’s it! Now, you can run the command:

➜  vendor/bin/laminas app:hello-world "Good Morning"
Hello to world: Good Morning! 

Create RESTful API in CodeIgniter 4

Posted in CodeIgniter 4, Tutorial PHP by samsonasik on April 9, 2020

In CodeIgniter 4, there is already CodeIgniter\RESTful\ResourceController that make building RESTful API easier. It can consume the model by provide modelName property.

Preparation

1. Register Routes

// app/Config/Routes.php
$routes->resource('products');

2. Create Model and Entity Classes

For example, we have the ProductModel class:

<?php namespace App\Models;

use App\Entities\Product;
use CodeIgniter\Model;

class ProductModel extends Model
{
    protected $table           = 'product';

    /**
     *  $returnType as entity class  in RESTful API might not work in CodeIgniter 4.0.2.
     *  You can define as "object" at CodeIgniter 4.0.2 for RESTful API usage.
     *
     *       protected $returnType      = 'object';
     *
     */
    protected $returnType      = Product::class;

    protected $allowedFields   = [
        'product_code',
        'product_name',
    ];
    protected $validationRules = [
        'product_code' => 'required|alpha_numeric|exact_length[5]|is_unique[product.product_code,id,{id}]',
        'product_name' => 'required|alpha_numeric_space|min_length[3]|max_length[255]|is_unique[product.product_name,id,{id}]',
    ];
}

Above model require entity class, so, we can create as follow:

<?php namespace App\Entities;

use CodeIgniter\Entity;

class Product extends Entity
{
    protected $attributes = [
        'product_code' => null,
        'product_name' => null,
    ];

        // filter on create/update data if necessary
    public function setProductCode(string $productCode): self
    {
        $this->attributes['product_code'] = strtoupper($productCode);
        return $this;
    }

        // filter on create/update data if necessary
    public function setProductName(string $productName): self
    {
        $this->attributes['product_name'] = ucwords($productName);
        return $this;
    }
}

3. Ensure the pages has “csrf” filter DISABLED for the RESTful API pages

CSRF usually uses only for public web interation forms. For API, we can use authorization token (eg: for Oauth usage). We can disable csrf filter in app/Config/Filters.php like the following:

<?php namespace Config;

use App\Filters\PostRequestOnly;
use CodeIgniter\Config\BaseConfig;

class Filters extends BaseConfig
{
    // ... 

    public $globals = [
        'before' => [
            'csrf' => [
                'except' =>  [
                    '/products',
                    '/products/*',
                ],
            ],
        ],
    ];

    // ...
}

The API

We can create a controller for it, that extends CodeIgniter\RESTful\ResourceController:

<?php namespace App\Controllers;

use CodeIgniter\RESTful\ResourceController;

class Products extends ResourceController
{
    protected $modelName = 'App\Models\ProductModel';
    protected $format    = 'json';
}

For display all products, we can create method index:

// ...
    public function index()
    {
        return $this->respond($this->model->findAll());
    }
// ...

This is the output of all products:

product index show all API RESTful

For display product by id, we can add method show:

// ...
    public function show($id = null)
    {
        $record = $this->model->find($id);
        if (! $record)
        {
            return $this->failNotFound(sprintf(
                'product with id %d not found',
                $id
            ));
        }

        return $this->respond($record);
    }
// ...

This is the output when product found and not found:

product detail API RESTful

For create new product data, we can add method create:

// ...
    public function create()
    {
        $data = $this->request->getPost();
        if (! $this->model->save($data))
        {
            return $this->fail($this->model->errors());
        }

        return $this->respondCreated($data, 'product created');
    }
// ...

Above, we use getPost() from request object to get POST data. This is the output when product creation is succeed and failed:

product creation API RESTful

For update product data, we can add method update:

// ...
    public function update($id = null)
    {
        $data       = $this->request->getRawInput();
        $data['id'] = $id;

        if (! $this->model->save($data))
        {
            return $this->fail($this->model->errors());
        }

        return $this->respond($data, 200, 'product updated');
    }
// ...

Above, we use getRawInput() from request object to get PUT data. Currently, there is no “respondUpdated” method, I created Pull Request for it at https://github.com/codeigniter4/CodeIgniter4/pull/2816 for addition of “respondUpdated” method.

This is the output when product update is succeed and failed:

product update API RESTful

Now, the last one, the delete, we can add delete method:

// ...
    public function delete($id = null)
    {
        $delete = $this->model->delete($id);
        if ($this->model->db->affectedRows() === 0)
        {
            return $this->failNotFound(sprintf(
                'product with id %d not found or already deleted',
                $id
            ));
        }

        return $this->respondDeleted(['id' => $id], 'product deleted');
    }
//...

This is the output when product delete is succeed and failed:

product detail API RESTful

That’s it 😉

Tagged with: ,

Using Swoole in Mezzio application with Sdebug

Posted in Laminas, Mezzio by samsonasik on April 5, 2020


If you didn’t try Swoole, you need to try it. It is a PECL extension for developing asynchronous applications in PHP. If you build a Mezzio Application, there is already mezzio-swoole component that ease for its settings and usage.

First, if you didn’t have a mezzio skeleton, you can install the skeleton:

➜ composer create-project mezzio/mezzio-skeleton

Next, install the swoole extension:

➜ sudo pecl install swoole

After it, you can install the mezzio-swoole component:

➜ composer require mezzio/mezzio-swoole

That’s it, you can now open mezzio-skeleton directory and run the mezzio-swoole command, and we will get the following output:

➜ cd mezzio-skeleton
➜ ./vendor/bin/mezzio-swoole start

Swoole is running at 127.0.0.1:8080, in /Users/samsonasik/www/mezzio-skeleton

PHP Warning:  Swoole\Server::start(): Using Xdebug in coroutines is extremely dangerous, please notice that it may lead to coredump! in /Users/samsonasik/www/mezzio-skeleton/vendor/mezzio/mezzio-swoole/src/SwooleRequestHandlerRunner.php on line 169

If you have Xdebug installed, you will get above command output “PHP Warning” output. To fix it, we can uninstall the Xdebug, and install Sdebug instead. We can do the following command:

➜ sudo pecl uninstall xdebug
➜ git clone https://github.com/swoole/sdebug.git
➜ cd sdebug && sudo ./rebuild.sh

Now, you will get the Sdebug information if we run php -v:

➜  ~ php -v
PHP 7.4.4 (cli) (built: Mar 24 2020 10:45:52) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Sdebug v2.9.3-dev, Copyright (c) 2002-2020, by Derick Rethans
    with Zend OPcache v7.4.4, Copyright (c), by Zend Technologies

Let’s try run mezzio-swoole command again:

➜ cd mezzio-skeleton
➜ ./vendor/bin/mezzio-swoole start

Swoole is running at 127.0.0.1:8080, in /Users/samsonasik/www/mezzio-skeleton
Worker started in /Users/samsonasik/www/mezzio-skeleton with ID 0

If you got Segmentation fault in the future, that may because of the `Sdebug`, if you don’t require the ‘Xdebug’/’Sdebug’ feature. You can just uninstall them all together

Succeed! Now, time to benchmark! I used wrk for it. I tested it in Macbook Pro 2011, core i5, with 16GB RAM. I access the page with HTML+JS+CSS in there.

1. Without Swoole

Let’s CUT the previous mezzio-swoole ( type CTRL + C ) command and use PHP Development server:

➜ cd mezzio-skeleton
➜ composer serve

> php -S 0.0.0.0:8080 -t public/
[Sun Apr  5 12:24:15 2020] PHP 7.4.4 Development Server (http://0.0.0.0:8080) started

Now, we can run the benchmark with run wrk command in separate terminal:

➜ wrk -c 1000 -t 10 http://localhost:8080/     
   
Running 10s test @ http://localhost:8080/
  10 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.33s   629.53ms   2.00s    50.33%
    Req/Sec    16.09     12.96    60.00     69.26%
  544 requests in 10.09s, 4.25MB read
  Socket errors: connect 759, read 580, write 1, timeout 393
Requests/sec:     53.90
Transfer/sec:    430.92KB

2. With Swoole

Let’s CUT the previous PHP Development server command ( type CTRL + C ) command and use mezzio-swoole command:

➜ cd mezzio-skeleton
➜ ./vendor/bin/mezzio-swoole start

Swoole is running at 127.0.0.1:8080, in /Users/samsonasik/www/mezzio-skeleton
Worker started in /Users/samsonasik/www/mezzio-skeleton with ID 0

Now, we can run the benchmark with run wrk command in separate terminal:

➜ wrk -c 1000 -t 10 http://localhost:8080/

Running 10s test @ http://localhost:8080/
  10 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.15s   590.35ms   2.00s    54.14%
    Req/Sec    30.13     35.31   170.00     83.03%
  1373 requests in 10.09s, 25.10MB read
  Socket errors: connect 759, read 80, write 0, timeout 418
Requests/sec:    136.07
Transfer/sec:      2.49MB

Above, we get double total requests with swoole in same time! That’s it!

References:

Tagged with: , ,