Conceptual Flow
This repository is powered by a PSR-15 implemented with league/container.
- Set up a container that has autowiring enabled and is loaded with
\CascadiaPHP\Site\ProviderAggregate
which does essentially all of our bootstrapping. - Create a PSR-7 HTTP server powered by zend diactoros
Once we have everything bootstrapped, we call $diactorosServer->listen()
which does the following:
- Resolve a
\Psr\Http\Message\ServerRequestInterface
from the superglobals - Set up a middleware stack
- Send that request through the middleware stack
request -> ( mw1 -> ( mw2 -> ( mw3 -> response -> ) -> ) -> ) -> response
That middleware stack contains two special middleware that resolves request handlers based on registered routes and then uses that request handler to get a response that it can send. Route handlers can be any callable that returns a string
, __toString
, or a \Psr\Http\Message\ResponseInterface
.
Typical route handlers will either generate and return a response themselves, or will use PlatesPHP to render a template into a response. They are set up with autowiring so you can simply typehint for the dependencies your route handler wants as arguments, and you will get those object so long as they can be created by the container.
An example of a route handler that renders a template would look like this:
$routeHandler = function(\League\Plates\Engine $plates): \League\Plates\Template\Template {
return $plates->make('/path/to/template');
};
Routes in this repository are implemented using FastRoute. We use a middleware to take a request and match it to a route. Once this route is matched, its handler gets packed into an attribute on the request object. You can see it by getting a copy of the request and checking the attribute:
$request = $container->get(ServerRequestInterface::class);
$requestHandlerCallable = $request->getAttribute('request-handler');
The next middleware in the stack acts as a dispatcher. It takes that request and effectively does:
$requestHandlerCallable = $request->getAttribute('request-handler');
$response = $requestHandlerCallable();
thereby "resolving" the request into a response.
All routes are registered in bootstrap/routes.php
which gets an injected \FastRoute\RouteCollector
allowing you to add routes. An example route could look like this:
// Return a simple string and let the middleware build it into a response
$r->get('/some/path', function(): string { return 'Hello World'; });
// Use a controller method to handle the request
$r->get('/some/other/path', '\Some\Controller::method`);
// Make a simple rand endpoint with password protection
$r->post('/rand', function(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface {
$post = $request->getParsedBody();
// If the password doesn't match, return a 403
if ($post['password'] ?? '' === 'trustno1') {
return new \Zend\Diactoros\Response('A password must be provided.', 403);
}
// Otherwise return the random number
return \Zend\Diactoros\Response(mt_rand());
});
// A route that renders a template and returns the resulting string
$r->get('/page', function(\League\Plates\Engine $plates): string {
return $plates->render('/path/to/template');
});
Pages aren't anything fancy, they are simply route handlers that use the PlatesPHP engine to generate a response. If you want to add a "page", the steps are as follows:
- Create a template for it in
templates/pages
, giving it a path correlating directly to the page path for organization (with home being lefthome.php
).
So for example,/about/contact-us
should be in a template attemplates/pages/about/contact-us.php
- Create a minimal controller for it in
src/Controller
keeping the same discipline from step 1 in naming. Using the same contact-us page as an example, our controller would be atsrc/Controller/About/ContactUs.php
<?php
declare(strict_types=1);
namespace \CascadiaPHP\Site\Controller\About;
class ContactUs
{
/**
* Handle the /about/contact-us route
* @param Engine $plates
* @return string
*/
public function mainRouteHandler(Engine $plates): string
{
return $plates->render('about/contact-us');
}
}
- Add a route to
bootstrap/routes.php
mappingGET
requests to our new controller method.
$r->get('/about/contact-us', '\CascadiaPHP\Site\Controller\About\ContactUs::mainRouteHandler`);
- Start a
SASS
file for the page. We are using AMP which has serious restrictions to the size of the CSS so we have to be mindful and build our CSS per page.resources/sass/pages/about/contact-us.sass
@import "../elements/layout"
@import "../util/_all"
- Add your new
SASS
file to the build routine inwebpack.mix.js
mix.sass('resources/sass/pages/about/contact-us.sass', 'resources/css/pages/about')
- Compile using
NPM
or better yet,Yarn
:
npm run dev
or
yarn dev
And you're ready to begin working on the new page!
We are using AMP which restricts us from using JavaScript beyond predefined AMP components. Luckily there is complete coverage of components and they are implemented with performance in mind. This frees us from ever needing to touch JavaScript beyond our build routine.
That said, we still need to manage CSS. AMP requires that any css used on a page be:
- Included inline in a
<style>
tag in the<head>
of the page - Less than 50KB per page.
To satisfy that, we are using a subset of Basscss and compiling our CSS per page.
We use Laravel Mix to compile our assets which makes it dead simple. In development you can use yarn watch
to constantly compile the CSS as you work. See package.json
for other ways you can compile.
Type | Vendor | Location | Usage |
---|---|---|---|
Templates | PlatesPHP | ./templates | To render template/sample.php : $engine->render('sample') |
Content | parsedown | ./content | To render content/sample.md : $template->markdown('sample') |
Routes | FastRoute | ./src/Router/ServiceProvider.php | $r->get('path', $callable) ->get ->post ->put ->delete |
Controllers | Custom | ./src/Controller | There is no defined structure to controllers |
Container | league/container | Created in ./dispatcher.php | $container->get($binding) |
Cache | cache/filesystem-adapter | N/a | PSR-16: $container->get(\Psr\SimpleCache\CacheInterface::class) |