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.
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.
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 servercomposer 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 runningcs-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.
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.
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.