Teil 2: Routen, Navigation und Seiten

Symfony CMF: Routing aus dem Werkzeugkasten
Keine Kommentare

Der Verweis auf eine andere Seite gehört zu den Grundpfeilern des Internets. Einen Link auf einer Seite kann ich mühelos kopieren und weiterleiten. Es ist ja nur ein Stück Text. In diesem Artikel werden wir sehen, dass man solche Verweise gar abdrucken kann. Mit diesem kleinen Element wird das WWW zu dem Medium, wie wir es kennen. Es verbindet Webseiten miteinander und ermöglicht einen grenzenlosen Austausch.

Mir fehlt das Talent eines Poeten, die Einleitung hätte ein Lobgesang an das Internet werden können. Denn ohne den Verweis und dessen Einfachheit wäre das Internet nie zu dem geworden, was es heute ist. Doch wen interessiert es? Meine technische Sichtweise auf die Welt beeinflusst die Struktur des URL einer Seite immens, doch wie sieht es beim Nutzer aus? Der Browser hat dafür extra ein Eingabefeld eingerichtet, doch es wird für viel mehr als nur der Eingabe eines Suchbegriffs verwendet.

Artikelserie

Im ersten Teil dieser Reihe haben wir uns einfache Textblöcke angeschaut. Solange sie in ihrer Verschachtelung keine Tiefe besitzen, könnte man diese Blöcke auch ohne CMS-Unterstützung ablegen, verwalten und ausspielen. Doch mit Anforderungen wie der Übersetzbarkeit, einer Verschachtelung der Blöcke und einem einheitlichen Weg der Ausspielung und Verwaltung bin ich als Entwickler froh über Tools, die mich hier bei der Struktur unterstützen. Genau über diesen Weg sind wir zum Symfony CMF gelangt – jenem Content Management Framework, das uns auch in diesem Teil der Artikelreihe mit Funktionen versorgen wird.

Am Anfang war der URL

Vielleicht ein wenig pathetisch, doch die Beweggründe hinter der Einleitung sollen die Relevanz des URLs hervorheben. Für den Nutzer verliert der URL mehr und mehr an Bedeutung. Im Browser kann man kaum noch zwischen der Funktion eines Suchfelds und der Navigationsleiste unterscheiden. Ich habe sogar Nutzer kennen lernen dürfen, deren Navigationsleiste gänzlich ausgeblendet war. Das Anwählen einer Seite geschieht nur noch über Lesezeichen oder die Eingabe eines Suchbegriffs, da die bevorzugte Suchmaschine als Startseite eingerichtet ist. Dazu kommt, dass in neuen Applikationen ein Schalter Hurra die Seite erscheint im Dark Theme nicht mehr von einem Verweis unterscheidbar ist. Links werden als Buttons umgestaltet, Buttons leiten den Nutzer weiter auf eine andere Seite.

Doch diese Seiten – hinter jeder versteckt sich im Grunde eine eigene Ressource – brauchen eine eindeutige Adresse. Wenn wir diese Adresse in eine Aktion unserer Webseite umwandeln wollen, ist das der Punkt, an dem für mich das Routing anfängt. Auch wenn es immer mehr an Relevanz verliert, kann ich z. B. den Adressen meiner Seiten folgendermaßen die Struktur eines URLs geben:

  • Als Homepage: http://meine-seite.de/ oder http://meine-seite.de/de
  • Als wichtige Seiten in der ersten Ebene: http://meine-seite.de/eine-wichtige-seite oder http://meine-seite.de/die-firma
  • Zusammenfassen aller Services unter „/services“: http://meine-seite.de/services/lesen, http://meine-seite.de/services/schreiben oder http://meine-seite.de/services/schlafen

Diese Struktur kann man sich vorstellen wie Ordner auf einem PC. Der Inhalt der Seite kann ein einzelnes Dokument sein, das in einem Ordner liegt. Die Struktur der Inhaltsablage muss aber nicht zwingend mit der Struktur der URLs übereinstimmen. Natürlich ist es für den Content-Manager am einfachsten, wenn er eine parallele Struktur hat und im Idealfall vom Routing gar nichts bemerkt, doch allein der Fall, dass ich eine Seite mehrfach verlinken muss (Canonical Link nicht vergessen), hebt diese Vereinfachung schon wieder auf. Das heißt aber: Wir haben zwei Bäume – also Strukturen – die für die CMS-Funktionen benötigt werden.

Auch hier kann man sich wieder die Adaption mit den Ordnern vorstellen. Nur sind diese in diesem Fall wie Ordner, in denen ich Dokument verteile. In dem einen sortiere ich den Inhalt, im zweiten die Routen. Die Verknüpfung wäre dann wie ein Symlink. Und um diese Form der Verknüpfung geht es beim Routing. Ich habe den URL https://meine-seite.de/services/schlafen in eine Seite umgewandelt, deren Inhalt ausgespielt werden soll. Das Ausspielen dieser Seite übernimmt in einer Webapplikation der Controller. Es ist dann egal, ob sich hinter dem URL ein komplexer Teil einer Applikation verbirgt oder einfach nur das Ausspielen eines Titels mit Inhalt in Textform. Der URL wird im Grunde in einen Controller samt Action umgewandelt.

Das S in SOLID

Gemäß dem Single Responsibility Principle geht man in Symfony-Applikationen inzwischen zu sogenannten Action Controllern über. Das heißt, ein Controller als Objekt, der dann selbst Infokanal ist, hat nur noch eine Action. Das heißt, er ist nur noch für genau eine Sache zuständig.

Im Kontext einer Symfony-Applikation ist dieses Routing statisch gelöst. Auch wenn wir hier inzwischen mithilfe von Annotationen das Routing regeln können, möchte ich für mein Beispiel trotzdem eine YAML-Datei verwenden (es ginge auch XML), denn es macht diesen Prozess des Routings offensichtlicher:

# config/routes.yaml
service_list:
  path:     /services
  controller: App\Controller\ServiceController::list

