Welcome to Abdul Malik Ikhsan's Blog

How to Update to PHP 7.4 Typed Property Without BC Break with Rector

Posted in php, Rector, Teknologi by samsonasik on September 29, 2021

In Rector 0.12.9, `TypedPropertyRector` is not configurable, it will only change:

– private property
– protected property on final class without extends

In Rector 0.12.16, `TypedPropertyRector` has configurable to allow change protected and public modifier as well as far when possible with new configurable:

   $services->set(TypedPropertyRector::class)
        ->configure([
            TypedPropertyRector::INLINE_PUBLIC => true,
        ]);

This Article is valid for Rector <= 0.12.8

Typed Property is one of the PHP 7.4 feature that allow to write that previously like this:

namespace Lib;

class SomeClass
{
    /** @var int */
    public $a;

    /** @var string */
    protected $b;

    /** @var bool */
    private $c;
}

to this:

namespace Lib;

class SomeClass
{
    public int $a;

    protected string $b;

    private bool $c;
}

If you follow Semver for versioning, and you don’t want to update to major change, eg: version 1 to version 2, changing this will make Break Backward Compatibility, for example:

namespace Lib;

class SomeClass
{
    protected string $b;
}

has child in application consumer:

namespace App;

use Lib\SomeClass;

class AChild extends SomeClass
{
    protected $b;
}

will result a fatal error:

Fatal error: Type of AChild::$b must be string (as in class SomeClass)

see https://3v4l.org/X9Yvd . To avoid that, you should only change to private modifier only, so, the change will only to private property:

namespace Lib;

class SomeClass
{
    /** @var int */
    public $a;

    /** @var string */
    protected $b;

-    /** @var bool */
-    private $c;
+    private bool $c;
}

Want to automate that? You can use Rector for it. First, let say, we have a re-usable package that can be consumed in our applications, with the following package structure:

lib
├── composer.json
├── composer.lock
├── src
│   └── SomeClass.php

with composer.json config like this:

{
    "require": {
        "php": "^7.4"
    },
    "autoload": {
        "psr-4": {
            "Lib\\": "src/"
        }
    }
}

Your package will be hosted in packagist or your own server.

Now, what you need is require the rector as dev dependency by go to lib directory:

cd lib/
composer require --dev rector/rector

Rector has rule named TypedPropertyRector, that part of SetList::PHP_74 constant.

It default will update all modifiers:

  • public
  • protected
  • private

If you are using on projects that not re-usable project, you can just use SetList::PHP_74 constant as is.

For our use case, you can override it by configure it to only apply to private property only.

You can create a rector.php configuration inside the root of lib directory as follow:

<?php

declare(strict_types=1);

use Rector\Core\Configuration\Option;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Php74\Rector\Property\TypedPropertyRector;
use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();
    $parameters->set(Option::PATHS, [
        __DIR__ . '/src'
    ]);
    $parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_74);

    // import php 7.4 set list for php 7.4 features
    $containerConfigurator->import(SetList::PHP_74);

    // set Typed Property only for private property
    $services = $containerConfigurator->services();
    $services->set(TypedPropertyRector::class)
        ->call('configure', [[
            TypedPropertyRector::PRIVATE_PROPERTY_ONLY => true,
        ]]);
};

Above, we import php 7.4 set list, with configured TypedPropertyRector for update to typed property to only change private property only.

Now, let’s run rector to see the diff and verify:

cd lib
vendor/bin/rector --dry-run

Everything seems correct! Let’s apply the change:

cd lib
vendor/bin/rector

Now, you have typed property in your code!

That’s it!

Install Imagick extension for PHP 8 in macOS Big Sur

Posted in Teknologi, Tutorial PHP by samsonasik on January 4, 2021

I am really grateful I’ve had a chance to have the Mac mini M1 on a new year 2021. It’s really fast, even I only use 8 gigs ram version! Also, the software that in my old mac no longer can use its latest update – like php with homebrew – is back! In this post, I will show you how to install imagick for PHP 8 in macOS Big Sur for it.

First, I assume that you already have Homebrew installed. Next is install PHP 8, we can use shivammathur/php for it, the installation steps are in its readme.

When PHP 8 installed, we can ensure that imagemagick already installed, if not, if we can install via Homebrew as well:

$ brew install imagemagick

Now, time to install imagick. There is no imagick release yet for php 8, but we can install the dev-master already following this issue :

$ git clone https://github.com/Imagick/imagick
$ cd imagick
$ phpize && ./configure
$ make
$ sudo make install

When you run make, if you got the following error:

/opt/homebrew/Cellar/php/8.0.0_1/include/php/ext/pcre/php_pcre.h:23:10: fatal error: 'pcre2.h' file not found

You can copy installed pcre2.h from Homebrew:

cp /opt/homebrew/Cellar/pcre2/10.36/include/pcre2.h .

Next, you can re-run :

$ make
$ sudo make install

