Teil 3: Das Plug-in-System und eigene Erweiterungen

Das Plug-in-System von Shopware 6
Keine Kommentare

In den vorherigen zwei Artikeln haben wir uns allgemein um Shopware 6 und um das neue API gekümmert. Im dritten und letzten Teil dieser Serie werden wir uns nun dem neuen Plug-in-System widmen.

Wir werden ein kleines Plug-in schreiben, das uns in einem eigenen Controller die letzten PHP-Magazin-Hefte mit Cover und Kauflink ausgibt. Dafür benötigen wir über das grundsätzliche Plug-in hinaus eine eigene Entity, eine Migration und einen eigenen Controller mit Theme.

Artikelserie

Einem erfahrenen Shopware-Entwickler werden in Shopware 6 viele Konzepte und Strukturen bekannt vorkommen – auch wenn sich z. B. die Plug-in-Struktur deutlich verändert hat. Wir werden nun eine Demoumgebung aufsetzen, uns die neue Plug-in-Struktur anschauen und die composer.json erforschen. Danach werden wir eine eigene Datenbanktabelle via Migration anlegen, eine eigene Entity über den Data Abstraction Layer (DAL) erstellen und schlussendlich alles in einem Controller integrieren und anwenden.

Wer aus den vorherigen Artikeln noch eine funktionierende Demoumgebung hat, kann den folgenden Abschnitt getrost überspringen.

Demoumgebung aufsetzen

Wir verwenden die Docker-Umgebung, die auf einem Linux-Client ausgeführt wird. Zuerst klonen wir das Entwicklungstemplate von GitHub mit git clone git@github.com:shopware/development.git. Nun haben wir die Entwicklungsvorlage für Shopware 6 im Verzeichnis development. Anschließend gehen wir mit cd development in das Verzeichnis und klonen noch das eigentliche Shopware-Plattform-Repository mit git clone git@github.com:shopware/platform.git in das Standardverzeichnis. Achtung: Bitte kein anderes Verzeichnis beim Klonen angeben, da das wichtig für das Autoloading ist.

Damit haben wir jeglichen Quellcode, den wir zum Starten auf unserem Rechner benötigen. Um nun die notwendigen Docker-Container zu bauen und zu starten, geben wir ./psh.phar docker:start ein. Mit diesem Befehl werden alle Docker-Container gebaut und gestartet. Anschließend verbinden wir uns mit dem Application Container via ./psh.phar docker:ssh und starten die Installation mit ./psh.phar install.

Das kann beim ersten Mal einige Zeit in Anspruch nehmen, da bei der initialen Ausführung einige Caches erstellt werden müssen. Um zu prüfen, ob die Installation erfolgreich war, könnt ihr einfach euren Lieblingsbrowser öffnen und auf http://localhost:8000 zugreifen.

International PHP Conference

How Much Framework?

by Arne Blankerts (thePHP.cc)

Building a Cloud-Friendly Application

by Larry Garfield (Platform.sh)

Crafting Maintainable Laravel Applications

by Jason McCreary (Pure Concepts, LLC)

JavaScript Days 2020

Wie ich eine React-Web-App baue

mit Elmar Burke (Blendle) und Hans-Christian Otto (Suora)

Architektur mit JavaScript

mit Golo Roden (the native web)


Wenn ihr z. B. Mac-Anwender seid, könnt ihr das Ganze auch lokal aufsetzen. Eine beispielhafte VirtualHost-Konfiguration könnt ihr im Installationsguide unter „Setting up your webserver“ finden. Anschließend müsst ihr nur bin/setup ausführen und werdet dann durch einen interaktiven Installationsprozess geführt. Wenn etwas während der Installation nicht funktioniert hat, prüft ihr, ob es die .psh.yaml.override gibt, wenn nicht, startet ihr das Set-up-Skript mit ./psh.phar install erneut.

Plug-in Basic

