$30 off During Our Annual Pro Sale. View Details »

Drupal 8 - The Symfony and Drupal partnership

Drupal 8 - The Symfony and Drupal partnership

Symfony has changed the PHP world forever, not only with its reusable and decoupled components, but also by introducing development standards which were not widely used or even known in the PHP world before. Thanks to its component architecture, Symfony is being widely adopted by many Open Source projects, custom-built applications, other frameworks, and even legacy applications.

Symfony’s growing community has always pushed for better practices and turned many good developers into awesome aficionados. Due to Drupal’s migration to Symfony, both communities have already started to blend and collaborate to bring benefit to both camps.

During the talk Jakub will explain exactly what benefits this new collaboration can offer. Following this, he will introduce several components to help debunk Symfony myths and increase developer confidence in the framework ahead of Drupal 8 later this year.

Jakub Zalas

May 31, 2014
Tweet

More Decks by Jakub Zalas

Other Decks in Programming

Transcript

  1. Drupal 8 - The Symfony and Drupal partnership Jakub Zalas

    DrupalCamp Yorkshire 31 May 2014
  2. @jakzal @jakub_zalas @SensioLabsUK £  

  3. Drupal in the eyes of a Symfony developer

  4. None
  5. None
  6. DRUPAL IS AWESOME NO, REALLY!

  7. Drupal community

  8. None
  9. None
  10. None
  11. None
  12. None
  13. None
  14. None
  15. None
  16. Drupal is a perfect tool for building websites

  17. BUT…

  18. IS IT PERFECT FOR EVERYONE?

  19. THE DRUPAL WAY PHP developer vs Drupal developer

  20. BARRIER TO ENTRY

  21. SO…

  22. FOCUS ON THE CORE OF YOUR BUSINESS AND LET OTHERS

    DO THE REST
  23. FOLLOW THE WIDELY ADOPTED INDUSTRY STANDARDS AND YOUR COMMUNITY WILL

    GROW
  24. Storm is coming in the Drupal world

  25. After every storm the sun will smile

  26. None
  27. What is Symfony? First, Symfony2 is a reusable set of

    standalone, decoupled, and cohesive PHP components that solve common web development problems. http://fabien.potencier.org/article/49/what-is-symfony2
  28. Fixing PHP Debug HttpFoundation Yaml Tools Filesystem Finder Process Stopwatch

    Web Framework Foundation HttpKernel Routing Templating Data Helpers Config OptionsResolver PropertyAccess Serializer Patterns DependencyInjection   EventDispatcher   I18n Intl + Icu Locale Translation Crawlers BrowserKit CssSelector DomCrawler Framework+ ClassLoader Console ExpressionLanguage Form Security Validator
  29. What is Symfony? Then, based on these components, Symfony2 is

    also a full-stack web framework. http://fabien.potencier.org/article/49/what-is-symfony2
  30. None
  31. Projects on packagist.org 4373 21% 1206 6% 14838 73% Depends

    on Symfony Depends on Zend Other December  2013  
  32. 0 100 200 300 400 500 600 700 Bundles Other

    Symfony Component based projects December  2013  
  33. Symfony community

  34. GROWING BETTER DEVELOPERS

  35. None
  36. 3178 forks 1025 code contributors 739 doc contributors

  37. 8421 stars

  38. HttpFoundation

  39. $_GET $_POST $_SERVER $_COOKIE $_FILES

  40. None
  41. use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $request->query->...(); // $_GET $request->request->...(); //

    $_POST $request->cookies->...(); // $_COOKIE $request->files->...(); // $_FILES $request->server->...(); // $_SERVER $request->headers->...(); // $_SERVER $request->attributes->...(); // up to you :)
  42. $id = isset($_GET['id']) ? $_GET['id'] : -1; becomes $id =

    $request->query->get('id', -1);
  43. $request->query->get('name', 'default'); $request->query->set('name', 'default'); $request->query->has('name'); $request->query->all(); $request->query->keys(); $request->query->replace('name', 'other'); $request->query->remove('name');

    $request->query->getAlpha('name'); $request->query->getAlnum('name'); $request->query->getInt('name'); $request->query->getDigits('name');
  44. function router(Request $request) { $path = $request->getPathInfo(); $id = preg_replace('/node/(\d+)',

    '$1', $path); $request->attributes->set('id', $id); $request->attributes->set( '_controller', 'myController' ); } function myController (Request $request) { $id = $request->attributes->get('id'); // do your stuff } router($request); $controller = $request->attributes->get('_controller'); $controller($request);
  45. $expiresAt = new \DateTime('+1hour'); $expiresAt->setTimezone( new \DateTimeZone('UTC') ); $expires =

    $expresAt->format('D, d M Y H:i:s'); $expires.= ' GMT'; header('X-Event: SymfonyCon'); header('Cache-Control: public'); header('Expires: '.$expires); echo 'Hello!';
  46. use Symfony\Component\HttpFoundation\Response; $response = new Response('Hello!', 200); $response->headers->set('X-Event', 'SymfonyCon'); $response->setPublic();

    $response->setExpires(new \DateTime('+1hour')); $response->send();
  47. // Somewhere in a controller $id = $request->query->get('id'); $date =

    $this->getPageUpdatedAtById($id); $response = new Response(); $response->setPublic(); $response->setLastModified($date); if ($response->isNotModified($request)) { return $response; } // else do heavy processing to render the page
  48. HttpKernel

  49. namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; interface HttpKernelInterface { const

    MASTER_REQUEST = 1; const SUB_REQUEST = 2; /** * @return Response */ public function handle( Request $request, $type = self::MASTER_REQUEST, $catch = true ); }
  50. Request Response /hello/drupalcamp

  51. use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class SimpleKernel implements HttpKernelInterface

    { public function handle(Request $request /*,...*/) { return new Response('Hello Leeds!'); } }
  52. use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); $kernel = new SimpleKernel(); $response

    = $kernel->handle($request); $response->send();
  53. Request Response callable   callable   callable   callable  

    / /hello/{name} /contact /events Router /hello/drupalcamp
  54. Routing

  55. use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; $routes = new RouteCollection(); $routes->add( 'hello',

    new Route( '/hello/{name}', ['controller' => 'HelloController'] ) );
  56. hello: path: '/hello/{name}' defaults: _controller: 'HelloController' requirements: name: 'a-zA-Z' methods:

    ['GET'] OR…
  57. use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; $context = new RequestContext( '/hello/Leeds', 'GET'

    ); $matcher = new UrlMatcher($routes, $context); $parameters = $matcher->match('/hello/Leeds'); var_dump($parameters); // [ // 'controller' => 'HelloController', // 'name' => 'Leeds', // '_route' => 'hello' // ]
  58. use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; $request = Request::createFromGlobals(); $context

    = new RequestContext(); $context->fromRequest($request); $matcher = new UrlMatcher($routes, $context);
  59. EventDispatcher

  60. Handle Request Execute  a   controller   Geoip  lookup  

    Mobile   redirect   Authen<cate   ?   ?   Add     debug  to   response   ?   Match  a   request  
  61. Request Response Router Controller Resolver Event Dispatcher GeoipListener SessionListener LocaleListener

    Firewall kernel.request TemplateListener ControllerListener ParamConverter Listener TemplateListener
  62. Request Response Router Controller Resolver Event Dispatcher GeoipListener SessionListener LocaleListener

    Firewall kernel.request TemplateListener ControllerListener ParamConverter Listener TemplateListener
  63. Subjects Listeners Event Dispatcher Mailer Listener Registration Dispatcher add listener

    registration.success notify registration.success call register
  64. Subjects Listeners Event Dispatcher Mailer Listener Registration Dispatcher add listener

    registration.success notify registration.success call SMS Listener add listener registration.success call register
  65. use Symfony\Component\EventDispatcher\EventDispatcherInterface; class Registration { private $dispatcher; public function __construct(

    EventDispatcherInterface $dispatcher ) { $this->dispatcher = $dispatcher; } // ... }
  66. use Symfony\Component\EventDispatcher\GenericEvent; class Registration { // ... public function register($user)

    { // register the user ... // dispatch an event if registration succeeded $this->dispatcher->dispatch( 'registration.success', new GenericEvent($user) ); } }
  67. class MailerListener { private $mailer; private $twig; public function __construct(

    Mailer $mailer, TwigEngine $twig ) { $this->mailer = $mailer; $this->twig = $twig; } // ... }
  68. use Symfony\Component\EventDispatcher\GenericEvent; class MailerListener { // ... public function sendMail(GenericEvent

    $event) { $user = $event->getSubject(); $message = $this->createMessage($user) $this->mailer->send($message); } private function createMessage($user) { return $this->twig->render(/* */); } }
  69. use Symfony\Component\EventDispatcher\EventDispatcher; $mailerListener = new MailerListener($mailer, $twig); $smsListener = new

    SmsListener($smsGateway); $dispatcher = new EventDispatcher(); $dispatcher->addListener( 'registration.success', array($mailerListener, 'sendMail') ); $dispatcher->addListener( 'registration.success', array($smsListener, 'sendSms') ); $registration = new Registration($dispatcher); $registration->register($user);
  70. Serializer

  71. namespace Symfony\Component\Serializer; interface SerializerInterface { /** * @return string */

    public function serialize( $data, $format, array $context = [] ); /** * @return object */ public function deserialize( $data, $type, $format, array $context ); }
  72. None
  73. class Person { private $age; private $name; public function getName()

    { return $this->name; } public function getAge() { return $this->age; } public function setName($name) { $this->name = $name; } public function setAge($age) { $this->age = $age; } }
  74. $person = new Person(); $person->setAge(18); $person->setName('Kuba');

  75. use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer \GetSetMethodNormalizer; $normalizers = [

    new GetSetMethodNormalizer() ]; $encoders = [ new JsonEncoder(), new XmlEncoder() ]; $serializer = new Serializer($normalizers, $encoders); $serializer->serialize($person, 'json'); // {"name":"foo", "age": 18}
  76. Validator

  77. use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\Email; class User { /** * @Email

    * @NotBlank */ public $email; }
  78. User: properties: name: - Email: ~ - NotBlank: ~ OR…

  79. use Symfony\Component\Validator\Validation; $validator = Validation::createValidatorBuilder() ->enableAnnotationMapping() ->getValidator(); $errors = $validator->validate(new

    User()); if (count($errors) > 0) { var_dump($errors); } Cau<on:  the  annota<on  namespace  needs  to   be  registered  in  the  annota<on  registry.  
  80. var_dump($errors); class Symfony\Component\Validator\ConstraintViolationList#18 (1) { private $violations => array(1) {

    [0] => class Symfony\Component\Validator\ConstraintViolation#31 (8) { private $message => string(31) "This value should not be blank." private $propertyPath => string(5) "email" private $invalidValue => NULL /* ... */ } } }
  81. Yaml

  82. parameters: username: kuba password: 123123 groups: [ 'admin', 'editor' ]

  83. use Symfony\Component\Yaml\Yaml; $data = Yaml::parse('config.yml'); var_dump($data); // [ // 'parameters'

    => [ // 'username' => 'kuba', // 'password' => '123123', // 'groups' => [ 'admin', 'editor' ] // ] // ]
  84. foo: &foo bar: ~ hello: drupal test: <<: *foo bar:

    true obj: !!php/object:O:8:"stdClass":1:{s:5:"title";s: 14:"Foo in the bar";}
  85. ClassLoader

  86. use Symfony\Component\ClassLoader\ClassLoader; $loader = new ClassLoader(); $loader->addPrefix('Acme\\', __DIR__.'/src'); $loader->register(); //

    Acme\Search\Query => src/Acme/Search/Query.php
  87. // Retrieve the Composer autoloader $loader = include __DIR__ .

    '/../vendor/autoload.php'; $loader->unregister(); // Wrap the Composer autoloader into cached autoloader require_once __DIR__.'/../vendor/.../ApcClassLoader.php'; $apcLoader = new ApcClassLoader('drupal.', $loader); $apcLoader->register();
  88. DependencyInjection

  89. class appDevDebugProjectContainer extends Container { protected function getSymfonyConApiService() { $this->services['drupal.api']

    = $instance = new SymfonyConApi(); return $instance; } }
  90. <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/..." ...> <services> <service id="drupal.api" class="Drupal\Api"

    /> </services> </container>
  91. Few ideas for the next steps

  92. { "require": { "monolog/monolog": "1.2.*" } } https://getcomposer.org/

  93. <ul> {% for user in users %} <li>{{ user.username |

    title }}</li> {% else %} <li>no user found</li> {% endfor %} </ul> http://twig.sensiolabs.org/
  94. more ?

  95. { "require": { "symfony/console": "~2.3" }, "autoload": { "psr-4": {

    "DrupalCamp\\": "core/lib/DrupalCamp" } } } composer.json
  96. curl -sS https://getcomposer.org/installer | php composer update symfony/console

  97. <?php # core/lib/DrupalCamp/Command/AddArticleCommand.php namespace DrupalCamp\Command; class AddArticleCommand { }

  98. <?php # core/lib/DrupalCamp/Command/AddArticleCommand.php namespace DrupalCamp\Command; use GuzzleHttp\Client; use Symfony\Component\Console\Command\Command; use

    Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class AddArticleCommand extends Command { protected function configure() { } protected function execute( InputInterface $input, OutputInterface $output ) { } }
  99. protected function configure() { $this->setName('drupal:add-article'); $this->setDescription('Adds an article to Drupal');

    $this->setHelp('Very helpful and moderately long help message.'); $this->addArgument( 'title', InputArgument::REQUIRED, 'The title' ); }
  100. <?php # drush.php require_once __DIR__.'/core/vendor/autoload.php'; $app = new \Symfony\Component\Console\Application(); $app->add(new

    \DrupalCamp\Command\AddArticleCommand()); $app->run();
  101. None
  102. None
  103. protected function execute( InputInterface $input, OutputInterface $output ) { $title

    = $input->getArgument('title'); $body = $this->getBody($title); $response = $this->post($body); $message = sprintf( '<info>%d %s</info> %s', $response->getStatusCode(), $response->getReasonPhrase(), $response->getHeader('Location') ); $output->writeln($message); }
  104. private function getBody($title) { return [ 'title' => [['value' =>

    $title]], 'body' => [ [ 'format' => 'restricted_html', 'summary' => '', 'value' => '<p>hello there!</p>' ] ], '_links' => [ 'type' => [ 'href' => 'http://localhost:8000/rest/type/node/article' ] ] ]; }
  105. private function post($body) { $client = new Client(); $client->setDefaultOption( 'headers',

    [ 'Accept' => 'application/hal+json', 'Content-Type' => 'application/hal+json' ] ); return $client->post( 'http://localhost:8000/entity/node', ['body' => json_encode($body)] ); }
  106. None
  107. None
  108. None
  109. Thank you http://bit.ly/sflive2014

  110. Credits •  https://www.flickr.com/photos/bfs_man/4939624151 •  https://www.flickr.com/photos/marine_corps/6771070707/in/photostream/ •  https://www.flickr.com/photos/katerha/5746905652/in/photostream/ •  https://www.flickr.com/photos/40855483@N00/13247934334 • 

    https://www.flickr.com/photos/eschipul/683341572 •  https://www.flickr.com/photos/harusday/2971387785 •  https://www.flickr.com/photos/kaptainkobold/3203311346 •  http://www.sxc.hu/photo/338038 •  http://www.sxc.hu/photo/1223174 •  http://www.freeimages.com/photo/308460 •  http://www.sxc.hu/photo/771223 •  https://www.flickr.com/photos/so_wrong_its_kelly/4380499403/in/ photolist-7F6cRM-9gU8zv-8QNo5g-6FA4u-TMjzu-4FxwKz-7jeqDa- HHoYw-5YAqDi-9m5quF-6AZ7DQ-4oqxmn-9TLn7Y-4ovcMN-4ouUou-9THiy2-4or7dt-4oqqje- 4oraCR-4ouCKo-4ovgv3-4oqYKp-4ouZa9-4ouJF1-4oquKK-4ouwgY-4ouFj5-4ouWB7-4ouQSQ- 4or3Nz-4ouMEd-8vcTct-7B9zWL-HSGyD-dxiE3c-2bfNXS-4jH2nf-7V8NfZ-ab7nsC-8ngLev- ab79CA-zwpJh-7V5twX-9TLm6G-9TLjZd-ebUsRF-9THuXc-9TLnKA-9GF6oi-ab4ogP