Introduction
The World Wide Web has changed significantly since its inception. What was once just a way to share documents has now transformed into a channel to provide full-fledged applications to users around the world.
These applications are often not just a way to read text and look at picture, but provide services like instant messaging, all sorts of content editing, marketplaces and so on so forth.
As these applications became more and more complex, backend developers also transitioned towards large and complex frameworks. Consequently this move has led to issues like supply chain attacks, but this article is not about any of this. In fact, when it comes to backend development I too agree that it's better to use a featureful and battle-tested framework even if it means security becomes harder.
On the other hand, many times I've seen simple applications built on top of hundreds of third-party packages providing features developers don't even need but are used in some specific part of the platform and thus must be installed anyway. I think that's not just a waste of computing resources, but complicates the developer's job too as now there is a need to follow updates for every dependency in order to avoid unknown yet important issues.
I asked myself: can a simple and modern application be built with no dependencies while also providing enough backend security? In this article I show my experiments with the PHP language, specifically with features from version 8 onwards.
How much is simple?
Here I'm specifically talking about simple applications, but what does that mean? How do you qualify an application as simple?
There is no definitive answer as everybody has its own metrics. For the purpose of this article, a simple web application is defined in terms of the number of relations between stored data.
No matter how information is stored, from plain files to relational databases to document storage, applications always need to store some state in order to provide their service to users. Trivially, if a service can be used only by registered users, the application must store each user's access details.
As the number of stored “structures” increases, relationships between items form: in an instant messaging services, users could be part of one-to-one conversations or be part of “rooms” with more people all talking at the same time.
Here, a simple application is a service with, at most, three or four relationships between stored data, no matter how many “types” of information are stored. It's based on an indicative amount and I'm sure it is possible to write an application with few relationships yet is by no means simple, but in my experience most of the time this much is appropriate.
To make an example, a service to store and share files is a simple application: a user owns a number of virtual directories and each virtual directory owns a file. Sharing a file or a directory only requires generating a unique URI, maybe from hashing the file's contents.
What is modern?
In addition to simple, I also talk about an application being modern, but what does that mean?
Originally, files were shared across the web by providing
the full path inside a “document root”
directory. For example, if the document root was the
path /var/www/mysite
and the requested path
was /home/banner.png
, the server would send the
contents of the file located
at /var/www/mysite/home/banner.png
.
This practice is very straightforward and fast and if you have a website whose only purpose is to display some text, a few images, maybe some videos and never accept submissions from visitors, it's really more than enough.
On the other hand, this approach means any file inside
the document root can be accessed freely, including
configurations with sensitive information such as
passwords. If you have ever looked at the access logs of
an HTTP server, you probably noticed all the requests for
paths like /.git
or /.env
with
varying path lengths and depths. That noise is a
consequence of people mistakingly sharing secret files
with the world.
In order to prevent these trivial attacks, backend
software started to use virtual routing: instead
of directly serving the file, each requests is sent to a
single entrypoint, for example index.php
. The
application would then match routes with the requested
path, with regular expressions or other solutions, until
one of them is able to handle it. If none matches the
server will respond with an error, usually a 404 Not
Found
.
Because resources are not located at physical paths
anymore, the application can now handle dynamic
URIs. The most basic example is requesting the profile
page of a user given its ID: before virtual routing the
request might have been to /users.php?id=123
,
with virtual routing it could
be /users/123
. It might be a trivial change,
but it provides many benefits, like writing a route to
handle only user profile requests, simplifying code
structure for maintainability.
In addition to virtual routing, another feature of modern applications are services. This is especially true for software written using object-oriented languages, including PHP.
A service is a single “unit” with methods to operate on specific data. A user service might provide methods to extract information about profiles from a database, a security service might implement credential checking for logins, a templating service might generate human-readable views of provided values.
Implementing a service with object-oriented features is trivial: define it as a class whose methods are the allowed operations, instantiate is a needed and it's ready. Most services are plain objects, but some common patterns are really services in disguise, like Active Records.
Thanks to services it's trivial to structure the application's code according to common practices. For example in a Model View Controller structure each component can be a service in itself. The rest of the article will mostly follow this definition.
Application design
To summarize, this article will show how to use PHP 8 to implement a simple application with virtual routing and services as needed to handle requests and database access. The application will mostly follow the MVC pattern.
I already built a tech demo called Share24, a service to share plain text for 24 hours. You can check its source code too.
This article will not explain how to re-implement the demo, it will explain only the important details and show only a handful of code snippets.
The application's entrypoint
The application will process all requests going to
the public/index.php
file. It is usually
necessary to configure the server, typically Apache, in
order to redirect everything there, but how to do so
depends on the hosting provider so I will omit the
procedure here. Refer to what your provider offers to
understand how to do it.
It is specifically placed inside the public
directory in order to isolate it from the rest of the
backend code. This directory can also contain other files
like images, client-side scripts and styles, videos and
other medias, as long as it's not code executed by the
server.
The whole application will rely on classes and it will be structured so that each class is in its own file. As such, we must first ensure PHP is able to locate these files, by registering an autoload function.
spl_autoload_register(function ($class_name) { $root_path = dirname(__DIR__); $file_name = str_replace('\\' DIRECTORY_SEPARATOR, $class_name); $full_path = $root_path . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . $file_name . '.php'; require_once($full_path); });
The code is trivial: first find the actual location of
the entire application on the filesystem, then replace
each backslash with the directory separator symbol, then
build a path inside the src directory and require
the file located there. For example, the
class App\Service
might be loaded
from /var/www/mysite/src/App/Service.php
. If
the file does not exist, an error is generated.
This simple autoloading forces us to put everything and everyone under the src directory, but that's good practice anyway. On the other hand this is nothing more than simple autoload to get things done, so really it can be exchanged for any other clever solution.
Once classes can be autoloaded, request processing can start.
use Core\Core; try { (new Core())->run(); } catch (\Throwable $t) { error_log($e->getMessage()); http_response_code(500); }
The run
method of the Core
class does all the heavy lifting: request processing,
virtual routing, generating responses and so
on. The index.php
file should really be as
small as possible, defining anything else here should be
reserved only for functions that must be available
everywhere at any time.
The tech demo's index.php
is slightly more
complex in order to make development easier, but anything
beyond what was shown above is optional.
The Core
The core of the application, provided by the aptly
named Core
class, needs to follow these steps
for each request:
- find the route able to handle the request;
- execute the code to handle the route or generate an error;
- send the route's response back to the client.
For our simple case, routes are found by matching the
request's URI with regular expression patterns using
PHP's preg_match
function.
$http_uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); $route_item = null; $route_components = []; foreach ($this->routes as $name => $item) { $item_uri = ...; // explained later if (empty($item_uri)) { continue; } if ('/' === $item_uri && $item_uri === $http_uri) { $route_item = $item; break; } $pattern = '/^' . str_replace('/', '\\/', $item_uri) . '$/u'; $matches = preg_match($pattern, $http_uri, $route_components, PREG_UNMATCHED_AS_NULL); if ($matches) { $route_item = $item; break; } }
Here, the code iterates over all the known routes and
does a number of checks on the URI path obtained from
the $_SERVER
superglobal. Some parts are
omitted as they are explained later. Other than checking
if the route matches, the preg_match
function will also fill the $route_components
array with values from regular expression groups,
i.e. patterns enclosed in parentheses.
Once we have a route, the core needs to execute it. In order to do so, it will rely on PHP's reflection facilities by following these steps. The rest of the article will go into details.
- Obtain a
\ReflectionMethod
from the route; - obtain the method's
\ReflectionClass
object; - resolve any required method argument;
- invoke the method.
Finally, the value returned by the invoked method will be
used to build the HTTP response to send back to the
client. Trivially, this value is an instance of
a Core\Response
class to easily configure headers, cookies, the status code
and the body.
Getting data out of it is trivial:
http_response_code($response->getCode()); foreach ($response->getHeaders() as $name => $value) { header($name . ': ' . $value, true); } $expiry = time() + (60 * 24 * 60 * 60); // 60 days as an example foreach ($response->getCookies() as $name => $value) { $options = [ 'expires' => $expiry, 'path' => '/', 'httponly' => true, 'samesite' => 'Strict', ]; setcookie($name, $value, $opts); } print $response->getBody();
The print
statement ends processing: the
response body was finally sent over the network to the
client and we can terminate.
Routing
How can the application define a route? Most trivially, the application can simply write all possible routes inside an array and have the core iterate over it.
This method is fine but has a number of shortcomings. Obviously, one is that every time a new route is created, the array must be manually changed to associate the pattern to match with the method to call, and that's a lot of development burden. A less noticeable but subtle issue is that whenever someone needs to check how a route works, that person must look into two files: the one defining the method and the one associating it with a route.
The same issues are present even if the array is defined in a configuration file that's parsed by the core, a solution that might be used in order to keep the core “generic” instead of dealing with application-specific details.
Thankfully, PHP 8 introduced a feature allowing developers to describe a route in the same place as its definition: attributes.
Thanks to attributes, it is now possible to define a controller class, to follow the MVC pattern, whose methods are “decorated” with routing details.
namespace Controller; use Core\Route; use Core\Response; final class MyController { #[Route(name: 'homepage', uri: '/', methods: ['GET'])] public function homepage() : Response { // code goes here return new Response()->setCode(200); } }
Here, Core\Route
is an attribute with three
properties: the route name, the URI pattern and which HTTP
methods are allowed. The array of methods gives the
application power to stop unwanted requests with a 403
Method Not Allowed
error.
The route attribute is trivial to define: simply store the given values and use defaults where appropriate.
namespace Core; #[\Attribute(\Attribute::TARGET_METHOD)] final class Route { public function __construct(public readonly string $name, public readonly string $uri, public readonly array $methods = ['GET']) { } }
Now that methods can be decorated with route details, the core needs a way to discover these attributes. Much like when executing the method associated with a route, explained later, PHP's reflection mechanism plays a key role.
The code itself is fairly verbose and mostly filled with boilerplate, so it will be omitted here, to keep things short and easy to read. Here I'll give an overview of its behaviour.
First it checks the contents of a specific directory, in
this case src/Controller
, iterating over
every PHP file contained therein. The directory itself is
not important and can potentially be made configurable, if
desired.
For each file, it will try to create
a \ReflectionClass
instance for the class with
the same name as the file, under the Controller
namespace. For example, the
class Controller\MyController
must be defined
in the file src/Controller/MyController.php
and
viceversa.
Then, all methods of this class are extracted as an array
and iterated over: if the method has any attributes and at
least one of them is a Core\Route
, it will be
stored in an array together with the attribute under the
route's name.
The demo application has its routing component
implement \ArrayAccess
in order to allow
expressions like $routing['homepage']
, but this
is optional.
Now that methods are stored, executing a route is as
simple as calling invokeArgs
on the
stored \ReflectionMethod
, for example in an
expression
like: $routing['homepage']['method']->invokeArgs(...)
.
Processing routes
Now that routes can be accessed at any time, it's time to
actually execute their associated method. In particular the
core will make use of the invokeArgs
method,
but why is that?
It's natural for routes to depend on other objects to perform some actions: a route handling a POST request needs to access the submitted data; a route fetching something from a database must first connect to the DBMS.
A developer could simply create these instances in the actual body of the method, but if any of those objects have its own dependencies then the developer will then have to instantiate them too, recursively until a “plain” object is met. That is a lot of code to write manually, maintain and it also exposes implementation details. The code might be entirely under our control, but the more it's built on “contracts” rather than implementations, the easier to maintain.
A simple solution to recursive instantiation is factories, objects whose purpose is to create instances of other objects while hiding the gory details. It's a fine solution, but as the number of dependencies increase, the code will end up littered with calls to multiple factories and it quickly becomes a mess.
Is even there a way to obtain objects with complex initializations without having to deal with manual creation or factories? The answer is actually yes and anyone familiar with the Symfony framework will immediately recognize it as the autowiring feature.
Instead of creating objects inside the method's body, they are declared in the method's signature with their type. Thus, a route handling a POST request to store something in the database might be defined as following.
#[Route(name: 'submit_something', uri: '/submit_something', methods: ['POST'])] public function submit_something(Request $request, Database $database) : Response { // code here... }
By listing each parameter's type, the application's core can use reflection to extract information about each item and automatically instantiate it, recursively. The dependency resolver is effectively a factory, but instead of generating only one type of object, it is able to instantiate any object of a known class.
The resolveInstance
method takes a fully
qualified class name, obtains the __construct
method, instantiates all the parameters and finally
creates a new instance with the resolved
arguments. Parameter resolution is simply a recursive call
of resolveInstance
on
each \ReflectionNamedType
obtained from the
method's signature.
Now, when the core finds a route all it needs to do is resolve the route method's parameters and execute it with real automatically instantiated objects with the correct class.
$controller_class = $method->getDeclaringClass()->getName(); $controller_instance = $this->resolver->resolveInstance($controller_class); $method_parameters = $this->resolver->resolveParameters($method); $response = $method->invokeArgs($controller_instance, $method_parameters);
Now that dependencies are resolved automatically,
implementing services is as easy as defining a class and
declaring its use either in a route method or in a
class's __construct
. For example, a
“repository” pattern becomes trivial to use.
final class ItemRepository { public function __construct(Database $database) { } public function getSomeItems() : array { $values = $this->database->... // whatever code is needed // other code here return $processed; } } final class ItemController { #[Route(name: 'get_items', uri: '/items')] public function get_items(ItemRepository $items) : Response { $values = $items->getSomeItems(); // rest of code here } }
Detour: some useful services
A quick section to lay out some useful services, namely session management and database access.
Sessions can be handled by PHP's own mechanism, but wrapping it with a service class helps maintenance, especially since the use of the service becomes explicit in the route method or constructor's signature.
The wrapper is very simple, providing methods to store or fetch values, but it's important to note that it's a lazy service: even though the object is created, no sessions are started until one of its methods are called. This is important to avoid wasting resources or unintentionally open a security hole.
Database
access works the same way: the actual object does
nothing until a connection is requested, when
a \PDO
object is created. By deferring
everything to PHP's built-in class the application does
not need to re-implement features like prepared
statements.
However, to connect to a database the application needs to know secret credentials which cannot be hard-coded inside the application's code, especially if the code is then shared freely like the tech demo is. These values are obtained through an environment service.
Instead of relying on just real environment variables
defined when the application is started,
the Core\Environment
service will parse a
file called .env
located in the application's
“root” directory, where src
and public
are. This file follows the same
syntax as environment variables defined for example in a
shell, in which the name and the value are separated by an
equal sign: VARIABLE_NAME=VARIABLE_VALUE
.
It's common for development machines to have different
database credentials than servers where applications are
deployed, so in order to allow both “default”
values and “overrides” for each machine, the
service will merge the contents of a file
called .env.local
over what's
inside .env
. This process can be trivally
extended to check for other files
like .env.local.dev
, .env.local.test
and so on based on arbitrary heuristics, but this is left as
an exercise to the reader.
This override allows .env
to be commited
inside a version control system like Git or Fossil with fake
values, while the real credentials are kept hidden in a
“local” file.
Validating forms with reflection
Forms are the way users can send data back to
the server for processing. Front-end frameworks have
constantly re-implemented them for inane reasons, but even
after so many years they still work perfectly fine and
most applications really don't need any more than
a <form>
element in the right
place.
Of course, it's not all roses and forms still need to be secured, but by handling them the same way as models in the MVC pattern, many issues can be solved all in one place easily.
The Core\Form\Form
model is the entry point
to form submission and validation. It contains all fields
to be sent to the server and will validate each of
them. It is not important if the field was made visible to
the user or not, as that's something left for the
“view” part.
In particular, during instantiation the form should automatically create as much security measures as possible, like setting up an anti-CSRF token and the likes. The demo implementation shows how to configure this token, however please note that the demo is built on the assumption that there will be at most one form per page to simplify some things, and thus the code is not re-usable everywhere.
Once initialized, the Core\Form\Form
class
itself doesn't do much: the submitted values are stored
inside Core\Request
instances while
constraints on these values are handled by implementations
of the Core\Form\FormField
interface.
Each form field is a class “decorated” by attributes expressing constraints on the value of the field. These constraints can be as simple as checking that the field is not empty for mandatory fields, but they can express complex conditions too, like ensuring the value matches certain patterns or follows a well-defined syntax.
Thanks to attributes, the code can once again leverage reflection the same way virtual routing does, providing a readable and structured way to express our intentions without scattering directives among multiple files.
foreach ($this->input_fields as $input_field) { $item = new \ReflectionObject($input_field); foreach ($item->getAttributes() as $attribute) { $attribute_item = $attribute->newInstance(); if (!($attribute_item instanceof Core\Form\FormConstraint)) { continue; } $field_value = $request_body[$field->getName()] ?? null; if (!$attribute_item->validate($field_value)) { $field->setValid(false); } } }
The tech demo's code is more detailed and provides more features, but the previous snippet is the fundamental process.
Now, all it takes to validate user submission is to define classes for each field used by the application and the respective constraints, which is a quite trivial task.
#[\Attribute(\Attribute::TARGET_CLASS)] final class NonEmptyFormConstraint extends FormConstraint { #[\Override] public function validate(mixed $value) : bool { return !empty($value); } } #[NonEmptyFormConstraint] final class NonEmptyFormField extends AbstractFormField { } $form->addInputField(new NonEmptyFormField('username'));
The tech demo does not have
an AbstractFormField
class and later it's shown
there is no need for one, but it's used here for
illustrative purposes.
HTML and Templating
All the code so far purely handles server-side processing: it takes the request, extracts relevant information from it and finally operates on this information. How can the application present the result of this process to the user?
To display “static” information, something that never changes from request to request, is very simple: just copy the structure into the response body. However, most of the time an application has to show dynamic data which can change entirely or in part according to the processed request.
The most trivial example of dynamic data is a user's profile page. A user, other than its log-in name, might want to have a different name displayed, or add a short autobiography, or add its birthday. These are all information taken from storage, like a database, and injected into the structure of the response data.
Since this is a web application this structure is most likely plain HTML, but an online service might provide other serialization methods like JSON or XML. Thus, the application must find a way to take a known structure, a template, and substitute certain patterns with the actual data taken from storage.
In “proper” frameworks, this is accomplished by providing a dedicated markup language, which is then compiled into a format for fast processing when requested, then it is cached somewhere and usually the server uses this cached file as a response. When the original file is changed, the cache is invalidated and the file compiled again.
PHP itself was born as a templating language and in fact many compilers simply produce plain PHP files as their output.
We could spend time writing a compiler for our application too… but do we really want to spend time on a compiler for an application with three database tables and four routes? It seems a lot of work for no real gain to me. Additionally, cache invalidation is the second most difficult task, after naming things. I'm not really looking forward to spend time writing reliable and robust cache-busting code.
How do we escape this conundrum? One possible solution is
to actually write a file in a special markup language, copy
its contents inside a PHP string, substitute the special
patterns with preg_replace
or equivalent, and
use the result as the response. This is perfectly fine, but
it has some major issues: to begin with, reading a file for
every request is very slow and a simple application really
has no reason to be that slow; substitution is also slow, as
for each pattern it needs to scan a possibly very large
string linearly and modify it; finally, it uses a lot of
resources for a very inefficient operation and that's never
a good thing.
A more clever approach can be found if we look at how dynamic data is usually represented, no matter if it's HTML, JSON or XML.
<div id="10" class="user-details"> <div class="user-details-name">John Doe</div> <div class="user-details-birthday">1970-01-01</div> <ul class="user-details-posts"> <li id="1039" class="user-details-post>Hello World</li> <li id="1040" class="user-details-post>About me</li> <li id="2392" class="user-details-post>On my carreer as a janitor</li> </ul> </div> { "type": "user-details", "name": "John Doe", "birthday": "1970-01-01", "posts": [ "1039": "Hello World", "1040": "About me", "2392": "On my carreer as a janitor ] }
Of course in actual real application the structure can be
different, especially in HTML where, for example, the list
of posts might be part of a completely
different <div>
, but fundamentally it's
made of some kind of “container” whose
contained elements can either be other containers or the
dynamic information itself. In some cases these containers
map perfectly with internal representations used by the
server… and PHP comes with its own internal cache
for compiled bytecode… as you probably have already
guessed, the application will use PHP classes as
its template engine!
Class to HTML
As bizarre as it might seem, developing a class hierarchy to generate HTML, or any other format, is actually simple.
First, let's define the HTML document that will contain everything else: according to HTML rules, the document must have a head and a body, and these two elements can then contain other items.
namespace Core\HTML; use Core\HTML\Head; use Core\HTML\Body; class Document implements \Stringable { public function __construct(private readonly Head $head = new Head(), private readonly Body $body = new Body()) { } public function getHead() : Head { return $this->head; } public function getBody() : Body { return $this->body; } public function __toString() : string { return '<!DOCTYPE html>' . '<html>' . (string)$this->head . (string)$this->body . '</html>'; } }
Here I'm explicitly initializing the
(readonly
) properties because I want to
create Core\HTML\Document
instances explicitly
only when I want to throught the use of new
,
but it can be defined as an “autowired” service
too. The explicit getter methods are simply how I personally
prefer to approach instance or class properties and do not
affect how the system works.
By implementing the \Stringable
interface,
the document can be automatically generated when
the print
statement to send the response back
to the client is executed; until then, data can be handled
through more efficient structures.
The Core\HTML\Head
and Core\HTML\Body
classes represent
the <head>
and <body>
tags. Even though they are
distinct, in the end they behave the same way: they have
other tags as children, they accept attributes (not
usually useful for <head>
, but very
much so for <body>
) and web browsers
treat them the same way as other HTML nodes. As such,
making them inherit from a
common Core\HTML\Tag
class simplifies them a
lot, especially since their __toString
method
would be the same.
namespace Core\HTML; class Tag implements \Stringable { public function __construct(protected string $name = 'div', protected ?string $id = null, protected array $classes = [], protected array $attributes = [], protected array $children = []) { } public function __toString() : string { $id = (empty($this->id)) ? '' : ('id="' . htmlspecialchars($this->id) . '"'); $class = ((empty($id)) ? '' : ' ') . ((empty($this->classes)) ? '' : 'class="' . implode(' ', array_map('htmlspecialchars', $this->classes)) . '"'); $attribute = (empty($class)) ? '' : ' '; foreach ($this->attributes as $name => $value) { $attribute = $attribute . ' ' . htmlspecialchars($name) . '=' . '"' . htmlspecialchars($value) . '"'; } $body = implode('', array_map(function (Tag $e) { return (string)$e; }, $this->children)); $str = '<' . htmlspecialchars($this->name) . ((empty($id) && empty($class) && empty($attribute)) ? '' : ' ') . ($id . $class . $attribute); $void_tags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; if (in_array($this->name, $void_tags, true)) { $str = $str . '>'; } else { $str = $str . '>' . $body . '</' . htmlspecialchars($this->name) . '>'; } return $str; } }
A number of getter and setter methods have been omitted for brevity, consult the tech demo's source code for more details.
The __toString
method is somewhat
intimidating, but it does very trivial operations. First, it
creates the class
attribute value by joining
every member of the $this->classes
array;
then, the associative $this->attributes
array is iterated over in order to
produce name="value"
pairs; the HTML element's
body is created by casting every object inside
the $this->children
array to string, which
will execute their __toString
method. The $void_tags
array is simply used to
ensure proper HTML syntax is respected.
Now, Core\HTML\Head
and Core\HTML\Body
just needs to extend this
new Core\HTML\Tag
class and initialize
themselves with anything that must always be
present, <head>
especially will benefit
from always adding responsive tags to its
children. However, the constructor contains a lot of
implementation details which, especially when
defining non-core classes, is never something one
should deal with in object-oriented programming.
To escape from these details,
the Core\HTML\Tag
class just needs to define
an overridable configure
method (the name is
not important) accepting no arguments and call it in its
constructor. That way, any subclass can override a
“neutral” method and be sure it can initialize
itself as if it were overriding the constructor.
final class Head extends Tag { #[\Override] protected function configure() : static { parent::configure(); $this->name = 'head'; $meta_charset = new Tag('meta'); $meta_viewport = new Tag('meta'); $meta_charset->addAttribute('charset', 'utf-8'); $meta_viewport->addAttribute('name', 'viewport') ->addAttribute('content', 'width=device-width,initial-scale=1') ; $this->appendChild($meta_charset)->appendChild($meta_viewport); return $this; } } final class Body extends Tag { #[\Override] protected function configure() : static { parent::configure(); $this->name = 'body'; return $this; } }
Using the same process, Core\HTML\Document
can be subclassed too, in order to create more specific
templates that can be further subclassed as needed.
final class BaseDocument extends Document { #[\Override] protected function configure() : static { $this->title = new Tag('title'); $this->title->appendChild(new Text('Generic Template Page')); return $this; } }
The Core\HTML\Text
class is, admittedly, a bit of a hack to allow plain text
as the contents of an element, but it ensures every child
is always a subtype of Tag
instead
of Tag|string|\Stringable
, which is quite the
looser combination and requires careful handling. It might
not look elegant, but the type safety it provides more
than make it up for it.
However, this still misses the important point: how can
we provide dynamic values to these “templates”?
The configure
method is called during
instantiation with the new
keyword and thus
cannot be used to obtain values from the outside
world. Additionally, a controller might need to access some
element of the template for processing before being able to
pass some data in; a simple example is when forms are
defined inside the document rather than getting created by
the controller and given to the document later. The tech
demo does this, for instance.
The most simple way is to simply define another method in
the base Core\HTML\Document
class and let
subclasses override it to handle dynamic values.
public function with(array $values) : static { // code to inject the contents of the $value array inside the document return $this; }
Now, injecting values becomes an explicit and fairly readable operation, which is always a good thing for maintainability.
#[Route(name: 'submit_something', uri: '/submit_something', methods: ['GET', 'POST'])] public function submit_something(Request $request) : Response { $document = new SubmitSomethingDocument(); $some_value = null; $form = $document->getForm(); if ($form->validate($request) && $form->isSubmitted()) { $some_value = $request->getSubmittedValue('some_value'); } $response = new Response(); $response->setBody($document->with([ 'some_value' => $some_value, ])); return $response; }
Closing
This article shows very little code, most of which is stripped down to the bare minimum and might not even work, in order to be illustrative without being too distracting with administrative details.
It really only scratches the surface by only explaining essential points, despite the impressive length. The tech demo is a full-fledged application from which you can learn all the other details that have been omitted here.
In the end writing the core part, the one upon which applications can be built, the framework if you will, takes about one or two days of work to implement entirely from the ground up. It's a lot of work but not as much as one might think when hearing the words “no dependencies” or “from scratch”.
Nonetheless, even though it's a fun learning project, I still suggest relying on a bigger and better developed framework like Symfony or Laravel or whatever is popular nowadays, unless you know, a priori that the set up explained here is more than enough and that you can patch it up whenever a hole opens.
Post Scriptum: What about an ORM?
When talking about accessing the database I quickly
described a wrapper service
class, Core\Database
and left the heavy work
to PHP's \PDO
. Some readers might wonder why
I never took a detour to create an ORM with no
dependencies.
While it would certainly be an interesting learning experience, for the scope of this project an entire ORM is overkill.
Sure, it's very nice to say, for example, $post =
$posts->get(1)
and obtain a PHP object with methods
to operate on the fetched data, and then have something
like $post->save()
or $posts->save($post)
to transparently store
it again without dealing with DBMS details.
However, at the start of this article I stressed that the entire approach is only suitable for applications with two, three, maybe four tables and only a handful of relationships between them. With a set that small, writing raw SQL…
SELECT a.id,b.name,c.address_string FROM users AS a LEFT JOIN user_details AS b ON a.id = b.id LEFT JOIN user_addresses AS c ON a.id = c.id WHERE a.id = 18
…is really not any more intimidating than writing a Doctrine ORM class.
In terms of DBMS agnosticism… I never really saw applications moving from one DBMS to another and to be honest, I think for the small size of the typical application this “framework” is aimed at, re-writing the SQL is not the gargantuan task that would require the protection of an ORM.
In short, with only a few tables and a small number
of JOIN
operations, an ORM is useless and
there's no need to spend time writing one. You can write
something similar to an ORM for your application, maybe
something similar to Symfony's repository pattern, but it
will be specific to your application.