How to Build a NASA Photo Gallery with Zend Expressive

来源:互联网 时间:1970-01-01

This article was peer reviewed by Abdul Malik Ikhsan and Matthew Weier O’Phinney . Thanks to all of SitePoint’speer reviewers for making SitePoint content the best it can be!

In this article, we are going to use Zend Expressive to build a photo gallery using the NASA Astronomy Picture of the Day API. The end result will be the AstroSplash website which was created for the purpose of this article.

Zend Expressive is an exciting new micro-framework for building PSR-7 middleware applications. Micro-frameworks are smaller, faster and more flexible than full stack frameworks. They tend to be aimed at more experienced developers who do not require as much assistance designing their applications and prefer the flexibility of building their applications from a variety of decoupled components.

Middleware is a term that will be used a lot in this article. A good definition of middleware is given by the Zend Expressive documentation :

“Middleware is any code sitting between a request and a response; it typically analyzes the request to aggregate incoming data, delegates it to another layer to process, and then creates and returns a response.”

StackPHP has provided a method for PHP developers to create middleware since 2013. However, there are some key differences between StackPHP middleware and the middleware that we will encounter in this article. For our intents and purposes, the only compatible elements are theoretical.

Do not worry if this still sounds confusing, these concepts are all best demonstrated by example! So, without further ado, let us dive into making our app.

Introducing Our App

We are going to make an app using the API provided by NASA for their Astronomy Picture of the Day website . This is a great website that provides some fascinating daily images, but it is a little bit out-dated. With some work we could use this API to create a really easy to browse photo gallery!

Whilst reading this article, it may help to reference the AstroSplash public repository on GitHub . This contains the source code for the finished app, which is live at .

Creating a Zend Expressive Project

It is recommended, but not required, to use the Homestead Improved Vagrant VM to quickly create a development environment.

Zend Expressive provides a very useful skeleton project installer that we can use to configure the framework and our chosen components. We can use the followingcomposer command to create our application:

composer create-project -s rc zendframework/zend-expressive-skeleton <project-directory>

We should replace <project-directory> with the name of the directory that we are going to install Zend Expressive into. When using the Homestead Improved Vagrant VM, this will be Project and the command should be run in the Code directory. If the installer complains about the Project directory already existing, just remove it and run the command again.

The installer will give us the option to choose from some different components that the framework supports. We are going to stick largely to the defaults and use FastRoute, Zend ServiceManager and the Whoops error handler. There is no default choice for a templating engine, so we are going to use Plates.

If we load up the app in a browser we should now see a page welcoming us to Zend Expressive! Have a browse around the files that have been created for us, paying particular attention to the config directory. This contains all the data that Zend ServiceManager will use to build the container, which is the heart of our Zend Expressive application.

Next, we need to remove all the example code that we will not be using. Change into the project directory and run the following commands:

rm public/favicon.icorm public/zf-logo.pngrm src/Action/*rm test/Action/*rm templates/app/*rm templates/layout/* Configuring the Container

The container is a key part of our app. It will contain routes, middleware definitions, services and the rest of our app’s configuration.

In a moment, we will need to create a service for our app’s index page action. Before we start, let us borrow a good practice from the Zend Expressive documentation on naming our services :

“We recommend using fully-qualified class names whenever possible as service names, with one exception: in cases where a service provides an implementation of an interface used for typehints, use the interface name.”

With that in mind, head to config/autoload/ and replace the contents with this:

<?phpreturn [ 'dependencies' => [ 'factories' => [ Zend/Expressive/Application::class => Zend/Expressive/Container/ApplicationFactory::class, ], ],];

We have removed the invokables key, as we will not need to define any services of this type for our app here. Invokable services are services that can be instantiated without constructor arguments.

The first service to create is the application service. If you have a look at the front controller ( public/index.php ) you will see that it retrieves the application service from the container to run our app. This service has dependencies, so we have to list it under the factories key. By doing this, we have told Zend ServiceManager that it must use the given factory class to create the service. Zend Expressive provides many other factories for creating some of the core services.

Next, open up config/autoload/ and replace the contents with:

<?phpreturn ['dependencies' => ['invokables' => [Zend/Expressive/Router/RouterInterface::class => Zend/Expressive/Router/FastRouteRouter::class,],'factories' => [App/Action/IndexAction::class => App/Action/IndexFactory::class,]],'routes' => [['name' => 'index','path' => '/','middleware' => App/Action/IndexAction::class,'allowed_methods' => ['GET'],],],];

The first entry under the dependencies key just tells the framework that it can create a router by instantiating the FastRoute adapter class without passing it any constructor parameters. The entry under the factories key is for our index action service. We will write the code for this service, and its factory, in the next section.

The routes key will be loaded into the router by Zend Expressive, and should contain an array of route descriptors. In the single route descriptor that we have defined, the path key matches the entry to the index route, the middleware key tells the framework which service to use as a handler and the allowed_methods key specifies which HTTP methods are allowed. The allowed_methods key can be set to Zend/Expressive/Router/Route::HTTP_METHOD_ANY to specify that any HTTP method is allowed.

Route Middleware

It is time for us to create the index action service that we attached to the index route in our routes configuration file. Action classes take the form of route middleware in Zend Expressive, which is just middleware that we only want to bind to certain routes.

Our action class will be located, relative to our project root, at src/Action/IndexAction.php . Inside, it will look like this:

<?phpnamespace App/Action;use Psr/Http/Message/ServerRequestInterface;use Psr/Http/Message/ResponseInterface;use Zend/Expressive/Template/TemplateRendererInterface;use Zend/Stratigility/MiddlewareInterface;class IndexAction implements MiddlewareInterface{private $templateRenderer;public function __construct(TemplateRendererInterface $templateRenderer){$this->templateRenderer = $templateRenderer;}public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null){$html = $this->templateRenderer->render('app::index');$response->getBody()->write($html);return $response->withHeader('Content-Type', 'text/html');}}

Here we have used dependency injection to obtain an implementation of the template renderer interface. Later we will need to create the factory class that handles this dependency injection.

The presence of the __invoke magic method makes this class callable . It is called with PSR-7 messages as parameters and also an optional next piece of middleware in the chain. As all index requests are handled by this middleware, we do not need to call the next middleware in the chain and can instead directly return a response. The signature used here for callable middleware is very common:

public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null);

Middleware created using this pattern will also be supported by Relay , a PSR-7 middleware dispatcher. Likewise, middleware created for the Slim v3 framework, another PSR-7 middleware framework, will be compatible with Zend Expressive. Slim currently provides middleware for CSRF protection and HTTP caching .

When our action is called, it will render the app::index template, write it to the body of our response and return the response with the text/html content type. Because PSR-7 messages are immutable , every time we want to add a header to the response we have to create a new response object. The reasons for this are explained in the meta document for the PSR-7 specification .

Next, we have to write the factory class that the container will use to instantiate our index action class. Our factory class will be located, relative to our project root, at src/Action/IndexFactory.php . Inside, our factory will look like this:

<?phpnamespace App/Action;use Interop/Container/ContainerInterface;use Zend/Expressive/Template/TemplateRendererInterface;class IndexFactory{public function __invoke(ContainerInterface $container){$templateRenderer = $container->get(TemplateRendererInterface::class);return new IndexAction($templateRenderer);}}

Again, we use the __invoke magic method to make our class callable . The container will call this class, passing an instance of itself as a single parameter. We then use the container to retrieve an implementation of the template renderer service, inject this into our action and return it. Here it might be worth taking the time to look back at the configuration for our container, so we can see how it all ties up.


The only missing piece to our puzzle so far is the templating. In our index action, we ask the template renderer for the app::index template, but we have not created this yet. Zend Expressive uses the namespace::template notation for referring to templates. In our container configuration, Plates has been configured to know that all templates under the app namespace could be found at templates/app relative to the project root and that it should use .phtml as the template file extension. Two other namespaces have been configured, error and layout .

First, let us create the layout template. As the name for this template will be layout::default , under our configuration its path will be templates/layout/default.phtml .

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" /><title><?=$this->e($title);?></title></head><body><?=$this->section('content')?></body></html>

Next, we will create the app::index template at templates/app/index.phtml . We are going to make it extend the layout::default template that we just created. The templates in the error namespace have already been configured to extend the layout::default template.

<?php $this->layout('layout::default', ['title' => 'Astronomy Picture of the Day']) ?><h1>Astronomy Picture of the Day App</h1><p>Welcome to my Astronomy Picture of the Day App. It will use an API provided by NASA to deliver awesome astronomy pictures.</p>

Load the app up in your browser and you should see the template that we just created!

Pipe Middleware

The Zend Expressive documentation on pipe middleware states the following:

“When you pipe middleware to the application, it is added to a queue, and dequeued in order until a middleware returns a response instance. If none ever returns a response instance, execution is delegated to a “final handler”, which determines whether or not to return an error, and, if so, what kind of error to return.”

Pipe middleware can be used to create application firewalls, authentication layers, analytics programs and much more. Zend Expressive actually uses pipe middleware to perform the routing. In our app, we are going to use pipe middleware to create an application level cache.

To start, we need to obtain a caching library.

composer require doctrine/cache ^1.5

Next, we need to make the below additions to our config/autoload/ file:

<?phpreturn ['dependencies' => ['factories' => [ // ...Doctrine/Common/Cache/Cache::class => App/DoctrineCacheFactory::class,],],'application' => ['cache_path' => 'data/doctrine-cache/',],];

We have added a doctrine cache service that requires a custom factory class which we will write shortly. As the fastest way to get our app up and running is using a file system cache, we have to create a directory for this service to use.

mkdir data/doctrine-cache

The last change that we need to make to our configuration is to tell Zend Expressive about our middleware service and add it to the middleware pipe, before the routing takes place. Open up config/autoload/ and replace it with this:

<?phpreturn ['dependencies' => ['factories' => [App/Middleware/CacheMiddleware::class => App/Middleware/CacheFactory::class,]],'middleware_pipeline' => ['pre_routing' => [[ 'middleware' => App/Middleware/CacheMiddleware::class ],],'post_routing' => [],],];

Our factory for the doctrine cache will be located at src/DoctrineCacheFactory.php . If we ever needed to change the cache that our app used, all we would need to do is change this file (and its configuration) to use a different doctrine cache driver.

<?phpnamespace App;use Doctrine/Common/Cache/FilesystemCache;use Interop/Container/ContainerInterface;use Zend/ServiceManager/Exception/ServiceNotCreatedException;class DoctrineCacheFactory{public function __invoke(ContainerInterface $container){$config = $container->get('config');if (!isset($config['application']['cache_path'])) {throw new ServiceNotCreatedException('cache_path must be set in application configuration');}return new FilesystemCache($config['application']['cache_path']);}}

Our middleware factory, located at src/Middleware/CacheFactory.php , will inject the cache service into our middleware:

<?phpnamespace App/Middleware;use Doctrine/Common/Cache/Cache;use Interop/Container/ContainerInterface;class CacheFactory{public function __invoke(ContainerInterface $container){$cache = $container->get(Cache::class);return new CacheMiddleware($cache);}}

All that leaves is the middleware itself. Create src/Middleware/CacheMiddleware.php and place the following code inside:

<?phpnamespace App/Middleware;use Doctrine/Common/Cache/Cache;use Psr/Http/Message/ResponseInterface;use Psr/Http/Message/ServerRequestInterface;use Zend/Stratigility/MiddlewareInterface;class CacheMiddleware implements MiddlewareInterface{private $cache;public function __construct(Cache $cache){$this->cache = $cache;}public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null){$cachedResponse = $this->getCachedResponse($request, $response);if (null !== $cachedResponse) {return $cachedResponse;}$response = $next($request, $response);$this->cacheResponse($request, $response);return $response;}private function getCacheKey(ServerRequestInterface $request){return 'http-cache:'.$request->getUri()->getPath();}private function getCachedResponse(ServerRequestInterface $request, ResponseInterface $response){if ('GET' !== $request->getMethod()) {return null;}$item = $this->cache->fetch($this->getCacheKey($request));if (false === $item) {return null;}$response->getBody()->write($item['body']);foreach ($item['headers'] as $name => $value) {$response = $response->withHeader($name, $value);}return $response;}private function cacheResponse(ServerRequestInterface $request, ResponseInterface $response){if ('GET' !== $request->getMethod() || !$response->hasHeader('Cache-Control')) {return;}$cacheControl = $response->getHeader('Cache-Control');$abortTokens = array('private', 'no-cache', 'no-store');if (count(array_intersect($abortTokens, $cacheControl)) > 0) {return;}foreach ($cacheControl as $value) {$parts = explode('=', $value);if (count($parts) == 2 && 'max-age' === $parts[0]) {$this->cache->save($this->getCacheKey($request), ['body'=> (string) $response->getBody(),'headers' => $response->getHeaders(),], intval($parts[1]));return;}}}}

Our middleware will first attempt to retrieve a response from the cache. If the cache contains a valid response, this response is returned and the next middleware is not called. If, however, the cache contains no valid response, the responsibility for generating a response will be passed on to the next middleware in the pipe.

Before returning the final response from the pipe, it will attempt to cache it for next time. A brief check is performed to see if the response is cacheable before persisting it.

If we go back to our index action class, we can add a cache control header to the response object that tells the cache middleware we just created to cache the response for an hour:

public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null){$html = $this->templateRenderer->render('app::index');$response->getBody()->write($html);return $response->withHeader('Content-Type', 'text/html')->withHeader('Cache-Control', ['public', 'max-age=3600']);}

This is an incredibly primitive cache that will only work when we assume that the response object returned by the later middleware in the pipe will be quite simple. There are a range of other headers that could influence how our cache should handle a response. For now, this will suffice as a demonstration of how pipe middleware can take advantage of the layered design of our application.

Whilst developing our app, it might be worth disabling the cache control header to prevent the caching of old responses. If we need to clear the cache, we can use the command:

rm -rf data/doctrine-cache/*

It should be noted that the Cache-Control header activates client side caching. Our browser will remember responses stored in its own cache, even after they have been removed from the server cache.

Integrating the NASA API

Whilst we could use the NASA API directly, there are several complications to this approach. The two major ones are that the NASA API does not provide any method to retrieve collections of results or obtain thumbnails. Our solution is to use a wrapper API that has been created specifically for this article.

Run the following command in the project root:

composer require andrewcarteruk/astronomy-picture-of-the-day ^0.1

Make the following additions to the config/autoload/ file:

<?phpreturn ['dependencies' => ['factories' => [ // ...AndrewCarterUK/APOD/APIInterface::class => App/APIFactory::class,],],'application' => [ // ...'results_per_page' => 24,'apod_api' => ['store_path' => 'public/apod','base_url' => '/apod',],],];

We will also need to create a local dependencies file at config/autoload/dependencies.local.php :

<?phpreturn ['application' => ['apod_api' => ['api_key' => 'DEMO_KEY', // DEMO_KEY might be good for a couple of requests // Get your own here:],],];

And add the following routes to the config/autoload/ file:

<?phpreturn [ 'dependencies' => [ // ... 'factories' => [ // ...App/Action/PictureListAction::class => App/Action/PictureListFactory::class, ], ], 'routes' => [ // ... ['name' => 'picture-list','path' => '/picture-list[/{page:/d+}]','middleware' => App/Action/PictureListAction::class,'allowed_methods' => ['GET'], ], ],];

So what do these changes to our configuration do? Well, we have added a route that we can use to list recent pictures from the NASA API. This route accepts an optional integer page attribute which we can use for pagination. We have also created services for our API wrapper and the action that we will attach to this route.

We will need to make the store path that we specify in the apod_api key and, if applicable, add the path to the .gitignore file. The API wrapper will store thumbnails in this directory so it must exist within the public directory, otherwise it will not be able to create public URLs for the thumbnails.

mkdir public/apod

The factory for the API is quite simple. Create a file at src/APIFactory.php and place the following code inside:

<?phpnamespace App;use AndrewCarterUK/APOD/API;use GuzzleHttp/Client;use Interop/Container/ContainerInterface;use Zend/ServiceManager/Exception/ServiceNotCreatedException;class APIFactory{public function __invoke(ContainerInterface $container){$config = $container->get('config');if (!isset($config['application']['apod_api'])) {throw new ServiceNotCreatedException('apod_api must be set in application configuration');}return new API(new Client, $config['application']['apod_api']);}}

The API wrapper uses Guzzle to make HTTP requests to the API endpoint. All we need to do is inject a client instance and the configuration from our config service and we are good to go!

The action that will handle the routes that we have just created will need to be injected with the API service. Our action factory will be located at /src/Action/PictureListFactory.php and should look like this:

<?phpnamespace App/Action;use AndrewCarterUK/APOD/APIInterface;use Interop/Container/ContainerInterface;use Zend/ServiceManager/Exception/ServiceNotCreatedException;class PictureListFactory{public function __invoke(ContainerInterface $container){$apodApi = $container->get(APIInterface::class);$config = $container->get('config');if (!isset($config['application']['results_per_page'])) {throw new ServiceNotCreatedException('results_per_page must be set in application configuration');}return new PictureListAction($apodApi, $config['application']['results_per_page']);}}

Now all that is left is the action itself. Create src/Action/PictureListAction.php and place the following code inside:

<?phpnamespace App/Action;use AndrewCarterUK/APOD/APIInterface;use Psr/Http/Message/ServerRequestInterface;use Psr/Http/Message/ResponseInterface;use Zend/Stratigility/MiddlewareInterface;class PictureListAction implements MiddlewareInterface{private $apodApi;private $resultsPerPage;public function __construct(APIInterface $apodApi, $resultsPerPage){$this->apodApi= $apodApi;$this->resultsPerPage = $resultsPerPage;}public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $out = null){$page = intval($request->getAttribute('page')) ?: 0;$pictures = $this->apodApi->getPage($page, $this->resultsPerPage);$response->getBody()->write(json_encode($pictures));return $response // ->withHeader('Cache-Control', ['public', 'max-age=3600'])->withHeader('Content-Type', 'application/json');}}

All this action does is retrieve a page of pictures from the API and export the result as JSON. The example shows how to add a cache control header to the response for our cache middleware, however, it is best to leave this commented out during development.

Now, all that we need to do is create a utility for populating our store. The file below can be run in the command line. It obtains the container from the configuration, installs a signal handler so that it can shutdown cleanly and runs the updateStore method from the API wrapper. Create it at bin/update.php .

<?phpchdir(__DIR__.'/..');include 'vendor/autoload.php';$container = include 'config/container.php';// Create a SIGINT handler that sets a shutdown flag$shutdown = false;declare(ticks = 1);pcntl_signal(SIGINT, function () use (&$shutdown) { $shutdown = true; });$newPictureHandler = function (array $picture) use (&$shutdown) { echo 'Added: ' . $picture['title'] . PHP_EOL; // If the shutdown flag has been set, die if ($shutdown) {die; }};$errorHandler = function (Exception $exception) use (&$shutdown) { echo (string) $exception . PHP_EOL; // If the shutdown flag has been set, die if ($shutdown) {die; }};$container->get(AndrewCarterUK/APOD/APIInterface::class)->updateStore(20, $newPictureHandler, $errorHandler);

Now we can run the command below to update our store with the pictures of the last 20 days from the API. This can take a while, but as the store is updated we should be able to monitor the /picture-list route in our browsers to see a JSON feed of pictures. It may be worth disabling the cache headers on the response whilst monitoring the feed, otherwise it will not appear to be updating!

Make sure that you get your own API key off NASA , the DEMO_KEY will hit a request limit very quickly and start returning 429 response codes.

php bin/update.php

If we want our app to update automatically, we will need to set this command to run daily. We should also change the first parameter of the updateStore method call to 1 , so that it only tries to download today’s picture.

And that is where our journey ends (or begins!) with Zend Expressive for this app. All that is left to do is to modify our templates to use AJAX to load pictures from our new routes. The AstroSplash repository shows one way of doing this ( templates/app/index.phtml and templates/layout/default.phtml ) – but this is where we get to put a personal touch on our app, so do have a play!

More from this author Flyweight Design Pattern and Immutability: A Perfect Match Summary

Using a middleware oriented framework such as Zend Expressive lets us design our application in layers. In the simplest form, we can use route middleware to imitate the controller actions that we might be familiar with from other frameworks. However, the power of middleware lies in its ability to intercept and modify requests and responses at all stages of the application.

Zend Expressive is a brilliant framework as it tries to get out of your way. All of the code that we have written could be very easily transferred to work in a different framework, or even used in an application without a framework.

The variety of components that are supported out of the box by Zend Expressive make it very difficult to dislike anything about the framework; any component that we are not happy with can be easily changed. Currently, the framework supports three routers ( FastRoute , Aura.Router , ZF2 Router ), three containers ( Zend ServiceManager , Pimple , Aura.DI ) and three templating engines ( Plates , Twig , Zend View ).

On top of all of this, the Zend Expressive documentation contains in-depth documentation on the framework and all of the support components. It also contains handy quick start guides for getting up and running straight away.

Have you tried it? Did you follow along? What do you like / dislike about Zend Expressive? Let us know in the comments below, and be sure to hit that like button if you found this tutorial useful!

Tags:BrunoS,middleware,OOPHP,PHP,psr,psr7,Zend,zend framework,zend-expressive

Andrew Carter

Andrew is a software developer from the United Kingdom with a Master's Degree in Physics. He's the Head of Technical Development at MoneyMaxim, a developer of open source software and an up-and-coming speaker. In his spare time he dabbles in photography and practices Muay Thai.