More recently, I finished another project, which I decided to implement using Symfony2 Standard Edition. Similar to my earlier project, it had the business requirement that it needed tight integration with a Drupal site; so, for this new project, I decided to write a Symfony2 Drupal integration bundle.
Overall, I'm quite impressed with Symfony2 (in its various flavours), and I enjoy coding in it. I've been struggling to enjoy coding in Drupal (and PHP in general) – the environment that I know best – for quite some time. That's why I've been increasingly turning to Django (and other Python frameworks, e.g. Flask), for my dev projects. Symfony2 is a very welcome breath of fresh air in the PHP world.
However, I can't help but think: is Symfony2 "as good as PHP gets"? By that, I mean: Symfony2 appears to have borrowed many of the best practices that have evolved in the non-PHP world, and to have implemented them about as well as they physically can be implemented in PHP (indeed, the same could be said of PHP itself of late). But, PHP being so inferior to most of its competitors in so many ways, PHP implementations are also doomed to being inferior to their alternatives.
I try to be a pragmatic programmer – I believe that I'm getting more pragmatic, and less sentimental, as I continue to mature as a programmer. That means that my top concerns when choosing a framework / environment are:
Symfony2 definitely gets more brownie points from me than Drupal does, on the pragmatic front. For projects whose data model falls outside the standard CMS data model (i.e. pages, tags, assets, links, etc), I need an ORM (which Drupal's field API is not). For projects whose business logic falls outside the standard CMS business logic model (i.e. view / edit pages, submit simple web forms, search pages by keyword / tag / date, etc), I need a request router (which Drupal's menu API is not). It's also a nice added bonus to have a view / template system that gives me full control over the output without kicking and screaming (as is customary for Drupal's theme system).
However, Symfony2 Standard Edition is a framework, and Drupal is a CMS. Apples and oranges.
Django is a framework. It's also been noted already, by various other people, that many aspects of Symfony2 were inspired by their counterparts in Django (among other frameworks, e.g. Ruby on Rails). So, how about comparing Symfony2 with Django?
Although they're written in different languages, Symfony2 and Django actually have quite a lot in common. In particular, Symfony2's Twig template engine is syntactically very similar to the Django template language; in fact, it's fairly obvious that Twig's syntax was ripped off from inspired by that of Django templates (Twig isn't the first Django-esque template engine, either, so I guess that if imitation is the highest form of flattery, then the Django template language should be feeling thoroughly flattered by now).
The request routing / handling systems of Symfony2 and Django are also fairly similar. However, there are significant differences in their implementation styles; and in my personal opinion, the Symfony2 style feels more cumbersome and less elegant than the Django style.
For example, here's the code you'd need to implement a basic 'Hello World' callback:
app/AppKernel.php
(in AppKernel->registerBundles()
):
<?php
$bundles = array(
// ...
new Hello\Bundle\HelloBundle(),
);
app/config/routing.yml
:
hello:
resource: "@HelloBundle/Controller/"
type: annotation
prefix: /
src/Hello/Bundle/Controller/DefaultController.php
:
<?php
namespace Hello\Bundle\Controller;
use Symfony\Component\HttpFoundation\Response;
class DefaultController extends Controller
{
/**
* @Route("/")
*/
public function indexAction()
{
return new Response('Hello World');
}
}
project/settings.py
:
INSTALLED_APPS = [
# ...
'hello',
]
project/urls.py
:
from django.conf.urls import *
from hello.views import index
urlpatterns = patterns('',
# ...
url(r'^$', index, name='hello'),
)
project/hello/views.py
:
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello World")
As you can see above, the steps involved are basically the same for each system. First, we have to register with the framework the "thing" that our Hello World callback lives in: in Symfony2, the "thing" is called a bundle; and in Django, it's called an app. In both systems, we simply add it to the list of installed / registered "things". However, in Symfony2, we have to instantiate a new object, and we have to specify the namespace path to the class; whereas in Django, we simply add the (path-free) name of the "thing" to a list, as a string.
Next, we have to set up routing to our request callback. In Symfony2, this involves using a configuration language (YAML), rather than the framework's programming language (PHP); and it involves specifying the "path" to the callback, as well as the format in which the callback is defined ("annotation" in this case). In Django, it involves importing the callback "callable" as an object, and adding it to the "urlpatterns" list, along with a regular expression defining its URL path.
Finally, there's the callback itself. In Symfony2, the callback lives in a FooController.php
file within a bundle's Controller
directory. The callback itself is an "action" method that lives within a "controller" class (you can have multiple "actions", in this example there's just one). In Django, the callback doesn't have to be a method within a class: it can be any Python "callable", such as a "class object"; or, as is the case here, a simple function.
I could go on here, and continue with more code comparisons (e.g. database querying / ORM system, form system, logging); but I think what I've shown is sufficient for drawing some basic observations. Feel free to explore Symfony2 / Django code samples in more depth if you're still curious.
Basically, my criticism is not of Symfony2, as such. My criticism is more of PHP. In particular, I dislike both the syntax and the practical limitations of the namespace system that was introduced in PHP 5.3. I've blogged before about what bugs me in a PHP 5.3-based framework, and after writing that article I was accused that my PHP 5.3 rants were clouding my judgement of the framework. So, in this article I'd like to more clearly separate language ranting from framework ranting.
In the PHP 5.3+ namespace system:
In Symfony2:
Let me repeat: I really do think that Symfony2 is a great framework. I've done professional work with it recently. I intend to continue doing professional work with it in the future. It ticks my pragmatic box of supporting me in building a maintainable, well-documented, re-usable solution. It also ticks my box of avoiding reverse-engineering and manual deployment steps.
However, does it help me get the job done in the most efficient manner possible? If I have to work in PHP, then yes. If I have the choice of working in Python instead, then no. And does it help me avoid frustrations such as repetitive coding? More-or-less: Symfony2 project code isn't too repetitive, but it certainly isn't as compact as I'd like my code to be.
Symfony2 is brimming with the very best of what cutting-edge PHP has to offer. But, at the same time, it's hindered by its "PHP-ness". I look forward to seeing the framework continue to mature and to evolve. And I hope that Symfony2 serves as an example to all programmers, working in all languages, of how to build the most robust product possible, within the limits of that product's foundations and dependencies.
]]>Turns out that, after a bit of digging and poking around, it's not so hard to cobble together a solution that meets this use case. I'm sharing it here, in case anyone else finds themselves with similar needs in the future.
Assuming that you've installed both Silex and Monolog (by adding silex/silex
and monolog/monolog
to the require
section of your composer.json
file, or by some alternate install method), you'll need something like this for your app's bootstrap code (in my case, it's in my project/app.php
file):
<?php
/**
* @file
* Bootstraps this Silex application.
*/
$loader = require_once __DIR__ . '/../vendor/autoload.php';
$app = new Silex\Application();
function get_app_env() {
$gethostname_result = gethostname();
$gethostname_map = array(
'prodservername' => 'prod',
'stagingservername' => 'staging',
);
$is_hostname_mapped = !empty($gethostname_result) &&
isset($gethostname_map[$gethostname_result]);
return $is_hostname_mapped ? $gethostname_map[$gethostname_result]
: 'dev';
}
$app['env'] = get_app_env();
$app['debug'] = $app['env'] == 'dev';
$app['email.default_to'] = array(
'Dev Dude <dev.dude@nonexistentemailaddress.com>',
'Manager Dude <manager.dude@nonexistentemailaddress.com>',
);
$app['email.default_subject'] = '[My App] Error report';
$app['email.default_from'] =
'My App <my.app@nonexistentemailaddress.com>';
$app->register(new Silex\Provider\MonologServiceProvider(), array(
'monolog.logfile' => __DIR__ . '/../log/' . $app['env'] . '.log',
'monolog.name' => 'myapp',
));
$app['monolog'] = $app->share($app->extend('monolog',
function($monolog, $app) {
if (!$app['debug']) {
$monolog->pushHandler(new Monolog\Handler\NativeMailerHandler(
$app['email.default_to'],
$app['email.default_subject'],
$app['email.default_from'],
Monolog\Logger::CRITICAL
));
}
return $monolog;
}));
return $app;
I've got some code here for determining the current environment (which can be prod
, staging
or dev
), and for only enabling the error emailing functionality for environments other than dev
. Up to you whether you want / need that functionality; plus, this example is just one of many possible ways to implement it.
I followed the Silex docs for customising Monolog by adding extra handlers, which is actually very easy to use, although it's lacking any documented examples.
That's about it, really. Using this code, you can have a Silex app which logs errors to a file (the usual) when running in your dev environment, but that also sends an error email to one or more addresses, when running in your other environments. Not rocket science – but, in my opinion, it's an important setup to be able to achieve in pretty much any web framework (i.e. regardless of your technology stack, receiving email notification of critical errors is a recommended best practice); and it doesn't seem to be documented anywhere so far for Silex.
]]>On a project I'm currently working on, I decided to try out something of a related flavour. I built a stand-alone app in Silex (a sort of Symfony2 distribution); but, per the project's requirements, I also managed to heavily integrate the app with an existing Drupal 7 site. The app does almost everything on its own, except that: it passes its output to drupal_render_page()
before returning the request; and it checks that a Drupal user is currently logged-in and has a certain Drupal user role, for pages where authorisation is required.
The result is: an app that has its own custom database, its own routes, its own forms, its own business logic, and its own templates; but that gets rendered via the Drupal theming system, and that relies on Drupal data for authentication and authorisation. What's more, the implementation is quite clean (minimal hackery involved) – only a small amount of code is needed for the integration, and then (for the most part) Drupal and Silex leave each other alone to get on with their respective jobs. Now, let me show you how it's done.
To start with, set up a new bare-bones Drupal 7 site. I won't go into the details of Drupal installation here. If you need help with setting up a local Apache VirtualHost, editing your /etc/hosts
file, setting up a MySQL database / user, launching the Drupal installer, etc, please refer to the Drupal installation guide. For this guide, I'll be using a Drupal 7 instance that's been installed to the /www/d7silextest
directory on my local machine, and that can be accessed via http://d7silextest.local
.
Once you've got that (or something similar) up and running, and if you're keen to follow along, then keep up with me as I outline further Drupal config steps. Firstly, go to administration > people > permissions > roles
, create a new role called 'administrator'
(if it doesn't exist already). Then, assign the role to user 1.
Next, download the patches from Need DRUPAL_ROOT in include of template.php and Need DRUPAL_ROOT when rendering CSS include links, and apply them to your Drupal codebase. Note: these are some bugs in core, where certain PHP files are being included without properly appending the DRUPAL_ROOT
prefix. As of writing, I've submitted these patches to drupal.org, but they haven't yet been committed. Please check the status of these issue threads – if they're now resolved, then you may not need to apply the patches (check exactly which version of Drupal you're using, as of Drupal 7.19 the patches are still needed).
If you're using additional Drupal contrib or custom modules, they may also have similar bugs. For example, I've also submitted Need DRUPAL_ROOT in require of include files for the Revisioning module (not yet committed as of writing), and Need DRUPAL_ROOT in require of og.field.inc for the Organic Groups module (now committed and applied in latest stable release of OG). If you find any more DRUPAL_ROOT
bugs, that prevent an external script such as Symfony2 from utilising Drupal from within a subdirectory, then please patch these bugs yourself, and submit patches to drupal.org as I've done.
Enable the menu module (if it's not already enabled), and define a 'Page' content type (if not already defined). Create a new 'Page' node (in my config below, I assume that it's node 1), with a menu item (e.g. in 'main menu'). Your new test page should look something like this:
That's sufficient Drupal configuration for the purposes of our example. Now, let's move on to Silex.
To start setting up your example Silex site, create a new directory, which is outside of your Drupal site's directory tree. In this article, I'm assuming that the Silex directory is at /www/silexd7test
. Within this directory, create a composer.json
file with the following:
{
"require": {
"silex/silex": "1.0.*"
},
"minimum-stability": "dev"
}
Get Composer (if you don't have it), by executing this command:
curl -s http://getcomposer.org/installer | php
Once you've got Composer, installing Silex is very easy, just execute this command from your Silex directory:
php composer.phar install
Next, create a new directory called web
in your silex root directory; and create a file called web/index.php
, that looks like this:
<?php
/**
* @file
* The PHP page that serves all page requests on a Silex installation.
*/
require_once __DIR__ . '/../vendor/autoload.php';
$app = new Silex\Application();
$app['debug'] = TRUE;
$app->get('/', function() use($app) {
return '<p>You should see this outputting ' .
'within your Drupal site!</p>';
});
$app->run();
That's a very basic Silex app ready to go. The app just defines one route (the 'home page' route), which outputs the text You should see this outputting within your Drupal site!
on request. The Silex app that I actually built and integrated with Drupal did a whole more of this – but for the purposes of this article, a "Hello World" example is all we need.
To see this app in action, in your Drupal root directory create a symlink to the Silex web folder:
ln -s /www/silexd7test/web/ silexd7test
Now you can go to http://d7silextest.local/silexd7test/
, and you should see something like this:
So far, the app is running under the Drupal web path, but it isn't integrated with the Drupal site at all. It's just running its own bootstrap code, and outputting the response for the requested route without any outside help. We'll be changing that shortly.
Open up the web/index.php
file again, and change it to look like this:
<?php
/**
* @file
* The PHP page that serves all page requests on a Silex installation.
*/
require_once __DIR__ . '/../vendor/autoload.php';
$app = new Silex\Application();
$app['debug'] = TRUE;
$app['drupal_root'] = '/www/d7silextest';
$app['drupal_base_url'] = 'http://d7silextest.local';
$app['is_embedded_in_drupal'] = TRUE;
$app['drupal_menu_active_item'] = 'node/1';
/**
* Bootstraps Drupal using DRUPAL_ROOT and $base_url values from
* this app's config. Bootstraps to a sufficient level to allow
* session / user data to be accessed, and for theme rendering to
* be invoked..
*
* @param $app
* Silex application object.
* @param $level
* Level to bootstrap Drupal to. If not provided, defaults to
* DRUPAL_BOOTSTRAP_FULL.
*/
function silex_bootstrap_drupal($app, $level = NULL) {
global $base_url;
// Check that Drupal bootstrap config settings can be found.
// If not, throw an exception.
if (empty($app['drupal_root'])) {
throw new \Exception("Missing setting 'drupal_root' in config");
}
elseif (empty($app['drupal_base_url'])) {
throw new \Exception("Missing setting 'drupal_base_url' in config");
}
// Set values necessary for Drupal bootstrap from external script.
// See:
// http://www.csdesignco.com/content/using-drupal-data-functions-
// and-session-variables-external-php-script
define('DRUPAL_ROOT', $app['drupal_root']);
$base_url = $app['drupal_base_url'];
// Bootstrap Drupal.
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
if (is_null($level)) {
$level = DRUPAL_BOOTSTRAP_FULL;
}
drupal_bootstrap($level);
if ($level == DRUPAL_BOOTSTRAP_FULL &&
!empty($app['drupal_menu_active_item'])) {
menu_set_active_item($app['drupal_menu_active_item']);
}
}
/**
* Checks that an authenticated and non-blocked Drupal user is tied to
* the current session. If not, deny access for this request.
*
* @param $app
* Silex application object.
*/
function silex_limit_access_to_authenticated_users($app) {
global $user;
if (empty($user->uid)) {
$app->abort(403, 'You must be logged in to access this page.');
}
if (empty($user->status)) {
$app->abort(403, 'You must have an active account in order to ' .
'access this page.');
}
if (empty($user->name)) {
$app->abort(403, 'Your session must be tied to a username to ' .
'access this page.');
}
}
/**
* Checks that the current user is a Drupal admin (with 'administrator'
* role). If not, deny access for this request.
*
* @param $app
* Silex application object.
*/
function silex_limit_access_to_admin($app) {
global $user;
if (!in_array('administrator', $user->roles)) {
$app->abort(403,
'You must be an administrator to access this page.');
}
}
$app->get('/', function() use($app) {
silex_bootstrap_drupal($app);
silex_limit_access_to_authenticated_users($app);
silex_limit_access_to_admin($app);
$ret = '<p>You should see this outputting within your ' .
'Drupal site!</p>';
return !empty($app['is_embedded_in_drupal']) ?
drupal_render_page($ret) :
$ret;
});
$app->run();
A number of things have been added to the code in this file, so let's examine them one-by-one. First of all, some Drupal-related settings have been added to the Silex $app
object. The drupal_root
and drupal_base_url
settings, are the critical ones that are needed in order to bootstrap Drupal from within Silex. Because the Silex script is in a different filesystem path from the Drupal site, and because it's also being served from a different URL path, these need to be manually set and passed on to Drupal.
The is_embedded_in_drupal
setting allows the rendering of the page via drupal_render_page()
to be toggled on or off. The script could work fine without this, and with rendering via drupal_render_page()
hard-coded to always occur; allowing it to be toggled is just a bit more elegant. The drupal_menu_active_item
setting, when set, triggers the Drupal menu path to be set to the path specified (via menu_set_active_item()
).
The route handler for our 'home page' path now calls three functions, before going on to render the page. The first one, silex_bootstrap_drupal()
, is pretty self-explanatory. The second one, silex_limit_access_to_authenticated_users()
, checks the Drupal global $user
object to ensure that the current user is logged-in, and if not, it throws an exception. Similarly, silex_limit_access_to_admin()
checks that the current user has the 'administrator' role (with failure resulting in an exception).
To test the authorisation checks that are now in place, log out of the Drupal site, and visit the Silex 'front page' at http://d7silextest.local/silexd7test/
. You should see something like this:
The drupal_render_page()
function is usually – in the case of a Drupal menu callback – passed a callback (a function name as a string), and rendering is then delegated to that callback. However, it also accepts an output string as its first argument; in this case, the passed-in string is outputted directly as the content of the 'main page content' Drupal block. Following that, all other block regions are assembled, and the full Drupal page is themed for output, business as usual.
To see the Silex 'front page' fully rendered, and without any 'access denied' message, log in to the Drupal site, and visit http://d7silextest.local/silexd7test/
again. You should now see something like this:
And that's it – a Silex callback, with Drupal theming and Drupal access control!
The example I've walked through in this article, is a simplified version of what I implemented for my recent real-life project. Some important things that I modified, for the purposes of keeping this article quick 'n' dirty:
Silex\ControllerProviderInterface
) in a separate file, to being functions in the main index.php
fileIgorw\Silex\ConfigServiceProvider
, to being hard-coded into the $app
object in raw PHPSilex\Provider\MonologServiceProvider
My real-life project is also significantly more than just a single "Hello World" route handler. It defines its own custom database, which it accesses via Doctrine's DBAL and ORM components. It uses Twig templates for all output. It makes heavy use of Symfony2's Form component. And it includes a number of custom command-line scripts, which are implemented using Symfony2's Console component. However, most of that is standard Silex / Symfony2 stuff which is not so noteworthy; and it's also not necessary for the purposes of this article.
I should also note that although this article is focused on Symfony2 / Silex, the example I've walked through here could be applied to any other PHP script that you might want to integrate with Drupal 7 in a similar way (as long as the PHP framework / script in question doesn't conflict with Drupal's function or variable names). However, it does make particularly good sense to integrate Symfony2 / Silex with Drupal 7 in this way, because: (a) Symfony2 components are going to be the foundation of Drupal 8 anyway; and (b) Symfony2 components are the latest and greatest components available for PHP right now, so the more projects you're able to use them in, the better.
]]>