Rapid Enterprise App Development with Zend Expressive

Share this article

Rapid Enterprise App Development with Zend Expressive

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.

Key Takeaways

  • Zend Expressive Enhancements: Zend Expressive improves the initial setup experience of enterprise app development with a wizard-driven command, although it requires decisive upfront choices to tailor the development environment.
  • Project Setup Simplified: Utilizing commands like `composer create-project`, the setup process is streamlined, allowing for a quick initialization of projects with predefined settings for dependency injection, routing, and templates.
  • Efficiency in Dependency Management: By enabling the reflection-based abstract factory, Zend Expressive simplifies the dependency injection process, reducing the need for repetitive configuration and boosting development speed.
  • Integration with Doctrine ORM: The tutorial highlights the integration of Doctrine for object-relational mapping, emphasizing its utility in managing database operations effectively within Zend Expressive projects.
  • Frontend and Console Tooling: Guidance on setting up frontend workflows using Gulp and managing console commands with Symfony Console demonstrates Zend Expressive’s flexibility in accommodating extensive development needs beyond backend configurations.

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:password1@localhost/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/App, public/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:

<?php

return [
    'console' => [
        '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.

Frequently Asked Questions (FAQs) about Rapid Enterprise App Development with Zend Expressive

What is Zend Expressive and how does it differ from Zend Framework?

Zend Expressive is a micro-framework developed by Zend Technologies. It is designed to simplify the process of building enterprise-level applications by providing a streamlined, middleware-based architecture. Unlike the Zend Framework, which is a full-stack framework, Zend Expressive focuses on providing only the essential components needed for building applications, making it more lightweight and flexible. It allows developers to choose their own components and libraries, giving them more control over their application’s architecture and functionality.

How do I get started with Zend Expressive?

To get started with Zend Expressive, you first need to install it via Composer, a dependency management tool for PHP. Once installed, you can create a new project using the Expressive skeleton application, which provides a basic structure for your application. From there, you can start adding your own routes, handlers, and middleware.

What are the benefits of using Zend Expressive for enterprise app development?

Zend Expressive offers several benefits for enterprise app development. Its middleware-based architecture allows for greater flexibility and control over the application’s functionality. It also promotes clean code and separation of concerns, making the application easier to maintain and scale. Furthermore, it is built on top of the robust Zend Framework, ensuring stability and reliability.

How does Zend Expressive compare to other PHP frameworks?

Compared to other PHP frameworks, Zend Expressive stands out for its simplicity and flexibility. While full-stack frameworks like Laravel or Symfony provide a wide range of built-in features, Zend Expressive focuses on providing only the essential components, allowing developers to choose their own libraries and tools. This makes it a great choice for developers who prefer a more hands-on approach to their application’s architecture.

Can I use Zend Expressive with other Zend products?

Yes, Zend Expressive can be used in conjunction with other Zend products, such as Zend Server for application deployment and monitoring, or Zend Studio for PHP development. This allows you to leverage the full power of the Zend ecosystem for your application development needs.

What is middleware in Zend Expressive?

Middleware in Zend Expressive is a way to encapsulate specific functionality or logic in your application. Each middleware is responsible for processing an incoming HTTP request and producing an HTTP response. This allows you to build your application as a series of middleware, each handling a specific task or operation.

How do I handle errors in Zend Expressive?

Zend Expressive provides a built-in error handling mechanism. When an error occurs, an error middleware is invoked, which can handle the error and produce an appropriate response. You can also create your own custom error handlers to handle specific types of errors.

How do I test my Zend Expressive application?

Testing in Zend Expressive can be done using PHPUnit, a popular testing framework for PHP. The Expressive skeleton application comes with a pre-configured PHPUnit setup, allowing you to start writing tests right away. You can write unit tests for your handlers and middleware, as well as integration tests for your routes.

How do I deploy my Zend Expressive application?

Deploying a Zend Expressive application can be done using various methods, depending on your hosting environment. One common method is to use Composer’s built-in scripts to automate the deployment process. You can also use Zend Server, a PHP application server provided by Zend Technologies, which offers features like application monitoring, performance optimization, and more.

Can I migrate my existing Zend Framework application to Zend Expressive?

Yes, it is possible to migrate an existing Zend Framework application to Zend Expressive. However, due to the differences in architecture and functionality between the two frameworks, the migration process may require significant refactoring of your application’s code. It is recommended to carefully plan and test the migration to ensure a smooth transition.

Kirk MaderaKirk Madera
View Author

I am the Director of Technology at Robofirm with experience in Magento, Zend Framework, Symfony, and DevOps technologies. I work to ensure Robofirm continues to grow in its technical strengths and maintains a leading edge over the competition in order to allow delivery of higher quality work to our clients with less time and budget. I am a father of two girls and happily married. I code in my free time because it's fun to build new things. I also play guitar and listen to lots of heavy metal.

BrunoSenterpriseGulpOOPHPPHPquickstartZendzend frameworkzend-expressive
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week