On Github mkpeacock / talk-confoo-2014-refactoring-to-symfony-components
Michael Peacock
http://symfony.com/components
The symfony framework is built using a series of re-usable components, which have been released independantly. These range from browser emulation, to generating console output, to generating forms, validating forms, translating to application routing.Composer: the knight in shining armour
Download it
curl -s https://getcomposer.org/installer | php
Create a composer.json file
{ "require": { "symfony/the-project-name": "dev-master", } }
Run composer
php composer.phar installThe components are easily installable through composer, which makes adding them to your project either one at time or all in one go, a breeze. Using this approach it is also easy to check the components you have running are secure, by running your composer.lock file through the symfony security checker.
$namespaces = [ 'VendorName\\Namespace' => __DIR__ .'/', 'VendorName\\AnotherNamespace' => __DIR__ .'/' ];
$loader = new \Symfony\Component\ClassLoader\UniversalClassLoader(); $loader->register(); $loader->registerNamespaces($namespaces);Using it is really straight forward, we create an array map of our namespaces and their containing folder, we create a new instance of the universal class loader, register the class loader, and then tell the class loader to register our namespaces.
Support for APC available, just needs to be enabled; Not necessary if using PHP 5.5
Pimple is a dependency injection container which lets us easily manage and inject dependencies into projects. We put the dependencies into a container, and then we inject this container into our code which uses it.
<?php class SomeModel { public function __construct() { $sql = ""; $query = Database::query($sql); } }before
<?php class SomeModel { public function __construct($container=array()) { // TODO: further refactor once d.i.c. in place $sql = ""; $query = Database::query($sql); } }afterof course ot use a dependency injection container, our classes need to be updated to support an injected dependency injection container. As we don't yet have the container, we make the injection optional, and for the moment continue using the old, non injected methods. Now however, we can build our injection container and put dependencies in it.
By utilising closures, code isn't run until it is first requested / called; i.e. database connection is established only when you first try and use the connection
$container['db'] = function($c) { return new \PDO("...", $c['db_user'], $c['db_pwd']); };We can then add a dependency to our container by creating a new property in the object (using the ArrayAccess implementation which Pimple provides us). By wrapping the code in a closure, it isn't evaluated until we first try and access the PDO object from the container. The paramater passed to the closure is injected by pimple, and is a copy of the container itself.
<?php class SomeModel { public function __construct($container=array()) { $sql = ""; $query = $container['db']->query($sql); } }
Particularly useful for re-use and different use-cases (cli vs web)
<?php namespace Project\Framework\Container; class MyContainer extends \Pimple { public function __construct(array $values = array()) { parent::__construct($values); // add things to the container here } }we can extend pimple, all we need to do is implement the constructor, and from here add our dependencies. Makes the container reusable, distributable and lets us have different containers for different use-cases (e.g. web container and a CLI container - has proved quite useful)
<?php class SomeController { // ... public function someAction() { $model = new SomeModel($this->container); } }Now, since the initial refactoring to include pimple, we instantiate objects and pass pimple along...
<?php namespace Faceifi\Framework\Container; class DataAccessObjects extends \Pimple { public function __construct(array $values = array()) { parent::__construct($values); $this['user'] = $this->share(function($c) { return new UserDao($c['container']); }); } }Discuss about using pimple as a factory and dao container
<?php class SomeController { // ... public function someAction() { $model = $this->container['factories']['some_model']->newModel(); } }Similarly we also used these containers for factory management. This allowed us to reduce places where the new keyword is used, ensuring objects can be mocked, injected and changed as required. This was particularly helpful when we moved files, objects and classes around; since we had to move them this was an excuse to abstract this out, so any future file moves won't impact so many files.
db_mysql: host: 'localhost' user: 'root' pass: '' name: 'db' port: 3306 auto_patch: true general: production: false skin: 'release' site_url: 'http://localhost:4567/'
$yaml = new Symfony\Component\Yaml\Parser(); $parsed_settings = $yaml->parse(file_get_contents(__DIR__.'/config.yml'));Once parsed the data from the YAML file is converted into an array.
:-(
Unfortunately there isn't any built in caching, so you would probably want to consider adding your ownMostly taken care of when we ensured all controllers were objects and that the new structure followed PSR-0. Controllers refactored like so:
public function __construct($container) { $this->container = $container; } public function someRoute($date, $some_id) {}first we had to make everything an object; with a constructor which accepts a container, any paramaters to routes need to be defined in the routes file
Alias some of the namespaces
use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing;
Prepare dependencies
$locator = new FileLocator([FRAMEWORK_PATH]); $request = (isset($_SERVER['REQUEST_URI']))? $_SERVER['REQUEST_URI']:''; $context = new RequestContext($request, $_SERVER['REQUEST_METHOD']);
Construct
$router = new Routing\Router(new YamlFileLoader($locator), 'routes.yml', [], $context);3 fragments - alias some symfony namespaces - prepare the required dependencies - create the routing object
index: pattern: / defaults: { class: 'Project\Static\Controller', method: 'homePage' } requirements: _method: GETRoutes file (yaml) has a list of routes, they have a name, a pattern (some regex or string), defaults (explain them) and optionally some requiements (format of URL elements; post/get/put/delete) etc.
try { $url = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : ''; // get rid of the trailing slash $url = (strlen($requestURL) > 1) ? rtrim($requestURL, '/') : $url; $route = $router->match($url); $controller = new $route['class']($container); $action = $controller->$route['method'](); } catch (Routing\Exception\ResourceNotFoundException $e) { // todo: 404 }NB: routes in next slide. Now we can route the request, get the URL being requested, call the route method,.This gives us an array containing the route name, controller and method - as defined in our route file.We can then instantiate the correct controller, and call the correct route action.
comment_story_add: pattern: /news/{category}/{date}/{article} defaults: { class: 'Comments\Controller::addComment' } requirements: date: "[0-9]{2}-[0-9]{2}-[0-9]{4}" _method: POST
$route = $router->match($url); $controller = new $route['class']($container); $method = $route['method']; unset($route['name'], $route['class'], $route['method']); call_user_func_array([$controller, $method], $route);Our routes can also specify custom fragments, and formats for those, e.g. date. We need to pass these to the route, so we remove the other info (route name, controller and method) leaving us with an array of paramaters. call_user_func_array ensures they get passed to the route action.
account: pattern: /account defaults: { class: 'Project\Account\Controller', method: 'manage', logged_in: true } requirements: _method: GET
if (isset($route['logged_in'])) { if (is_null($container['user'])) { // User is trying to access logged in only content - redirect to login and store redirect } }
$router = new Routing\Router(new YamlFileLoader($locator), 'routes.yml', ['cache_dir' => '/var/www/cache/'], $context);
$url = preg_replace('/&?utm_(.*?)\=[^&]+/', '', $url); $url = (substr($url, -1) == '?') ? rtrim($url, '?') : $url;
http://forums.phpfreaks.com/topic/257622-remove-utm-tags-from-url-regex/
Ordering is important here as we don't want to redirect before setting the session!
<?php namespace Project\Framework\Events; use Symfony\Component\EventDispatcher\Event; class UserRegisteredEvent extends Event { protected $user; public function __construct($user = null) { $this->user = $user; } public function getUser() { return $this->user; } }
<?php namespace Project\Framework\Listeners; use Project\Framework\Events; use Symfony\Component\EventDispatcher\Event; class PersistantNotificationListener { public function setNewUserNotification($event) { $_SESSION['system_notification'] = 'Thanks for signing up!'; $_SESSION['system_notification_class'] = 'success'; } }
<?php namespace Project\Framework\Listeners; use Project\Framework\Events; use Symfony\Component\EventDispatcher\Event; class RedirectionListener { public function postRegistrationRedirect($event) { // TODO: use routing url generator! header("Location: /users/" . $event->getUser()->getId() ); exit(); } }
$dispatcher = new EventDispatcher(); // Notification (Success, Warning, Error) $listener = new Listeners\PersistantNotificationListener(); $dispatcher->addListener('user.registered', [$listener, 'setNewUserNotification'], 10); // Redirect $listener = new Listeners\RedirectionListener(); $dispatcher->addListener('user.registered', [$listener, 'postRegistrationRedirect'], 0);
$user = $this->processRegistration($http_request); $event = new Events\UserRegisteredEvent($user); $dispatcher->dispatch('user.registered', $event);
The approach we have discussed gets repetative over time. Instead:
Have your listeners implement an interface to expose what they listen for
interface EventListenerInterface { public function getImplementedEvents(); public function getDefaultEventPriority(); }
With this: redirection listeners can store a mapping of events and redirections, and perform the redirection from a single method call
get/set Name
We tend to use our own event object which extends the symfony one. This holds a payload which is our event related object.
<?php namespace Project\Framework\Events; class Event extends \Symfony\Component\EventDispatcher\Event { protected $payLoad; public function setPayLoad($payload) { $this->payLoad = $payload; } public function getPayLoad() { return $this->payLoad; } }
Great for command line tasks. Customisable with options and paramaters: default action for a command could be to process a job queue, but options could let you manually trigger actions. e.g. email sending
// create a twig filesystem loader so it can access templates $loader = new \Twig_Loader_Filesystem('templates'); // create a new twig environment and pass it the loader $twig = \Twig_Environment($loader);
Load and render template
// load the template $twig->loadTemplate('index.twig'); // render it $twig->render(['title' => 'variable']);
A place to prepare twig and also perform any non-twig presentation logic. Keeps the data de-coupled from the workings of the template engine
abstract class View { public function __construct($container) { $loader = new \Twig_Loader_FileSystem('templates'); $this->templateEngine = new \Twig_Environment($loader); } public function generate($model=null); public function render($template_file) { $this->templateEngine->loadTemplate($template_file); echo $twig->render($this->container->templateVariables); exit; } }
{{ some_variable }} {# some comment #} {% set list_of_items = variable.getItems() %} {% for item in list_of_items %} <li>{{loop.index}}: {{item.name}}</li> {% else %} <li>Empty :-(</li> {% endfor %}Dot syntax is very special. It can be used to access object properties, object methods, array elements, and even object getter methods. Templates can also be extended to chain multiple templates together.
This caches compiled templates not output
$this->twig = new \Twig_Environment($loader, [ 'cache' => '/var/www/cache/templates/, ]);
use Desarrolla2\Cache\Cache; use Desarrolla2\Cache\Adapter\File; $adapter = new File(); $adapter->setOption('ttl', (int) $container['misc_config']->cache->ttl); try { $adapter->setOption('cacheDir', '/var/www/cache/pages/'); } catch (\Exception $e) { // temporarily let the application use the /tmp folder? } $cache = new Cache($adapter);
$cache_key = md5($url); if ($cache_enabled && $route['cachable']) { if(is_null($this->container['user'] && $cache->has($cache_key)) { echo $cache->get($cache_key); exit; } }
use Symfony\Component\Validator\Constraints as Assert; $constraints = new Assert\Collection([ 'name' => [ new Assert\NotBlank(['message' => 'You must provide a name']), new Assert\Length(['min' => 5, 'max' => 255]) ], 'email' => [ new Assert\NotBlank(['message' => 'You must provide your email']), new Assert\Email(), new Assert\Callback(...) ], ];
$validator = \Symfony\Component\Validator\Validation::createValidator(); $violations = $validator->validateValue($_POST, $contstraints);
Abstracting superglobals, the HTTP request and the HTTP response
use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals();
Provides a parameter bag of properties
Property Purpose request store $_POST query store $_GET cookies store $_COOKIE attributes application specific files $_FILE server $_SERVER headers subset of $_SERVERRequest properties are all ParameterBag or sub-classes Provides special methods to manage contents, including:
use Symfony\Component\HttpFoundation\Response; $response = new Response(); $response->setContent('Hello Confoo'); $response->setStatusCode(200); $response->headers->set('Content-Type', 'text/plain'); // alternatively... $response = new Response('Hello Confoo', 200, ['content-type', 'text/plain']); $response->prepare(); // send the response to the user $response->send();
Worth a mention
$transport = \Swift_SmtpTransport::newInstance($container['settings']['smtp']['host'], 25) ->setUsername($container['settings']['smtp']['user']) ->setPassword($container['settings']['smtp']['pass']);
$this->message = \Swift_Message::newInstance($subject) ->setFrom([$from => $from_name]) ->setTo([$recipient => $recipient_name]) ->setBody($body, $content_type);
$mailer = \Swift_Mailer::newInstance($transport); return $mailer->send($message);
http://mkpeacock.github.io/talk-confoo-2014-refactoring-to-symfony-components/#/