dev-master
9999999-dev https://github.com/mudge/engineA tiny little PHP web framework written over a Bank Holiday weekend.
MIT
The Requires
- psr/log ^1.0
- twig/twig ^2.3
- php >= 7.1.0
The Development Requires
by Paul Mucur
A tiny little PHP web framework written over a Bank Holiday weekend.
A tiny little PHP web framework written over a Bank Holiday weekend., (*1)
Current version: Unreleased
Supported PHP versions: 7.1, 7.2, (*2)
$ composer create-project mudge/engine-skeleton:dev-master my-project $ cd my-project $ php -S localhost:8080 -t public
It is tempting to think of the HTTP request response cycle as a pure function f
that accepts some request (perhaps as a raw HTTP string or as some wrapper object) and produces a single response (again, perhaps as a raw string or a wrapper object):, (*3)
+-------------------+ +---+ +----------------------------------------+ | GET / HTTP/2 | --> | f | --> | HTTP/2 200 | | Host: example.com | +---+ | Content-Type: text/html; charset=utf-8 | +-------------------+ | | Request | <!DOCTYPE html>... | +----------------------------------------+ Response
However, this mental model isn't quite right as it's not true that all requests produce a single response all in one go. For example, it is possible for a response to be sent in chunks: perhaps a request immediately receives some headers in response before the body is returned in parts. It's also possible for a response to never end by continuously streaming (e.g. think of the Twitter streaming APIs)., (*4)
Therefore, Engine takes an alternative approach: modelling the typical request response cycle as a function that takes both a request and a response object, the latter of which is a sort of open file handle allowing the user to send headers, etc. at any point during processing. This means that responses are created purely through side-effects which is a messier way of thinking about it but maps more closely to reality., (*5)
+-------------------+ +---+ | GET / HTTP/2 | --> | | | Host: example.com | | | +-------------------+ | | Request | f | | | +-------------------+ | | | | --> | | +-------------------+ +---+ Response
Engine uses the Model-view-controller pattern but controllers are objects that are initialized with a Request
, Response
and a logger., (*6)
The Request
is a value object offering access to the request URI, method, any GET
or POST
parameters and any cookies or session data., (*7)
The Response
object allows you to send data in response to the request via various methods:, (*8)
redirect(string $location): void
: send a redirect header;header(string $header): void
: send any arbitrary header;render(string $template, array $variables = []): void
: render a Twig template with the given variables;notFound(): void
: return a 404 Not Found
response with a template called 404.html
;forbidden(): void
: return a 403 Forbidden
response with a template called 403.html
;methodNotAllowed(): void
: return a 405 Method Not Allowed
response with a template called 405.html
.Note that all methods return void
as they work purely through side-effects: namely, sending data to the client., (*9)
Controller actions are therefore typical methods on the controller instance with access to the request
, response
and logger
(see Usage for an example). As these are plain PHP objects, all the usual techniques such as composition and inheritance are available for sharing behaviour between controllers., (*10)
As for how controllers are instantiated and actions called: the Router
is responsible for this and contains a map of HTTP methods and paths to controller classes and actions, e.g., (*11)
+-------------+ +----------------------+ | GET / | --> | HomeController#index | +-------------+ +----------------------+ +-------------+ +---------------------------+ | POST /login | --> | SessionsController#create | +-------------+ +---------------------------+
The Engine Skeleton project will create a public/index.php
which sets up a new Router
, populates it with a default route, creates a Request
and empty Response
and routes it accordingly., (*12)
+-------------------+ +-----------------------------+ +------------------+ | GET / HTTP/2 | --> | GET / -> HomepageController | --> | | __O__ --> | Host: example.com | | index | | index | | +-------------------+ +-----------------------------+ | | | Request Router | | | | | | +--------+ | | | <------------------------------------------------- | | --> | | / \ | | <-- | | Client +--------+ +------------------+ Response HomepageController
Use Engine Skeleton to create a new Engine web application:, (*13)
$ composer create-project mudge/engine-skeleton:dev-master my-project
This will generate a project with the following layout in my-project
(contents of vendor
directory not shown):, (*14)
. โโโ README.md โโโ composer.json โโโ composer.lock โโโ log โโโ public โ โโโ css โ โ โโโ app.css โ โโโ index.php โโโ src โ โโโ HomepageController.php โโโ templates โ โโโ 404.html โ โโโ base.html โ โโโ index.html โโโ tests โ โโโ HomepageControllerTest.php โโโ tmp โโโ vendor
You can then start the development server:, (*15)
$ cd my-project $ php -S localhost:8080 -t public
You can then go to http://localhost:8080 in your web browser and see a welcome page from Engine., (*16)
You can run the automated tests:, (*17)
$ ./vendor/bin/phpunit
By default, your project will expect everything from your src
directory to be in the App
namespace. You can implement your own controllers by inheriting from Engine\Controller
:, (*18)
<?php declare(strict_types=1); namespace App; use Engine\Controller; /* Controllers are just plain classes that will be instantiated with a Request, Response and logger. */ final class HomepageController extends Controller { public function index(): void { /* Prevent CSRF attacks using tokens in form submissions. */ $this->verifyCsrfToken(); /* Query arguments or post data can be accessed through Parameters. */ $this->params->fetch('name'); /* The response object can be used to render templates and send them to the user. */ $this->response->render('index.html', ['csrf_token' => $this->session->crsfToken()]); /* Or, use the convenience methods on the controller itself. */ $this->renderForm('index.html'); /* Use convenience methods for common responses. */ $this->response->notFound(); $this->response->forbidden(); $this->response->redirect('http://www.example.com'); } }
The main entrypoint into your web application is public/index.php
which is executed on every request. This will have been generated for you by the project skeleton but can be edited as you see fit., (*19)
You will almost certainly want to edit your routes to route requests to your own controller actions but you can also configure logging and template caching here too., (*20)
<?php declare(strict_types=1); /** * Ensure all encoding is in UTF-8. */ mb_internal_encoding('UTF-8'); mb_http_output('UTF-8'); require_once __DIR__ . '/../vendor/autoload.php'; use Monolog\Logger; use Monolog\Handler\StreamHandler; use Engine\{Router, Request, Response}; /** * Set up logging. * * By default, this will log INFO-level messages to ../log/app.log */ $logger = new Logger('app'); $logger->pushHandler(new StreamHandler(__DIR__ . '/../log/app.log', Logger::INFO)); /** * Set up templating. * * By default, this will load templates from ../templates and cache them * to ../tmp (you may want to disable the cache during development) */ $loader = new \Twig_Loader_Filesystem(__DIR__ . '/../templates'); $twig = new \Twig_Environment($loader, ['cache' => __DIR__ . '/../tmp']); /** * Set up the request and response. */ $request = Request::fromGlobals(); $response = new Response($twig, $logger); /** * Set up the router. * * Add any routes of your own here, e.g. * * $router->get('/login', 'App\SessionsController', 'new'); * $router->post('/login', 'App\SessionsController', 'create'); */ $router = new Router($logger); $router->root('App\HomepageController', 'index'); /** * Serve the request with the response. */ $router->route($request, $response);
Because the various blogging systems I wrote over 15 years ago were invariably called "Engine" too., (*21)
Copyright ยฉ 2017 Paul Mucur, (*22)
Distributed under the MIT License., (*23)
A tiny little PHP web framework written over a Bank Holiday weekend.
MIT