Refactoring to symfony components
-
Upload
michael-peacock -
Category
Documents
-
view
349 -
download
4
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/