Contao 4 Extension Development

#ck2016

Inhalte

  1. Grundlegende Best Practices
  2. Composer
  3. Semantic Versioning
  4. Bundle-Installation
  5. Services
  6. Events & EventListeners
  7. Contao 3.x Erweiterungen unter Contao 4

#me

Contao 4 Extension Bundle Development

Best Practices für Bundles

Warum?

Meistgeladene Contao Extensions (ER2):


  1. BackupDB
  2. dlh_googlemaps
  3. MultiColumnWizard

Die folgenden Gegenüberstellung soll keine Wertung der Erweiterungen sein!

Drei Contao-Erweiterungen, drei verschiedene Ansätze

Mangelnde Konsistenz

... macht es für Entwickler schwieriger, den Code/die Extension zu verstehen.

Bundle Name

Bundle Name - mögliche Varianten

Namespace Bundle Name
Oneup\Bundle\DemoBundle OneupDemoBundle
Oneup\DemoBundle OneupDemoBundle
Contao\Bundle\NewsletterBundle ContaoNewsletterBundle
Contao\NewsletterBundle ContaoNewsletterBundle

Bundle Alias

<vendor>_<name>


Bundle-Name Alias
OneupDemoBundle oneup_demo
ContaoNewsletterBundle contao_newsletter

Verzeichnisstruktur (nach Symfony)

                    <oneup-demo-bundle>
                    ├─ Controller/                      <──┐
                    ├─ DependencyInjection/             <──┼─ PHP-Klassen (PSR-0/PSR-4)
                    ├─ EventListener/                   <──┘
                    ├─ Resources/
                    │   ├─ config                       <──── Bundle-Konfiguration
                    │   ├─ contao                       <──── bisheriger Contao-Code (config, dca, etc.)
                    │   ├─ doc
                    │   │   └─ ...                      <──── Dokumentation (.md/.rst)
                    │   └─ public                       <──── öffentliche Ressourcen (z.B. CSS/JS)
                    ├─ Tests/
                    │   └─ ...                          <──── Unit-Tests für die neuen Klassen
                    ├─ .gitignore
                    ├─ .travis.yml
                    ├─ OneupDemoBundle.php              <──── Neue Bundle-Klasse
                    ├─ LICENSE
                    ├─ README.md
                    ├─ CHANGELOG.md
                    ├─ composer.json
                    └─ phpunit.xml.dist
                
http://symfony.com/doc/current/cookbook/bundles/best_practices.html

Verzeichnisstruktur (nach Contao/Leo Feyer)

                    <oneup-demo-bundle>
                    ├─ src/
                    │   ├─ Controller/                  <──┐
                    │   ├─ DependencyInjection/         <──┼─ PHP-Klassen (PSR-0/PSR-4)
                    │   ├─ EventListener/               <──┘
                    │   └─ Resources/
                    │       ├─ config                   <──── Bundle-Konfiguration
                    │       ├─ contao                   <──── bisheriger Contao-Code (config, dca, etc.)
                    │       ├─ doc
                    │       │   └─ ...                  <──── Dokumentation (.md/.rst)
                    │       └─ public                   <──── öffentliche Ressourcen (z.B. CSS/JS)
                    ├─ tests/
                    │   └─ ...                          <──── Unit-Tests für die neuen Klassen
                    ├─ .gitignore
                    ├─ .travis.yml
                    ├─ OneupDemoBundle.php              <──── Neue Bundle-Klasse
                    ├─ LICENSE
                    ├─ README.md
                    ├─ CHANGELOG.md
                    ├─ composer.json
                    └─ phpunit.xml.dist
                
https://leofeyer.de/files/slides/2015/workshop/#35

someone's gotta decide

Symfony Best Practice ist ohne /src.

Beispiele für Verzeichnisse

Typ Verzeichnis
Commands Command/
Controllers Controller/
Service Container Extensions DependencyInjection/
Event Listeners EventListener/
Model Klassen Model/
Konfiguration Resources/config/
Contao Ressourcen Resources/contao/
Assets (CSS, JS, Bilder) Resources/public/
Übersetzungen (Symfony) Resources/translations/
Übersetzungen (Contao) Resources/contao/languages/
Templates (.twig) Resources/views/
Templates (.html5) Resources/contao/templates/
Unit-Tests Tests/

Neue Bundle-Klasse

                    // OneupDemoBundle.php

namespace Oneup\Bundle\DemoBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class OneupDemoBundle extends Bundle
{

}

                    
                

Dependency Manager für PHP

Composer Metadaten

Key Wert
name oneup/demo-bundle (<vendor>/<name>-bundle)
description Kurze Beschreibung
type contao-bundle
license MIT (auch andere Lizenzen möglich)
require Abhängigkeiten
require-dev Abhängigkeiten (nur dev-Mode)
autoload PSR-4 (Standard)
PSR-0 (auch unterstützt, aber deprecated)
http://www.php-fig.org/psr/psr-0/
http://www.php-fig.org/psr/psr-4/

composer.json (ohne src/-Verzeichnis)

                    {
  "name":"oneup/demo-bundle",
  "description":"This demo bundle does whatsoever.",
  "keywords":["contao", "demo", "whatsoever", "bundle", "module"],
  "type":"contao-bundle",
  "license":"MIT",
  "homepage":"http://1up.io",
  "authors":[
    {
      "name":"David Greminger",
      "email":"dg@1up.io",
      "homepage":"http://1up.io",
      "role":"Developer"
    }
  ],
  "require": {
    "php":">=5.4",
    "contao/core-bundle":"~4.0",
  },
  "require-dev": {
    "phpunit/phpunit": "~3.7"
  },
  "autoload":     {
    "psr-4": { "Oneup\\Bundle\\OneupDemoBundle\\": "" }
  }
}
                

composer.json (mit src/-Verzeichnis)

                    {
  "name":"oneup/demo-bundle",
  "description":"This demo bundle does whatsoever.",
  "keywords":["contao", "demo", "whatsoever", "bundle", "module"],
  "type":"contao-bundle",
  "license":"MIT",
  "homepage":"http://1up.io",
  "authors":[
    {
      "name":"David Greminger",
      "email":"dg@1up.io",
      "homepage":"http://1up.io",
      "role":"Developer"
    }
  ],
  "require": {
    "php":">=5.4",
    "contao/core-bundle":"~4.0",
  },
  "require-dev": {
    "phpunit/phpunit": "~3.7"
  },
  "autoload":     {
    "psr-4": { "Oneup\\Bundle\\OneupDemoBundle\\": "src/" }
  }
}
                

Versionierung

a.k.a Semantic Versioning

MAJOR . MINOR . PATCH (z.B. Contao 4.2.0)

  1. MAJOR bei nicht rückwärts-kompatiblen Änderungen (z.B. an der API)
  2. MINOR bei neuen Funktionen/Features aber rückwärtskompatibel
  3. PATCH bei rückwärts-kompatiblen Bugfixes

Strategie für neue Bundles


http://semver.org/

Bundle installieren

                    $ composer require oneup/demo-bundle
                

Irgendwann wird das auch über den Contao Manager (Tenside & UI) möglich sein :)

Bundle aktivieren

                    // app/AppKernel.php

public function registerBundles()
{
    // Reihenfolge beachten, falls Ressourcen überschrieben werden sollen
    $bundles = [
        // ...
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Contao\CoreBundle\ContaoCoreBundle(),
        new Contao\CalendarBundle\ContaoCalendarBundle(),
        // ...

        new Oneup\Bundle\DemoBundle\OneupDemoBundle(),  // <-- unser neues Bundle
    ];

    if (in_array($this->getEnvironment(), ['dev', 'test'])) {
        $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
        $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
        $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
        $bundles[] = new Contao\InstallationBundle\ContaoInstallationBundle();
    }

    return $bundles;
}
                    
                

Bundle-Konfiguration (optional)

                    // app/config/config.yml

# Oneup DemoBundle configuration
oneup_demo:
  email: dev@1up.io
  debug: "%kernel.debug%"
  attributes:
    secure: true
    hidden: false
                    
                

Neue Möglichkeiten in Contao 4

Services

Was ist ein Service?

Service-Beispiele


oder


Service-Container (dependency injection container, DIC)

Ein PHP-Objekt, welches für die Instanzierung von Objekten bzw. Services zuständig ist.

http://symfony.com/doc/current/components/dependency_injection/introduction.html

Mailer-Service

Beispiel

use Oneup\Bundle\DemoBundle\Mailer;

$mailer = new Mailer('sendmail');
$mailer->send('dev@1up.io', ...);

            

Service Konfiguration

# Resources/config/services.yml
services:
  oneup_demo.mailer:
    class: Oneup\Bundle\DemoBundle\Mailer
    arguments: [sendmail]

                

Module-Code

// Module/DemoModule.php
public function compile()
{
    // ...
    $container = $this->getContainer();

    $mailer = $container->get('oneup_demo.mailer');
    $mailer->send('dev@1up.io', ...);
}

                

Services Zusammenfassung

Definition

#Resources/config/services.yml
services:
  oneup_demo.mailer:
    class: Oneup\Bundle\DemoBundle\Mailer
    arguments: [sendmail]

                    

Service Container holen

/** @var Symfony\Component\DependencyInjection\ContainerInterface $container */
$container = $this->getContainer();
$container = System::getContainer();

                    

Service nutzen

$mailer = $container->get('oneup_demo.mailer');
$mailer->send('dev@1up.io', ...);

                    
Tipp: http://symfony.com/doc/current/book/service_container.html

Welche Services sind verfügbar?

                    $ app/console debug:container
                

                    Symfony Container Public Services
=================================

 --------------------------------- ---------------------------------------------
  Service ID                        Class name
 --------------------------------- ---------------------------------------------
  annotation_reader                 Doctrine\Common\Annotations\CachedReader
  contao.framework                  Contao\CoreBundle\Framework\ContaoFramework
  contao.image.image_sizes          Contao\CoreBundle\Image\ImageSizes
  database_connection               alias for "doctrine.dbal.default_connection"
  event_dispatcher                  alias for "debug.event_dispatcher"
  filesystem                        Symfony\Component\Filesystem\Filesystem
  mailer                            alias for "swiftmailer.mailer.default"
  ...                               ...
                    
                

Service-Details

                    $ app/console debug:container contao.framework
                

                    Information for Service "contao.framework"
==========================================

 ------------------ ---------------------------------------------
  Option             Value
 ------------------ ---------------------------------------------
  Service ID         contao.framework
  Class              Contao\CoreBundle\Framework\ContaoFramework
  Tags               -
  Scope              container
  Public             yes
  Synthetic          no
  Lazy               no
  Synchronized       no
  Abstract           no
  Autowired          no
  Autowiring Types   -
 ------------------ ---------------------------------------------
                    
                

Events & EventListeners

(EventDispatcher)

Event-Klasse

// Event/OrderPlacedEvent.php
namespace Oneup\Bundle\DemoBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use Oneup\Bundle\Entity\Order;

class OrderPlacedEvent extends Event
{
    const NAME = 'order.placed';

    protected $order;

    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    public function getOrder()
    {
        return $this->order;
    }
}

                

Event erzeugen

// Module\DemoModule.php
use Contao\Module;
use Oneup\Bundle\DemoBundle\Entity\Order;
use Oneup\Bundle\DemoBundle\Event\OrderPlacedEvent;

class DemoModule extends Module
{
    public function compile {
    {
        $eventDispatcher = $this->getContainer()->get('event_dispatcher');

        $order = new Order();
        $event = new OrderPlacedEvent($order);

        $dispatcher->dispatch(OrderPlacedEvent::NAME, $event);
    }
}

                

EventListener

EventListener-Klasse


namespace Oneup\Bundle\DemoBundle\EventListener;

use Symfony\Component\EventDispatcher\Event;
use Oneup\Bundle\DemoBundle\Event\OrderPlacedEvent;
use Oneup\Bundle\DemoBundle\Entity\Order;

class OrderListener
{
    // ...

