Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sulu - Leverage the power of a framework based CMS

Sulu - Leverage the power of a framework based CMS

Batteries included! Thanks to Symfony.

Thomas Schedler

September 07, 2017
Tweet

More Decks by Thomas Schedler

Other Decks in Programming

Transcript

  1. I installed this free plugin and now the whole website

    crashed! It can´t take that long to add this simple feature! Why is our website so slow?! Can we add a new language to the site by tomorrow? Sulu CMS Batteries included! Thanks to Symfony.
  2. I'm Thomas Schedler @chirimoya | https://github.com/chirimoya ... head of development

    and technical consultant. Young father trying to master Heston Blumenthal recipes.
  3. / /services /blog Request Response Kernel Controller Router Request URI

    Controller & Action Front Controller indexAction() servicesAction() blogAction()
  4. / /services /blog Request Response Kernel Controller Router Request URI

    Controller & Action Front Controller indexAction() servicesAction() blogAction() Model View Services
  5. / /services /blog Request Response Kernel Controller Router Request URI

    Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services
  6. Router / /services /blog Request Response Kernel Controller Request URI

    Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services
  7. Router / /services /blog Request Response Kernel Controller Request URI

    Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services
  8. Router Chain Router / /services /blog Request Response Kernel Controller

    Request URI Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services
  9. Router Chain Router / /services /blog Request Response Kernel Controller

    Request URI Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services
  10. Router Chain Router / /services /blog Request Response Kernel Controller

    Request URI Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services Dynamic Router
  11. Sulu routing summarized – CMF ChainRouter replaces the default routing

    system – and works by accepting a set of prioritized Routers
  12. Sulu routing summarized – CMF ChainRouter replaces the default routing

    system – and works by accepting a set of prioritized Routers – The Symfony default Router is registered with the highest priority
  13. Sulu routing summarized – CMF ChainRouter replaces the default routing

    system – and works by accepting a set of prioritized Routers – The Symfony default Router is registered with the highest priority – DynamicRouters handle all the dynamically defined routes (pages, redirects, …)
  14. // app/WebsiteKernel.php
 
 class WebsiteKernel extends AbstractKernel
 {
 /**
 *

    {@inheritdoc}
 */
 protected $name = 'website';
 
 /**
 * @param string $environment
 * @param bool $debug
 */
 public function __construct($environment, $debug)
 {
 parent::__construct($environment, $debug);
 $this->setContext(self::CONTEXT_WEBSITE);
 }
 
 /**
 * {@inheritdoc}
 */
 public function registerBundles()
 {
 $bundles = parent::registerBundles();
 $bundles[] = new Symfony\Cmf\Bundle\RoutingBundle\CmfRoutingBundle();
 return $bundles;
 }
 }
  15. // app/WebsiteKernel.php
 
 class WebsiteKernel extends AbstractKernel
 {
 /**
 *

    {@inheritdoc}
 */
 protected $name = 'website';
 
 /**
 * @param string $environment
 * @param bool $debug
 */
 public function __construct($environment, $debug)
 {
 parent::__construct($environment, $debug);
 $this->setContext(self::CONTEXT_WEBSITE);
 }
 
 /**
 * {@inheritdoc}
 */
 public function registerBundles()
 {
 $bundles = parent::registerBundles();
 $bundles[] = new Symfony\Cmf\Bundle\RoutingBundle\CmfRoutingBundle();
 return $bundles;
 }
 } $bundles[] = new AppBundle\AppBundle();
  16. // app/config/website/routing.yml app:
 resource: "@AppBundle/Controller/"
 type: annotation
 prefix: /app //

    src/AppBundle/Controller/DefaultController.php namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class DefaultController extends Controller { /** * @Route("/") */ public function indexAction() { return $this->render('AppBundle:Default:index.html.twig'); } }
  17. // app/config/website/routing.yml app:
 resource: "@AppBundle/Controller/"
 type: annotation
 prefix: /app //

    src/AppBundle/Controller/DefaultController.php namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; class DefaultController extends Controller { /** * @Route("/") */ public function indexAction() { return $this->render('AppBundle:Default:index.html.twig'); } } // src/AppBundle/Resources/views/Default/index.html.twig Hallo World!
  18. Controller Chain Router Router / /services /blog Request Response Kernel

    Request URI Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services Dynamic Router
  19. Chain Router Router / /services /blog Request Response Kernel Request

    URI Controller & Action Front Controller indexAction() servicesAction() blogAction() Response Response Response Model View Services Dynamic Router DefaultController
  20. // app/Resources/templates/pages/default.xml <?xml version="1.0" ?>
 <template xmlns="http://schemas.sulu.io/template/template"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://schemas.sulu.io/template/template http://

    schemas.sulu.io/template/template-1.0.xsd">
 
 <key>default</key>
 
 
 <cacheLifetime>2400</cacheLifetime>
 
 <meta>
 <title lang="en">Default</title>
 <title lang="de">Standard</title>
 </meta>
 
 ... </template>
 <controller>SuluWebsiteBundle:Default:index</controller> <view>templates/default</view>
  21. // app/Resources/templates/pages/default.xml <?xml version="1.0" ?>
 <template xmlns="http://schemas.sulu.io/template/template"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://schemas.sulu.io/template/template http://

    schemas.sulu.io/template/template-1.0.xsd">
 
 <key>default</key>
 
 
 <cacheLifetime>2400</cacheLifetime>
 
 <meta>
 <title lang="en">Default</title>
 <title lang="de">Standard</title>
 </meta>
 
 ... </template>
 <controller>AppBundle:Custom:index</controller> <view>templates/default</view>
  22. // app/Resources/templates/pages/default.xml <?xml version="1.0" ?>
 <template xmlns="http://schemas.sulu.io/template/template"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://schemas.sulu.io/template/template http://

    schemas.sulu.io/template/template-1.0.xsd">
 
 <key>default</key>
 
 
 <cacheLifetime>2400</cacheLifetime>
 
 <meta>
 <title lang="en">Default</title>
 <title lang="de">Standard</title>
 </meta>
 
 ... </template>
 <view>AppBundle:Custom:index</view> <controller>AppBundle:Custom:index</controller>
  23. // src/AppBundle/Controller/CustomController.php
 
 namespace AppBundle\Controller;
 
 use Sulu\Bundle\WebsiteBundle\Controller\WebsiteController;
 use Sulu\Component\Content\Compat\StructureInterface;


    
 class CustomController extends WebsiteController
 {
 /**
 * My custom controller action.
 *
 * @param StructureInterface $structure
 * @param bool $preview
 * @param bool $partial
 *
 * @return Response
 */
 public function indexAction(StructureInterface $structure, $preview = false, $partial = false)
 {
 $response = $this->renderStructure(
 $structure,
 [
 // here you can add some custom data for your template
 'myData' => $this->get('my_custom_service')->getMyData(),
 ],
 $preview,
 $partial
 );
 
 return $response;
 }
 }
  24. // app/Resources/templates/pages/default.xml <?xml version="1.0" ?>
 <template xmlns="http://schemas.sulu.io/template/template"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">


    
 <key>default</key>
 
 <view>AppBundle:Custom:index</view>
 <controller>AppBundle:Custom:index</controller>
 <cacheLifetime>2400</cacheLifetime>
 
 ... </template> // src/AppBundle/Resources/views/Custom/index.html.twig {% extends "master.html.twig" %} {% block content %} <h1 property="title">{{ content.title }}</h1> <div property="article"> {{ content.article|raw }} </div> {% endblock %} .html.twig
  25. // app/Resources/templates/pages/default.xml <?xml version="1.0" ?>
 <template xmlns="http://schemas.sulu.io/template/template"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://schemas.sulu.io/template/template http://schemas.sulu.io/template/template-1.0.xsd">


    
 <key>default</key>
 
 <view>AppBundle:Custom:index</view>
 <controller>AppBundle:Custom:index</controller>
 <cacheLifetime>2400</cacheLifetime>
 
 ... </template> // src/AppBundle/Resources/views/Custom/index.html.twig {% extends "master.html.twig" %} {% block content %} <h1 property="title">{{ content.title }}</h1> <div property="article"> {{ content.article|raw }} </div> {% endblock %} // src/AppBundle/Resources/views/Custom/index.json.twig {{ content|json_encode|raw }} .html.twig
  26. Reverse Proxy Caches – A HTTP Cache is a full

    page cache – It bypasses your application entirely, if the cache entry is valid
  27. Reverse Proxy Caches – A HTTP Cache is a full

    page cache – It bypasses your application entirely, if the cache entry is valid – HTTP cache headers are used to mark a response cacheable and for how long
  28. Reverse Proxy Caches – A HTTP Cache is a full

    page cache – It bypasses your application entirely, if the cache entry is valid – HTTP cache headers are used to mark a response cacheable and for how long – Symfony comes with a reverse proxy written in PHP
  29. Reverse Proxy Caches – A HTTP Cache is a full

    page cache – It bypasses your application entirely, if the cache entry is valid – HTTP cache headers are used to mark a response cacheable and for how long – Symfony comes with a reverse proxy written in PHP – Switch to something more robust like Varnish without any problem
  30. Reverse Proxy Caches – A HTTP Cache is a full

    page cache – It bypasses your application entirely, if the cache entry is valid – HTTP cache headers are used to mark a response cacheable and for how long – Symfony comes with a reverse proxy written in PHP – Switch to something more robust like Varnish without any problem https://tomayko.com/blog/2008/things-caches-do
  31. Reverse Proxy Caches – A HTTP Cache is a full

    page cache – It bypasses your application entirely, if the cache entry is valid – HTTP cache headers are used to mark a response cacheable and for how long – Symfony comes with a reverse proxy written in PHP – Switch to something more robust like Varnish without any problem https://tomayko.com/blog/2008/things-caches-do
  32. ESI - Edge Side Includes – The ESI specification describes

    tags to communicate with the gateway cache
  33. ESI - Edge Side Includes – The ESI specification describes

    tags to communicate with the gateway cache – In Symfony the <esi:include/> is implemented
  34. ESI - Edge Side Includes – The ESI specification describes

    tags to communicate with the gateway cache – In Symfony the <esi:include/> is implemented – If the response contains ESI tags, the cache either requests the page fragment from the backend or embeds the fresh cache entry
  35. // app/config/config.yml framework:
 ... esi: { enabled: true } //

    app/Resources/views/Default/index.html.twig {# you can use a controller reference #} {{ render_esi(controller('AppBundle:News:latest', { 'limit': 5 })) }} {# ... or a URL #} {{ render_esi(url('latest_news', { 'limit': 5 })) }}
  36. Customizing Models – Doctrine doesn't support model customization – Inheritance

    leads to multiple tables for the same data structure – Sulu’s PersistenceBundle allows to replace models via configuration
  37. Customizing Models – Doctrine doesn't support model customization – Inheritance

    leads to multiple tables for the same data structure – Sulu’s PersistenceBundle allows to replace models via configuration – Inspired by Sylius ResourceBundle
  38. // src/AppBundle/EntityTag.php <?php namespace AppBundle\Entity; use Sulu\Bundle\TagBundle\Entity\Tag as SuluTag; class

    Tag extends SuluTag { public $description; } // src/AppBundle/Resources/config/doctrine/Tag.orm.xml <?xml version="1.0" encoding="utf-8"?> <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas orm/doctrine-mapping.xsd"> <entity name="AppBundle\Entity\Tag" table="ta_tags"> <field name="description" type="text" column="description" nullable="true"/> </entity> </doctrine-mapping>
  39. // src/AppBundle/EntityTag.php <?php namespace AppBundle\Entity; use Sulu\Bundle\TagBundle\Entity\Tag as SuluTag; class

    Tag extends SuluTag { public $description; } // src/AppBundle/Resources/config/doctrine/Tag.orm.xml <?xml version="1.0" encoding="utf-8"?> <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas orm/doctrine-mapping.xsd"> <entity name="AppBundle\Entity\Tag" table="ta_tags"> <field name="description" type="text" column="description" nullable="true"/> </entity> </doctrine-mapping> // app/config/config.yml sulu_tag: objects: tag: model: AppBundle\Entity\Tag
  40. Symfony Events Kernel Events – kernel.request – kernel.response – kernel.controller

    – kernel.view – kernel.terminate … Doctrine Events Lifecycle Events – [pre|post]Remove – [pre|post]Persist – [pre|post]Update – [pre|on|post]Flush – onClear …
  41. Sulu Document Manager Events +----------------------+ | Events | +----------------------+ |

    persist | | hydrate | | remove | | refresh | | copy | | move | | create | | clear | | find | | reorder | | publish | | unpublish | | remove_draft | | flush | | query.create | | query.create_builder | | query.execute | | configure_options | | metadata_load | | restore | +----------------------+ bin/adminconsole sulu:document:subscriber:debug
  42. // src/AppBundle/Document/Subscriber/MailSubscriber.php
 
 namespace AppBundle\Document\Subscriber;
 
 use Sulu\Component\DocumentManager\Event\PublishEvent;
 use Sulu\Component\DocumentManager\Events;


    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 class MailSubscriber implements EventSubscriberInterface
 {
 ...
 
 /**
 * {@inheritdoc}
 */
 public static function getSubscribedEvents()
 {
 return [
 Events::PUBLISH => ['sendNotification', -1000],
 ];
 }
 
 public function sendNotification(PublishEvent $event)
 {
 $message = new \Swift_Message('Page Published', 'URL: ' . $event->getDocument()->getResourceSegment());
 
 $this->mailer->send($message);
 }
 }
  43. // src/AppBundle/Document/Subscriber/MailSubscriber.php
 
 namespace AppBundle\Document\Subscriber;
 
 use Sulu\Component\DocumentManager\Event\PublishEvent;
 use Sulu\Component\DocumentManager\Events;


    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 class MailSubscriber implements EventSubscriberInterface
 {
 ...
 
 /**
 * {@inheritdoc}
 */
 public static function getSubscribedEvents()
 {
 return [
 Events::PUBLISH => ['sendNotification', -1000],
 ];
 }
 
 public function sendNotification(PublishEvent $event)
 {
 $message = new \Swift_Message('Page Published', 'URL: ' . $event->getDocument()->getResourceSegment());
 
 $this->mailer->send($message);
 }
 } <service id="app.document_subscriber.email" class="AppBundle\Document\Subscriber\MailSubscriber"> <argument type="service" id="mailer" /> <tag name="sulu_document_manager.event_subscriber"/> </service>
  44. Service Container – Foundation for extensibility & customizability – Sulu

    heavily uses service definitions – Add new functionality (Modulnavigation, Content-Type, ...)
  45. Service Container – Foundation for extensibility & customizability – Sulu

    heavily uses service definitions – Add new functionality (Modulnavigation, Content-Type, ...) – Extend existing (Smart-Content, Teaser, ...)
  46. Service Container – Foundation for extensibility & customizability – Sulu

    heavily uses service definitions – Add new functionality (Modulnavigation, Content-Type, ...) – Extend existing (Smart-Content, Teaser, ...) – Overwrite services
  47. // src/AppBundle/Admin/AppAdmin.php 
 namespace AppBundle\Admin;
 
 use Sulu\Bundle\AdminBundle\Admin\Admin;
 use Sulu\Bundle\AdminBundle\Navigation\Navigation;


    use Sulu\Bundle\AdminBundle\Navigation\NavigationItem;
 
 class AppAdmin extends Admin
 {
 public function __construct($title)
 {
 $rootNavigationItem = new NavigationItem($title);
 $section = new NavigationItem('navigation.modules');
 
 $myModule = new NavigationItem('app.my_module');
 $myModule->setIcon('custom');
 
 $item = new NavigationItem('app.my_module.title');
 $item->setAction('my_module/custom');
 $myModule->addChild($item);
 
 $rootNavigationItem->addChild($section);
 $section->addChild($myModule);
 
 $this->setNavigation(new Navigation($rootNavigationItem));
 }
 }
  48. // src/AppBundle/Admin/AppAdmin.php 
 namespace AppBundle\Admin;
 
 use Sulu\Bundle\AdminBundle\Admin\Admin;
 use Sulu\Bundle\AdminBundle\Navigation\Navigation;


    use Sulu\Bundle\AdminBundle\Navigation\NavigationItem;
 
 class AppAdmin extends Admin
 {
 public function __construct($title)
 {
 $rootNavigationItem = new NavigationItem($title);
 $section = new NavigationItem('navigation.modules');
 
 $myModule = new NavigationItem('app.my_module');
 $myModule->setIcon('custom');
 
 $item = new NavigationItem('app.my_module.title');
 $item->setAction('my_module/custom');
 $myModule->addChild($item);
 
 $rootNavigationItem->addChild($section);
 $section->addChild($myModule);
 
 $this->setNavigation(new Navigation($rootNavigationItem));
 }
 } <service id="app.admin" class="AppBundle\Admin\AppAdmin"> <argument>%sulu_admin.name%</argument> <tag name="sulu.admin"/> <tag name="sulu.context" context="admin"/> </service>
  49. // src/AppBundle/DependencyInjection/AppCompilerPass.php
 
 namespace AppBundle\DependencyInjection;
 
 use AppBundle\Contact\CustomContactManager;
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;


    use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Reference;
 
 class AppCompilerPass implements CompilerPassInterface
 {
 public function process(ContainerBuilder $container)
 {
 $definition = $container->getDefinition('sulu_contact.contact_manager');
 $definition->setClass(CustomContactManager::class);
 $definition->addArgument(new Reference('app.my_custom_service'));
 }
 }
  50. // src/AppBundle/DependencyInjection/AppCompilerPass.php
 
 namespace AppBundle\DependencyInjection;
 
 use AppBundle\Contact\CustomContactManager;
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;


    use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Reference;
 
 class AppCompilerPass implements CompilerPassInterface
 {
 public function process(ContainerBuilder $container)
 {
 $definition = $container->getDefinition('sulu_contact.contact_manager');
 $definition->setClass(CustomContactManager::class);
 $definition->addArgument(new Reference('app.my_custom_service'));
 }
 } // src/AppBundle/AppBundle.php namespace AppBundle; use AppBundle\DependencyInjection\AppCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class AppBundle extends Bundle { public function build(ContainerBuilder $container) { $container->addCompilerPass(new AppCompilerPass()); } }