After it installed, you can register imagick.so to /opt/homebrew/etc/php/8.0/php.ini:

extension="imagick.so"

Now, let’s check if it is installed:

➜  ~ php -a
Interactive shell

php > echo phpversion('imagick');
@PACKAGE_VERSION@

Yes, above @PACKAGE_VERSION@ should be just fine. That’s it!

How to Create Typo Variable Fixer with Rector

Posted in php, Rector, Teknologi by samsonasik on October 21, 2020

Rector is a code refactoring tool that can help us with major code changes (like upgrade legacy code) or daily work. There are already many rules that ready to use for us.

What if we want a custom rule, like we want a daily work can to do “Typo” check in variables? In this post, I want to show you how to create a Typo Variable Fixer with Rector, a custom Rector rule!

Preparation

First, let say, we build a new app, we use composer for it with add rector/rector to require-dev:

composer init


  Welcome to the Composer config generator



This command will guide you through creating your composer.json config.

Package name (<vendor>/<name>) [samsonasik/how-to-create-typo-variable-fixer]: samsonasik/app

Description []: App Demo

Author [Abdul Malik Ikhsan <samsonasik@gmail.com>, n to skip]:

Minimum Stability []:

Package Type (e.g. library, project, metapackage, composer-plugin) []:

License []: MIT

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? no
Would you like to define your dev dependencies (require-dev) interactively [yes]? yes
Search for a package: rector/rector
Enter the version constraint to require (or leave blank to use the latest version):

Using version ^0.8.40 for rector/rector

Search for a package:

{
    "name": "samsonasik/app",
    "description": "App Demo",
    "require-dev": {
        "rector/rector": "^0.8.40"
    },
    "license": "MIT",
    "authors": [
        {
            "name": "Abdul Malik Ikhsan",
            "email": "samsonasik@gmail.com"
        }
    ],
    "require": {}
}

Do you confirm generation [yes]? yes
Would you like to install dependencies now [yes]? yes

After it, let say we need an app directory, we can create an app directory and write a php file inside it:

mkdir -p app && touch app/app.php

with file app/app.php content:

<?php

namespace App;

$previuos = 0;
$begining = 1;
$statment = $previuos . ' is lower than ' . $begining;

Yes, there are 3 typos in above file! For example, we will have a sample library.php file for common typos, for example, inside utils directory:

mkdir -p utils && touch utils/library.php

with file utils/library.php content:

<?php

namespace Utils;

return [
    'previous' => ['previuos', 'previuous'],
    'beginning' => ['begining', 'beginign'],
    'statement' => ['statment'],
];

We can setup composer autoload for with add the following to our composer.json file:

    "autoload": {
        "psr-4": {
            "App\\": "app"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Utils\\": "utils"
        }
    }

After it, run composer dump-autoload command:

composer dump-autoload

The preparation is done!

Create the Typo Fixer Rule

We can follow the Rector‘s documentation to create new custom rule. So, for example, we create TypoVariableFixerRule under own utils/Rector directory:

mkdir -p utils/Rector && touch utils/Rector/TypoVariableFixerRule.php

Our directory will looks like the following:

.
├── app
│   └── app.php
├── composer.json
├── utils
│   ├── Rector
│   │   └── TypoVariableFixerRule.php
│   └── library.php

Now, we can start create the TypoVariableFixerRule:

<?php

declare(strict_types=1);

namespace Utils\Rector;

use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;

final class TypoVariableFixerRule extends AbstractRector
{
    public function getNodeTypes(): array
    {
        return [Variable::class];
    }

    /**
     * @param Variable $node
     */
    public function refactor(Node $node): ?Node
    {
        return $node;
    }

    public function getDefinition(): RectorDefinition
    {
        return new RectorDefinition(
            'Change Typo in variable', [
                new CodeSample(
                    // code before
                    '$previuos',
                    // code after
                    '$previous'
                ),
            ]
        );
    }
}

Above, we extends AbstractRector for new Rector rule. We operate with nikic/php-parser to do refactor. The getNodeTypes returns the node that we want to refactor, at this case, we want to refactor Variable node in our refactor method.

Before we continue, let’s register our new TypoVariableFixerRule to rector config to ensure it works. We can create rector config as follow:

touch rector.php

with file rector.php content:

<?php

use Rector\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Utils\Rector\TypoVariableFixerRule;

return static function (ContainerConfigurator $containerConfigurator): void {
    $parameters = $containerConfigurator->parameters();
    $parameters->set(Option::PATHS, [__DIR__ . '/app']);

    $services = $containerConfigurator->services();
    $services->set(TypoVariableFixerRule::class);
};

and test with run:

vendor/bin/rector process

So we see the “Green” OK:

Now, time to make refactor work! We can modify the refactor method:

    public function refactor(Node $node): ?Node
    {
        // get the variable name
        $variableName = $this->getName($node);

        // get the library content
        $library = include 'utils/library.php';

        foreach ($library as $correctWord => $commonTypos) {
            if (! in_array($variableName, $commonTypos, true)) {
                continue;
            }

            $node->name = $correctWord;
            return $node;
        }

        return null;
    }

