Bereits im Juli 2013 hatten wir an dieser Stelle eine kurze Einführung in die Entwicklung mit der Oro Platform gegeben. Mittlerweile ist die Entwicklung von OroCRM und der OroPlatform stetig weiter fortgeschritten. Anfang April 2014 wurde von beiden Produkten schließlich die jeweils erste stable Version 1.0 veröffentlicht.
In unserem ersten Blogbeitrag hatten wir gezeigt, wie man ein eigenes Bundle anlegt, eine einfache „Hello World“-Seite erstellt und diese ins Menü integriert. Darauf aufbauend möchten wir nun in diesem und weiteren folgenden Tutorials eine konkrete Beispielanwendung erstellen, die eine einfache Verwaltung von Bestellungen für ein E-Commerce System ermöglicht. In diesem Beitrag soll es als ersten Schritt darum gehen, Bestellungen („orders“) in einer Listen- und Einzelansicht darzustellen.
Ausgangspunkt
Wir setzen eine installierte Oro-Platform voraus, bei der wie im ersten Tutorial beschrieben ein eigenes Bundle angelegt wurde, ein Eintrag für das Hauptmenü definiert und ein Controller erstellt wurde. Das Bundle für unsere Bestellungen sollte aber diesmal den Namen „OrderBundle“ tragen. Zur Demonstration kann der vollständige Code als OrderManager.tar heruntergeladen werden. Darin enthalten ist das komplette OrderBundle. Eine Installationsanleitung findet sich in der Datei README.md.
Model-Klassen anlegen
Zunächst muss eine Model-Klasse für die Entität „Bestellung“ definiert werden. Da die Oro-Platform auf Symfony und Doctrine basiert, folgt sie dabei den dafür geltenden Konventionen. Im OrderBundle benötigt man ein Verzeichnis Entity. Dort legt man eine Datei Order.php an, die das Model repräsentiert.
OrderBundle/Entity/Order.php:
use Oro\Bundle\DataAuditBundle\Metadata\Annotation as Oro; use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="acme_order") * @ORM\Entity * @Oro\Loggable * @Config(routeName="acme_orders_order_index") */ class Order { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="smallint") * @Oro\Versioned */ protected $status; /** * @ORM\ManyToOne(targetEntity="\Acme\OrderBundle\Entity\Customer") * @ORM\JoinColumn(name="customer", referencedColumnName="id") */ protected $customer; /** * @ORM\OneToMany(targetEntity="\Acme\OrderBundle\Entity\OrderPosition", mappedBy="order", cascade={"persist"}) */ protected $positions; /** * Set id * @param integer $id * @return Order */ public function setId($id) { $this->id = $id; return $this; } /** * Get id * @return integer */ public function getId() { return $this->id; } }
Für alle Felder neben der ID sollten Getter- und Settermethoden angelegt werden, aus Platzgründen wurden diese hier jedoch ausgespart. Für die Variablen $customer und $positions werden per Annotation automatisch Relationen zu anderen Entites für die Datentypen Kunde und Bestellposition hergestellt. Für diese beiden Datentypen muss auch jeweils eine Entity-Klasse angelegt werden.
OrderBundle/Entity/Customer.php:
namespace Acme\OrderBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Oro\Bundle\DataAuditBundle\Metadata\Annotation as Oro; use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="acme_customer") * @Config(routeName="acme_orders_customer") */ class Customer { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string", length=255) */ protected $name; }
OrderBundle/Entity/OrderPosition.php:
namespace Acme\OrderBundle\Entity; use Acme\OrderBundle\Entity\Order; use Doctrine\ORM\Mapping as ORM; use Oro\Bundle\DataAuditBundle\Metadata\Annotation as Oro; use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="acme_orderposition") * @Config(routeName="acme_orders_orderposition") */ class OrderPosition { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="integer") */ protected $quantity; /** * @ORM\ManyToOne(targetEntity="\Acme\OrderBundle\Entity\Order", inversedBy="positions") * @ORM\JoinColumn(name="order", referencedColumnName="id") */ protected $order; }
Auch hier wurden die Getter und Setter wieder ausgespart.
Oro bringt zahlreiche Annotationen mit denen das Verhalten der Entitäten bzw. der Felder konfiguriert werden können. Durch die @Oro\Versioned Annotation im obigen Beispielcode werden Änderungen am Status der Order automatisch im Data-Audit genannten Logging-Tool von Oro erfasst.
Mit dem Doctrine Befehl
php app/console doctrine:schema:update
kann man sich anschließend aus den gemappten Entites die 3 Tabellen acme_order, acme_customer und acme_orderposition in der Datenbank erzeugen lassen.
Listenansicht
Eines der mächtigsten Features der OroPlatform ist die Möglichkeit Daten in Tabellenform auszugeben, wobei die Zusammenstellung der Daten, die Darstellung und die Filterung auf vielfältige Art und Weise angepasst werden können. Ein sogenanntes Datagrid wird in einer YAML Datei konfiguriert. Im OrderBundle muss dazu eine Datei datagrid.yml im Verzeichnis Resources/config/ angelegt werden.
Um eine Liste mit Bestellungen anzuzeigen, die jeweils den Namen des Kunden sowie die Anzahl bestellter Artikel enthält, wird das Datagrid wie folgt konfiguriert:
OrderBundle/Resources/config/datagrid.yml:
datagrid: acme-orders-order-grid: # Der Name des Datagrids source: # hier werden Datenquelle und Abfragen definiert type: orm query: select: # Definition der einzelnen Felder - o.id - o.status - c.name as customerName # Einfache Datenbank-Funktionen wie SUM und COUNT kann man direkt nutzen - SUM(p.quantity) as quantity from: - { table: Acme\OrderBundle\Entity\Order, alias: o } join: # hier werden die Joins auf die Kunden- und Bestellpositionen Entities definiert left: customer: join: o.customer alias: c positions: join: o.positions alias: p # ermöglicht eine Gruppierung über die ID der Order groupBy: o.id columns: # Definiert der Reihe nach einzelne Spalten für die Ausgabe der Tabelle id: label: ID customerName: label: Kundenname quantity: label: Anzahl status: label: Status frontend_type: select choices: 0: eingegangen 1: in Bearbeitung 2: ausgeliefert sorters: # Die hier angegebenen Spalten sind durch Klick auf den Namen im Tabellenkopf sortierbar columns: customerName: data_name: customerName status: data_name: o.status default: status: ASC
Um dieses Datagrid nun auch auszugeben sind noch zwei weitere Schritte nötig. Im Controller muss zunächst eine indexAction für die Listenansicht erstellt und konfiguriert werden.
OrderBundle/Controller/OrderController.php:
/** * @Route(name="acme_orders_order_index) * @Template */ public function indexAction(Request $request) { return []; }
Anschließend benötigt man noch ein Template für die Darstellung. Oro verwendet standardmäßig die twig Templating Engine. Per Konvention wird automatisch im Verzeichnis Resources/views nach einem Template gesucht, das den gleichen Namen trägt wie die Action. Für die indexAction muss also die Datei index.html.twig im Verzeichnis Acme/OrderBundle/Resources/views/Order angelegt werden. Dort integriert man das vorher definierte Datagrid.
OrderBundle/Resources/views/Order/index.html.twig:
{% extends 'OroUIBundle:actions:index.html.twig' %} {% import 'OroUIBundle::macros.html.twig' as UI %} {% set gridName = 'acme-orders-order-grid' %}
Beim Aufruf der URL example.org/orders erhält man nun als Ausgabe eine Tabelle mit einer Liste aller Bestellungen.
OrderBundle/Controller/OrderController.php:
/** * @Route("/view/{id}", name="acme_orders_order_view", requirements={"id"="\d+"}) * @Template */ public function viewAction(Order $order) { return array( 'entity' => $order, ); }
Damit werden alle Aufrufe an die URL mit dem Muster example.org/orders/view/1 an die viewAction weitergeleitet und zeigen die Einzelansicht für die Bestellung mit der jeweils übergebenen ID.
Natürlich benötigt man noch ein twig-Template view.html.twig für diese Einzelansicht im Verzeichnis Acme/OrderBundle/Resources/views/Order an.
OrderBundle/Resources/views/Order/view.html.twig:
{% extends 'OroUIBundle:actions:view.html.twig' %} {% import 'OroUIBundle::macros.html.twig' as UI %} {% block content_data %} {% set id = 'order' %} {% set dataBlocks = [{ 'title': 'Bestellung'|trans, 'class': 'active', 'subblocks': [ { 'title': 'Auftragsdaten'|trans, 'data': [ UI.attibuteRow('Order ID'|trans, entity.id), UI.attibuteRow('Status'|trans, entity.status), UI.attibuteRow('Anz. Positionen'|trans, entity.positions|length), ] }, { 'title': 'Kundendaten'|trans, 'data': [ UI.attibuteRow(Kunden-Nr'|trans, entity.customer.id), UI.attibuteRow('Name'|trans, entity.customer.name), ] }, ] }] %} {% set data = { 'dataBlocks': dataBlocks } %} {{ parent() }} {% endblock content_data %}
Um aus der Liste bei Klick auf einen Eintrag zu dessen Einzelansicht weiterzuleiten, muss in der datagrid.yml die Datagrid-Konfiguration um Sektionen für Properties und Actions ergänzt werden.
properties: view_link: type: url route: acme_orders_order_view params: { id: id } actions: view: type: navigate label: Bestellung anzeigen link: view_link rowAction: true
Ausblick
Natürlich macht eine Listen- und Einzelansicht von Bestellungen nur Sinn, wenn man diese Daten bequem in der Applikation anlegen und bearbeiten kann. Darum werden wir im nächsten Tutorial zeigen, wie man die dafür benötigten Formulare integriert.
Downloads
OrdnerManager.tar
Oro-Tutorial Listenansicht
Oro-Tutorial Singleview