A small group of students wants to learn PHP and use it to develop a small portal for a university project.

It should show best practices, and have the usual set of features (comments, a bit of ajax, sending an email, login / logout, a small admin panel).

They are already familiar with OOP through previous Java work, have learned the basic PHP syntax, and are now asking you how to proceed. A framework? Which one?

I was asked the same thing back in June, and I wasn’t sure what to answer. The last PHP frameworks I developed with were Zend Framework 1 (ugh) and CodeIgniter. Some research had to be done.

Making the choice

The basic requirements were:

Modern PHP code.

This means PHP 5.3+, namespaces, PSR0, hopefully Composer. Decent number of users.

More popular frameworks have more documentation, StackExchange answers and other resources. They are also more likely to be useful later in the job market. Minimal and clear.

We wanted something that is easy to read and understand, approachable to a beginner.



Taking this into account left us with a few options:

Zend Framework 2 looks to have been largely ignored by the web development community, probably because Symfony 2 was already the king of the party. Lithium looked very tempting.

The three other choices shared quite a bit of code, they all relied on the Symfony Components. These are standalone classes dealing with http requests, forms, validation, configuration, translation, etc. Furthermore, other PHP projects have been adopting them, and some can now be found in Drupal, phpBB, ezPublish and many others. This has opened a new era of collaboration and code reuse in the PHP community, and positioned the components as the default libraries to use in the covered use cases.

Completing the picture are awesome projects like SwiftMailer, Doctrine, Twig (the best thing since sliced bread, with its inheritance system).

So, we knew that’s the road we wanted to take. This left us with Laravel 4, Symfony 2 full stack, and Silex.

Laravel 4 was still in beta, so we left it for another time.

The initial feedback I received for Symfony full stack was that it was overwhelming (yaml, bundles…). We wanted / needed something simpler.

Silex didn’t convince me at first because most examples show the anonymous function style of usage:

$app = new Silex\Application(); $app->get('/hello/{name}', function($name) use($app) { return 'Hello '.$app->escape($name); }); $app->run();

which while cool for REST apps seemed a bit too simple for our use case. The professor wanted to see real controllers.

And then I found this awesome blog post by the Silex co-maintainer: https://igor.io/2012/11/09/scaling-silex.html.

(Note, since then the official Silex documentation has been extended to cover this in a more powerful example)

I now knew what my recommendation would be (Silex + Twig + a Doctrine DBAL powered model layer), and I was ready to assist with backend development.

Getting started – Dependency injection

The dependency injection container can be seen as a magical array that is

passed to all controllers, and contains libraries and other application components.

The container doesn’t instantiate any objects until they are first accessed.

When writing tests, the objects in the container can be replaced with their stub versions for easier testing.

In Silex and Laravel 4, the application object ($app) is the dependency injection container.

Just like in Pimple the injected objects are wrapped in an anonymous function, this is the trick that allows their construction to be postponed until they are first needed (when a controller calls $app[‘soundcloud’]->getWidget() for example).

$app = new Silex\Application(); // DoctrineServiceProvider will register all db services. $app->register(new Silex\Provider\DoctrineServiceProvider()); // Register the custom soundcloud service. $app['soundcloud'] = $app->share(function ($app) { return new MusicBox\Service\SoundCloud(); }); // Register the custom artist repository, and inject the db service. $app['repository.artist'] = $app->share(function ($app) { return new MusicBox\Repository\ArtistRepository($app['db']); });

Routing and controllers

Silex wraps the powerful Symfony Routing component, allowing us to do pretty much anything.

We define all of our routes explicitly, bind them to a HTTP verb (or all of them, using match()), give them a machine name (for easy link generation in the views), and point them to a controller.

Route variable converters allow us to pass fully loaded entities to the controllers.

$app['controllers']->convert('artist', function ($id) use ($app) { if ($id) { return $app['repository.artist']->find($id); } }); $app->get('/artists', 'MusicBox\Controller\ArtistController::indexAction') ->bind('artists'); $app->match('/artist/{artist}', 'MusicBox\Controller\ArtistController::viewAction') ->bind('artist');

Each controller method gets the request and the dependency injection container ($app), and returns a rendered twig template:

namespace MusicBox\Controller; use Silex\Application; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Exception\ResourceNotFoundException; class ArtistController { public function viewAction(Request $request, Application $app) { $artist = $request->attributes->get('artist'); if (!$artist) { $app->abort(404, 'The requested artist was not found.'); } $data = array( 'artist' => $artist, ); return $app['twig']->render('artist.html.twig', $data); } }

Putting the M in MVC: entities and repositories

Out of the box Silex doesn’t provide a model layer.

It doesn’t integrate with Doctrine ORM (which is used by Symfony full stack), nor does it provide its own (like Laravel 4 does).

What we get is Doctrine DBAL, which allows us to connect to a database, and run queries (though I yearned for the simplicity of drupal’s db_select() when using the doctrine query builder). That’s it.

Of course, we want our database records (artists, users, etc) to be represented by objects, so that we can pass them to Symfony Form, Twig, have autocompletion in IDEs, etc.

We call these objects entities (just like Drupal and Doctrine do), and implement them as Plain Old PHP Objects (POPO), consisting only of properties and their getters / setters.