Above, we find if the variable name is in common typos, then we return node (as variable) with updated its name with the correct word. Now, let’s run it with --dry-run to see the diff that can be made:

vendor/bin/rector process --dry-run

and we can see:

Seems great! Let’s apply the changes:

vendor/bin/rector process

Awesome! We now already make typo fixer succesfully working! Let’s run again, and it will take no effect as already fixed:

That’s it!

Using React.js in Mezzio Application

Posted in Mezzio, React.js, Tutorial PHP by samsonasik on June 25, 2020

Ok, in 3 previous JavaScript posts, I posted how to use Vue.js in Mezzio Application. Now, in this post, I will show you how to use React.js in Mezzio Application.

Let’s start with download the mezzio skeleton:

composer create-project mezzio/mezzio-skeleton mezzio-react

I assume next you choose the following options:

  • Type of Installation: Modular (3)
  • Container: Laminas ServiceManager (3)
  • Router: Laminas Router (3)
  • Template Engine: Laminas View (3)

Now, we are on the same page!

The scenario is same, we want to create an SPA application. In Mezzio part, to make it work, it require template handling for ajax request.

We can create middleware for that:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class XMLHttpRequestTemplateMiddleware implements MiddlewareInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        if (in_array('XMLHttpRequest', $request->getHeader('X-Requested-With'), true)) {
            (function ($template) {
                $template->layout = false;
            })->bindTo($this->template, $this->template)($this->template);
        }

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

In above middleware, we set template layout to false to disable layout when request has X-Requested-With = XmlHttpRequest as an ajax detection. Let’s register above middleware in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\XMLHttpRequestTemplateMiddleware::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the pipeline before DispatchMiddleware:

<?php
 // config/pipeline.php
use App\Middleware\XMLHttpRequestTemplateMiddleware;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    // ...
};

We need to handle 404 Pages that can work in Ajax request, so we can create a new middleware for that, for example: App\Middleware\NotFoundMiddleware:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NotFoundMiddleware implements MiddlewareInterface
{
    private $template;
    private $config;

    public function __construct(TemplateRendererInterface $template, array $config)
    {
        $this->template = $template;
        $this->config   = $config;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        return new HtmlResponse(
            $this->template->render($this->config['mezzio']['error_handler']['template_404'])
        );
    }
}

The above middleware need to be registered to ConfigProvider:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\NotFoundMiddleware::class  => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

And then, add to config/pipeline after DispatchMiddleware:

<?php
 // config/pipeline.php

use App\Middleware\NotFoundMiddleware;
use App\Middleware\XMLHttpRequestTemplateMiddleware;

return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    $app->pipe(NotFoundMiddleware::class);
    // ...
};

Now, let’s add About and Contact page handlers:

1. About Page

<?php
// src/App/src/Handler/AboutPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AboutPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::about-page'));
    }
}

With templates:

<!-- src/templates/app/about-page.phtml -->
<h1>About Me</h1>
<p>
    I'm a web developer.
</p>

2. Contact Page

<?php
// src/App/src/Handler/ContactPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ContactPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::contact-page'));
    }
}

With templates:

<!-- src/templates/app/contact-page.phtml -->
<h1>Contact Me</h1>
<p>
    You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a>
</p>

Handlers Registration

Both AbooutPageHandler and ContactPageHandler need to be registered in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\AboutPageHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\ContactPageHandler::class => ReflectionBasedAbstractFactory::class,
                // ...
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/about', App\Handler\AboutPageHandler::class, 'about');
    $app->get('/contact', App\Handler\ContactPageHandler::class, 'contact');
};

Now, we have 3 html pages: Home, About, and Contact. It’s enough for demonstration.

JS dependencies

We can register js dependencies in the layout by add the following js:

<?php
// src/App/templates/layout/default.phtml

$this->headLink()
    ->prependStylesheet('https://use.fontawesome.com/releases/v5.12.1/css/all.css')
    ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css');
$this->inlineScript()

    ->prependFile('/js/app.js', 'module')
    ->prependFile('https://unpkg.com/react-router-dom@5.2.0/umd/react-router-dom.js')
    ->prependFile('https://unpkg.com/html-react-parser@0.13.0/dist/html-react-parser.js')
    ->prependFile('https://unpkg.com/dompurify@2.0.12/dist/purify.js')
    ->prependFile('https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js')
    ->prependFile('https://unpkg.com/react@16.13.1/umd/react.production.min.js')

    ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js')
    ->prependFile('https://code.jquery.com/jquery-3.5.0.min.js');
?>

React dependencies are react, react-dom, html-react-parser, and react-router-dom. The dompurify will be used to purify the js before displaying. For /js/app.js, that’s our code in public directory to make routing definition. In the layout, we need element for mounting, let’s define is as “root” under body:

