Rapid Enterprise App Development with Zend Expressive

0
482

If you’ve ever done a Zend Framework quick start, you’ve probably never worked in Zend Framework. The quick start has historically been anything but quick, and it’s easy to lose interest and move on to the next thing.

Zend Expressive greatly improves upon this experience with the wizard driven composer create-project command. However, it can still be daunting to set up because there are so many choices to make up front. This tutorial guides you through my recommended setup for rapid development which will
yield an enterprise level, robust application.

Zend Framework logo

This tutorial is not about setting up your environment, so I am going to assume that you have a good working environment like Homestead Improved.

If you’re not familiar with Vagrant or isolated virtual environments, we’ve got an amazing book to guide you through the concepts available in our shop here.

Project Setup

Start your project by running the following command your the folder where you keep your projects (Code on Homestead Improved):

composer create-project zendframework/zend-expressive-skeleton expressive

You will be prompted to make a few decisions along the way. Use these answers:

  • What type of installation would you like?
    • Modular
  • Which container do you want to use for dependency injection?
    • Zend ServiceManager
  • Which router do you want to use?
    • Zend Router
  • Which template engine do you want to use?
    • Twig
  • Which error handler do you want to use during development?
    • Whoops
  • Please select which config file you wish to inject ‘Zend\Validator\ConfigProvider’ into?
    • config/config.php
  • Remember this option for other packages of the same type?
    • y

Then, run these commands:

cd expressive &&
git init &&
git config color.ui true &&
git add . &&
git commit -m "Initial commit" &&
chmod -R +w data;

This initializes a repository in the newly created folder and makes the data folder writable.

Then, start up a php server for testing with

composer serve

… and browse to http://localhost:8080 or just visit the VM’s IP or virtual host if you’re using Homestead Improved.

Zend Expressive Homepage

Understanding Expressive

Expressive’s folder structure looks like this:

bin/
config/
data/
  cache/
public/
  index.php
src/
  App
test/
  AppTest
vendor/

Most of it is self explanatory. Expressive provides an App module by default. You can put all your code in here, or build separate modules as you build larger features.

Expressive comes with some handy commands:

  • ./vendor/bin/expressive – Create, register, and deregister modules. Create a middleware class, etc.
  • composer serve – Alias to run a php-fpm server
  • composer cs-check – Perform a coding standards check on your code.
  • composer cs-fix – Perform a coding standards check on your code and fix issues, where possible.
  • composer test – Run PHPUnit tests on your code.
  • composer check – Alias for running cs-check, then test.

Expressive also comes with the Whoops error handler. To test it, open src/App/src/Action/HomePageAction.php and type echo $badVar in the process() method, then refresh the page. You will see the Whoops error handler.

Whoops Error Handler

Necessary Improvements

Reflection Based Abstract Factory

Zend Expressive uses the Zend ServiceManager for Dependency Injection. In the default setup, you need to add configuration and potentially create a factory class for every single class you write. This feels burdensome after doing this about twice.

To avoid this, we will enable the reflection based abstract factory provided with Zend Expressive.

Add this to config/autoload/dependencies.global.php within the dependencies array:

'abstract_factories' => [
    \Zend\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory::class
],

Now, whenever you are working in a class and need a dependency, just add it to your constructor. The reflection abstract factory will see what your class needs and automatically grab it from the service container. You only need to create factories now in exceptional cases where you need something different from the default service provided by the service container.

If you’re concerned about speed; In production, we can have a process that generates factories for your classes that were being handled by the reflection factory with vendor/bin/generate-factory-for-class.

Doctrine

Zend Expressive provides no database tooling or ORM. I’ve chosen Doctrine as my ORM of choice after much research and building a few ORMs of my own. It just works.

Install Doctrine and Symfony Yaml via Composer:

composer require dasprid/container-interop-doctrine symfony/yaml

Create a file config/cli-config.php with these contents:

<?php

use Doctrine\ORM\Tools\Console\ConsoleRunner;

chdir(dirname(__DIR__));
require 'vendor/autoload.php';

/**
 * Self-called anonymous function that creates its own scope and keep the global namespace clean.
 */
return call_user_func(function () {
    /** @var \Interop\Container\ContainerInterface \$container */
    $container = require 'config/container.php';

    $entityManager = $container->get(\Doctrine\ORM\EntityManager::class);
    return ConsoleRunner::createHelperSet($entityManager);
});

Replace the contents of config/autoload/dependencies.global.php with the following:

<?php

use Zend\Expressive\Application;
use Zend\Expressive\Container;
use Zend\Expressive\Delegate;
use Zend\Expressive\Helper;
use Zend\Expressive\Middleware;