Diese Definition dient nicht nur dem Auflösen eines URL (hier beispielsweise https://meine-seite.de/services), sondern auch für den Rückweg. Wir können dann in unserem Template oder dem Controller den Router nach dem URL fragen und nutzen dazu nur noch den Identifier service_list. Das hat den Vorteil, dass die URL-Segmente an (fast) einer Stelle hinterlegt sind und nicht im Code verteilt liegen. Natürlich kann man diese Routen auch parametrisiert definieren:

# config/routes.yaml
service_show:
  path:     /services/{slug}
  controller: App\Controller\ServiceController::show

Der slug in der Definition des Pfads dient der Identifizierung des Service, von dem wir die Informationen darstellen wollen. Das heißt, mit den Beispielen aus dem obigen Block wäre slug = schreiben|lesen|schlafen. Damit haben wir eine eindeutige Zuordnung und dem Ausspielen der jeweiligen Service-Seite steht nichts mehr im Weg.

Dynamisches Routing

Wenn man Routen aus der Applikation heraus erstellen, ändern oder wieder löschen muss, so spricht man von einem dynamischen Routing. Eigentlich genügt eine einfache Zuordnung zwischen der URL und dem Controller samt Action, also wie z. B. in der Tabelle 1 zu sehen.

path controller
/services/schlafen ServiceController::schlafen
/services/schreiben ServiceController::schreiben
/services/lesen ServiceController::lesen

Tabelle 1: Beispiel einer Zuordnung beim dynamischen Routing

Mit einem Index auf dem Pfad wäre es im Grunde ein indexierter Lookup, der die Performance nur geringfügig beeinflussen sollte. Bei der Performance hingegen liegt der Vorteil des statischen Routings von Symfony: Sämtliche Konfiguration wird im Livebetrieb in PHP-Code umgewandelt. Das heißt, der Lookup für das statische Routing wäre nur:

  public function getController(string $path): string
{
    if (\array_key_exists($path, $this->routes)) {
      return $this->routes[$path];
    }
}

Das kann man mit einer Datenbanklösung natürlich nicht schlagen, doch wenn man hier auf eine saubere Implementierung achtet und natürlich mit Indexen arbeitet, kommt man doch sehr nah heran.

Die anfänglichen Beispiele sind noch relativ einfach, doch Content und dessen Routing können ungemein komplex werden.

Zum Routing in Symfony gehört aber nicht nur das Zuweisen des Controllers. Wir haben in Symfony zu jeder Route einen ganzen Sack (Parameter-Bag) an Informationen, die sowohl durch den Router als auch durch Listener im Symfony-Event-System angereichert werden. Im einfachsten Fall handelt es sich nur um Parameter aus dem URL. In komplexeren Fällen schaffen es kleine Helferlein, uns für einen gegebenen Parameter sofort das zugehörige Objekt aus Datenbankinformationen bereitzustellen. Und dabei meine ich nicht das Objekt, das zur Route gehört, sondern beispielsweise das Content-Objekt, das uns die Informationen zur Service-Seite liefert.

International PHP Conference

Migrating to PHP 7

by Stefan Priebsch (thePHP.cc)

A practical introduction to Kubernetes

by Robert Lemke (Flownative GmbH)


Im obigen Beispiel für das statische Routing einer Service-Seite war ein solcher Parameter der slug. Das heißt hinter dem URL https://meine-seite.de/services/schlafen gehört der Inhalt zur Seite mit dem slug „schlafen“. In einem einfachen Beispiel könnte solch ein Content-Objekt die beiden Eigenschaften title und body haben. Für den Service, der sich hinter „schlafen“ verbirgt, würde ich beispielsweise folgendes Objekt erwarten:

$content = new Content();
$content->setTitle('Schlafen ist gesund');
$content->setBody('<p>Schlafen am Morgen, Schlafen am Abend, wir schlafen den ganzen Tag</p>');

Wenn ich auf allen Services-Seiten solch ein Content Document erwarten kann, das title und body als Eigenschaften aufweist, muss ich auch nicht jedem dieser Dokumente ein Controller-Action-Pärchen anhängen, denn im Grunde habe ich hier eine Form von Typisierung. Im Symfony-Routing-Beispiel werden wir ihm mit einem Pattern /services/{slug} gerecht. Doch wenn ich nun auch noch von meinem Router verlangen würde: „gib mir einmal die Route für das Dokument zur Content-Seite ,schlafen‘, um es irgendwo in der Applikation zu verlinken“, wäre eine Lösung, die Content und Route in der Datenbank verknüpft, sehr hilfreich. Wenn dann der Content auch noch übersetzt in verschiedenen Sprachen zur Verfügung steht, würde diese Frage in den Router noch einmal komplizierter – wenn man es sauber lösen möchte.

Routing und Content aus dem Werkzeugkasten

Die anfänglichen Beispiele sind noch relativ einfach, doch Content und dessen Routing kann ungemein komplex werden. In dem Fall zahlt es sich aus, gleich zu Beginn auf ein Framework zurückzugreifen. Als Maintainer geht es mir natürlich um dasselbe Framework, das ich im ersten Teil bereits vorgestellt habe – das Symfony Content Management Framework, kurz Symfony CMF. Wenn nur das Anlegen von dynamischen Routen in einer Symfony-Applikation ansteht, kommt man am RoutingBundle des CMF nicht vorbei.

Aus dem ersten Teil sollte die Demoapplikation mit den nötigen Symfony-CMF-Paketen noch vorhanden sein. Ansonsten können Sie aus Listing 1die Installation in Kurzform entnehmen.

composer create-project symfony/skeleton application
cd application
composer req webserver orm ormfixtures
composer req symfony-cmf/cmf-phpcr-dbal-pack
docker run -d \
--network host \
--env MYSQL_ALLOW_EMPTY_PASSWORD=yes \
--env MYSQL_DATABASE=cmf \
--env MYSQL_ROOT_PASSWORD=root \
--env MYSQL_USER="cmf" \
--env MYSQL_PASSWORD="cmf" \
--volume data_mag_mysql:/var/lib/mysql \
mysql:5.7 .

bin/console doctrine:phpcr:init:dbal --drop --force --env=dev
bin/console doctrine:phpcr:workspace:create prb --env=dev
bin/console doctrine:phpcr:repository:init --env=dev

In dem Rezept symfony-cmf/cmf-phpcr-dbal-pack sind sowohl das Routing als auch das ContentBundle enthalten. Die nötige Konfiguration ist bereits angelegt. Wir können also direkt anfangen zu arbeiten. Um nun direkt mit dem Routing zu beginnen, hilft es, einen Blick in die Konfiguration zu wagen:

# config/packages/cmf_phpcr_dbal.yaml
cmf_routing:
  chain:
    routers_by_id:
      cmf_routing.dynamic_router: 20
      router.default: 100

Hier sehen wir nun schon den Kern des CmfRoutingBundle. Das Routing des CMF ersetzt nicht nur einfach den Router vom Symfony. Es führt eine sogenannte Router Chain ein. In dieser Liste von Routern können nun die verschiedenen Router mit einer Priorisierung eingetragen werden. Das Eintragen wird mithilfe ihrer Service-ID vorgenommen. Damit sind nun ein dynamischer und der statische Router gemeinsam in der Applikation verankert.

Zuerst wird der dynamische gefragt und dann der ursprüngliche. Das Routing über eine Datenbank passiert, wie anfänglich beschrieben, über einen Pfad, der indexiert in der Datenbank hinterlegt ist. Dieser Pfad entspricht nun auch wieder einer Baumstruktur, nämlich genau einem Baum, in dem die Routing-Dokumente im PHPCR abgelegt sind. Das heißt, wenn ich beispielsweise meine Routen allesamt unter /cms/routes/ ablege, würde im Content Repository unter /cms/routes/about nach dem Routing-Dokument zur Seite https://meine-seite.de/about gesucht werden.

$session = $manager->getPhpcrSession();
NodeHelper::createPath($session, “/cms/routes/”);
$parent = $manager->find(null, $basepath);

$aboutRoute = new Route();
$aboutRoute>setPosition($parent, “/about”);
$aboutRoute->setDefaults([“_controller” => “App\Controller\BaseController::about”]);
$manager->persist($aboutRoute);

Kann ich dann unter https://meine-seite.de/about die Seite schon erreichen? Fast! Doch zuerst zum Pfad: Der Teil, der für die Strukturierung im PHPCR-Baum benötigt wird, ist hier einfach weggeschnitten. Was übrig bleibt, entspricht genau dem Pfad im Request. Wie man dem Beispiel entnehmen kann, können wir hier ein Array von Defaults setzen. Das entspricht genau dem Pendant im statischen Routing. Das ist auch der einfachste Fall für ein Routing – also genau die Zuordnung von URL und Controller Action. Das Ganze ist nun in der Datenbank abgelegt. Was wir hier gewinnen, ist der leichte Einbau in eine Symfony-Applikation und das strukturierte Halten von Routen als Dokumente in einem Baum.

Das C in CMS steht für Inhalt

Mit dem vorherigen Beispiel haben wir für unsere CMS-Funktionen noch wenig gewonnen, da wir immer noch keinen Inhalt in unserer Applikation haben. Doch mit dem CmfContentBundle des Symfony CMF wird hier einiges an Arbeit abgenommen. Wenn ich hier von Inhalt spreche, meine ich im Gegensatz zum ersten Teil nun Aspekte einer ganzen Seite. Das heißt, wir könnten damit beispielsweise einen Artikel oder einen ganzen Blogbeitrag abbilden. Dazu müssen wir ähnlich wie wir es bei einer Entität im ORM tun, die Struktur des Dokuments deklarieren, das unseren Inhalt beschreibt. Dazu habe ich in Listing 2 eine Klasse samt Annotationen vorbereitet, die wiederum ein Basisdokument für Inhalte aus dem CmfContentBundle erweitert.

<?php

namespace App\Document;

use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;
use Symfony\Cmf\Bundle\ContentBundle\Doctrine\Phpcr\StaticContent;
use Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr\SeoMetadata;
use Symfony\Cmf\Bundle\SeoBundle\SeoAwareInterface;

/**
* @PHPCRODM\Document(referenceable=true, translator="child")
*/
class DemoSeoContent extends StaticContent implements SeoAwareInterface
{
  /**
    * @var SeoMetadata
    *
    * @PHPCRODM\Child
    */
  protected $seoMetadata;

  public function __construct()
  {
    $this->seoMetadata = new SeoMetadata();
    parent::__construct();
  }

  /**
    * {@inheritdoc}
    */
  public function getSeoMetadata()
  {
    return $this->seoMetadata;
  }

  /**
    * {@inheritdoc}
    */
  public function setSeoMetadata($seoMetadata)
  {
    $this->seoMetadata = $seoMetadata;
  }

  /**
   * @return string
   */
  public function __toString(): string
  {
    return $this->id;
  }
}

Die Basisklasse StaticContent enthält schon alles, was wir für den Anfang brauchen. Das heißt, wir müssen uns hier jetzt nicht darum kümmern, welche Properties für die Persistenz im PHPCR nötig sind. Wir wollen für das Beispiel eine weitere Eigenschaft teaser hinzufügen. Die Eigenschaften wie body und title sind bereits in der Basisklasse enthalten. Auch die Referenz zum Routing ist mit drin. Das heißt, wir können diesem Content-Dokument bereits eine Route beim Anlegen zuweisen (Listing 3).

use Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route;
use Symfony\Cmf\Bundle\ContentBundle\Doctrine\Phpcr\StaticContent;

$pageParent = $manager->find(null, '/cms/content');
$routeParent = $manager->find(null, '/cms/routes/services');

$page = new StaticContent();
$page->setName('my-name');
$page->setTitle('Schlafen ist gut für die Umwelt');
$page->setBody('Langer Text über das Schlafen');
$page->setParentDocument($pageParent);
$manager->persist($page);

$route = new  Route();
$route->setParentDocument($routeParent);
$route->setName('schlafen');
$route->setContent($page);
$manager->persist($route);

$manager->flush();

Wie auch die Route, benötigt der Content eine Position im Baum. Darum wird auch dem Content-Dokument ein Elternelement zugewiesen. Damit erstellen wir eine Route, damit das Content-Dokument unter https://meine-seite.de/services/schlafen zu erreichen ist. Den Artikel selbst finden wir im PHPCR unter /cms/content/schlafen. Die Referenzen zwischen der Route und dem Artikel sind in den Basisklasse unseres Artikels zu finden. Wir können es uns wie ein Many-to-one Relation im ORM vorstellen. Ähnlich wie im ORM gibt es auch hier ein Mapping.

Nun haben wir die Route gespeichert. Der Router würde sie finden, wenn er die oben genannte Seite aufruft, doch wie kommt der Inhalt nun in den Browser? Einen Controller haben wir in diesem Fall nicht definiert, und das müssen wir auch nicht für jeden Inhalt. Wir nutzen dazu ein weiteres Feature des CmfRoutingBundle. Die sogenannten Enhancer sind in der Lage, anhand der Klasse oder eines Typs auf Controller und/oder auf Templates zu verweisen.

Wie die Route, benötigt der Content eine Position im Baum. Darum wird auch dem Contentdokument ein Elternelement zugewiesen.

Das geschieht nicht als zusätzliche Information beim Speichern der Route. Die Enhancer wirken im Event Flow von Symfony kurz bevor der Controller wirklich benötigt wird. Das heißt, hier werden aufgrund einer Konfiguration dem Request-Objekt die nötigen Informationen wie bspw. _controller oder _template als Parameter untergeschoben. Ich habe oben schon von dem Parameter-Bag gesprochen, das ein Symfony Request mit sich herumträgt. Genau hier landen die Informationen. Für das Auflösen des Controllers im Symfony-Kontext fühlt es sich nun genauso an, als ob wir eine statische Route definiert hätten. Symfony findet den Parameter _controller und ruft ihn auf. Wie sieht diese Konfiguration aus? Schauen wir uns dazu Listing 4 an.

cmf_routing:
  chain:
# … same we had above
  dynamic:
    route_collection_limit: 10
    controllers_by_class:
      App\Document\RedirectRoute: cmf_routing.redirect_controller:redirectAction
    templates_by_class:
      App\Document\Report: pages/report.html.twig
      App\Document\News: page/news.html.twig

Hier kann man nun erkennen, dass ich mit der Klasse eine Art Typisierung für den Inhalt vornehmen kann. Entweder weise ich, unterschieden nach der Klasse, einen Controller samt Action zu. Dazu nutze ich eine Liste unter controllers_by_class. Mit templates_by_class kann ich dieselbe Zuordnung für die Klasse und ein Template machen. Ein Template, das ich auf diese Art und Weise zuordnen kann, kann so wie in Listing 5 aussehen.

{# translations/pages/new #}
{% include base.html.twig %}

{% block content %}
<h>{{ cmfMainContent.title }}</h>
<p>{{ cmfMainContent.body|raw }}</p>
{% endblock %}s.html.twig #}

Die Applikation läuft trotzdem nicht ohne einen Controller. Ein ContentController aus dem CmfContentBundle sorgt dafür, dass unser Dokument unter der Variable cmfMainContent zur Verfügung gestellt wird. Sowohl der Name für diese Variable als auch dieser DefaultController können konfiguriert werden. Das Ganze kann man jetzt noch schön über einen eigenen Admin editierbar machen und fertig wäre eine Grundform eines CMS.

Inhalt in verschiedenen Sprachen

Wichtig in der globalisierten Welt sollte es sein, den Inhalt übersetzt in anderen Sprachen anbieten zu können. Schauen wir uns dazu Listing 6 an.

doctrine_phpcr:
  # enable the ODM layer
  odm:
    auto_mapping: true
    auto_generate_proxy_classes: true
    locales:
      en: ['de', 'fr']
      de: ['en', 'fr']
      fr: ['en', 'de']

Mit dieser Konfiguration erhalten wir zusätzlich die Möglichkeit, eine Fallback-Lösung zu nutzen. Findet PHPCR-ODM keinen englischen Inhalt, schaut es nach dem deutschen. Bevor wir uns an das Beispiel für das Speicher eines übersetzen Inhalts wagen, werfen wir einen Blick auf das Mapping in Listing 2. Hier haben wir bereits den Artikel durch den Eintrag referenceable=true referenzierbar gemacht. Das ist sowohl für die Referenz beim Routing als auch für die Übersetzbarkeit essenziell.

Dazu finden wir in der Annotation zur Klasse auch die Eigenschaft translator=“child“. Neben translator=“attribute“ ist es eine von zwei Möglichkeiten einzustellen, wie die Übersetzung im PHPCR verankert wird. Eine Erläuterung würde sehr tief in die Eigenschaften des Content Repositorys einsteigen. Kurz gesagt, kann man es sich wie in einem XML vorstellen: Entweder leben die Übersetzungen als einzelne Kindsknoten unter einem Knoten, der den Inhalt repräsentiert. Oder die Übersetzungen werden als Attribute mit der Sprache als Präfix vor dem Attribute-Namen gehalten. Doch nun wollen wir endlich speichern.

In Listing 3 sehen wir zum einen das Mapping in Annotation-Form. Selbstverständlich stehen hier auch YAML und XML zur Verfügung. Damit die neue Inhaltsklasse nun übersetzbar gespeichert werden kann, benötigt sie den Parameter referenceable, auf true gesetzt. Dazu bestimmen wir über das translator-Attribut, wie die Übersetzung abgelegt wird. Die Erklärung dafür kann in der Dokumentation nachgelesen werden, sie würde den Rahmen dieses Artikels sprengen. Nun sind wir in der Lage, den Code aus Listing 3 mit nur wenigen Zeilen multilingual werden zu lassen (Listing 7).

$pageParent = $manager->find(null, '/cms/content');
$deRouteParent = $manager->find(null, '/cms/routes/de/services');
$enRouteParent = $manager->find(null, '/cms/routes/en/services');

$page = new StaticContent();
$page->setName('my-name');
$page->setParentDocument($pageParent);
$manager->persist($page);

$page->setTitle('Schlafen ist gut für die Umwelt');
$page->setBody('Langer Text über das Schlafen');
$manager->bindTranslation($page, 'de');

$route = new  Route();
$route->setParentDocument($deRouteParent);
$route->setName('schlafen');
$route->setContent($page);
$manager->persist($route);

$page->setTitle('Sleep, Sleep, sleep');
$page->setBody('Do a long sleep');
$manager->bindTranslation($page, 'de');

$route = new  Route();
$route->setParentDocument($enRouteParent);
$route->setName('sleep');
$route->setContent($page);
$manager->persist($route);

$manager->flush();

Der Inhalt wird nun quasi ohne die übersetzbaren Eigenschaften gespeichert. Danach wird, wie der Name der Funktion schon sagt, eine Übersetzung an das Dokument gebunden. Das Dokument bleibt dasselbe, doch im Grunde wird jede Übersetzung mit einer eigenen Route versehen. Im Baum existiert das Dokument zu dem Inhalt nun in zwei Repräsentationen. Als Resultat erhalten wir zwei URLs https://meine-seite.de/de/services/schlafen und https://meine-seite.de/en/services/sleep. Auf dem ersten URL sucht uns der Router nun den deutschen Inhalt heraus, auf dem zweiten dann den englischen Inhalt.

Wie man sieht, habe ich eine Unterteilung des Pfads in /de und /en vorgenommen. Damit haben wir eine saubere Struktur. Doch solange jede Übersetzung unterschiedlich ist, würde man den Sprachpräfix nicht brauchen. Allein die Seite /services kann da aber schon zu Problemen führen, da wir im Deutschen inzwischen das Wort Service als Dienstleistung verwenden. Hier wäre also die Unterscheidung in https://meine-seite.de/services und https://meine-seite.de/dienstleistungen. Wie man sieht, geht es ohne die komplette Trennung der Routing-Bäume nach Sprache. Doch damit ist mehr Aufwand verbunden, und das Google-Ranking interessiert die Sprache im URL auch nicht mehr. Wichtig ist, dass es eindeutig bleibt.

Nicht einmal aufgeteilt auf zwei Teile, wie in dieser Artikelserie, ist es möglich, alle Facetten und Features des Symfony CMF zu beschreiben. Im Grunde wird auch kaum eine Applikation alles benötigen. Doch aus dem Werkzeugkasten des CMF gibt es noch einiges mehr, wie z. B. das CmfRoutingAutoBundle.

CmfRoutingAutoBundle

Wenn es irgendwann nervig werden sollte, zu jedem Content-Dokument eine Route anzulegen und die Route sich dann doch durch eine Art Pattern umsetzen lässt, kann das CmfRoutingAutoBundle helfen. Getrennt nach Klassen, kann man Regeln für eine zu erstellende Route angeben. Selbst ein Update lässt sich hiermit automatisieren. Als Pattern könnte man /services/{title-slug} festlegen, wobei title-slug durch eine Funktion am Dokument bestimmt wird. Man kann sozusagen in der Funktion, unterstützt durch eine weitere Utils-Funktion, einen browserfreundlichen String generieren. Aus einem Titel im Content-Dokument „Wir können auch Service“ würde dann eine Route zum URL /services/wir-koennen-auch-service automatisch angelegt werden.

CmfSeoBundle

Es scheint immer, als sei SEO die Aufgabe von Marketing und SEO-Beratern. Doch damit diese arbeiten können, müssen wir als Entwickler erst unsere Hausaufgaben gemacht haben. Wir sollten einfache Integrationen in den Admin und in die schlussendliche Ausspielung bereitstellen. Wenn ein Marketingmitarbeiter ggf. schon einen Titel pflegt, sollte dieser per Default auch für SEO-Aufgaben bereitstehen. Das Überschreiben dieses Werts muss aber trotzdem möglich sein. Und das sollte sowohl für den Titel in den Metadaten, den Titel in der Twitter-Card als auch den Titel für die OpenGraph-Data funktionieren. Auch die strukturierten Daten, die für die Google-Cards hilfreich sind, möchte man nicht extra pflegen, wenn sich die Werte hier nicht vom eigentlichen Inhalt unterscheiden.

Hier hilft uns das CmfSeoBundle weiter. Viele kleine Helferlein geben uns als Entwickler auf der Basis des SonataSeoBundle alles an die Hand, um die genannten Aufgaben zu erfüllen. Aber nicht nur diese. Es ist möglich, aus dem Navigationsbaum die Sitemap zu generieren, sich canonical (gleicher Inhalt) und alternate (in einer anderen Sprache) Links erzeugen zu lassen. Selbst Hinweise für die weitere Navigation auf Fehlerseiten fallen quasi aus dem Baum. Allein mit dem CmfSeoBundle könnte ich hier einen ganzen Artikel füllen.

SonataAdmin-Integration

In vielen Beispielen sehen wir nur Code. Doch mithilfe des SonataAdminBundle und dessen DoctrinePHPCR-Implementierung lassen sich einfache Admin-Oberflächen sehr schön umsetzen. Jegliche CRUD-Operationen mit Referenzen lassen sich hier mit einer Formularlösung implementieren. Für WYSIWYG nutzen wir den CKEditor. Was hier fehlt, ist bisher immer noch ein Editor, in dem ich freihand inline bearbeiten könnte. Doch das ist weder ein Manko von Sonata Project noch vom CMF. Wir vom CMF-Team haben uns bisher nur darauf geeinigt, eben nur ein Framework und nicht ein fertiges Interface anzubieten. Ob dann in der Zukunft, wie für den Tree-Browser, ein eigenes kleines JavaScript-Widget herauskommt, steht noch in den Sternen und hängt auch immer ein wenig von meiner persönlichen Zeit ab.

Damit endet sie nun, unsere Reise durch Funktionen, die uns in einem CMS begegnen können. Es wäre unsinnig, auf dem Rücken des CMF jetzt ein neues Neos oder Pimcore aufzusetzen. Zum einen gibt es hier mit Sulu schon einen Vertreter, der sich richtig gut anfühlt, und zum anderen geht es auch nicht darum, andere Tools zu ersetzen. Es geht darum, dass uns in unserer täglichen Arbeit Aufgaben begegnen, bei denen es weniger Sinn macht, ein ganzes CMS zu installieren. Es können genau die Aufgaben sein, die in den letzten beiden Artikeln erläutert wurden. Um nicht alles mehrfach zu implementieren, bietet das CMF Helfer für diese Aufgaben. Zu alledem steckt hinter dem CMF das PHPCR – ein Baum zum Ablegen von semistrukturierten Daten. Und wir müssen hier nichts Neues mehr lernen. Die Doctrine-Implementierung hilft einem hier sicher, sich wie daheim zu fühlen.

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 Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft 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 -