<!-- // src/App/templates/layout/default.phtml -->
<body class="app">
    <div id="root">
        
    </div>
    <?=$this->inlineScript()?>
</body>

Now, we can define a Page component creator function, eg: createPage(), we can create a js file for it that will be consumed by /js/app.js under public directory later:

// public/js/create-page.js
let createPage = (title) => class Page extends React.Component {
    constructor(props) {
        super(props);
        this.state = { content: ''};
    }

    componentDidMount() {
        new Promise( (resolve) => {
            fetch(
                this.props.location.pathname,
                {
                    method: 'GET',
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest',
                    }
                }
            ).then(response =>  resolve(response.text()));
        }).then(result => {
            this.setState({ content : result });
            document.title = title;
        });
    }

    render() {
      return React.createElement(
            'div',
            {
                className : "app-content",
            },
            React.createElement(
                "main",
                {
                    className : "container"
                },
                HTMLReactParser(DOMPurify.sanitize(this.state.content))
            )
        );
    }
}

export default createPage;

Above, we use location route path page, and change title after content grabbed based on passed title parameter. We use HTMLReactParser to generate html for the raw html from the server response.

Now, let create a Navigation, we can create Navigation component, eg, in /js/Navigation.js under public directory:

// public/js/Navigation.js
const {
    NavLink
} = ReactRouterDOM;

const {
    Component,
    createElement
} = React;

class Navigation extends Component {
    render() {
      return createElement(
            'nav',
            {
                className : "navbar navbar-expand-sm navbar-dark bg-dark fixed-top",
                role: "navigation"
            },
            createElement(
                'div',
                {
                    className: "container"
                },
                createElement(
                    'div',
                    {
                        className: "navbar-header"
                    },
                    createElement(
                        'button',
                        {
                            className: "navbar-toggler",
                            "data-toggle": "collapse",
                            "data-target": "#navbarCollapse",
                            "aria-controls": "#navbarCollapse",
                            "aria-expanded": "false",
                            "aria-label": "Toggle navigation"
                        },
                        createElement(
                            'span',
                            {
                                className: "navbar-toggler-icon"
                            }
                        )
                    ),
                    createElement(
                        NavLink,
                        {
                            to: "/",
                            className: "navbar-brand"
                        },
                        createElement(
                            'img',
                            {
                                src: "https://docs.laminas.dev/img/laminas-mezzio-rgb.svg",
                                alt: "Laminas Mezzio",
                                height: 56
                            }
                        )
                    )
                ),
                createElement(
                    'div',
                    {
                        className: "collapse navbar-collapse",
                        id: "navbarCollapse"
                    },
                    createElement(
                        "ul",
                        {
                            className: "navbar-nav mr-auto"
                        },
                        createElement(
                            "li",
                            {
                                className: "nav-item"
                            },
                            createElement(NavLink, { className: 'nav-link', to: "/", exact: true }, "Home")
                        ),
                        createElement(
                            "li",
                            {
                                className: "nav-item"
                            },
                            createElement(NavLink, { className: 'nav-link', to: "/about", exact: true }, "About")
                        ),
                        createElement(
                            "li",
                            {
                                className: "nav-item"
                            },
                            createElement(NavLink, { className: 'nav-link', to: "/contact", exact: true }, "Contact")
                        )
                    )
                )
            )
        );
    }
}

export default Navigation;

Above, we define the navigation, with add “active” class on link selected.

Finally, our /js/app.js under public directory that consume createPage function and Navigation component to be used for routing definition and navigation.

// public/js/app.js
import createPage from './create-page.js';
import Navigation from './Navigation.js';

const {
    BrowserRouter,
    Switch,
    Route
} = ReactRouterDOM;

const Main = () => React.createElement(
    "main",
    null,
    React.createElement(
        Switch,
        null,
        React.createElement(
            Route, {
                exact: true,
                path: "/",
                component: createPage('Home')
            }
        ),
        React.createElement(
            Route, {
                exact: true,
                path: "/about",
                component: createPage('About')
            }
        ),
        React.createElement(
            Route, {
                exact: true,
                path: "/contact",
                component: createPage('Contact')
            }
        ),
        React.createElement(
            Route, {
                exact: true,
                path: "*",
                component: createPage('404 Page')
            }
        )
    )
);

const Header = () => React.createElement(
    'header',
    {
        className: 'app-header'
    },
    React.createElement(Navigation)
);

const App = () => React.createElement(
    "div",
    null,
    React.createElement(Header, null),
    React.createElement(Main, null)
);

ReactDOM.render(
    React.createElement(
        BrowserRouter,
        null,
        React.createElement(App, null)
    ),
    document.getElementById('root')
);

// https://reactjs.org/docs/react-without-jsx.html
// https://www.pluralsight.com/guides/just-plain-react
// https://codepen.io/pshrmn/pen/YZXZqM?editors=1010

Now, if we check, we will get SPA working:

That’s it! I uploaded the sample source code at github: https://github.com/samsonasik/mezzio-react

References:
https://reactjs.org/docs/react-without-jsx.html
https://www.pluralsight.com/guides/just-plain-react
https://codepen.io/pshrmn/pen/YZXZqM?editors=1010

Tagged with: ,

Using Vuex’s Vue.js and sessionStorage combo for searchable get api data and cached in Mezzio Application

Posted in Javascript, Mezzio, Tutorial PHP, Vue.js by samsonasik on June 13, 2020

So, this is the 3rd post about usage of Vue.js in Mezzio Application. If you haven’t read my previous 2 posts, I suggest you to read them first:

Ok, let’s continue. Now, we are going to use Vuex as state management (when without refresh) and native sessionStorage combo to handle searched data in next refresh to avoid unnecessary re-query data as previously already searched. For note, I use sessionStorage so next close – re-open browser will clear the data.

Load the Vuex Library

We can load Vuex library in the layout:

<?php
// src/App/templates/layout/default.phtml
// ...
    ->prependFile('/js/app.js')
    ->prependFile('https://unpkg.com/vuex@3.4.0/dist/vuex.js')
    ->prependFile('https://unpkg.com/vue-router@3.3.2/dist/vue-router.js')
    ->prependFile('https://unpkg.com/vue@2.6.11/dist/vue.js')
// ...

The Data

For example, we want to display portfolio data via API. For example, we have the following portfolio array data example (in real life, you an use DB ofcourse)

<?php
// data/portfolio.php
return [
    [
        'id'    => 1,
        'title' => 'Website A',
        'image' => 'https://via.placeholder.com/150/FF0000/FFFFFF?text=website%20A',
        'link'  => 'https://www.website-a.com',
    ],
    [
        'id'    => 2,
        'title' => 'Website B',
        'image' => 'https://via.placeholder.com/150/0000FF/808080?text=website%20B',
        'link'  => 'https://www.website-b.com',
    ],
    [
        'id'    => 3,
        'title' => 'Website C',
        'image' => 'https://via.placeholder.com/150/000000/FFFFFF?text=website%20C',
        'link'  => 'https://www.website-c.com',
    ]
];

If you use GIT with mezzio skeleton, the data need to be registered to .gitignore to allow to be committed:

# data/.gitignore
*
!cache
!cache/.gitkeep
!.gitignore
!portfolio.php

The API

Now, time to create API page, for example App\Handler\Api\PortfolioApiHandler:

<?php
// src/App/src/Handler/Api/PortfolioApiHandler.php
declare(strict_types=1);

namespace App\Handler\Api;

use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class PortfolioApiHandler implements RequestHandlerInterface
{
    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        $data  = include './data/portfolio.php';
        $keyword = $request->getQueryParams()['keyword'] ?? '';

        if ($keyword) {
            $data = array_filter($data, function ($value) use ($keyword) {
                return (
                    strpos(strtolower($value['title']), strtolower($keyword)) !== false
                    ||
                    strpos(strtolower($value['link']), strtolower($keyword)) !== false
                );
            });
        }

        return new JsonResponse($data);
    }
}

Above, we use array_filter to search portfolio data for title and link with keyword query parameter.

Next, we can register to our ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\Api\PortfolioApiHandler::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/api/portfolio', App\Handler\Api\PortfolioApiHandler::class, 'api-portfolio');
};

The Page

We need to consume the API via a page, for example, we create handle for it: App\Handler\PortfolioPageHandler:

<?php
// src/App/src/Handler/PortfolioPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class PortfolioPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::portfolio-page'));
    }
}

Next, we can register to our ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\Api\PortfolioApiHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\PortfolioPageHandler::class    => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/api/portfolio', App\Handler\Api\PortfolioApiHandler::class, 'api-portfolio');
    $app->get('/portfolio', App\Handler\PortfolioPageHandler::class, 'portfolio');
};

The Template

For view, we need to display portfolio data with allow to search by keyword via input text. We can create view as follow:

<!-- src/App/templates/app/portfolio-page.phtml-->
Keyword: <input type="keyword" id="keyword" v-on:input="this.$parent.search" v-on:focus="this.$parent.search"/> <br /><br />

<table class="table">
	<tr>
		<th>Title</th>
		<th>Image</th>
        <th>Link</th>
	</tr>

    <tr v-if="this.$parent.portfolio.length == 0">
        <td colspan="3" class="text-center">No portfolio found.</td>
    </tr>

    <tr v-for="loop in this.$parent.portfolio" :key="loop.id">
        <td>{{ loop.title }}</td>
        <td><img :src="`${ loop.image }`" /></td>
        <td><a v-bind:href="`${ loop.link }`">{{ loop.link }}</a></td>
    </tr>
</table>

<script type="application/javascript">
const store = new Vuex.Store({
    state: {
        portfolio : {}
    },
    mutations: {
        search (state, data) {
            sessionStorage.setItem('search-' + data.keyword, JSON.stringify(data.value));
            state.portfolio[data.keyword] = data.value;
        }
    }
});

