(We do the things in this presentation!)
The highly extensible, highly enjoyable, BDD testing framework for PHP.
(It's how I test the things in this presentation!)
POST or GET everything to/from a single api endpoint.
The request content determines the action.
(Remote Procedure Invocation)
Divide and conquer complexity with different resource endpoints.
The request content still determines the action.
Use the protocol (HTTP verbs) to determine resource actions.
GET a resource or collection.
DELETE a resource.
PATCH a resource.
POST to a collection.
Use links to declare actions and resources.
Allows discoverability and automation.
(fast, stable, reliable)
(slower and more fragile)
fastcgi_finish_request();
use Silex\Application;
use Symfony\Component\HttpFoundation\Response;
$app = new Application();
$app->get('/hello/world', function () {
return new Response('Hello World!');
});
$app->run();
use Symfony\Component\HttpFoundation\Response;
$app = require __DIR__.'/../bootstrap/app.php';
$app->get('/hello/world', function () {
return new Response('Hello World!');
});
$app->run();
$app['my.resource.repository'] = $app->share(function (Application $app) {
return new My\Resource\Repository($app['my.db.connection']);
});
$app->get('my/resource/{id}', function ($app, $request, $id) {
$token = $app['my.auth.token.repository']->find($request->get('access_token'));
if (!$token) {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
$resource = $app['my.resource.repository']->find($id);
if (!$resource) {
return new Response('', Response::HTTP_NOT_FOUND);
}
return new Response(json_encode($resource->toArray()));
});
$app->bind(Repository::class, function () {
return new My\Resource\Repository($app['my.db.connection']);
});
$app->get('my/resource/{id}', function ($id) use ($app) {
$token = $app['My\Auth\Token\Repository']->find($app['request']->get('access_token'));
if (!$token) {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
$resource = $app[Repository::class]->find(123);
if (!$resource) {
return new Response('', Response::HTTP_NOT_FOUND);
}
return new Response(json_encode($resource->toArray()));
});
$app['my.resource.repository'] = $app->share(function (Application $app) {
return new My\Resource\Repository($app['my.db.connection']);
});
$app->get('my/resource/{id}', function ($app, $request, $id) {
$token = $app['my.auth.token.repository']->find($request->get('access_token'));
if (!$token) {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
$resource = $app['my.resource.repository']->find($id);
if (!$resource) {
return new Response('', Response::HTTP_NOT_FOUND);
}
return new Response(json_encode($resource->toArray()));
});
$app->bind(Repository::class, function () {
return new My\Resource\Repository($app['my.db.connection']);
});
$app->get('my/resource/{id}', function ($id) use ($app) {
$token = $app['My\Auth\Token\Repository']->find($app['request']->get('access_token'));
if (!$token) {
return new Response('', Response::HTTP_UNAUTHORIZED);
}
$resource = $app[Repository::class]->find(123);
if (!$resource) {
return new Response('', Response::HTTP_NOT_FOUND);
}
return new Response(json_encode($resource->toArray()));
});
$app->register(new Silex\Provider\ServiceControllerServiceProvider());
$app['my.resource.controller'] = $app->share(function ($app) {
return new \My\Resource\Controller($app['my.resource.repository']);
});
$app->get('my/resource/{id}', 'my.resource.controller:get');
namespace My\Resource;
use My\Resource\RepositoryInterface;
use Symfony\Component\HttpFoundation\Response;
class Controller
{
protected $repo;
public function __construct(RepositoryInterface $repo)
{
$this->repo = $repo;
}
public function get($app, $request, $id)
{
$resource = $this->repo->find($id);
if (!$resource) {
return new Response('', Response::HTTP_NOT_FOUND);
}
return new Response(json_encode($resource->toArray()));
}
}
use My\Resource\Repository;
use My\Resource\RepositoryInterface;
$app->bind(RepositoryInterface::class, Repository::class);
$app->get('my/resource/{id}', 'ResourceController@get');
namespace App\Http\Controllers;
use Laravel\Lumen\Routing\Controller as BaseController;
use My\Resource\RepositoryInterface;
use Symfony\Component\HttpFoundation\Response;
class ResourceController extends BaseController
{
protected $repo;
public function __construct(RepositoryInterface $repo)
{
$this->repo = $repo;
}
public function get($id)
{
$resource = $this->repo->find($id);
if (!$resource) {
return new Response('', Response::HTTP_NOT_FOUND);
}
return new Response(json_encode($resource->toArray()));
}
}
use Pimple;
// ...
class Application extends Pimple implements HttpKernelInterface, TerminableInterface
{
// ...
}
$app['my.parameter'] = 123;
$app['my.service'] = function () { /* This function will be invoked */ };
$app['my.shared.service'] = $app->share(function ($app) {
return $app['my.service'] // only instantiated once!
});
$foo = $app['the.thing.i.want'];
use Illuminate\Container\Container;
// ...
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
// ...
}
$app->instance('my.parameter', 123);
$app->bind('My\Service', function () { /* This function will be invoked */ };
$app->singleton('My\Shared\Service', function ($app) {
return $app['My\Service'] // only instantiated once!
});
$app->bind('My\Interface', 'My\Implementation');
$foo = $app['My\Thing'];
$bar = $app->make('My\Other\Thing');
$app->before('my.before.routing.middleware:doAThing');
$app->get('/my/resource', 'my.resource.controller:get')
->before('my.before.this.route.middlware:doSomething')
->after('my.after.this.route.middlware:doSomethingElse');
$app->after('my.after.route.after.middlewares:doMore');
$app->finish('my.after.response.is.returned.middleare:doSoemthingSlow');
namespace My\Middlware;
class BeforeRouteMiddlware
{
protected $dependecy;
public function __construct($dependency)
{
$this->dependency = $dependency;
}
public function doSomething($request, $app)
{
// do something..
}
{
$app->middleware([
App\Http\Middleware\BeforeRoutingMiddleware::class,
App\Http\Middleware\AfterRoutingMiddleware::class,
App\Http\Middleware\TerminableMiddleware::class,
]);
$app->routeMiddleware([
'before' => App\Http\Middleware\BeforeRouteMiddleware::class,
'after' => App\Http\Middleware\AfterRouteMiddleware::class,
]);
$app->get('my/resource/{id}', [
'uses' => 'ResourceController@get',
'middleware' => ['before', 'after'],
]);
use Closure;
class BeforeMiddleware
{
public function handle($request, Closure $next)
{
// do something here
return $next($request);
}
}
use Closure;
class AfterMiddleware
{
public function handle($request, Closure $next)
{
$response = $next($request);
// do something here
return $response;
}
}
use Closure;
class TerminableMiddleware
{
public function handle($request, Closure $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// do stuff after the request was sent
}
}
$app->routeMiddleware([
'auth' => App\Http\Middleware\ResourceAuthMiddleware::class,
]);
$app->post('my/resource', [
'uses' => 'ResourceController@post',
'middleware' => ['auth'],
]);
class ResourceAuthMiddleware
{
protected $tokenRepo;
public function __construct(TokenRepository $tokenRepo)
{
$this->tokenRepo = $tokenRepo;
}
public function handle($request, Closure $next)
{
$token = $this->tokenRepo->find($request->get('access_token'));
if (!$token) {
return new Response('Please login.', Response::HTTP_UNAUTHORIZED);
}
if (!$token->hasPermission('resource')) {
return new Response('Go Away!', Response::HTTP_FORBIDDEN);
}
return $next($request);
}
}
$app['ContentNegotiation'] = $app->share(function () {
return new ContentNegotiation();
)};
$app->post('/my/resource', 'ResourceController:post')
->before('ContentNegotiation:checkForJson');
class ContentNegotiation
{
public function checkForJson(Request $request)
{
if (!strstr('application/json', $request->headers->get('Content-Type'))) {
return new Response('', Response::HTTP_UNSUPPORTED_MEDIA_TYPE);
}
}
}
$app->routeMiddleware([
'authenticate' => App\Http\Middleware\Authentication::class,
'authorize' => App\Http\Middleware\ResourceAuthorization::class,
'negotiateContent' => App\Http\Middleware\ContentNegotiation::class,
'jsonDecode' => App\Http\Middleware\JsonDecode::class,
]);
$app->post('my/resource', [
'uses' => 'ResourceController@post',
'middleware' => ['authenticate', 'authorize', 'negotiateContent', 'jsonDecode'],
]);
class JsonDecode
{
protected $app;
public function __construct(Application $app)
{
$this->app = $app;
}
public function handle($request, Closure $next)
{
$decodedData = json_decode($request->getContent(), true);
if ($decodedData === null) {
return new Response('Invalid json content.', Response::HTTP_BAD_REQUEST);
}
$this->app->instance('request_data', $decodedData); // optional
$next($request, $decodedData)
}
}
$app->run(My\Http\Request::createFromGlobals());
namespace My\Http;
use Symfony\Component\HttpFoundation\Request as HttpFoundationRequest;
class Request extends HttpFoundationRequest
{
protected $requestArray;
protected $requestResource;
// include accessor methods...
}
class ResourceHydrator
{
protected $resourceFactory
public function __construct(ResourceFactory $resourceFactory)
{
$this->resourceFactory = $resourceFactory;
}
public function hydrateFromArray($request)
{
$resource = $this->resourceFactory->fromArray($resource->getResourceArray());
$request->setRequestResource($resource);
}
}
class MyController
{
protected $repository
public function __construct(RepositoryInterface $repository)
{
$this->repository = $repository;
}
public function postAction($request)
{
$this->repository->add($request->getResource)->flush();
return new Response('the content', Response::HTTP_CREATED;
}
}
fastcgi_finish_request();
$app->post('/my/resource', 'ResourceController:post')
->after('FinishMiddlewareCreator:registerFinishMiddleware');
class FinishMiddlewareCreator
{
public function registerFinishMiddleware($request, $response, $app)
{
$app->finish('TheRealFinishMiddleware:doSomethingSlow');
}
}
$app->routeMiddleware(['after' => MyAfterRouteMiddleware::class]);
$app->post('my/resource/{id}', [
'uses' => 'ResourceController@get',
'middleware' => ['after'],
]);
class MyAfterRouteMiddleware
{
protected $app
public function __construct($app)
{
$this->app = $app;
}
public function handle($request, Closure $next)
{
$response = $next($request);
$this->app->middleware([MyTerminableMiddleware::class]);
return $response;
}
}