G'day:
Updated 2024-08-30
Reworded to avoid my misuse of the word "singleton" which I was using to mean "an object that is created once and reused", which is not a singleton. A singleton is about implementation, not usage. This was pointed out to me by John Whish of the CFML community, during a conversation we were having about misuse of that term.
As you know, I've recently shifted most of my attention in my day job from CFML to PHP. This has had a coupla false starts: firstly I was needed back on the CFML team for a while to oversee some maintenance work, and then I was off work with eye trouble for a week or so. And then bloody Xmas & New Year came around, interfering with everything as well. All in all: I am
well behind in my PHP learning / baptism-by-fire. I've good a month to catch up before I'm off work
again for a month whilst I follow New Zealand's trials and tribulations in the Cricket World Cup. Conveniently being held in New Zealand (oh, and Aussie too, I s'pose) this year. This is only "convenient" as I was scheduled to be back in NZ around that time anyhow, checking in on the family and drinking beer with mates.
[disclosure... I am victim of a flight delay so have been killing time coding and drinking Guinness. I am halfway through my fourth pint, so my verbal diarrhoea will be worse than usual, as that previous paragraph evidenced. On review of the rest during proofreading, it doesn't get any better].
Anyway, I sat down and looked at our new app's PHP codebase the other day and went "Blimey! This is complicated!". Now I'm a newbie with PHP so I don't expect to follow the minutiae of each line of code, but I figured I should understand the general flow of what's going on. But no. For what should be a reasonably simple website (a front for online accommodation booking), we seemed to have an awful lot of Routings and Factories and Providers and Builders and Controllers and Repositories and combinations of the above (eg: ProviderFactoryBuilders... or it might be RepositoryBuilderFactories or something). It looked to me like a Java application in its structure, not one written in a dynamic web language. Still: I could be missing something, so decided to look into the app's architecture.
Basically we're using a framework called
Silex. This is based on
Symfony, but is very cut down. I guess if it was the CFML world, Silex might be taking
FW/1's approach, and Symfony might be more like how
ColdBox views the world. My position on frameworks is that FW/1 is about right; and ColdBox is too all-inclusive. But mileage varies, and let's not get into that. I'd heard of Symfony - but never used it - but had not heard of Silex. However reading the docs it sounds quite good (in that the docs are fairly short, I understand them).
I've been using
ColdSpring for years, and like the idea of
DI. Coming from CFML, it makes slightly less sense to me in the context of PHP as everything is request-centric: there's no sense of application (and hardly any sense of session in what I'd consider to be a professional implementation). So unlike how ColdSpring will cache bean configs for the life of the application, one has to tread more cautiously with PHP because the whole lot starts from scratch every request. That said, it's still desirable to define the wiring of one's classes & dependencies
once, and then when one comes to need a new service (or ProviderFactoryRepositoryBuilder ;-), it's just a matter of saying "one of those please", rather than having to hand code initialising the thing with all its dependencies (and their dependencies, etc). Fortunately Silex ships with a DI framework -
Pimple - baked in. Nice. But I need to know how to use it. I also needed to know whether we really need a lot of our factory classes, as they didn't really seem to be doing much... and I knew that if I was using CFML and ColdSpring I'd not need them at all. I was hoping this'd be the same for our new app.
Another new concept to me - I've been stuck using
Fusebox (the XML version, not the CFC version) for years - is having the framework handle the request routing too. We've been using .htaccess for this previously. TBH: I actually think this is the correct approach, too: routing is the job of the HTTP tier, not the application tier. However frameworks seem to want to do this stuff themselves these days. They seem to have migrated from "MVC" to be more like "RMVC" (or, more sensibly: "RCMV"), with the R standing for routing. Our routing code seemed to be very very repetitive (copy and paste boilerplate ceremony with a few differences per route), and also seemed to be a mishmash of config and code, which struck me as being less than ideal. As well as just making for a lot of code that we oughtn't need. I was buggered if I knew how to fix it, but I figured I'd try to understand how we got to where we did, and whether there might be a better approach.
Finally, this new app hits a REST API to get its data. I'm hesitant about using REST for internal server-to-server comms... it's just slow and not the right tool for the job if one can just run the code to do the work, instead of making an HTTP request... to
run the code to do the job. But we have no choice here, so REST is what we use. I know the theory of REST adequately (and a lot better having read Adam Tuttle's book: "
Book review: REST Web APIs: The Book (win a copy here)"), and can wire together REST stuff in CFML, but had no idea how it works in PHP. It's not just a matter of calling
<cfhttp>
, which is about where my experience of
consuming REST web services extends to. On our project we use something called
GuzzleHttp. Which also sounds pretty bloody cool, actually.
Oh... that wasn't the last bit. We're also using
Twig as our templating engine, and whilst I had not specific qualms about our usage of it, I simply didn't know how it worked, so wanted to have a brief look at that too. Well: one qualm. I dunno why mark-up-aware languages like PHP and CFML actually
need a templating engine... given that's what the languages themselves are for. PHP's less good at it than CFML is (no custom tags, for example), but IMO all the templating engines I've seen make sense in the context of the languages they do the templating for, but most of the considerations justifying a specific additional tier for views just doesn't apply with either PHP or CFML. But "it's the way it's done", so be it.
I'm in Ireland this weekend, and I had Saturday and Sunday afternoons free to either stare at walls (of my B&B or Shannon Airport, respectively), or write some code. So my weekend mission was to install Silex, Pimple, GuzzleHttp, Twig, etc, and write a proof of concept site doing some routing, some DI, and some REST requests and displaying them with some Twiggage. I had a false start yesterday as the wireless at the pub (which is where I do my work on Saturdays) was off when I arrived, so I had to do something which didn't require any RTFM. So I continued to work on "
Learn CFML in 24 hours". I knocked out another coupla thousand words, which was good. It was only today @ Shannon I've been able to work on the PHP stuff. "fortunately" my flight has been delayed so I have had a good few hours sitting here with a Guinness and messing around with this PHP stuff.
Pleasingly... it's only taken me a coupla hours to knock out the basic skeleton of what I wanted to test, I'm moderately happy with it, and it covers all the bases. And I think I know how everything works now, too. Hopefully. Here it is.
Installation
This is just a pleasure. It's all done by
Composer, and it all just works. This probably wouldn't be mention-worthy for most people, but coming from the CFML world (where
CommandBox is only just
now beginning to offer any sort of package management), it's a great new experience. I've already got Composer installed, so all I need for my app is the
composer.json file:
{
"require": {
"silex/silex": "~1.1",
"symfony/config": "~2.6",
"symfony/yaml": "~2.6",
"guzzlehttp/guzzle": "~5.0",
"twig/twig": ">=1.8,<2.0-dev"
},
"autoload": {
"psr-4": {
"dac\\silexdemo\\": "src/",
"app\\": "app/"
}
}
}
I then do
composer update
from the command line, and everything installs. Oh the
PSR-4 stuff is just my namespacing for my own code, which Composer looks after the autoloading of these days too. Nice.
My plan is to create a site which has a single route:
/user/n/
where
n
is an ID, and the site will dash off and call the REST service, get the requested user, and then - via Twig - output the details (the details being : ID, firstName, lastName). That's it. I wanted to use the minimum of code (whilst still architecting things adequately), and especially a minimum of
procedural code. I wanna be using OO, not just PHP script files.
File organisation
My app is organised as follows:
phpsilex.local/ - app root
composer.json
composer.lock
vendor/ - Silex, Twig, Pimple, Symfony, GuzzleHttp stuff, as installed by Composer
app/ - app bootstrap stuff
app.php
Dependencies.php
Routes.php
config/ - non PHP files
routes.yml
src/ - the code for the app itself
beans/
User.php
controllers/
User.php
services/
User.php
views/
user.html.twig
public/ - site root (only this stuff is web browseable)
index.php
Website code
As one has come to expect these days, the publicly accessible code is minimal:
<?php
// index.php
require __DIR__ . '/../app/app.php';
It just bootstraps the app.
Application config and bootstrap
<?php
// app.php
use Silex\Application;
require_once __DIR__.'/../vendor/autoload.php';
$app = new Application();
$app["debug"] = true;
$app->register(new Silex\Provider\ServiceControllerServiceProvider());
$app->register(new Silex\Provider\TwigServiceProvider(), [
"twig.path" => __DIR__.'/../src/views'
]);
app\Dependencies::configure($app);
app\Routes::configure($app);
$app->run();
app.php does the boilerplate stuff of
declaring the app to be a Silex one, and
configuring it and
running it. My two intrusions here are
how I am doing the DI, and
how I'm doing the routing.
The general Silex guidance seems to err towards procedural code, which sux a bit. I want to keep things as encapsulated as possible, and also making them as testable as possible. From start to finish. This means classes and methods. So I've put my dependency injection config into a class, as I have with the routing.
Dependencies (and their injection)
<?php
// Dependencies.php
namespace app;
use \dac\silexdemo\controllers;
use \dac\silexdemo\services;
use \dac\silexdemo\beans;
use GuzzleHttp\Client;
class Dependencies {
static function configure($app){
$app["controllers.hello"] = $app->share(function() {
return new controllers\Hello();
});
$app["controllers.user"] = $app->share(function($app) {
return new controllers\User($app["twig"], $app["services.user"]);
});
$app["services.user"] = $app->share(function($app) {
return new services\User($app["factories.user"], $app["services.guzzle.client"]);
});
$app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
return new beans\User($id, $firstName, $lastName);
});
$app["services.guzzle.client"] = function() {
return new Client();
};
}
}
Look how easy that lot is. Pimple is baked into Silex, so all I need to do is to stick a bunch of function expressions into the
$app
itself. Then when I come to use them, I just reference them in exactly the same way. This is still using configuration over convention (so like how ColdSpring would do it, more than how DI/1 or even more so WireBox would want to do it), and this suits me as well. But the configuration is clean because it uses actual code.This comment flies in the face of other comments I've made regarding separating config from code, but I think it's the appropriate handling here as the config actually
is code: it's defining how code should be run.
Note that because everything here is a function expression, nothing actually
runs to start with. I was concerned that if one had a huge dependency-injected application that there's be a lot of upfront overhead whilst the dependencies are executed. This would be disastrous with PHP needing to do this every request, but the DI config is only ever actually executed when it's needed. Nice.
There are three main conceits I'm leveraging here.
Firstly, the default syntax is to create a new object each time a service reference is used, eg:
$app["services.guzzle.client"] = function() {
return new Client();
};
Each time I reference
$app["services.guzzle.client"]
in my code, I'll get a new
Client
object. This is fine in this situation, but for a lot of my services I can reuse the same object, so I use this syntax instead:
$app["controllers.user"] = $app->share(function($app) {
return new controllers\User($app["twig"], $app["services.user"]);
});
The
share()
call there returns the same object every time. We have a lot of boilerplate code in our app currently which ensures this, and as far as I can tell it's simply not necessary.
We also have a lot of hand-written factory classes for handling creation of transient beans. This is baked into Pimple too:
$app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
return new beans\User($id, $firstName, $lastName);
});
That basically defines a factory for creating transient User beans. I didn't need any specific factory code at all: all I needed to do is to wrap my function expression with the
protect()
call. Nice one.
There are no-doubt situation wherein we might actually need a bespoke factory method or two, but I can't recall seeing any in our existing code. And certainly not for my purposes. Still: the way to handle this is to only write the code that's necessary. We don't need a factory for
every transient if Pimple can handle it for us with configuration. We should only write the code that we
need. Code that fulfills only a ceremonial or "just in case" role is bad code.
Aside: I can't help but think if we'd been using TDD whilst writing this lot, we'd have a lot less code to contend with, because TDD encourages the writing of only necessary code. Unfortunately a lot of this code was written before our current team existed, so we had no say in that particular decision.
That's my dependencies wired.
Routing
<?php
// Routes.php
namespace app;
use Silex\Application;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\RouteCollection;
class Routes {
static function configure($app){
$app["routes"] = $app->extend("routes", function (RouteCollection $routes, Application $app) {
$loader = new YamlFileLoader(new FileLocator(__DIR__ . "/../config"));
$collection = $loader->load("routes.yml");
$routes->addCollection($collection);
return $routes;
});
}
}
This code I copied from some blog or other (I'll try to dig it up, but don't have it in front of me just now... found it: "
Scaling Silex applications (part II). Using RouteCollection"). The documented approach to routing in Silex is to write each of them with PHP code, within the app.php file. That's bloody awful. Not only do I want them out of the app.php file, I also want them out of PHP code completely. Route definitions are not code. Plain and simple. So this code above lets me define my routes using
YAML (which I am unimpressed with as a format, but hey) instead:
# routes.yml
_hello:
path: /hello/{name}
defaults: { _controller: "controllers.hello:doGet"}
_user:
path: /user/{id}
defaults: { _controller: "controllers.user:getUser"}
requirements:
id: \d+
I'm not doing much with the routing here, but you get the general idea. One thing I
am doing here is that my controllers are DI-ed services (Silex's guidance is to do
those inline too!), which the Symfony RouteCollection approach supports just fine. Note that
controllers.user
is a reference to the controller's definition in Dependencies.php, and
getUser
is the relevant method in same. I'll get to that.
Application code
Indeed I'll get to that now. Here's the controller;
<?php
// User.php
namespace dac\silexdemo\controllers;
class User {
protected $twig;
protected $userService;
function __construct($twig, $userService){
$this->twig = $twig;
$this->userService = $userService;
}
function getUser($id){
$user = $this->userService->getUser($id);
return $this->twig->render('user.html.twig', array(
'user' => $user,
));
}
}
This shows the correlation between the DI config and the actual classes. From Dependencies.php:
$app["controllers.user"] = $app->share(function($app) {
return new controllers\User($app["twig"], $app["services.user"]);
});
The controller needs to know about the User service so it can go grab some data, and it needs to know about Twig, which is configured for all the views. This is from app.php:
$app->register(new Silex\Provider\TwigServiceProvider(), [
"twig.path" => __DIR__.'/../src/views'
]);
(I say "all": there's only the one view in this app).
getUser()
calls the model (
services.user
) and renders the view (
user.html.twig
), passing the User fetched from the model to it.
Here's the model:
<?php
// User.php
namespace dac\silexdemo\services;
class User {
protected $userFactory;
protected $guzzleClient;
function __construct($userFactory, $guzzleClient){
$this->userFactory = $userFactory;
$this->guzzleClient = $guzzleClient;
}
function getUser($id){
$startTime = self::getElapsed("Start");
$response = $this->guzzleClient->get('http://cf11.local:8511/rest/api/person/' . $id,["future"=>true]);
self::getElapsed("After async guzzle call", $startTime);
$response->wait();
self::getElapsed("After wait() call", $startTime);
$userAsArray = $response->json();
$userFactory = $this->userFactory;
$user = $userFactory($userAsArray["ID"], $userAsArray["FIRSTNAME"], $userAsArray["LASTNAME"]);
return $user;
}
private static function getElapsed($message, $start=-1){
if ($start == -1){
$start = time();
}
error_log(sprintf("%s: %d", $message, time() - $start));
return $start;
}
}
There's a lot more code there than is
necessary, as I am also
recording some metrics when I run that code (
getElapsed()
and the calls to same).
I hit my REST web service using
an async call to Guzzle, as this is also a requirement which we seem to have hand-coded instead of using what's provided for us already. I just wanted to make sure it all works.
Here the Guzzle call returns a FutureResponse object rather than a result, per se. This lets the rest of the code continue whilst Guzzle makes the (non-performant) REST call. I'm being slightly contrived here, but it's not until the
wait()
call is made that the calling code will actually block until Guzzle is done. This would not be how I'd do this usually, but it demonstrates the concept. I've tweaked my web service to take five seconds to run, and if we look in the logs, we can see that the blocking only occurs when
wait()
is called:
Start: 0
After async guzzle call: 0
After wait() call: 5
There's a bunch of other stuff one can do with the future / promise pattern here, but that's a discussion for another day. This just proves Guzzle does a good job of making async HTTP calls, without any coding on our part.
The web service returns JSON, which Guzzle can expect, and I populate a new bean with this. This is where we use that config-free bean factory. Remember this from Dependencies.php?
$app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
return new beans\User($id, $firstName, $lastName);
});
Getting and populating a new User bean is as simple as:
$userFactory = $this->userFactory;
$user = $userFactory($userAsArray["ID"], $userAsArray["FIRSTNAME"], $userAsArray["LASTNAME"]);
Calling
$userFactory()
is creating a new User object. Oh... the upper case key names are ColdFusion's fault, btw. It does that (uppercases stuff). Stoopid.
Here's the User bean:
<?php
// User.php
namespace dac\silexdemo\beans;
class User {
protected $id;
protected $firstName;
protected $lastName;
function __construct($id, $firstName, $lastName){
$this->id = $id;
$this->firstName = $firstName;
$this->lastName = $lastName;
}
function getId(){
return $this->id;
}
function getFirstName(){
return $this->firstName;
}
function getLastName(){
return $this->lastName;
}
}
Note there's nothing in there to help the factory process: it's just a standard class.
That gets passed to the Twig file, which is then rendered:
{# user.html.twig #}
ID: {{ user.id }}<br>
First Name: {{ user.firstName }}<br>
Last Name: {{ user.lastName }}<br>
Note how I don't need to call the getter methods explicitly: Twig works that out for me. Because I'm accessing
user.id
, it knows to get that with
getId()
. Cool!
And all this renders the - very spectacular - result of:
ID: 1
First Name: Zachary
Last Name: Cameron Lynch
Job done.
That's about it, really. I've put Silex through its motions, testing its routing and its integration with Pimple. I've had a look at Guzzle, and rendered stuff with Twig.
From start to finish - including research and doc-reading time - this took about eight hours I guess. 4h the other day to do the installs and get the routing sorted out; another 4h today for the DI, controllers, model (including Guzzle) and view stuff. And maybe 2h to write up and proofread. Pretty quick. This is not a brag (at all), it demonstrates that everything is documented well, googling for stuff is easy with PHP because the community is so big, and that the apps actually do what they set out to do, and just get on with it, rather than throwing unnecessary shite at me (like some of my experiences with popular CFML solutions to similar situations).
And my flight's finally in the air, so I'm gonna shut this thing down and do something more entertaining. Sleep, probably.
Righto.
--
Adam