Fangen wir nun mit dem Erstellen des Beispiel-Plug-ins an. Eine beispielhafte Struktur findet ihr in Listing 1. Wir wechseln zuerst in das Verzeichnis shopware-root/custom/plugins und erstellen das Verzeichnis PHPMagazin. Das Verzeichnis muss nach dem technischen Plug-in-Namen benannt werden. Daher verwenden wir hier PHPMagazin als Verzeichnisnamen. Jedes Plug-in basiert auf einer composer.json die den Namen, die Version, die Requirements, und viele weitere Metainformationen enthält. Die Entwickler, die mit Composer vertraut sind, werden hier auf viel Bekanntes stoßen. Jedes Plug-in, das wir entwickeln, kann, wie jedes andere Package, automatisch via composer require hinzugefügt werden. Die Anwendung von jeglichen Composer-Schema-Elementen ist ausdrücklich erwünscht.

Wir erstellen nun die composer.json im Pluginroot-Verzeichnis. Einen beispielhaften Inhalt kann man sich in Listing 2 anschauen. Jedes Composer Package benötigt einen technischen Namen als eindeutiges Identifizierungsmerkmal. Das Naming-Schema ist identisch mit dem von Composer empfohlenen. Der Name soll aus einem Vendor- und Projektnamen bestehen, die durch ein / getrennt sind, z. B. „name“: „swag/php-magazin“.

Des Weiteren kann der Vendor-Name auch gleichzeitig der Vendor-Präfix sein, in diesem Beispiel verwenden wir als Prefix swag. Sollte der Projektname aus mehreren Wörtern bestehen, sollten diese mit einem – konkateniert werden, das wird oft auch als Kebab-Case-Schreibweise beschrieben. Was benötigen wir noch? Eine Beschreibung („description“: „PHPMagazin Example„), eine Versionsnummer des Plug-ins („version“: „v1.0.0„), die verwendete Lizenz, unter der das Plug-in veröffentlicht wird („license“: „MIT„), und den Autor des Plug-ins. Zusätzlich müssen wir noch das Element type wie folgt definieren: „type“: „shopware-platform-plugin„. Wenn das fehlt, ist das Plug-in nicht gültig und seine Installation nicht möglich.

Im nächsten Schritt definieren wir die Autoloader-Eigenschaft, die genauso funktioniert, wie es bei Composer in der Dokumentation zu finden ist. Kurz gesagt: Man definiert damit den Speicherort und den verwendeten Namespace der jeweiligen PHP-Klassen, die das Plug-in benutzt. Diese Freiheit erlaubt es, dem Entwickler sein Plug-in so zu strukturieren, wie er es möchte. Feste Vorgaben, wie der Namespace und die Struktur der Verzeichnisse auszusehen haben, sind damit vorbei.

Da jedes Plug-in prinzipiell auch ein Composer Package ist, habe ich in dem Beispiel den Quellcode unter dem Verzeichnis src abgelegt, wie das auch beim Quellcode von Shopware 6 der Fall ist. Man kann das Plug-in zwar beliebig strukturieren, wir empfehlen jedoch, es auf diese Weise zu tun und so nah wie möglich am De-facto-Standard von Composer Packages zu bleiben.

Zu guter Letzt müssen wir noch die extra-Eigenschaft in die composer.json einfügen. Man kann dort alle anderen Informationen ablegen, die sonst keinen Platz hätten. Shopware 6 verwendet diese Eigenschaft, um ein paar weitere Metainformationen über das Plug-in zu erhalten wie z. B. Copyright, Label und Plug-in-Icon. Der wichtigste Punkt ist aber shopware-plugin-class, wo der Fully Qualified Class Name (FQCN) der Plug-in-Basis-Klasse hinterlegt ist. Diese Angaben sind für Shopware 6 notwendig, da sich der jeweilige Plug-in-Entwickler an keine Vorgaben mehr halten muss. Somit weiß Shopware gar nicht, wo sich die Plug-in-Basis-Klasse befindet.

Damit hätten wir den Bereich Metainformationen für ein Shopware-6-Plug-in ausführlich abgehandelt. Nun müssen wir für ein funktionierendes Plug-in noch dessen Basis-Klasse erstellen. Diese müssen wir, wie in composer.json bereits angegeben, unter plugin-root/src/ mit dem Dateinamen PHPMagazin.php erstellen. Der Inhalt der Basis-Klasse ist in Listing 3 zu sehen.

