Harmony
A simple and flexible PHP middleware dispatcher based on PSR-7, PSR-11, and PSR-15
Install / Use
/learn @woohoolabs/HarmonyREADME
Woohoo Labs. Harmony
[![Latest Version on Packagist][ico-version]][link-version] ![Software License][ico-license] [![Build Status][ico-build]][link-build] [![Coverage Status][ico-coverage]][link-coverage] [![Quality Score][ico-code-quality]][link-code-quality] [![Total Downloads][ico-downloads]][link-downloads] [![Gitter][ico-support]][link-support]
Woohoo Labs. Harmony is a PSR-15 compatible middleware dispatcher.
Harmony was born to be a totally flexible and almost invisible framework for your application. That's why Harmony supports the PSR-7, PSR-11, as well as the PSR-15 standards.
Table of Contents
- Introduction
- Install
- Basic Usage
- Advanced Usage
- Examples
- Versioning
- Change Log
- Testing
- Contributing
- Support
- Credits
- License
Introduction
Rationale
This blog post explains the idea best why Harmony was started back in 2014: http://www.catonmat.net/blog/frameworks-dont-make-sense/
Features
- High performance due to Harmony's simplicity
- High flexibility thanks to the vast middleware ecosystem of PSR-15
- Full control over HTTP messages via PSR-7
- Support for many DI Containers via PSR-11 (formerly known as Container-Interop)
Why Harmony?
There are a lot of very similar middleware dispatcher libraries out there, like Laminas-Stratigility, Slim Framework 3 or Relay. You might ask yourself, what is the purpose of yet another library with the same functionality?
We believe Harmony offers two key features which justify its existence:
-
It is the most simple library of all. Although simplicity is subjective, one thing is certain: Harmony offers the bare minimum functionality of what a library like this would need. That's why Harmony itself fits into a single class of ~140 lines.
-
As of version 3, Harmony natively supports the concept of Conditions which is a rare feature for middleware dispatchers. This eases dealing with a major weakness of the middleware-oriented approach, namely, the ability to invoke middleware conditionally.
Use cases
Certainly, Harmony won't suit the needs of all projects and teams: this framework works best for an experienced team with a longer term project. Less experienced teams - especially if they have short deadlines - should probably choose a framework with more features - working out-of-the box - in order to speed up development in its initial phase. Harmony's flexibility is the most advantageous when your software should be supported for a longer time.
Concepts
Woohoo Labs. Harmony is built upon two main concepts: middleware, which promote separation of concerns, and common interfaces, making it possible to rely on loosely coupled components.
By using middleware, you can easily take hands on the course of action of the request-response lifecycle: you can authenticate before routing, do some logging after the response has been sent, or you can even dispatch multiple routes in one request. This all can be achieved because everything in Harmony is a middleware, so the framework itself only consists of cc. 140 lines of code. This is why there is no framework-wide configuration, only middleware can be configured. What you do with Harmony depends only on your imagination and needs.
But middleware must work in cooperation (the router and the dispatcher are particularly tightly coupled to each other). That's why it is also important to provide common interfaces for the distinct components of the framework.
Naturally, we decided to use PSR-7 for modelling the HTTP request and response. In order to facilitate the usage of different DI Containers, we adapted PSR-11 (former Container-Interop) which is supported by various containers out of the box.
Middleware interface design
Woohoo Labs. Harmony's middleware interface design is based on the the PSR-15 de-facto standard.
If you want to learn about the specifics of this style, please refer to the following articles which describe the very concept:
Install
The only thing you need before getting started is Composer.
Install a PSR-7 implementation:
Because Harmony requires a PSR-7 implementation (a package which provides the psr/http-message-implementation virtual
package), you must install one first. You may use Laminas Diactoros or
any other library of your preference:
$ composer require laminas/laminas-diactoros
Install Harmony:
To install the latest version of this library, run the command below:
$ composer require woohoolabs/harmony
Note: The tests and examples won't be downloaded by default. You have to use
composer require woohoolabs/harmony --prefer-sourceor clone the repository if you need them.
Harmony 6.2+ requires PHP 7.4 at least, but you may use Harmony 6.1 for PHP 7.1+.
Install the optional dependencies:
If you want to use the default middleware stack then you have to require the following dependencies too:
$ composer require nikic/fast-route # FastRouteMiddleware needs it
$ composer require laminas/laminas-httphandlerrunner # LaminasEmitterMiddleware needs it
Basic Usage
Define your endpoints:
The following example applies only if you use the
default dispatcher middleware.
There are two important things to note here: first, each dispatchable endpoint receives a Psr\Http\Message\ServerRequestInterface
and a Psr\Http\Message\ResponseInterface object as parameter and the latter is expected to be manipulated and returned. Secondly,
you can not only use class methods as endpoints, it is possible to define other callables too (see below in the routing section).
namespace App\Controllers;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
class UserController
{
public function getUsers(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$users = ["Steve", "Arnie", "Jason", "Bud"];
$response->getBody()->write(json_encode($users));
return $response;
}
public function updateUser(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$userId = $request->getAttribute("id");
$userData = $request->getParsedBody();
// Updating user...
return $response;
}
}
Define your routes:
The following example applies only if you use the default router middleware which is based on FastRoute, the library of Nikita Popov. We chose to use it by default because of its performance and simplicity. You can read more about it in Nikita's blog.
Let's add the routes for the aforementioned endpoints to FastRoute:
use App\Controllers\UserController;
$router = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) {
// An anonymous function endpoint
$r->addRoute("GET", "/me", function (ServerRequestInterface $request, ResponseInterface $response) {
// ...
});
// Class method endpoints
$r->addRoute("GET", "/users", [UserController::class, "getUsers"]);
$r->addRoute("POST", "/users/{id}", [UserController::class, "updateUser"]);
});
Finally, launch the app:
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use WoohooLabs\Harmony\Harmony;
use WoohooLabs\Harmony\Middleware\DispatcherMiddleware;
use WoohooLabs\Harmony\Middleware\FastRouteMiddleware;
use WoohooLabs\Harmony\Middleware\LaminasEmitterMiddleware;
$harmony = new Harmony(ServerRequestFactory::fromGlobals(), new Response());
$harmony
->addMiddleware(new LaminasEmitterMiddleware(new SapiEmitter()))
->addMiddleware(new FastRouteMiddleware($router))
->addMiddleware(new DispatcherMiddleware())
->run();
You have to register all the prior middleware in order for the framework to function properly:
LaminasEmitterMiddlewaresends the response to the ether via laminas-httphandlerrunnerFastRouteMiddlewaretakes care of routing ($routerwas configured in the previous step)DispatcherMiddlewaredispatches a controller which belongs to the request's current route
Note that there is a second optional argument of Harmony::addMiddleware() with which you can define the ID of a
middleware (doing so is necessary if you want to call Harmony::getMiddleware() somewhere in your code).
Of course, it is completely up to you how you add additional middleware or how you replace them with your own
implementations. When you'd like to go live, call $harmony->run()!
Advanced Usage
Using invokable class controllers
Most of the time, you will define your endpoints (~controller actions) as regular callables as shown in the section about the default router:
$router->addRoute("GET", "/users/me", [\App\Controllers\UserController::class, "getMe"]);
Nowadays