return [
    // Provides application-wide services.
    // We recommend using fully-qualified class names whenever possible as
    // service names.
    'dependencies' => [
        'abstract_factories' => [
            \Zend\ServiceManager\AbstractFactory\ReflectionBasedAbstractFactory::class
        ],
        // Use 'aliases' to alias a service name to another service. The
        // key is the alias name, the value is the service to which it points.
        'aliases' => [
            'Zend\Expressive\Delegate\DefaultDelegate' => Delegate\NotFoundDelegate::class,
        ],
        // Use 'invokables' for constructor-less services, or services that do
        // not require arguments to the constructor. Map a service name to the
        // class name.
        'invokables' => [
            // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class,
            \Doctrine\DBAL\Logging\DebugStack::class => \Doctrine\DBAL\Logging\DebugStack::class,
            Helper\ServerUrlHelper::class => Helper\ServerUrlHelper::class,
            Middleware\ImplicitHeadMiddleware::class => Middleware\ImplicitHeadMiddleware::class,
            Middleware\ImplicitOptionsMiddleware::class => Middleware\ImplicitOptionsMiddleware::class,
        ],
        // Use 'factories' for services provided by callbacks/factory classes.
        'factories'  => [
            Application::class                => Container\ApplicationFactory::class,
            Delegate\NotFoundDelegate::class  => Container\NotFoundDelegateFactory::class,
            \Doctrine\ORM\EntityManager::class  => \ContainerInteropDoctrine\EntityManagerFactory::class,
            Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
            Helper\UrlHelper::class           => Helper\UrlHelperFactory::class,
            Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
            Zend\Stratigility\Middleware\ErrorHandler::class => Container\ErrorHandlerFactory::class,
            Middleware\ErrorResponseGenerator::class         => Container\ErrorResponseGeneratorFactory::class,
            Middleware\NotFoundHandler::class                => Container\NotFoundHandlerFactory::class,
        ],
    ],
];

Create this file to set up the Doctrine driver config/autoload/doctrine.global.php.

<?php

return [
    'doctrine' => [
        'driver' => [
            'orm_default' => [
                'class' => \Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain::class,
                'drivers' => [],
            ],
        ],
    ],
];

Create this file for your database credentials config/autoload/doctrine.local.php.

<?php
return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'params' => [
                    'url' => 'mysql://root:[email protected]/expressive',
                ],
            ],
        ],
    ],
];

Test by running ./vendor/bin/doctrine. You should see the help prompt.

Gulp

Gulp is my current tool of choice for frontend workflow. There are many, many frontend build tools available. Look if you like, but you may get lost in the sea of shiny new JavaScript libraries out there. I don’t want to get too involved here as this is more a PHP tutorial than JS, but I do want to show how gulp should be configured to work with Zend Expressive.

Create a package.json file with these contents:

{
  "name": "expressive",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "devDependencies": {
    "del": "^3.0.0",
    "gulp": "github:gulpjs/gulp#4.0",
    "gulp-cached": "^1.1.1",
    "gulp-imagemin": "^3.3.0",
    "gulp-minify-css": "^1.2.4",
    "gulp-rename": "^1.2.2",
    "gulp-sass": "^3.1.0",
    "gulp-uglify": "^2.1.2",
    "gulp-usemin": "^0.3.28"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Run npm install. You may want to run npm update also, if you are reading this tutorial a while after it was written.

Then, create a gulpfile.js with these contents:

'use strict';

var cache = require('gulp-cached');
var del = require('del');
var gulp = require('gulp');
var imagemin = require('gulp-imagemin');
var minifyCss = require('gulp-minify-css');
var path = require('path');
var rename = require('gulp-rename');
var sass = require('gulp-sass');
var uglify = require('gulp-uglify');

// CSS Processing
gulp.task('clean-css', function() {
    return del('public/css', { force: true });
});

gulp.task('compile-sass', function() {
    return gulp.src('src/*/public/sass/**/*.scss', { base: './' })
        .pipe(cache('compile-sass'))
        .pipe(sass().on('error', sass.logError))
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace(/^src\/([^\/]+\/)public\/sass/, '$1');
        }))
        .pipe(gulp.dest('public/css/'));
});

gulp.task('copy-css', function() {
    return gulp.src('src/*/public/css/**/*.css', { base: './' })
        .pipe(cache('copy-css'))
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace(/^src\/([^\/]+\/)public\/css/, '$1');
        }))
        .pipe(gulp.dest('public/css/'));
});

gulp.task('minify-css', function() {
    return gulp.src(['public/css/**/*.css', '!public/css/**/*.min.css'], { base: './' })
        .pipe(cache('minify-css'))
        .pipe(minifyCss())
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace(/^public\/css/, '');
        }))
        .pipe(rename({ extname: '.min.css' }))
        .pipe(gulp.dest('public/css'))
    ;
});
gulp.task('process-css', gulp.series(['compile-sass', 'copy-css'], 'minify-css'));

// JS Processing
gulp.task('clean-js', function() {
    return del('public/js', { force: true });
});

gulp.task('copy-js', function() {
    return gulp.src('src/*/public/js/**/*.js', { base: './' })
        .pipe(cache('copy-js'))
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace(/^src\/([^\/]+\/)public\/js/, '$1');
        }))
        .pipe(gulp.dest('public/js/'));
});