Dem erfahrenen Shopware-Entwickler wird das sehr bekannt vorkommen. Bis auf die nun erweiterte Klasse Shopware\Core\Framework\Plugin ist alles identisch zu Shopware 5 geblieben.

Nun wollen wir mal schauen, ob unser frisch entwickeltes Plug-in auch installiert werden kann. Dafür gehen wir in den shopware-root-Ordner und navigieren dort ins bin-Verzeichnis. Mit dem Befehl ./console plugin:refresh führen wir eine Aktualisierung aller vorhandenen und installierbaren Plug-ins durch. Danach sollte mit ./console plugin:install –activate –clearCache PHPMagazin unser Plug-in installiert, aktiviert und anschließend der Shopcache geleert werden. Wenn wir keine Fehlermeldung erhalten, haben wir gerade erfolgreich unser erstes Shopware-6-Plug-in entwickelt und installiert. Vorerst natürlich ohne jegliche Funktionalität.

PHPMagazin
> src
--> Controller
--> Entity
--> Migration
--> Resources
----> config
----> views
--> PHPMagazinExample.php
> composer.json
{
  "name": "swag/php-magazin",
  "description": "PHPMagazin Example",
  "version": "v1.0.0",
  "license": "MIT",
  "authors": [
    {
      "name": "Thomas Eiling"
    }
  ],
  "require": {
    "shopware/core": "*",
    "shopware/storefront": "*"
  },
  "type": "shopware-platform-plugin",
  "autoload": {
    "psr-4": {
      "Swag\\PHPMagazin\\": "src/"
    }
  },
  "extra": {
    "shopware-plugin-class": "Swag\\PHPMagazin\\PHPMagazin",
    "copyright": "(C) Thomas Eiling",
    "label": {
      "de-DE": "Beispiel fürs PHP-Magazin",
      "en-GB": "Example for PHP-Magazin"
    }
  }
}
<?php declare(strict_types=1);

namespace Swag\PHPMagazin;

use Shopware\Core\Framework\Plugin;

class PHPMagazin extends Plugin
{
}

Migration

Damit wir im späteren Schritt auch eine eigene Entity erstellen können, benötigen wir natürlich eine Datenbanktabelle. Mit Shopware 6 wurde für Plug-ins eine eigene Möglichkeit geschaffen, Migrationen zu erstellen und diese im Update- und Installationsprozess automatisiert ausführen zu lassen. Das ermöglicht es uns, eine Migration, wie sie in Listing 4 zu sehen ist, einfach zu erstellen. Hierbei ist es wichtig, dass der Zeitstempel aus getCreationTimestamp() identisch ist mit dem aus dem Klassennamen der Migration. In der Methode update() dürfen nur keine destruktiven Änderungen umgesetzt werden, um auch für Plug-ins ohne viel Aufwand ein einfaches Blue-Green Deployment zu ermöglichen.

Alle destruktiven Änderungen finden einen Platz in der updateDestructive()-Methode. Damit wir später diese Datenbanktabelle auch über das DAL verwenden können, müssen zusätzlich zu der benötigten title-, cover– und buy_link-Spalte auch ein id-, created_at– und updated_at-Feld erstellt werden. Diese Felder wird das DAL später automatisch mit Leben füllen.

<?php declare(strict_types=1);

namespace Swag\PHPMagazin\Migration;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1567588155Magazin extends MigrationStep
{
  public function getCreationTimestamp(): int
  {
    return 1567588155;
  }

