Woohoo Labs. Harmony is a flexible micro-framework developed for PHP applications.
Our aim was to create an invisible, easily extensible, but first of all, extremely flexible framework for your quality application. We wanted to give you total control via PSR-7 and Container-Interop.
- Introduction
- Install
- Basic Usage
- Advanced Usage
- Examples
- Versioning
- Change Log
- Contributing
- Credits
- License
This post summarizes the best why Harmony was born: http://www.catonmat.net/blog/frameworks-dont-make-sense/
The general problem with frameworks is that they suggest you using the set of tools they have. Initially, everything seems to be OK, because either the scope of your project is too small or you are sure that you've found the best framework ever. But as soon as your requirements change (e.g. things get more complicated, you have to maintain the application longer than you thought or you need more scaling), there is a good chance that you will face issues with your framework.
However, as Phil Sturgeon pointed out in his great blog post, in a complex enough situation, with a skilled enough development team, you don't need a framework at all in its original meaning thanks to the modern era of Composer. All you need is only a set of inter-pluggable components of your preference so that they can be easily integrated into your application.
In conclusion, requirements will always change. What seemed to be a good choice once, it is not enough next time. If things are impossible to change in your framework then it might lead to hard times when refactoring your application even though if you tried hard to decouple your business objects from the framework.
We created Harmony to remedy this issue.
Certainly, Harmony won't suit the needs of all projects and teams. Firstly, this framework works best for advanced teams. So less experienced teams should probably choose a less lenient framework with more features in order to speed up development in its initial phase. Harmony's flexibility is the most advantageous if your software is a long-term, strategic project. That's why legacy applications can also profit from Harmony because it eases gradual refactoring.
- Extreme flexibility through middlewares
- Full control over HTTP requests and responses via PSR-7
- Support for any IoC Containers via Container-Interop
- Totally object-oriented workflow
Woohoo Labs. Harmony is built upon two main concepts: middlewares which promote separation of concerns and common interfaces allowing you to band your favourite tools together!
Middlewares - that are described in detail by Igor Wiedler - make it possible to 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 if you want. These can be achieved because everything in Harmony is a middleware, so the framework itself only consists of cc. 200 lines of code. And that's why there is no framework-wide configuration (only middlewares can be configured). Basically it only depends on your imagination and needs what you do with Harmony.
But middlewares must work in cooperation (especially the router and the dispatcher are 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 IoC Containers, we adapted the Container-Interop standard interface (which is supported by various containers off-the-shelf).
Woohoo Labs. Harmony's middleware interface design is based on the "request, response, next" style advocated by such prominent developers as Matthew Weier O'Phinney (you can read more on the topic in his blog post). That's why Harmony's middlewares are compatible with middlewares built for Zend-Stratigility, Zend-Expressive or Slim Framework 3.
Furthermore, you can find various other middlewares available for Harmony:
- Woohoo Labs. Yin-Middlewares: A bunch of middlewares to integrate Woohoo Labs. Yin - the elegant JSON API framework - into Harmony.
- MiniUrl: A simple URL shortener, which can be used as a free, open-source replacement for bit.ly's core functionality: creating short links and redirecting users.
The steps of this process are quite straightforward. The only thing you need is Composer.
To install this library, run the command below and you will get the latest version:
$ composer require woohoolabs/harmonyIf you want to use the default middlewares then you have to ask for the following dependencies too:
$ composer require nikic/fast-route:^1.0.0
$ composer require zendframework/zend-diactoros:^2.3.0The following example applies only if you use the default dispatcher middleware.
There are two important things to notice here: first, each endpoint receives a Psr\Http\Message\ServerRequestInterface
and a Psr\Http\Message\ResponseInterface object and they are expected to manipulate and return the latter.
Second, you are not forced to only use classes for the 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
{
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function getUsers(ServerRequestInterface $request, ResponseInterface $response)
{
$users = ["Steve", "Arnie", "Jason", "Bud"];
$response->getBody()->write(json_encode($users));
return $response;
}
/**
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
public function updateUser(ServerRequestInterface $request, ResponseInterface $response)
{
$userId = $request->getAttribute("id");
$userData = $request->getParsedBody();
// Updating user...
return $response;
}
}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 this library because of its performance and elegance. You can read more about it in Nikita's blog.
Let's add three routes to FastRoute:
$router = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute("GET", "/me", function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write("Welcome to the real world!");
return $response;
});
$r->addRoute("GET", "/users", [\App\Controllers\UserController::class, "getUsers"]);
$r->addRoute("POST", "/users/{id}", [\App\Controllers\UserController::class, "updateUser"]);
};You have to register all the following middlewares in order for the framework to function properly:
FastRouteMiddlewaretakes care of routing ($routerwas configured in the previous step)DispatcherMiddlewaredispatches a controller which belongs to the request's current routeDiactorosResponderMiddlewaresends the response to the ether via Zend Diactoros
Note that the first argument of Harmony::addMiddleware() and Harmony::addFinalMiddleware() is the ID of the
middleware (which should be unique) and the middlewares attached via Harmony::addFinalMiddleware() will always be
executed after the normal ones! In this case, we always want our response to be sent by DiactorosResponderMiddleware.
use WoohooLabs\Harmony\Harmony;
use WoohooLabs\Harmony\Middleware\FastRouteMiddleware;
use WoohooLabs\Harmony\Middleware\DispatcherMiddleware;
use WoohooLabs\Harmony\Middleware\DiactorosResponderMiddleware;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\SapiEmitter;
$harmony = new Harmony(ServerRequestFactory::fromGlobals(), new Response());
$harmony
->addMiddleware("fast_route", new FastRouteMiddleware($router))
->addMiddleware("dispatcher", new DispatcherMiddleware())
->addFinalMiddleware("responder", new DiactorosResponderMiddleware(new SapiEmitter()));
$harmony();Of course, it is completely up to you how you add additional middlewares or how you replace them with your own
implementations. When you'd like to go live, just call $harmony()!
Most of the time, you will define your route handlers (~controller actions) as regular callables like it was seen in the section about the default router:
$r->addRoute("GET", "/users/me", [\App\Controllers\UserController::class, "getMe"]);But nowadays, there is an increasing popularity of controllers containing only one action. To do so, it is a general
practice to implement the __invoke() magic method. In former versions of Harmony, if you wanted to apply this pattern,
you had to define the example route above the following way (at least if you used the default router and dispatcher):
$r->addRoute("GET", "/users/me", [\App\Controllers\GetMe::class, "__invoke"]);As of Harmony 2.1.0, your route definition can be simplified to:
$r->addRoute("GET", "/users/me", \App\Controllers\GetMe::class);Note: If you use other router or dispatcher than the default ones, please make sure whether the feature is available for you.
If you are interested in how you could benefit from invokable controllers in the context of the Action-Domain-Responder pattern, you can find an insightful description in Paul M. Jones' blog post.
The motivation of creating Woohoo Labs. Harmony was to become able to change every single aspect of the framework. That's why you can use such a DI Container you want.
For this purpose, we chose
the Container-Interop standard
(it is PSR-11 now) to be the common interface for DI Containers in the built-in DispatcherMiddleware.
It's also important to know that the DispatcherMiddleware uses the BasicContainer by default. It's nothing more
than a very silly DIC which tries to create objects based on their class name (so calling
$basicContainer->get(Foo::class) would create a new Foo instance).
But if you provide an argument to the middleware's constructor, you can use your favourite Container-Interop compliant
DIC too. Let's have a look at an example where one would like to swap BasicContainer with the awesome PHP-DI:
$container = new \DI\Container();
$harmony->addMiddleware("dispatcher", new DispatcherMiddleware($container));It's not a big deal to add a new middleware to your stack. For a basic scenario, you can use anonymous functions. Let's say you would like to log all the requests:
$middleware = function(ServerRequestInterace $request, ResponseInterface $response, callable $next) {
// Logging
return $next();
}And then you have to attach the middleware to Harmony:
$harmony->addMiddleware("logging", $middleware);A middleware must return a ResponseInterface instance in any cases, but the most important thing it can do is to
call $next() to invoke the next middleware when its function was accomplished. Failing to call this method results
in the interruption of the framework's operation (of course the final middlewares will still be executed)!
But what to do if you want to pass a manipulated request or response to the next middleware? Then, you should call
$next($request, $response). This way, the following middleware will receive the modified request or response.
Calling $next(null, $response) will pass the original request and the possibly changed response to the next
middleware!
If you need more sophistication, you can use an invokable class as a middleware too. For example let's create an authentication middleware:
use WoohooLabs\Harmony\Middleware\MiddlewareInterface;
use WoohooLabs\Harmony\Harmony;
class AuthenticationMiddleware
{
/**
* @var string
*/
protected $apiKey;
/**
* @param string $apiKey
*/
public function __construct($apiKey)
{
$this->apiKey = $apiKey;
}
/**
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Message\ResponseInterface $response
* @param callable $next
* @return \Psr\Http\Message\ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
if ($request->getHeader("x-api-key") !== [$this->apiKey]) {
return $response->withStatusCode(401);
}
return $next();
}
}then
$harmony->addMiddleware("authentication", new AuthenticationMiddleware("123"));As you can see, the constructor receives the API Key, while the __invoke() method is responsible for performing the
authentication.
Instead of callable, you can also typehint the $next argument against Harmony according to the
MiddlewareInterface.
By implementing this interface, you can use some specific features of Harmony (like Harmony::getMiddleware()) but lose the ability to reuse your middleware in other frameworks.
Again: a middleware must return a ResponseInterface instance in any cases, but the most important thing it can do is to call $next() to invoke the next middleware when its function was accomplished. Failing to call this method results in
the interruption of the framework's operation (of course the final middlewares will still be executed)! That's why we
only invoke $next() in this example when the authentication was successful.
Very important to notice that when authentication is unsuccessful, no other middlewares will be executed (as $next()
is not called), so possibly only the final middlewares will be invoked afterwards. As you want to pass a modified
response with status code 412 to the final middlewares, you must return the response (as seen in the prior example)
in order to inform the framework from the changed response.
Have a look at the examples directory for a really basic
application structure. Don't forget to run composer install first in Harmony's root directory if you want to try it out!
This library follows SemVer v2.0.0.
Please see CHANGELOG for more information what has changed recently.
Please see CONTRIBUTING for details.
The MIT License (MIT). Please see the License File for more information.