document.querySelector('#keyword').focus();
</script>

In Vue.js template, we can fill JavaScript with “application/javascript” script type. Above, I initialize Vuex’s Store instance with definition of portfolio state data that on search mutation, set stringified object data.value into sessionStorage item based on keyword and fill the state.portfolio[data.keyword] with data.value. We will fill data as keyword and value later in the public/js/app.js. On very first page accessed, we set focus to keyword text field that trigger search function we register in public/js/app.js when define portfolio page component. So, we can finally loop the data searched.

The JavaScript

In public/js/app.js, now, we can portfolio component with the following defintion:

const routes = [
    // ... other page definition here ...
    {
        path: '/portfolio',
        component: createPage(
            'portfolio',
            {
                portfolio : []
            },
            {
                search: function (e) {
                    let keyword = e.target.value;

                    if (typeof store.state.portfolio[keyword] !== 'undefined') {
                        this.portfolio = store.state.portfolio[keyword];

                        return;
                    }

                    if (sessionStorage.getItem('search-' + keyword)) {
                        portfolio     = JSON.parse(sessionStorage.getItem('search-' + keyword));
                        store.commit('search', { keyword: keyword, value: portfolio });
                        this.portfolio = portfolio;

                        return;
                    }

                    (async () => {
                        await new Promise( (resolve) => {
                            fetch(
                                '/api/portfolio?keyword=' + keyword,
                                {
                                    method: 'GET',
                                    headers: {
                                        'Accept': 'application/json',
                                    }
                                }
                            ).then(response =>  resolve(response.json()));
                        }).then(result => this.portfolio = result);

                        store.commit('search', { keyword: keyword, value: this.portfolio });
                    })();
                }
            }
        ),
        meta: {
            title: 'My Portfolio'
        }
    }
];

Above, in definition of portfolio component, we define a portfolio data attribute to empty array. On search function (that we know it triggered in template input focus and input event), we have the following flow:

a. get keyword from e.target.value as keyword input value
b. check if Vuex store.state.portfolio[keyword] not undefined, means it already in Vuex stage, then fill portfolio data attribute with it, then return early.
c. check if there is session storage data with item key “search-” + keyword value, means it already in session storage, then fill portfolio data attribute with its parsed to object from json stringified data, then return early.
d. otherwise, use async/await function to fill portfolio data attribute, and then commit to Vuex store.

Last but not least, add link to /portfolio page in the layout:

<div class="collapse navbar-collapse" id="navbarCollapse">
    <!-- other menu here -->

    <li class="nav-item">
        <router-link to="/portfolio" class="nav-link">Portfolio</router-link>
    </li>

</div>

That’s it, now we have fully functional searchable and cached even on refresh, unless browser is closed and re-open.

I published the code at https://github.com/samsonasik/mezzio-vue if you want to give it a try 😉

Refs:

Using Vue.compile() to activate Vue component’s data and method in Mezzio Application

Posted in Javascript, Mezzio, Tutorial PHP, Vue.js by samsonasik on June 6, 2020

In previous post, we already tried create an SPA application with template rendered via Fetch for XHR purpose. What if we want to call data or/and method in template? With v-html, we can’t! The way we can do is make it compiled with Vue.compile(). Let’s check the JS part:

createPage = (name, object = {}, methods = {}) => {
    return Vue.component('page-' + name, {
        data    : () => Object.assign({content: ''}, object),
        methods : methods,
        mounted () {
            (new Promise( (resolve) => {
                fetch(
                    this.$route.path,
                    {
                        method: 'GET',
                        headers: {
                            'X-Requested-With': 'XMLHttpRequest',
                        }
                    }
                ).then(response =>  resolve(response.text()));
            })).then(result => this.content = result);
        },
        render : function (c) {
            if (this.content == '') {
                return;
            }

            return c(Vue.compile('<div>' + this.content + '</div>'));
        }
    });
}

const routes = [
    { path: '/', component: createPage('home'), meta: {
        title: 'Home'
    } },
    { path: '/about', component: createPage(
        'about',
        {
            name: 'Abdul Malik Ikhsan'
        },
        {
            hit: () => alert('This alert already proof that I am a web developer!')
        }
    ), meta: {
        title: 'About Me'
    } },
    { path: '/contact', component: createPage('contact'), meta: {
        title: 'Contact Me'
    } },
    { path: "*", component: createPage('404'), meta: {
        title: '404 Not Found'
    } }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});

router.afterEach(to => document.title = to.meta.title);

vue = new Vue({
    router
}).$mount('#root');

In above JS, first, I create a createPage function that in 2nd parameter, can pass custom data besides the current content data, and in 3rd parameter, can pass custom methods definition. With content fetched that assigned to content data, finally, we use it in the Vue.compile() on render.

The another special part is in the template part, it requires to use this.$parent to get parent attribute/method. For example, on the ‘about’ page above, we need to get name data, and can call the hit method, we can do like the following:

