Refactoring to symfony components

Post on 29-Nov-2014

349 views 4 download

description

 

Transcript of Refactoring to symfony components

REFACTORING TOSYMFONY COMPONENTS

...AND THEIR FRIENDSMichael Peacock

@MICHAELPEACOCKHead Developer @ Ground SixTechnical authorOccasional conference speaker

THE COMPONENTShttp://symfony.com/components

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

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

THEIR FRIENDS

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

OUR REFACTORING TALESPEED UP DEVELOPMENT & MODERNISE LEGACY CODEBASE

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

MESSY STRUCTURE & PROCEDURAL CODE

CLASS LOADER

LAYING THE FOUNDATIONSControllersPSR-0

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

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

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

CACHINGSupport for APC available, just needs to be enabled

GLOBALS, SINGLETONS AND CRAZY OBJECTS

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.

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

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

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

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

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

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

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

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

HARDCODED CONFIGURATIONS

YAML

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/'

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

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

CACHING:-(

SCATTERED ROUTING LOGIC, LONG IF/ELSECONDITIONS

ROUTING

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())

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);

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

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}

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']();

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

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

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;

DUPLICATED LOGIC

EVENT DISPATCHER

WHY?

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

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!

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

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

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

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

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(); }

}

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(); }

}

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

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);

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

GOTCHASget/set Name

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

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();}

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)) }}

PHP & HTML MIXED TOGETHER

TWIG

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'));

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

PIMPLE ISSUE / ADD GLOBAL

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 %}

TEMPLATE CACHINGThis caches compiled templates not output

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

OUTPUT CACHING

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);

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

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

opted for the Fuel validation component.

HTTPFOUNDATIONAbstracting superglobals, the HTTP request and the HTTP

response

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();

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

Provides special methods to manage contents, including:

allkeysgetaddsethasremove

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();

TRANSLATIONWorth a mention

SWIFT MAILER

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

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

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

return $mailer->send($message);

THANKS!@MICHAELPEACOCK

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

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/