    public function onOrderPlaced(OrderPlacedEvent $event)
    {
        $order = $event->getOrder();

        // ... do something
    }
}

                    

Konfiguration

# Resources/config/listeners.yml
services:
  oneup_demo.order.placed_listener:
    class: Oneup\Bundle\DemoBundle\EventListener\OrderListener
    tags:
      - { name: kernel.event_listener, event: order.placed }

                    

EventListener mit Dependencies


namespace Oneup\Bundle\DemoBundle\EventListener;

use Symfony\Component\EventDispatcher\Event;
use Oneup\Bundle\DemoBundle\Event\OrderPlacedEvent;
use Oneup\Bundle\DemoBundle\Entity\Order;
use Oneup\Bundle\DemoBundle\Mailer;

class OrderListener
{
    protected $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function onOrderPlaced(OrderPlacedEvent $event)
    {
        $order = $event->getOrder();

        // ... do something
    }
}

            

Konfiguration

# Resources/config/listeners.yml
services:
  oneup_demo.order_placed_listener:
    class: Oneup\Bundle\DemoBundle\EventListener\OrderListener
    arguments: ['@oneup_demo.mailer']
    tags:
      - { name: kernel.event_listener, event: order.placed }

                    

Welche Events sind registriert?

                    $ app/console debug:event-dispatcher
                

                    Registered Listeners Grouped by Event
=====================================

"console.command" event
-----------------------

 ------- ------------------------------------------------------------------------------- ----------
  Order   Callable                                                                        Priority
 ------- ------------------------------------------------------------------------------- ----------
  #1      Symfony\Component\HttpKernel\EventListener\DebugHandlersListener::configure()   2048
  #2      Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand()                      255
  #3      Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand()                      255
 ------- ------------------------------------------------------------------------------- ----------

"console.terminate" event
-------------------------

 ------- ----------------------------------------------------------------------------------- ----------
  Order   Callable                                                                            Priority
 ------- ----------------------------------------------------------------------------------- ----------
  #1      Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate()   0
  #2      Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate()                        -255
  #3      Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate()                        -255
 ------- ----------------------------------------------------------------------------------- ----------
                    
            

Event-Details

                    $ app/console debug:event-dispatcher kernel.terminate
                

                    Registered Listeners for "kernel.terminate" Event
=================================================

 ------- ----------------------------------------------------------------------------------- ----------
  Order   Callable                                                                            Priority
 ------- ----------------------------------------------------------------------------------- ----------
  #1      Contao\CoreBundle\EventListener\AddToSearchIndexListener::onKernelTerminate()       0
  #2      Contao\CoreBundle\EventListener\CommandSchedulerListener::onKernelTerminate()       0
  #3      Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate()   0
  #4      Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelTerminate()    -1024
 ------- ----------------------------------------------------------------------------------- ----------
                    
            
Tipp: http://symfony.com/doc/current/cookbook/event_dispatcher/index.html

Contao 3.x Extensions unter Contao 4

Erweiterungen, die noch keine Bundles sind

  1. Dateien nach /system/modules/ kopieren
  2. Symlinks anlegen* mit:
    $ app/console contao:symlinks
  3. Registrierung in der AppKernel.php
* Nur für Erweiterungen mit öffentlichen Unterordnern (z.B. assets/js oder assets/css)

Registrierung im App-Kernel

// app/AppKernel.php
use Contao\CoreBundle\HttpKernel\Bundle\ContaoModuleBundle;

public function registerBundles()
{
    $bundles = [
        new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
        new Contao\CoreBundle\ContaoCoreBundle(),

        // Register old contao extension
        new ContaoModuleBundle('dlh_googlemaps', $this->getRootDir()),
    ];

    // ...

    return $bundles;
}


                

Refactoring

Beispiel-Bundle

https://github.com/1up-lab/contao-security-checker-bundle
https://packagist.org/packages/oneup/contao-security-checker-bundle
https://github.com/contao/core-bundle/pull/488
https://github.com/contao/core-bundle/pull/449

Weiterführende Literatur zu Symfony

Vielen Dank für die Aufmerksamkeit!




Happy coding!