@Ocramius
@asgrim
@RoaveTeam
Imma start calling you Gandalphp.
— Lee Davis (@leedavis81)
9 July 2015
The year is 2004.
A new client approaches us with a software project to be built.
The best you can find, according to 2004 standards!
The following content may contain elements that are not suitable for some audiences.
Accordingly, viewer discretion is advised
<?php
include 'functions.php.inc';
// register_globals polyfill
foreach ($_REQUEST as $var => $value) {
$$var = $value;
}
if ($type == 'purchase' && $userType == 'coyote') {
$products = mysql_query(
'SELECT * FROM product WHERE id = ' . $productId
);
if ($product = mysql_fetch_assoc($products)) {
if ($purchase && strlen($creditCard) == 16) {
$success = true;
mysql_query(
'INSERT INTO purchase (product_id, credit_card, user)
VALUES (' . $productId . ', "' . $creditCard . '", "' . $user . '")'
);
mail(
$userEmail,
'Purchase completed',
'Thank you for your purchase!'
);
}
}
}
include 'header.php';
if (isset($success)) {
echo "<h3>Thank you for your purchase, $user!</h3>";
}
include 'product-details.php';
include 'footer.php';
At least these people know what they're doing...
(sometimes)
(-ish)
(code is made up)
(I CBA to look up old ZF1 code)
<?php
class PurchaseController extends Zend_Controller_Action
{
public function purchaseAction()
{
if (! $this->isCoyote()) {
return $this->notAllowed();
}
$form = new PurchaseForm();
if (! $form->isValid($this->getPost())) {
return $this->render(['form' => $form]);
}
$data = $form->getData();
$product = $this->loadProduct($data['product']);
$product->purchase(); // magic
$this->mail($this->user(), 'Purchase completed');
return $this->render(['success' => true]);
(in theory)
Anybody tested a ZF1 controller?
Everything extends from it
Everything consumes its types
Everything talks to a singleton kernel
All plugins and helpers have a 0-argument-constructor
Zend_Loader used as a lazy-loading mechanism
It's gonna be a rewrite
You saw it coming!
... ok, we all didn't.
We didn't know any better!
... after many iterations of spit and polish ...
<?php
namespace App;
use Zend\Mvc\Controller\AbstractActionController;
use App\Helper\IsCoyote;
use App\Helper\NotAllowed;
use App\Helper\PurchaseForm;
use App\Helper\GetProduct;
use App\Helper\Notifications;
use App\Helper\CurrentUser;
class PurchaseController extends AbstractActionController
{
// private properties
Namespaces! Imports! We know what we use! Hooray!
public function __construct(
IsCoyote $isCoyote,
NotAllowed $notAllowed,
PurchaseForm $form,
GetProduct $getProduct,
CurrentUser $currentUser,
Notifications $notifications
) {
$this->isCoyote = $isCoyote;
$this->notAllowed = $notAllowed;
$this->form = $form;
$this->getProduct = $getProduct;
$this->currentUser = $currentUser;
$this->notifications = $notifications;
}
Constructor injection!
NO HORRIBLE MAGIC SINGLETON LOOPS!
public function purchaseAction()
{
$request = $this->getRequest(); // magic mvc stuff :-(
if (! $this->isCoyote->isCoyote($request)) {
return $this->notAllowed->notAllowed($request);
}
$this->form->setData($request->getPost());
if (! $this->form->isValid($this->getPost())) {
return ['form' => $this->form];
}
Removed all magic __call
-based helpers
Still rely on Zend\Mvc
lifecycle magic
Still rely on Zend\Stdlib\RequestInterface
$data = $this->form->getData();
$product = $this->getProduct->get($data['product']);
$product->purchase(); // magic
$this->notifications->notify(
$this->currentUser->get($request),
'Purchase completed'
);
return ['success' => true];
}
Still some magic in the domain - let's skip that for now
Our classes don't extend from the framework anymore.
We have a clean DI graph
We still have the benefits of lazy loading
Specifically, for the HTTP request
(that's what controllers were meant for)
... and framework coupling
Still developed by the friendly framework folks that brought you ZF1 and ZF2
Immutable representation of HTTP messages and server-side HTTP messages.
namespace Psr\Http\Message;
interface MessageInterface ...
interface RequestInterface ...
interface ServerRequestInterface ...
interface ResponseInterface ...
interface UploadedFileInterface ...
interface UriInterface ...
interface StreamInterface ...
namespace Psr\Http\ServerMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
interface MiddlewareInterface {
public function process(
ServerRequestInterface $request,
DelegateInterface $delegate
) : ResponseInterface;
}
namespace Psr\Http\ServerMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
interface DelegateInterface {
public function process(
ServerRequestInterface $request
) : ResponseInterface;
}
These standards will outlive the current frameworks
Simple abstractions, just interfaces
Bring your own implementation
Solved with function composition, not with magic!
final class NotFound implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
return new HtmlResponse('Not Found', 404);
}
}
final class InitializeSession implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
return $delegate->process(
$request->withAttribute(
'session',
$this->loadSession($request)
)
);
}
private function loadSession(ServerRequestInterface $request) : array
{
// ...
}
}
final class IsCoyote implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
if (! $this->isCoyote($request)) {
return $this->notAllowed->process($request, $delegate);
}
return $delegate->process($request);
}
private function isCoyote(ServerRequestInterface $request) : bool
{
// ...
}
}
final class NotAllowed implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
return new HtmlResponse('You are not allowed here', 403);
}
}
final class ValidatePurchase implements MiddlewareInterface
{
// ...
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
$validationResult = $this->form->validate($request);
$request = $request->withAttribute(
'validationResult',
$validationResult
);
if (! $validationResult->isValid()) {
return $this->validationError->process($request, $delegate);
}
return $delegate->process($request, $delegate);
}
}
final class FormValidationError implements MiddlewareInterface
{
private $renderer;
public function __construct(Renderer $renderer)
{
$this->renderer = $renderer;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
return new HtmlResponse(
$this->renderer->render(
'validation-error',
['result' => $request->getAttribute('validationResult')]
),
422
);
}
}
final class PurchaseProduct implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate) : ResponseInterface
{
$this->getProduct->get(
$request->getAttribute('validationResult')->get('product')
);
$product->purchase(); // still magic - not for this talk
$this->notifications->notify(
$request->getAttribute('session')['user'],
'Purchase completed'
);
return new HtmlResponse($this->renderer->render(
'product-purchased',
['product' => $product]
));
}
}
Each middleware is ludicrously simple
Each middleware handles orthogonal concerns
Easy to test
Easy to re-arrange
Easy to delete and replace
ServerRequestInterface#withAttribute()
and
ServerRequestInterface#getAttribute()
are hacks, and aren't type-safe. Use with care!
Middleware does only HTTP.
Command Bus does all the Business Logic.