<!-- src/templates/app/about-page.phtml -->
<h1>About Me</h1>
<p>
    I'm a web developer. My name is {{ this.$parent.name }}. <br />
    <button v-on:click="this.$parent.hit">Click this as a proof</button>
</p>

That’s it, now you can open the about page and can see like the following:

I uploaded the sample source code at github, if you need to see what the diff between my previous post and this, you can check this PR https://github.com/samsonasik/mezzio-vue/pull/1 😉

Refs:
https://vuejs.org/v2/api/#Vue-compile
https://vuejs.org/v2/guide/render-function.html#Functional-Components
https://stackoverflow.com/questions/51548729/vuejs-vue-app-render-method-with-dynamic-template-compiled-is-throwing-some/51552701

Using Vue.js in Mezzio Application

Posted in Javascript, Mezzio, Tutorial PHP, Vue.js by samsonasik on May 30, 2020

So, another JavaScript post! If you read my post at 2015 about Ember.js usage in Zend Framework 2 application, now let’s try with Vue.js, but for Mezzio application.

Let’s start with download the mezzio skeleton:

composer create-project mezzio/mezzio-skeleton mezzio-vue

I assume next you choose the following options:

  • Type of Installation: Modular (3)
  • Container: Laminas ServiceManager (3)
  • Router: Laminas Router (3)
  • Template Engine: Laminas View (3)

Now, we are on the same page!

The scenario is same, we want to create an SPA application. In Mezzio part, to make it work, it require template handling for ajax request.

We can create middleware for that:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class XMLHttpRequestTemplateMiddleware implements MiddlewareInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        if (in_array('XMLHttpRequest', $request->getHeader('X-Requested-With'), true)) {
            (function ($template) {
                $template->layout = false;
            })->bindTo($this->template, $this->template)($this->template);
        }

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

In above middleware, we set template layout to false to disable layout when request has X-Requested-With = XmlHttpRequest as an ajax detection. Let’s register above middleware in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\XMLHttpRequestTemplateMiddleware::class => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

and in the pipeline before DispatchMiddleware:

<?php
 // config/pipeline.php
use App\Middleware\XMLHttpRequestTemplateMiddleware;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    // ...
};

We need to handle 404 Pages that can work in Ajax request, so we can create a new middleware for that, for example: App\Middleware\NotFoundMiddleware:

<?php

declare(strict_types=1);

namespace App\Middleware;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

class NotFoundMiddleware implements MiddlewareInterface
{
    private $template;
    private $config;

    public function __construct(TemplateRendererInterface $template, array $config)
    {
        $this->template = $template;
        $this->config   = $config;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        return new HtmlResponse(
            $this->template->render($this->config['mezzio']['error_handler']['template_404'])
        );
    }
}

The above middleware need to be registered to ConfigProvider:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Middleware\NotFoundMiddleware::class  => ReflectionBasedAbstractFactory::class,
            ],
        ];
    }
    // ...
}

And then, add to config/pipeline after DispatchMiddleware:

<?php
 // config/pipeline.php

use App\Middleware\NotFoundMiddleware;
use App\Middleware\XMLHttpRequestTemplateMiddleware;

return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->pipe(XMLHttpRequestTemplateMiddleware::class);
    $app->pipe(DispatchMiddleware::class);
    $app->pipe(NotFoundMiddleware::class);
    // ...
};

Now, let’s add About and Contact page handlers:

1. About Page

<?php
// src/App/src/Handler/AboutPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class AboutPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::about-page'));
    }
}

With templates:

<!-- src/templates/app/about-page.phtml -->
<h1>About Me</h1>
<p>
    I'm a web developer.
</p>

2. Contact Page

<?php
// src/App/src/Handler/ContactPageHandler.php
declare(strict_types=1);

namespace App\Handler;

use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class ContactPageHandler implements RequestHandlerInterface
{
    private $template;

    public function __construct(TemplateRendererInterface $template)
    {
        $this->template = $template;
    }

    public function handle(ServerRequestInterface $request) : ResponseInterface
    {
        return new HtmlResponse($this->template->render('app::contact-page'));
    }
}

With templates:

<!-- src/templates/app/contact-page.phtml -->
<h1>Contact Me</h1>
<p>
    You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a>
</p>

Handlers Registration

Both AbooutPageHandler and ContactPageHandler need to be registered in ConfigProvider class:

<?php
 // src/App/src/ConfigProvider.php
declare(strict_types=1);

namespace App;

use Laminas\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory;

class ConfigProvider
{
    // ...
    public function getDependencies() : array
    {
        return [
            'invokables' => [
                 // ...
            ],
            'factories'  => [
                // ..
                Handler\AboutPageHandler::class => ReflectionBasedAbstractFactory::class,
                Handler\ContactPageHandler::class => ReflectionBasedAbstractFactory::class,
                // ...
            ],
        ];
    }
    // ...
}

and in the routes:

<?php
// config/routes.php
return static function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void {
    // ...
    $app->get('/about', App\Handler\AboutPageHandler::class, 'about');
    $app->get('/contact', App\Handler\ContactPageHandler::class, 'contact');
};

Now, we have 3 html pages: Home, About, and Contact. It’s enough for demonstration.

JS dependencies

We can register js dependencies in the layout by add the following js:

<?php
// src/App/templates/layout/default.phtml

$this->headLink()
    ->prependStylesheet('https://use.fontawesome.com/releases/v5.12.1/css/all.css')
    ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css');
$this->inlineScript()

    ->prependFile('/js/app.js')
    ->prependFile('https://unpkg.com/vue-router@3.3.2/dist/vue-router.js')
    ->prependFile('https://unpkg.com/vue@2.6.11/dist/vue.js')

    ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js')
    ->prependFile('https://code.jquery.com/jquery-3.5.0.min.js');
?>

Vue dependencies are Vue.js core and Vue Router for routing. For /js/app.js, that’s our code to make routing definition. In the layout, we need element for mounting, let’s define is as “root” under body:

<!-- // src/App/templates/layout/default.phtml -->
<body class="app">
    <div id="root">
        
    </div>
    <?=$this->inlineScript()?>
</body>

Now, we can define the router links inside “root” div:

<!-- // src/App/templates/layout/default.phtml -->
<div id="root">

        <header class="app-header">
            <nav class="navbar navbar-expand-sm navbar-dark bg-dark fixed-top" role="navigation">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="#navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                            <span class="navbar-toggler-icon"></span>
                        </button>
                        <router-link to="/" class="navbar-brand"><img src="https://docs.laminas.dev/img/laminas-mezzio-rgb.svg" alt="Laminas Mezzio" height="56" /></router-link>
                    </div>
                    <div class="collapse navbar-collapse" id="navbarCollapse">
                        <ul class="navbar-nav mr-auto">
                            <li class="nav-item">
                                <router-link to="/" class="nav-link">Home</router-link>
                            </li>
                            <li class="nav-item">
                                <router-link to="/about" class="nav-link">About</router-link>
                            </li>
                            <li class="nav-item">
                                <router-link to="/contact" class="nav-link">Contact</router-link>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        </header>

</div>

Next, time for the view content placeholder, we can define router-view inside “root” div as well for place to collect the content:

<!-- // src/App/templates/layout/default.phtml -->
<div id="root">
        <header class="app-header"> <!-- content app header before --> </header>

        <main class="container">
            <div id="app">
                <keep-alive> <!-- make content cached if already loaded -->
                    <router-view></router-view>
                </keep-alive>
            </div>
        </main>

        <!-- footer here -->
</div>

Now, our JS file in public/js/app.js for routing definition:

createPage = (name) => {
    return Vue.component('page-' + name, {
        data: () => {
            return  {
              content: 'Loading...'
            }
        },
        mounted () {
            (new Promise( (resolve) => {
                fetch(
                    this.$route.path,
                    {
                        method: 'GET',
                        headers: {
                            'X-Requested-With': 'XMLHttpRequest',
                        }
                    }
                ).then(response =>  resolve(response.text()));
            })).then(result => this.content = result);
        },
        template: '<div v-html="content"></div>'
    });
}

const routes = [
    { path: '/', component: createPage('home') },
    { path: '/about', component: createPage('about') },
    { path: '/contact', component: createPage('contact') },
    { path: '*', component: createPage('404') }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});
const app    = new Vue({router}).$mount('#root')

In above code, we create a page component on the fly via function createPage and apply to each path. Setup VueRouter with routes definition, and make a Vue instance with it, mount to div id #root.

Bonus

How about make page title changed after on change page? We can set meta title in each route definition, and use router.afterEach() to apply it:

// ...
const routes = [
	{ path: '/', component: createPage('home'),  meta: {
            title: 'Home'
        } },
	{ path: '/about', component: createPage('about'), meta: {
            title: 'About Me'
        } },
	{ path: '/contact', component: createPage('contact'), meta: {
            title: 'Contact Me'
        } },
    { path: '*', component: createPage('404'), meta: {
            title: '404 Not Found'
        } }
];

const router = new VueRouter({
    routes,
    base: '/',
    mode: 'history',
    linkExactActiveClass: "active"
});

router.afterEach(to => document.title = to.meta.title);
// ...

Now, if we check, we will get SPA working:

That’s it! I uploaded the sample source code at github: https://github.com/samsonasik/mezzio-vue

References:
https://vuejs.org/v2/guide/
https://router.vuejs.org/guide/#html
https://medium.com/badr-interactive/mengenal-lifecycle-hooks-pada-vue-js-78cd2225a69
https://forum.vuejs.org/t/setting-a-correct-base-url-with-vue-router/24726/2
https://forum.vuejs.org/t/how-do-i-make-an-html-tag-inside-a-data-string-render-as-an-html-tag/13074/3

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: , ,

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: ,