Using React.js in Mezzio Application
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
Using Vuex’s Vue.js and sessionStorage combo for searchable get api data and cached in Mezzio Application
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:
- Using Vue.js in Mezzio Application
- Using Vue.compile() to activate Vue component’s data and method in Mezzio Application
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
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
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
Using laminas-cli to Consume Symfony Console Command in Mezzio Application
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!
Using Swoole in Mezzio application with Sdebug
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:
leave a comment