namespace MusicBox\Entity; class Artist { protected $id; protected $name; protected $createdAt; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } public function getCreatedAt() { return $this->createdAt; } public function setCreatedAt(\DateTime $createdAt) { $this->createdAt = $createdAt; }

The entities are manipulated by repositories (controllers in Drupal terminology), classes that handle loading / saving / deleting through direct db queries.

namespace MusicBox\Repository; use Doctrine\DBAL\Connection; use MusicBox\Entity\Artist; class ArtistRepository implements RepositoryInterface { protected $db; public function __construct(Connection $db) { $this->db = $db; } public function save($artist) { $artistData = array( 'name' => $artist->getName(), 'short_biography' => $artist->getShortBiography(), // Other properties here. ); if ($artist->getId()) { $this->db->update('artists', $artistData, array('artist_id' => $artist->getId())); } else { // The artist is new, note the creation timestamp. $artistData['created_at'] = time(); $this->db->insert('artists', $artistData); // Get the id of the newly created artist and set it on the entity. $id = $this->db->lastInsertId(); $artist->setId($id); } } }

If we used an ActiveRecord pattern (Rails, Propel, Laravel’s Eloquent), the entity would know how to save / delete itself (usually by extending a base class that provides a generic version of those methods). The DataMapper / Repository patterns (Doctrine / Drupal) separate the persistence logic, which is something I definitely find cleaner.

This handmade model layer was a big win because it allowed existing knowledge to be used without having to jump into the complexity of Doctrine ORM, while still gradually seeing and learning the need for various things handled by Doctrine ORM.

Forms

Silex integrates with the Symfony Form and Validator components.

There is talk of using Symfony Form in Drupal 9, though I’ve found the form component to have worse documentation than Drupal’s Form API, making more advanced use cases (such as massaging values post-submit) harder to accomplish.

Forms are defined in Type classes such as this one:

namespace MusicBox\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\Validator\Constraints as Assert; class ArtistType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) $builder ->add('name', 'text', array( 'constraints' => new Assert\NotBlank(), )) ->add('shortBiography', 'textarea', array( 'attr' => array( 'rows' => '7', ) )) ->add('biography', 'textarea', array( 'attr' => array( 'rows' => '15', ) )) ->add('soundCloudUrl', 'url') ->add('file', 'file', array( 'required' => FALSE, 'label' => 'Image', )) ->add('save', 'submit'); } }

A controller then renders the form and processes submissions:

public function addAction(Request $request, Application $app) { $artist = new Artist(); $form = $app['form.factory']->create(new ArtistType(), $artist); if ($request->isMethod('POST')) { $form->bind($request); if ($form->isValid()) { $app['repository.artist']->save($artist); $message = 'The artist ' . $artist->getName() . ' has been saved.'; $app['session']->getFlashBag()->add('success', $message); // Redirect to the edit page. $redirect = $app['url_generator']->generate('admin_artist_edit', array('artist' => $artist->getId())); return $app->redirect($redirect); } } $data = array( 'form' => $form->createView(), 'title' => 'Add new artist', ); return $app['twig']->render('form.html.twig', $data); }

Login / Logout

The Symfony Security component was used for protecting the admin area.

It is probably the most “magical” part of Symfony, with docs that are incomplete and hard even for me (someone with dev experience) to grasp. Hence this was the only hard part in the development process, and I would gladly not repeat it.

The code below is all that’s needed, assuming that the UserRepository and User entity implement the correct interfaces. Even the submit of the login form is handled somehow / somewhere.

$app->register(new Silex\Provider\SecurityServiceProvider(), array( 'security.firewalls' => array( 'admin' => array( 'pattern' => '^/', 'form' => array( 'login_path' => '/login', 'check_path' => '/admin/login_check', 'username_parameter' => 'form[username]', 'password_parameter' => 'form[password]', ), 'logout' => true, 'anonymous' => true, 'users' => $app->share(function () use ($app) { return new MusicBox\Repository\UserRepository($app['db'], $app['security.encoder.digest']); }), ), ), 'security.role_hierarchy' => array( 'ROLE_ADMIN' => array('ROLE_USER'), ), )); $app->before(function (Request $request) use ($app) { $protected = array( '/admin/' => 'ROLE_ADMIN', '/me' => 'ROLE_USER', ); $path = $request->getPathInfo(); foreach ($protected as $protectedPath => $role) { if (strpos($path, $protectedPath) !== FALSE && !$app['security']->isGranted($role)) { throw new AccessDeniedException(); } } });

The end result

Two weekends and a lot of coffee and white wine later, MusicBox was ready in all of its Bootstrapy glory:

The code is up on GitHub and we hope it will serve as a useful example. Pull requests welcome.

Conclusion

So, why was this a good idea again?

Starting from a minimalistic framework allowed us to minimize the learning curve while still taking advantage of best-of-breed components.

The growing list of requirements quickly started showing the benefits of Doctrine ORM, Symfony Full Stack, Laravel 4, and other more complete frameworks.

Furthermore, all of the knowledge gained along the way can now be built upon, both Symfony Full Stack and Laravel 4 feel like logical next steps (especially Laravel 4 which feels like Silex’s big brother). The adventure continues!