  public function update(Connection $connection): void
  {
    $connection->executeQuery(
      'CREATE TABLE IF NOT EXISTS `php_magazin`
      (
        id BINARY(16) NOT NULL,
        title VARCHAR(100) NULL,
        cover VARCHAR(255) NULL,
        buy_link VARCHAR(255) NULL,
        `created_at` DATETIME(3) NOT NULL,
        `updated_at` DATETIME(3) NULL,
         PRIMARY KEY (`id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;   
      '
    );
  }

  public function updateDestructive(Connection $connection): void
  {
  }
}

Entity

Nachdem wir nun erfolgreich eine eigene Datenbanktabelle für unser Plug-in erstellt haben, müssen wir nun dem DAL von Shopware 6 mitteilen, dass diese Tabelle existiert. Das erfolgt durch eine sogenannte EntityDefinition. In unserem Beispiel-Plug-in legen wir sie unter src/Entity mit dem Namen PHPMagazinEntity.php an. Unsere eigene Definition muss von Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition ableiten, das uns zwingt, die Methoden getEntityName() und defineFields() zu implementieren.

Die Methode getEntityName() gibt einen String zurück, der identisch zu unseren Tabellennamen ist, in diesem Beispiel php_magazin. Die andere Methode defineFields() enthält alle Felder, die unsere eigene Datenbanktabelle beinhaltet. Wir haben eine id-, title-, cover– und buy_link-Spalte. Die beiden Spalten created_at und updated_at benötigen keine Definition, da sie vom DAL automatisch eingefügt werden. In unserem Beispiel sind alle Spalten Stringwerte. Welche Möglichkeiten es noch für die Definition der Spalten gibt, kann auf der Shopware-Homepage nachgeschlagen werden. Die StringField-Klasse benötigt für die Instanziierung zwei Parameter, einen storage-Namen, der für die Spalte auf Datenbankebene steht, und einen property-Namen, der nachher in der Entity zugewiesen wird. Zusätzlich können wir an einem StringField auch Flags hinzufügen, wie z. B. PrimaryKey oder Required. Welche Flags es sonst noch gibt, findest du auf der Shopware-Website.

Nun haben wir eine gültige Definition (Listing 5) erstellt, aber uns fehlt immer noch die eigentliche Entity. Wir erstellen unter src/Entity die Datei PHPMagazinEntity.php und fügen den Inhalt aus Listing 6 ein. Achtung: Aus Platzgründen sind die beiden Properties $cover und $buyLink inklusive ihrer Getter und Setter nicht abgedruckt. Bitte erstelle sie selbstständig. Die Entity-Klasse sollte nun alle Felder aus der Definition bis auf die ID inklusive Getter und Setter enthalten. Die Implementierung des id-Feldes wird in unserem Beispiel durch den EntityIdTrait umgesetzt.

Zu guter Letzt müssen wir noch die Definition in den Dependency Injection Container registrieren. Die services.xml (Listing 7) muss unter Shopware 6 relativ zu unserer Plugin-Basis-Klasse unter Resources/config liegen und nicht wie früher unter Resources. Wichtig dabei ist, dass unsere Definition den Tag shopware.entity.definition inklusive des entity-Attributs erhält. Das entity-Attribut muss identisch mit dem Namen aus PHPMagazinDefinition sein, in unserem Falle also php_magazin. Und das war es dann auch schon; nun ist unsere eigene Entity vollständig in Shopware 6 registriert und eine Administration über die API von nun an möglich.

<?php declare(strict_types=1);

namespace Swag\PHPMagazin\Entity;

use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;

class PHPMagazinDefinition extends EntityDefinition
{
  public const ENTITY_NAME = 'php_magazin';

  public function getEntityName(): string
  {
    return self::ENTITY_NAME;
  }

  public function getEntityClass(): string
  {
    return PHPMagazinEntity::class;
  }

  protected function defineFields(): FieldCollection
  {
    return new FieldCollection(
      [
        (new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey()),
        (new StringField('title', 'title'))->addFlags(new Required()), new StringField('cover', 'cover'),
        (new StringField('buy_link', 'buyLink'))->addFlags(new Required()),
      ]
    );
  }
}
<?php declare(strict_types=1);

namespace Swag\PHPMagazin\Entity;

use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityIdTrait;

class PHPMagazinEntity extends Entity
{
  use EntityIdTrait;

  protected $title;

  ...

  public function getTitle(): string
  {
    return $this->title;
  }

  public function setTitle(string $title): void
  {
    $this->title = $title;
  }
  ...
}
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

  <services>
    <service id="Swag\PHPMagazin\Entity\PHPMagazinDefinition">
      <tag name="shopware.entity.definition" entity="php_magazin" />
    </service>
  </services>
</container>

Controller

Wie bauen wir uns nun einen eigenen Controller. Zuerst erstellen wir PHPMagazinController.php in dem Verzeichnis src/Controller. Die darin zu erstellende Klasse PHPMagazinController erweitert Shopware\Storefront\Controller\StorefrontController. Im Vergleich zu Shopware 5 sind das keine Zend Framework Controller mehr, sondern Standard Symfony Controller. Wie in Listing 8 zu sehen ist, können über Annotations @Route(„/phpMagazin“) die jeweilige Routen definiert werden. Damit diese Routen auch im Shopware Context geladen werden, benötigen wir unter Resources/config noch eine routes.xml (Listing 9). In dieser XML-Datei legen wir fest, in welchen Verzeichnissen Shopware nach Route Annotations suchen soll, um diese dann anzuwenden. In unserem Beispiel ist der Controller nun unter shop.tld/phpMagazin aufrufbar.

Dadurch, dass wir unsere Tabelle durch eine Definition im Shopware Context bekannt gemacht haben, erstellt der DAL automatisch ein Repository mit der ID php_magazin.repository. Dieses Repository verwenden wir in der index()-Methode, um uns alle verfügbaren Datensätze aus der Datenbanktabelle php_magazin ausgeben zu lassen. Zum Schluss müssen wir nur noch ein Template für die Ausgabe definieren und die vorher aus der Datenbank geholten Einträge dem Template übergeben. Die Datei index.html.twig wird automatisch in dem Plug-in-Verzeichnis Resources/views gesucht. Bei der Entwicklung von größeren Plug-ins sollte man hier natürlich Unterverzeichnisse verwenden, um die Übersicht zu wahren.

Ein ganz triviales Beispiel für index.html.twig, die alle Einträge aus der Datenbank ausgibt, ist in Listing 10 zu sehen. Das ganze Beispiel-Plug-in kann man auf GitHub finden.

class PHPMagazinController extends StorefrontController
{
  /** @Route("/phpMagazin") */
  public function index(): Response
  {
    $PHPMagazins = $this->get('php_magazin.repository')->search(
      (new Criteria())->addSorting((new FieldSorting('title', 'ASC'))),
      Context::createDefaultContext()
    );

    return $this->renderStorefront('index.html.twig', ['PHPMagazins' => $PHPMagazins]);
  }
}
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
        https://symfony.com/schema/routing/routing-1.0.xsd">

  <import resource="../../**/Controller/*Controller.php" type="annotation" />
</routes>
<h1>PHP-Magazin</h1>
{% for magazine in PHPMagazins %}
Titel: {{ magazine.getTitle() }}<br />
Cover: {{ magazine.getCover() }}<br />
BuyLink: {{ magazine.getBuyLink() }}<br />
{% endfor %}

Fazit

Die weitere Konzentration auf allgemeine Standards und die Annäherung an Symfony im Plug-in-System machen den Einstieg in Shopware 6 auch für unerfahrene Entwickler zu einem leichten Unterfangen. Zusätzlich bietet das neue Plug-in-System in Shopware 6 dem Entwickler viel mehr Freiheiten, als das noch unter Shopware 5 der Fall war.

Nichtsdestotrotz muss der Entwickler mit den neu erworbenen Freiheiten auch umzugehen wissen. Nur weil man nun alle Dateien in einem Verzeichnis ablegen kann, heißt das noch lange nicht, dass man es auch tun sollte. Eine Gliederung der jeweiligen Klassen in vernünftige Module und Domänen obliegt nun ganz allein dem Entwickler.

PHP Magazin

Entwickler MagazinDieser Artikel ist im PHP Magazin erschienen. Das PHP Magazin deckt ein breites Spektrum an Themen ab, die für die erfolgreiche Webentwicklung unerlässlich sind.

Natürlich können Sie das PHP Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Kiosk ist das PHP Magazin weiterhin im Print-Abonnement erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -