Welcome to Abdul Malik Ikhsan's Blog

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: