Skip to content

bradyo/commando-php

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

77 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Commando PHP Framework

The Commando PHP Framework is minimalist framework that provides only the essentials for building applications for the web and command line.

It deviates from the ubiquitous MVC-style frameworks by providing high-level interfaces and modular structure for HTTP and Shell requests and leaving most other decisions up to the developer.

Commando has no View, Model, or DI layers. Those choices are all up to the developer and can vary between modules as needed.

Commando:

  • provides RequestHandler and ShellHandler interfaces
  • provides default web and shell error handlers

Principles:

  • construct applications in a compositional way using configs and Module objects, an Application is just a unit for hold config and Modules and execute commands
  • conventional Controller classes replaced by Handler classes, which do only one thing
  • provide simple interfaces to implement
  • favor composition over inheritance
  • favors modular code using composition, not conventions
  • provide basic building blocks for developers to extend
  • agnostic with respect to Domain Model, View, DI, Validation, Logging, and configuration

Getting Started

Get Started

Commando uses Handlers to processes requests from either the web or command line.

To implement a web RequestHandler, you need to create a Handler class and register it with the application.

namespace Sample;

use Commando\Web\Response;
use Commando\Web\Request;
use Commando\Web\RequestHandler;

class RootHandler implements RequestHandler
{
    public function handle(Request $request)
    {
        return new Response("Success!", 200);
    }
}
namespace Sample;

class Application extends \Commando\Application
{
    public function __construct()
    {
        parent::__construct();
        $this->setWebRequestHandler(new RootHandler());
    }
}

Make a script to create and execute your application in your web server folder:

require_once(__DIR__ . '/bootstrap.php');
$app = new \Sample\Application();
$app->handleRequest();

Structure of a Commando Application

Immutable Requests pass through a series of handlers to get converted to a Response. Handlers can do the work required to convert the given Request into a Response or delegate to other handlers via it's own internal routing to other Handlers. In many applications you will have global actions that occur on requests before passing them down to more specific modules, for example converting request body string into objects based on Content-Type header, or processing Authorization headers to get an accessing user. These actions are performed in a top level entry point that implements the Commando RequestHandler interface. After doing the conversions to application specific Request objects, the request is routed to contained sub-modules, which may implement more application specific handlers.

Commando Handler Flow

Request Handler Example

The following example shows just how you might implement a RequestHandler to handle a form POST for user registration:

namespace Sample\User;

use Sample\Core\NotAllowedResponse;
use Sample\Core\ValidationErrorResponse;
use Sample\Security\AuthenticatedRequest;
use Sample\Security\AuthenticatedRequestHandler;
use Sample\Security\Roles;

class PostUserHandler implements AuthenticatedRequestHandler
{
    private $userPostValidator;
    private $userService;

    public function __construct(
        UserPostValidator $userFormValidator,
        UserService $userService
    ) {
        $this->userPostValidator = $userFormValidator;
        $this->userService = $userService;
    }

    public function handle(AuthenticatedRequest $request)
    {
        if (! $request->getAccessToken()->hasRole(Roles::ADMIN)) {
            return new NotAllowedResponse('Not allowed to post Users');
        }

        $userPost = new UserPost($request->request);
        $errors = $this->userPostValidator->validate($userPost);
        if (count($errors) > 0) {
            return new ValidationErrorResponse('Invalid request', $errors);
        }

        $newUser = $this->userService->registerUser($userPost);

        return new UserResponse($newUser, 201);
    }
}

Each RequestHandler has explicit dependencies, making it much easier a developers to understand what is going on and also much easier to write unit tests by giving clear injection points for stubs and mocks.

The control flow is also much easier to follow since the handler must explicitly create and return a Response object or delegate to an object that can give one for it to return.

Delegating to Decorated RequestHandlers

The RequestHandler interface is used to construct more sophisticated handlers. For example, the security handler below uses an authentication service to transform a Request into an AuthenticatedRequest (decorating the request in a type-safe way with a security token), and delegates to an AuthenticatedRequestHandler that knows how to deal with the AuthenticatedRequest:

namespace Sample\Security;

use Commando\Web\Request;
use Commando\Web\RequestHandler;
use Commando\Web\Response;
use Sample\Core\NotAuthenticatedResponse;

interface AuthenticatedRequestHandler
{
    /**
     * @param AuthenticatedRequest $request
     * @return Response
     */
    public function handle(AuthenticatedRequest $request);
}

class GuardHandler implements RequestHandler
{
    private $guard;
    private $authenticatedHandler;

    public function __construct(
        Guard $guard,
        AuthenticatedRequestHandler $authenticatedHandler
    ) {
        $this->guard = $guard;
        $this->authenticatedHandler = $authenticatedHandler;
    }

    /**
     * @param Request $request
     * @return Response
     */
    public function handle(Request $request)
    {
        if ($request->getUserInfo() !== null) {
            $authenticatedRequest = $this->guard->authenticate($request);
            return $this->authenticatedHandler->handle($authenticatedRequest);
        } else {
            return new Response('Authentication required', 401);
        }
    }
}

Compositional Rest Modules

You have to create separate RequestHandler classes for every action and that results in a lot more classes than a conventional MVC architecture that groups actions into a single class. To take advantage of the separation, you need to create compositions of individual RequestHandler classes into more powerful RequestHandler classes.

To demonstrate how this can be done, we will create a RestHandler that implements RequestHandler and routes the various GET, POST, PUT, DELETE requests to internal RequestHandler objects managed by RestHandler.

class RestRequestHandler implements RequestHandler
{
    public function __construct(ResourceRepository $repository, ResourceConfig $config)
    {
        $listHandler = new ListHandler($repository, $config);
        $postHandler = new PostHandler($repository, $config);
        $getHandler = new GetHandler($repository, $config);
        $putHandler = new PutHandler($repository, $config);
        $deleteHandler = new DeleteHandler($repository, $config);

        $path = $config->getPath();
        $this->router = new Router([
            new Route('list',   Method::GET,    $path,         $listHandler);
            new Route('post',   Method::POST,   $path,         $postHandler);
            new Route('get',    Method::GET,    $path.'/{id}', $getHandler);
            new Route('put',    Method::PUT,    $path.'/{id}', $putHandler);
            new Route('delete', Method::DELETE, $path.'/{id}', $deleteHandler);
        ]);
    }

    public function handle(Request $request)
    {
        $handler = $this->router->match($request);
        return $handler->handle($request);
    }
}

Building such a compositional interface takes away all the hard work of creating individual RequestHandler classes for each HTTP method, we need only create a RestHandler with the required arguments. Constructing such a request handler would look like the following, and is therefore easy to write test cases against:

// create a RestHandler for resources
$db = new PDO('sqlite:db.sqlite');
$userConfig = new UserResourceConfig(new UserRepository($db));
$noteConfig = new NoteResourceConfig(new NoteRepository($db));
$repository = new ResourceRepository([$userConfig, $noteConfig]);
$noteHandler = new RestRequestHandler($repository, $noteConfig);

// process a request
$request = new TextRequest($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
$response = $noteHandler->handle($request);

Using composition, you can also create a RequestHandler one level higher that contains individual RestHandler objects for each resource and delegates down to them using it's own routing.

class AppRequestHandler implements RequestHandler
{
    public function __construct(ResourceRepository $repository, array $configs)
    {
        $userHandler = new RestRequestHandler($repository, $configs['user']);
        $noteHandler = new RestRequestHandler($repository, $configs['note']);
        $articleHandler = new RestRequestHandler($repository, $configs['article']);

        $this->router = new Router([
            new PathRoute('users',    '/users',    $userHandler),
            new PathRoute('notes',    '/notes',    $noteHandler),
            new PathRoute('articles', '/articles', $articleHandler)
        ]);
    }

    public function handle(Request $request)
    {
        $handler = $this->router->match($request);
        return $handler->handle($request);
    }
}

Running the Sample Application

Browse the sample application in the sample-api folder for more examples.

Start the application on a local port (installs composer dependencies and starts php webserver on port 8000):

cd sample-api/ && ./start.sh
curl -H 'Content-type: application/json' localhost:8000/users/1
{
    "status": "error",
    "message": "Authentication required"
}
curl --user admin:password -H 'Content-type: application/json' \
-X POST -d '{"email":"[email protected]","password":"12324234"}' \
localhost:8000/users
{
    "status": "error",
    "message": "Invalid request",
    "errors": [
        {
            "name": "email",
            "message": "A user with that email already exists"
        },
        {
            "name": "passwordRepeat",
            "message": "Password repeat does not match password"
        }
    ]
}
curl --user admin:password -H 'Content-type: application/json' \
localhost:8000/notes?expand=author
{
   "uri": "http:\/\/localhost:8000\/notes",
   "data": {
       "total": 3,
       "items": [
           {
               "id": 1,
               "authorId": 1,
               "content": "hello there",
               "author": {
                   "id": 1,
                   "email": "[email protected]"
               }
           },
           {
               "id": 2,
               "authorId": 1,
               "content": "hello again",
               "author": {
                   "id": 1,
                   "email": "[email protected]"
               }
           },
           {
               "id": 3,
               "authorId": 2,
               "content": "oh hai",
               "author": {
                   "id": 2,
                   "email": "[email protected]"
               }
           }
       ]
   },
   "links": [
       {
           "rel": "self",
           "uri": "http:\/\/localhost:8000\/notes"
       },
       {
           "rel": "first",
           "uri": "http:\/\/localhost:8000\/notes?offset=0"
       },
       {
           "rel": "next",
           "uri": "http:\/\/localhost:8000\/notes?offset=10"
       }
   ]
}

Integrating a Template Engine

The free-form nature of Commando can make it seem difficult to get started building a full-featured web app. See how easy it is to integrate Commando with the powerful Twig Template Engine.

We start by making a new Action interface similar to RequestHandler, but returns a View object instead of a Response. The View object just holds a view name and template parameters.

class View
{
    private $name;
    private $context;

    public function __construct($name, array $context = [])
    {
        $this->name = $name;
        $this->context = $context;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getContext()
    {
        return $this->context;
    }
}
interface Action
{
    /**
     * @param Request $request
     * @return View
     */
    public function handle(Request $request);
}
class HomeAction implements Action
{
    public function handle(Request $request)
    {
        $name = $request->query->get('name', 'Anonymous Coward');

        return new View('home', ['name' => $name]);
    }
}

Now we need a Commando RequestHandler that delegate Requests to Action instances and converts the returned View object into a HTTP Response object using Twig.

class TemplatedRequestHandler implements \Commando\Web\RequestHandler
{
    private $twig;

    public function __construct(\Twig_Environment $twig)
    {
        $this->twig = $twig;
        $this->action = new HomeAction();
    }

    public function handle(Request $request)
    {
        $view = $this->action->handle($request);
        $content = $this->twig->render($view->getName(), $view->getContext());

        return new Response($content, 200);
    }
}

Run the full example under sample-twig:

cd sample-twig/ && ./start.sh

Asynchronous Web Component Handling

Web components handle their own business logic and rendering, giving you highly decoupled, pluggable units for your website.

Handlers are very easy to componentize. You can think of a component as an object that renders it's own HTML content when invoked by a coordinating object.

For this example, we are going to create a master RequestHandler that asynchronously calls components to render, then assembles component content into a complete page.

namespace SampleAsync;

use Commando\Web\Request;
use Commando\Web\RequestHandler;
use Commando\Web\Response;
use Amp;
use Mustache_Engine;

class Application extends \Commando\Application implements RequestHandler
{
    /**
     * @var Component[] async components making up application
     */
    private $components;

    public function __construct()
    {
        parent::__construct();
        $this->setWebRequestHandler($this);
        $this->components = [
            new Component('component1', 'Component 1'),
            new Component('component2', 'Component 2'),
            new Component('component3', 'Component 3'),
            new Component('component4', 'Component 4'),
            new Component('component5', 'Component 5'),
            new Component('component6', 'Component 6'),
        ];
    }

    public function handle(Request $request)
    {
        $contentMap = [];
        Amp\run(function() use ($request, &$contentMap) {
            $promises = [];
            foreach ($this->components as $component) {
                $name = $component->getName();
                $promises[$name] = $component->getContentPromise($request);
            }
            $contentMapPromise = Amp\all($promises);
            $contentMap = Amp\wait($contentMapPromise);
            Amp\stop();
        });

        $mustache = new Mustache_Engine();
        $template = file_get_contents(dirname(__DIR__) . '/views/layout.mustache');
        $content = $mustache->render($template, $contentMap);

        return new Response($content);
    }
}

The first thing we do in RequestHandler::handle() is start an event reactor loop using Amp to manage the asynchronous processes. The Component returns a Promise for some HTML content that can be cashed in on when all the Components have finished generating content.

namespace SampleAsync;

use Amp;
use Amp\Future;
use Commando\Web\Request;
use Mustache_Engine;

class Component
{
    private $name;
    private $title;

    public function __construct($name, $title)
    {
        $this->name = $name;
        $this->title = $title;
    }

    public function getName()
    {
        return $this->name;
    }

    public function getContentPromise(Request $request)
    {
        $future = new Future();
        Amp\immediately(function() use ($future, $request) {
            $start = microtime(true);
            sleep(rand(0, 1)); // blocking

            $mustache = new Mustache_Engine();
            $template = file_get_contents(dirname(__DIR__) . '/views/component.mustache');
            $content = $mustache->render($template, [
                'title' => $this->title,
                'message' => md5(uniqid()),
                'duration' => round((microtime(true) - $start) * 1000, 2) . ' ms'
            ]);

            $future->succeed($content);
        });

        return $future->promise();
    }
}

The components in the Application are just simple objects, so any dependencies can be passed through from the the Application that composes them.

Run the full example under sample-async:

./sample-async/start.sh

About

Compositional PHP web framework demo

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages