Heute habe ich mich mit dem Thema dynamische Routen (zum Beispiel aus einer Datenbank) in Symfony 3 beschäftigt. Dabei ist mir aufgefallen, dass derzeit verfügbare Bundles noch nicht mit Symfony 3.0 kompatibel sind – ich musste also selbst ran.
Symfony bietet bereits sehr angenehme Möglichkeiten die eigenen URLs mit Routen zu verwalten – zum Beispiel mit Annotations. Wir wollen hier das Rad nicht neu erfinden, sondern lediglich ein paar dynamische Routen aus der Datenbank hinzufügen.
Für dieses Ziel müssen wir keinen neuen Router implementieren, sondern nur die vorhanden Lademechanismen für Routen erweitern.
Struktur der Routen
Die Struktur der Routen ist denkbar einfach und spiegelt das wider was ein Basis-CMS benötigt:
- URL (z. B. /symfony-3-hinzufuegen-dynamischer-routen)
- Definition von Titel und Inhalt der Zielseite
DynamicRouteLoader – Wie wir die Routen aus der Datenbank laden
Alles was wir benötigen ist ein Loader, der unsere Routen beim Erstellen des Routen-Caches aus der Datenbank holt.
Der Loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
namespace AcmeBundle\Routing use AcmeBundle\Entity\DynamicPage; use Doctrine\ORM\EntityManager; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderResolverInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; class DynamicRouteLoader implements LoaderInterface { /** @var EntityManager */ private $em; /** * DynamicRouteLoader constructor. * * @param EntityManager $em */ public function __construct(EntityManager $em) { $this->em = $em; } /** * Loads a resource. * * @param mixed $resource The resource * @param string|null $type The resource type or null if unknown * * @return RouteCollection * @throws \Exception If something went wrong */ public function load($resource, $type = null) { $collection = new RouteCollection(); /** @var DynamicPage[] $pages */ $pages = $this->em->getRepository(DynamicPage::class)->findAll(); foreach ($pages as $page) { $route = new Route($page->url, ['_controller' => 'AcmeBundle:Dynamic:dynamic', 'templateId' => $page->id]); $collection->add('_dynamic_id_'.$page->id, $route); } return $collection; } /** * Returns whether this class supports the given resource. * * @param mixed $resource A resource * @param string|null $type The resource type or null if unknown * * @return bool True if this class supports the given resource, false otherwise */ public function supports($resource, $type = null) { return ($type == 'dynamic'); } public function getResolver() { } public function setResolver(LoaderResolverInterface $resolver) { } } |
Entscheidend ist, das wir LoaderInterface
implementieren – andernfalls würde Symfony den Loader nicht erkennen. Als Typ definieren wir passenderweise dynamic
.
Innerhalb der foreach
erstellen wir für jeden Eintrag in der Datenbank eine Route. Der erste Parameter für Route()
ist die URL. Im zweiten (defaults
) definieren wir Controller und Action mit dem Platzhalter _controller
. In meinem Fall wird immer die selbe Kombination verwendet. Es steht euch natürlich frei, hier ebenfalls dynamische Werte einzusetzen.
Mit templateId
fügen wir noch eine Verbindung zum Template ein. Nennen wir später im Controller einen Parameter $templateId
, wird er entsprechend gefüllt. Hier könnt ihr beliebig viele weitere Parameter festlegen.
Konfiguration als Service
1 2 3 4 5 6 7 8 |
# services.yml services: acme.router_dynamic: class: AcmeBundle\Routing\DynamicRouteLoader public: false arguments: ["@doctrine.orm.entity_manager"] tags: - { name: routing.loader } |
Obiger Loader muss nun als Service in der entsprechenden Konfigurationsdatei hinterlegt werden. Den Namen könnt ihr frei wählen. Genauso ist es unerheblich ob ihr ihn im user space haben wollt oder nicht (public
true/false
).
Wichtig ist der Tag routing.loader
. Damit sagt ihr Symfony, dass es diesen Service beim Laden der Routen berücksichtigen soll. Er wird zwar immer noch nicht tatsächlich verwendet, aber immerhin ist er jetzt in der Liste.
Als Parameter für den Konstruktor (arguments
) benötigen wir noch den Datenbankzugang in Form des Entity Managers.
Aufruf des Loaders
1 2 3 4 |
# routing.yml acme_dynamic: resource: . type: dynamic |
Die Konfiguration für’s Routing ergänzen wir um obigen Eintrag.
Für resource
definieren wir nur einen Platzhalter. Wir benötigen diesen Parameter nicht, er ist aber eine Pflichtangabe.
Als Typ verwenden wir den vom Loader in der supports()
Methode definierten Typ dynamic
.
Entität zur Definition der dynamischen Routen
Der Vollständigkeit halber noch das Model (mit Doctrine) zur Definition der Routen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
namespace AcmeBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; /** * @ORM\Entity * @ORM\Table(name="dynamic_pages") * @ORM\HasLifecycleCallbacks() * @UniqueEntity(fields={"url"}) */ class DynamicPage { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ public $id; /** @ORM\Column(type="string", length=255) */ public $url; /** @ORM\Column(type="string", length=255) */ public $title; /** @ORM\Column(type="text") */ public $content; /** @ORM\Column(type="datetime") */ public $lastModified; /** * @ORM\PrePersist() * @ORM\PreUpdate() */ public function preUpdate() { $this->lastModified = new \DateTime('now'); } } |
Weiterverarbeitung im Controller
Alles bisher passiert beim Warm-Up der Seite (bzw. Cache). Wir haben lediglich einen Loader, keinen echten Router eingebunden.
Die zur Laufzeit notwendige weitere Verarbeitung übernimmt der zugehörige Controller. Dieser ist bis auf den zusätzlichen Parameter aber unspektakulär.
1 2 3 4 5 6 7 |
class DynamicController extends \Symfony\Bundle\FrameworkBundle\Controller\Controller { public function dynamicAction($templateId) { return []; } } |
tl;dr
- Loader erstellen der
\Symfony\Component\Config\Loader\LoaderInterface
implementiert - Loader als Service mit tag
routing.loader
definieren - Notwendige Infos automatisch an Controller/Action übergeben lassen