Refactoring to symfony components

78
REFACTORING TO SYMFONY COMPONENTS ...AND THEIR FRIENDS Michael Peacock

description

 

Transcript of Refactoring to symfony components

Page 1: Refactoring to symfony components

REFACTORING TOSYMFONY COMPONENTS

...AND THEIR FRIENDSMichael Peacock

Page 2: Refactoring to symfony components

@MICHAELPEACOCKHead Developer @ Ground SixTechnical authorOccasional conference speaker

Page 3: Refactoring to symfony components

THE COMPONENTShttp://symfony.com/components

Page 4: Refactoring to symfony components

WHY USE COMPONENTSSolve common web application problemsIncredibly well documented(relatively) Standalone: use them how you likeIdeal for refactoring

Page 5: Refactoring to symfony components

INSTALLATIONComposer: the knight in shining armour

Download it

Create a composer.json file

Run composer

curl -s https://getcomposer.org/installer | php

{ "require": { "symfony/the-project-name": "dev-master", }}

php composer.phar install

Page 6: Refactoring to symfony components

THEIR FRIENDS

Page 7: Refactoring to symfony components

WHAT'S IN STOREAutoloading classes with ClassLoaderRouting requests with RoutingListening for events with the EventDispatcherParsing YAML files with the YAML componentHTTP Requests and responses with HTTPFoundationInjecting dependencies with PimpleTemplates with Twig

Page 8: Refactoring to symfony components

OUR REFACTORING TALESPEED UP DEVELOPMENT & MODERNISE LEGACY CODEBASE

Page 9: Refactoring to symfony components

OUR JOURNEYMessy structure, some procedural code: ClassLoaderGlobals, singletons and crazy objects: PimpleScattered routing logic, long if/else conditions: RoutingHardcoded configurations: YAMLDuplicated logic: EventDispatcherPHP & HTML mixed together: TwigDuplicate form logic, spagetti code: Validator (Fuel)Other improvements: Mailer, HTTPFoundation,Translation & Validator

Page 10: Refactoring to symfony components

MESSY STRUCTURE & PROCEDURAL CODE

Page 11: Refactoring to symfony components

CLASS LOADER

Page 12: Refactoring to symfony components

LAYING THE FOUNDATIONSControllersPSR-0

Namespace the codeRestructure into a better directory heirarchyComponent based structure for our own code too

Page 13: Refactoring to symfony components

USAGE$namespaces = array( 'VendorName\\Namespace' => __DIR__ .'/', 'VendorName\\AnotherNamespace' => __DIR__ .'/');

$loader = new \Symfony\Component\ClassLoader\UniversalClassLoader();$loader->register();$loader->registerNamespaces($namespaces);

Page 14: Refactoring to symfony components

CACHINGSupport for APC available, just needs to be enabled

Page 15: Refactoring to symfony components

GLOBALS, SINGLETONS AND CRAZY OBJECTS

Page 16: Refactoring to symfony components

PIMPLE

Pimple is a dependency injection container which lets useasily manage and inject our dependencies into our projects.

We put the dependencies into a container, and then we injectthis container into our code which uses it.

Page 17: Refactoring to symfony components

REFACTORING TO USE A CONTAINER<?phpclass SomeModel{ public function __construct() { $sql = ""; $query = Database::query($sql); } }before

<?phpclass SomeModel{ public function __construct($container=array()) { // TODO: further refactor once d.i.c. in place $sql = ""; $query = Database::query($sql); }}after

Page 18: Refactoring to symfony components

LAZY LOADINGBy 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']);};

Page 19: Refactoring to symfony components

SHARING OBJECTS$container['db'] = $container->share(function($c) { return new \PDO("...", $c['db_user'], $c['db_pwd']);});

Page 20: Refactoring to symfony components

FURTHER REFACTORING<?phpclass SomeModel{ public function __construct($container=array()) { $sql = ""; $query = $container['db']->query($sql); }}

Page 21: Refactoring to symfony components

CREATING YOUR OWN CONTAINERParticularly useful for re-use and different use-cases (cli vs

web)<?phpnamespace Project\Framework\Container;

class MyContainer extends \Pimple{ public function __construct(array $values = array()) { parent::__construct($values); // add things to the container here }}

Page 22: Refactoring to symfony components

CONTROLLER REFACTORING (BEFORE)<?phpclass SomeController{ // ... public function someAction() { $model = new SomeModel($this->container); }}

Page 23: Refactoring to symfony components

REDUCING NEW...CONTAINERS WITHINCONTAINERS

<?phpnamespace 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']); }); }}

Page 24: Refactoring to symfony components

CONTROLLER REFACTORING (AFTER)<?phpclass SomeController { // ... public function someAction() { $model = $this->container['factories']['some_model']->newModel(); } }

Page 25: Refactoring to symfony components

HARDCODED CONFIGURATIONS

Page 26: Refactoring to symfony components

YAML

Page 27: Refactoring to symfony components

A YAML FILEdb_mysql: host: 'localhost' user: 'root' pass: '' name: 'db' port: 3306 auto_patch: true

general: production: false skin: 'release' site_url: 'http://localhost:4567/'

Page 28: Refactoring to symfony components

PARSING A YAML FILE$yaml = new Symfony\Component\Yaml\Parser();

$parsed_settings = $yaml->parse(file_get_contents(__DIR__.'/config.yml'));

Page 29: Refactoring to symfony components

CACHING:-(

Page 30: Refactoring to symfony components

SCATTERED ROUTING LOGIC, LONG IF/ELSECONDITIONS

Page 31: Refactoring to symfony components

ROUTING

Page 32: Refactoring to symfony components

REFACTORING FOUNDATIONSMostly 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 anOldAction($date, $some_id)

public function aNewAction($url_params=array())

Page 33: Refactoring to symfony components

SETTING IT UPAlias some of the namespaces

Prepare dependencies

Construct

use Symfony\Component\Config\FileLocator;use Symfony\Component\Routing\RequestContext;use Symfony\Component\Routing;

$locator = new FileLocator(array(FRAMEWORK_PATH));$request = (isset($_SERVER['REQUEST_URI']))? $_SERVER['REQUEST_URI']:'';$context = new RequestContext($request, $_SERVER['REQUEST_METHOD']);

$router = new Routing\Router(new YamlFileLoader($locator), 'routes.yml', array(), $context);

Page 34: Refactoring to symfony components

ROUTES FILEindex: pattern: / defaults: { class: 'Project\Static\Controller', method: 'homePage' } requirements: _method: GET

Page 35: Refactoring to symfony components

ROUTINGtry { $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}

Page 36: Refactoring to symfony components

ROUTE VARIABLEScomment_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); $variables = $route; unset($variables['name'], $variables['class'], $variables['method']); $action = $controller->$route['method']();

Page 37: Refactoring to symfony components

AUTHENTICATION CONTROLaccount: 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 $_SESSION['redirect'] = $_SERVER['REQUEST_URI']; $event = new Events\RequestRedirection($container['settings']['base_url'] . 'login/'); $this->container['dispatcher']->dispatch('redirect', $event); }}

Page 38: Refactoring to symfony components

ROUTE CACHING$router = new Routing\Router(new YamlFileLoader($locator), 'routes.yml', array('cache_dir' => '/var/www/cache/'), $context);

Page 39: Refactoring to symfony components

UTM DATA, ETC

http://forums.phpfreaks.com/topic/257622-remove-utm-tags-from-url-regex/

$url = preg_replace('/&?utm_(.*?)\=[̂&]+/', '', $url);$url = (substr($url, -1) == '?') ? rtrim($url, '?') : $url;

Page 40: Refactoring to symfony components

DUPLICATED LOGIC

Page 41: Refactoring to symfony components

EVENT DISPATCHER

Page 42: Refactoring to symfony components

WHY?

Page 43: Refactoring to symfony components

USE CASESRedirecting the user / flash notificationsSending transactional emailsAdding a product to a basketHooking into other features to share other features e.g.tweet on content creation

Page 44: Refactoring to symfony components

REDIRECTION & "FLASH" NOTIFICATIONS1. Raise an event2. Listen for notification events, and log the notification in-

session3. Listen for a redirect event, and redirect

Ordering is important here as we don't want to redirectbefore setting the session!

Page 45: Refactoring to symfony components

APPROACHNotifiableMessageInterfaceRequestRedirection eventRedirectableNotification event (extends and implementsthe above)Events must extend the symfony event

Page 46: Refactoring to symfony components

NOTIFIABLE MESSAGE INTERFACE<?phpnamespace Project\Framework\Events;

interface NotifiableMessageInterface{ public function getNotification(); // the html class to be applied public function getClass();}

Page 47: Refactoring to symfony components

REQUEST REDIRECTION EVENT<?phpnamespace Project\Framework\Events;use Symfony\Component\EventDispatcher\Event;class RequestRedirection extends Event{ protected $url;

public function __construct($url = null) { $this->url = $url; }

public function getURL() { return $this->url; }}

Page 48: Refactoring to symfony components

LISTENER<?phpnamespace Project\Framework\Listeners;

use Project\Framework\Events;use Symfony\Component\EventDispatcher\Event;

class SetPersistantNotification{ public function setNotification( Events\NotifiableMessageInterface $event ) { $_SESSION['system_notification'] = $event->getNotification(); $_SESSION['system_notification_class'] = $event->getClass(); }

}

Page 49: Refactoring to symfony components

ANOTHER LISTENER<?phpnamespace Project\Framework\Listeners;

use Project\Framework\Events;use Symfony\Component\EventDispatcher\Event;

class Redirect{ public function redirectUser( Events\RequestRedirection $event ) { // TODO: utilise httpframework header("Location: " . $event->getURL() ); exit(); }

}

Page 50: Refactoring to symfony components

LISTEN UP...Create an event dispatcherCreate instance of listenerAdd the listener

Event nameA callable e.g. Object/Method array combo, Closure, etcPriority: for multiple listeners listening for the sameevent

Page 51: Refactoring to symfony components

LISTEN UP$dispatcher = new EventDispatcher();

// Notification (Success, Warning, Error)$setPersistantNotification = new Listeners\SetPersistantNotification();$dispatcher->addListener('notify', array($setPersistantNotification, 'setNotification'), 10);

// Redirect$redirectUser = new Listeners\Redirect();$dispatcher->addListener('notifiy', array($redirectUser, 'redirectUser'), 0);

Page 52: Refactoring to symfony components

DISPATCH$url = $base_url . 'account';$message = 'Your password was changed successfuly.';$event = new Events\RedirectableNotification($url, $message, 'success');$dispatcher->dispatch('notify', $event);

Page 53: Refactoring to symfony components

GOTCHASget/set Name

Page 54: Refactoring to symfony components

STANDARD EVENTWe tend to use our own event object which extends the

symfony one. This holds a payload which is our event relatedobject.

<?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; }}

Page 55: Refactoring to symfony components

QUEUEABLEInterface to define an event as something that can bequeuedListener to queue the event e.g. in beanstalk<?phpnamespace Project\Framework\Events;

interface QueueableInterface{ public function getId();}

Page 56: Refactoring to symfony components

QUEUE AN EVENT IN YOUR LISTENERpublic function checkEvent(Events\Event $event){ if ($event->getPayLoad() instanceof QueueableInterface) { $tubes_map = array('new.user' => 'tweet'); $id = $event->getPayLoad()->getId(); $tube = $tubes_map[$event->getName()]; $this->container['q']->useTube($tube)->put($id)) }}

Page 57: Refactoring to symfony components

PHP & HTML MIXED TOGETHER

Page 58: Refactoring to symfony components

TWIG

Page 59: Refactoring to symfony components

SETUP AND LOAD

Load and render template

// 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 the template$twig->loadTemplate('index.twig');// render it$twig->render(array('title' => 'variable'));

Page 60: Refactoring to symfony components

REFACTORING TO TWIGA place to prepare twig and also perform any non-twigpresentation logic. Keeps the data de-coupled from the

workings of the template engineabstract 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; }}

Page 61: Refactoring to symfony components

PIMPLE ISSUE / ADD GLOBAL

Page 62: Refactoring to symfony components

TWIG TEMPLATES{{ 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 %}

Page 63: Refactoring to symfony components

TEMPLATE CACHINGThis caches compiled templates not output

$this->twig = new \Twig_Environment($loader, array( 'cache' => '/var/www/cache/templates/, ));

Page 64: Refactoring to symfony components

OUTPUT CACHING

Page 65: Refactoring to symfony components

SETUP OUTPUT CACHINGuse 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);

Page 66: Refactoring to symfony components

INTEGRATING OUTPUT CACHING$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; }}

Page 67: Refactoring to symfony components

VALIDATOR (FUEL)There is a symfony component which does this, though we

opted for the Fuel validation component.

Page 68: Refactoring to symfony components

HTTPFOUNDATIONAbstracting superglobals, the HTTP request and the HTTP

response

Page 69: Refactoring to symfony components

REQUEST

Provides a parameter bag of properties

PropertyProperty PurposePurposerequest store $_POSTquery store $_GETcookies store $_COOKIEattributes application specificfiles $_FILEserver $_SERVERheaders subset of $_SERVER

use Symfony\Component\HttpFoundation\Request;$request = Request::createFromGlobals();

Page 70: Refactoring to symfony components

A PARAMETER BAG?Request properties are all ParameterBag or sub-classes

Provides special methods to manage contents, including:

allkeysgetaddsethasremove

Page 71: Refactoring to symfony components

RESPONSEuse Symfony\Component\HttpFoundation\Response;

$response = new Response();$response->setContent('Hello PHPUK');$response->setStatusCode(200);$response->headers->set('Content-Type', 'text/plain');

// alternatively...$response = new Response('Hello PHPUK', 200, array('content-type', 'text/plain'));

$response->prepare();

// send the response to the user$response->send();

Page 72: Refactoring to symfony components

TRANSLATIONWorth a mention

Page 73: Refactoring to symfony components

SWIFT MAILER

Page 74: Refactoring to symfony components

SMTP TRANSPORT$transport = \Swift_SmtpTransport::newInstance($container['settings']['smtp']['host'], 25) ->setUsername($container['settings']['smtp']['user']) ->setPassword($container['settings']['smtp']['pass']);

Page 75: Refactoring to symfony components

CREATE THE MESSAGE$this->message = \Swift_Message::newInstance($subject) ->setFrom(array($from => $from_name)) ->setTo(array($recipient => $recipient_name)) ->setBody($body, $content_type);

Page 76: Refactoring to symfony components

SEND THE MESSAGE$mailer = \Swift_Mailer::newInstance($transport);

return $mailer->send($message);

Page 77: Refactoring to symfony components

THANKS!@MICHAELPEACOCK

WWW.MICHAELPEACOCK.CO.UKHTTPS://JOIND.IN/8046

Page 78: Refactoring to symfony components

IMAGE CREDITShttp://www.flickr.com/photos/oskay/275142789/http://www.flickr.com/photos/martin_bircher/5287769680/http://www.flickr.com/photos/tronixstuff/5122815499/http://www.flickr.com/photos/tronixstuff/4581416773/http://www.flickr.com/photos/oskay/437339684/http://www.flickr.com/photos/oskay/437342078/http://www.flickr.com/photos/laughingsquid/2885196845/