gulp.task('minify-js', function() {
    return gulp.src(['public/js/**/*.js', '!public/js/**/*.min.js'], { base: './' })
        .pipe(cache('minify-js'))
        .pipe(uglify())
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace(/^public\/js/, '');
        }))
        .pipe(rename({ extname: '.min.js' }))
        .pipe(gulp.dest('public/js'))
    ;
});
gulp.task('process-js', gulp.series('copy-js', 'minify-js'));

// Image processing
gulp.task('clean-img', function() {
    return del('public/img', { force: true });
});

gulp.task('process-img', function() {
    return gulp.src('src/*/public/img/**/*.{gif,jpg,jpeg,png,svg}', { base: './' })
        .pipe(cache('process-img'))
        .pipe(imagemin())
        .pipe(rename(function (path) {
            path.dirname = path.dirname.replace(/^src\/([^\/]+\/)public\/img/, '$1');
        }))
        .pipe(gulp.dest('public/img'));
});


// Top level commands
gulp.task('default', gulp.parallel('process-js', 'process-css', 'process-img'));
gulp.task('clean', gulp.parallel('clean-js', 'clean-css', 'clean-img'));

gulp.task('watch', function() {
    gulp.watch(['src/*/public/sass/**/*.scss','src/*/public/css/**/*.css'], gulp.series('process-css'));
    gulp.watch('src/*/public/js/**/*.js', gulp.series('process-js'));
    gulp.watch('src/*/public/img/**/*.{gif,jpg,jpeg,png,svg}', gulp.series('process-img'));
});

Run gulp and ensure that it runs without errors.

Now you can run gulp to compile sass, minify css, minify js, and optimize images across all of your modules. You can follow that up with gulp watch to have these all automatically be processed as they are changed. The cache gulp module ensures that only changed files are ever processed so this should process changes very quickly.

Test this by creating one of these files:

  • src/App/public/sass/sasstest.scss
  • src/App/public/css/test.css
  • src/App/public/js/test.js
  • src/App/public/img/test.jpg

And then run gulp. Look for the files in public/css/Apppublic/js/App, or public/img/App.

Run gulp watch, change the source files, and then watch for the files in public to update.

Console Commands

And last, but definitely not least, you will need a way to run console commands. We will use Symfony’s Console for this,which already ships with Zend Expressive so we do not need to manually require it.

Create a file called bin/console:

#!/usr/bin/env php
<?php

chdir(dirname(__DIR__));
require 'vendor/autoload.php';

/**
 * Self-called anonymous function that creates its own scope and keep the global namespace clean.
 */
call_user_func(function () {
    /** @var \Interop\Container\ContainerInterface $container */
    $container = require 'config/container.php';

    $app = new \Symfony\Component\Console\Application('Application console');

    $commands = $container->get('config')['console']['commands'];
    foreach ($commands as $command) {
        $app->add($container->get($command));
    }

    $app->run();
});

Then, you can create Symfony commands and register them via config/autoload/console.global.php or from within your modules like this:

'commands' => [
    \App\Command\HelloWorldCommand::class,
],

Add any dependencies your console commands need to the constructor just like any other class in Expressive. Make sure to call parent::__construct() in your constructor or your command won’t work.

Here is an example command with a dependency:

<?php

namespace App\Command;

use Doctrine\ORM\EntityManager;
use Monolog\Logger;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class HellowWorld extends Command
{
    /**
     * @var EntityManager
     */
    private $entityManager;

    public function __construct(EntityManager $entityManager, $name = null)
    {
        $this->entityManager = $entityManager;
        parent::__construct($name);
    }

    /**
     * Configures the command
     */
    protected function configure()
    {
        $this->setName('hello')
            ->setDescription('Says hello')
        ;
    }

    /**
     * Executes the current command
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln("Hello World!");

        // Do something with the entityManager
        $this->entityManager->find('Blog\Entity\BlogEntity');
    }
}

To run your command:

php bin/console hello

We can go a little bit further and add a logger. This is useful for passing to service models that encapsulate a lot of logic and need debug logging throughout.

Run this command:

composer require monolog/monolog symfony/monolog-bridge;

Then, add this to your execute method in your command:

protected function execute(InputInterface $input, OutputInterface $output)
{
    $logger = new \Monolog\Logger('collect-product-data');
    $logger->pushHandler(new \Symfony\Bridge\Monolog\Handler\ConsoleHandler($output));
    $logger->debug('Log something);
}

Conclusion

You are now ready to begin building an enterprise level application with all of the tools you could ever want at your fingertips.

In the next post we’ll start learning how to build modules on this foundation, starting off with a blog module.

Suggest

PHP for Beginners -Become a PHP Master – Project Included

PHP & Mysqli Tutorials for beginners and professionals

PHP Fundamentals : Learn PHP From Scratch

Web development with PHP : build a nice job recruitment site

Source viva: https://www.sitepoint.com/rapid-enterprise-app-development-zend-expressive/

LEAVE A REPLY

Please enter your comment!
Please enter your name here