Unser erstes Model
Wir legen heute ein Model an, das einfache Texte verwaltet und sie Produkten zuordnet. So wollen wir den „Hallo Welt“ Text aus den letzten Teilen der Serie etwas spezifischer auf die Produkte zuschneiden.
Erstellen der API Schnittstelle (Service Layer)
Erste große Neuerung ist der (optionale) Service Layer. Dieser dient dazu, sichere API-Schnittstellen zu definieren, die unser Model anbietet. Andere Models oder Dienste können den Service Layer verwenden, um unser Model auf eine verlässliche Art und Weise zu nutzen.
Wenn man sich entscheidet, für sein Model eine API-Definition zu erstellen, kommt diese in den Ordner Api\Data\. Unser Model wird HelloText heißen, daher trägt das Interface die Bezeichnung HelloTextInterface.php:
<?php
namespace Tudock\HelloWorld\Api\Data;
interface HelloTextInterface {
/**
* Get the id of the text
* @return int
* @api
*/
public function getId();
/**
* Get the product associated with this text
* @return \Magento\Catalog\Api\Data\ProductInterface
* @api
*/
public function getProduct();
/**
* Set the project associated with this text
* @param \Magento\Catalog\Api\Data\ProductInterface $productInterface
* @return \Tudock\HelloWorld\Api\Data\HelloTextInterface
* @api
*/
public function setProduct(\Magento\Catalog\Api\Data\ProductInterface $productInterface);
/**
* Get the text
* @return string
* @api
*/
public function getText();
/**
* Set the text
* @param string $text
* @return \Tudock\HelloWorld\Api\Data\HelloTextInterface
* @api
*/
public function setText($text);
/**
* Load another text by id
* @param integer $text_id
* @return \Tudock\HelloWorld\Api\Data\HelloTextInterface
* @api
*/
public function load($text_id);
/**
* Saves this text
* @return \Tudock\HelloWorld\Api\Data\HelloTextInterface
* @api
*/
public function save();
}
Das Interface liefert uns jetzt bereits wichtige Informationen über die Struktur unseres neuen Models:
- Das Interface wird die Methoden getText und setText haben, um den Text im Model zu manipulieren.
- Es wird weiterhin die Methoden getProduct und setProduct haben, um die Produktzuordnung zu ändern. Wir greifen dabei auf das Interface ProductInterface zurück, das heißt, wir nutzen hier selbst eine Service Layer API, nämlich die von Magento für Produkt-Models.
- load und save sind Standardmethoden von Models, genau wie in Magento 1. Wir sichern diese Methoden in unserer API zu.
- getId ist ebenfalls eine Standardmethode von Models, um die Id aus der Datenbank zu bekommen. Wir brauchen diese Methode vielleicht in späteren Kapiteln, wenn wir die Datenbank für unser Model eingerichtet haben.
- Der Namespace des Interfaces folgt wie immer der Ordnerstruktur.
Erstellen des Models
Jetzt, wo wir unsere Schnittstelle fertig haben, wird es Zeit, ein Model zu erstellen, das die Schnittstelle implementiert. Unser Model landet unter Model/HelloText.php. Das Model erweitert die Klasse \Magento\Framework\Model\AbstractModel. Diese Klasse hat bereits die Methoden save, load und getId, die wir im Interface definiert haben. Wir brauchen also nur noch den Rest zu implementieren:
<?php
namespace Tudock\HelloWorld\Model;
class HelloText extends \Magento\Framework\Model\AbstractModel implements \Tudock\HelloWorld\Api\Data\HelloTextInterface {
/**
* Get the product associated with this text
* @return \Magento\Catalog\Api\Data\ProductInterface
*/
public function getProduct() {
/* ... */
}
/**
* Set the project associated with this text
* @param \Magento\Catalog\Api\Data\ProductInterface $productInterface
* @return \Tudock\HelloWorld\Api\Data\HelloTextInterface
*/
public function setProduct(\Magento\Catalog\Api\Data\ProductInterface $productInterface) {
$this->setData('product', $productInterface->getId());
return $this;
}
/**
* Get the text
* @return string
*/
public function getText() {
return $this->getData('text');
}
/**
* Set the text
* @param string $text
* @return \Tudock\HelloWorld\Api\Data\HelloTextInterface
*/
public function setText($text) {
$this->setData('text', $text);
return $this;
}
}
Schauen wir uns diese Klasse einmal genauer an:
- getText und setText nutzen die bereits aus Magento 1 bekannte Methode getData, um die ‚text‚ Daten zu bekommen bzw. zu speichern.
- setProduct macht das Gleiche, nutzt allerdings die Methode getId des ProductInterface, um die Id des mitgelieferten Produktes im Model zu speichern.
Als Letztes fehlt noch die Methode getProduct. Diese habe ich mit Absicht erst einmal weggelassen. Denn dafür müssen wir uns mit Dependency Injection beschäftigen.
Laden von Objekten via Dependency Injection
Es gibt in Magento 2 kein „Mage::“ mehr. Also auch kein Mage::getModel.* Das macht nichts, denn wir benötigen Mage:: nicht mehr. Dank Dependency Injection können wir alle beliebigen Objekte in unsere Klasse laden, einfach indem wir im Konstruktor unserer Klasse danach verlangen.
[* Es gibt zwar den sogenannten ObjectManager, der quasi dasselbe macht wie Mage::getModel. Ich rate aber davon ab, den ObjectManager zu verwenden. Magento 2-Entwickler sollten immer Dependency Injection nutzen, auch wenn der ObjectManager aus Unwissen über Magento 2 und Dependency Injection wiederholt in Blogbeiträgen im Internet auftaucht.]
Wir haben im letzten Teil Dependency Injection bereits für das resultPageFactory-Objekt verwendet. Aber jetzt sehen wir uns einmal genauer an, was wir dort eigentlich gemacht haben – indem wir Dependency Injection benutzen, um in *getProduct* ein neues Produkt-Model zu erzeugen und zurückzugeben.
Zunächst einmal die Grundlagen:
- Objekte werden in eine Klasse geladen, indem man im Konstruktor danach verlangt (Beispiel folgt).
- Alle Objekte können geladen werden – so lädt eine Anfrage nach \Magento\Framework\Registry das Objekt, das die Registry verwaltet (siehe nächstes Kapitel).
- Magentos Dependency Injection generiert automatisch alle Klassen, die benötigt werden – zum Beispiel Factories. Diese werden in var/generation gespeichert. Kracht der Shop mal, kann es helfen, diesen Ordner zusammen mit var/di zu löschen, um alle Klassen neu zu generieren.
- Für Klassen, die mehrere Instanzen haben, werden Factories (Fabriken) genutzt. Factories sind Objekte, die Magento automatisch generiert. Nach ihnen kann durch Anhängen von Factory an den Klassennamen verlangt werden. Eine Factory hat eine Methode create, die eine neue Instanz des eigentlichen Objektes erstellt.
- Via di.xml-Dateien können Entwickler die Dependency Injection komplett manipulieren. Sie können beispielsweise Klassen austauschen oder erweitern (mehr folgt dazu in späteren Teilen).
Dependency Injection anwenden
Wir brauchen etwas, das die Funktionalität von Mage::getModel ersetzt. Wir wollen also ein Produkt-Model in unser Model laden. Halt, nicht das Model selbst, sondern eine Factory, die uns neue Model-Instanzen erzeugt. Objekte wie Models, die mehrere Instanzen haben, müssen via Factories geladen werden (siehe oben).
Dependency Injection findet im Konstruktor statt, wir überschreiben also den Konstruktor von AbstractModel:
/* ... */
class HelloText extends \Magento\Framework\Model\AbstractModel implements \Tudock\HelloWorld\Api\Data\HelloTextInterface {
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
*/
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry
) {
parent::__construct($context, $registry);
}
/* ... */
Im ersten Schritt haben wir den Konstruktor so angelegt, dass er den Konstruktor von AbstractModel zufriedenstellt. Das ist extrem wichtig! Wenn wir den Konstruktor überschreiben, müssen wir erstmal sichergehen, dass wir auch alle Objekte laden, die die Parent-Klasse braucht, und an sie weitergeben. Jetzt können wir dort noch unsere ProductFactory verlangen:
/* ... */
protected $_productFactory;
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
* @param \Magento\Catalog\Model\ProductFactory $productFactory
*/
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Catalog\Model\ProductFactory $productFactory
) {
parent::__construct($context, $registry);
$this->_productFactory = $productFactory;
}
/* ... */
Wie man sieht, laden wir jetzt die ProductFactory. Zur Erinnerung: Factories werden geladen, indem man an den Original-Klassennamen ein Factory hängt. Magento generiert standardmäßig die Factory-Klassen zur Laufzeit. Wollen wir also eine Factory für \Magento\Catalog\Model\Product (das Produkt-Model), dann nehmen wir \Magento\Catalog\Model\ProductFactory.
Diese ProductFactory speichern wir jetzt im Feld $_productFactory, damit wir sie in getProduct verwenden können:
/* ... */
/**
* Get the product associated with this text
* @return \Magento\Catalog\Api\Data\ProductInterface
*/
public function getProduct() {
return $this->_productFactory
->create()
->load($this->getData('product'));
}
/* ... */
Schritt für Schritt:
- Wir rufen auf der Factory create auf. Das erstellt ein neues \Magento\Catalog\Model\Product Objekt.
- Wir laden, genau wie in Magento 1, das Produkt mit der load-Methode.
- Wir geben der load-Methode die gespeicherte Produkt-Id mit (oder null, falls keine gespeichert ist, dann wird ein leeres Produkt geladen).
- Wir geben das Produkt zurück.
Damit sind wir fürs erste mit unserem Model fertig und haben die Grundlagen der Dependency Injection gelernt. In den nächsten Kapiteln sehen wir uns das Ganze noch ein wenig genauer an und werden erfahren, was wir damit noch machen können.
Einbinden und Testen unseres neuen Models
Da wir noch keine Resource Models und Collections haben (dafür müssten wir uns auch Install Scripts ansehen, was den Rahmen dieses Beitrags ein wenig sprengen würde), beschränken wir uns zunächst darauf, bei jedem Aufruf unseres HelloProduct Blocks ein neues HelloText Model zu laden und mit Werten zu füllen. Wir müssen also unserem HelloWorld Block beibringen, HelloText Models zu erstellen.
Hier könnt ihr am besten einmal kurz anhalten und das Ganze selbst versuchen.
Jetzt folgt die Lösung: Wir müssen im Konstruktor die Factory für unser neues Model laden. Nicht wundern, wenn die IDE sagt, dass sie die Klasse nicht kennt – Magento hat sie noch nicht generiert, wird dies aber tun, sobald wir das Model updaten:
protected $_helloTextFactory;
/**
* @param \Magento\Catalog\Block\Product\Context $context
* @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils
* @param array $data
*/
public function __construct(
\Magento\Catalog\Block\Product\Context $context,
\Magento\Framework\Stdlib\ArrayUtils $arrayUtils,
array $data,
\Tudock\HelloWorld\Model\HelloTextFactory $helloTextFactory
) {
parent::__construct($context, $arrayUtils, $data);
$this->_helloTextFactory = $helloTextFactory;
}
Wir bauen jetzt eine neue Methode getText ein, die den Text des Models ausgibt (und fürs Erste auch generiert):
/**
* Get product-specifc text
* @todo Currently only generates a fixed text
* @return string
*/
public function getText() {
$helloText = $this->_helloTextFactory->create();
$helloText
->setProduct($this->getProduct())
->setText('Das ist ein Test');
return $helloText->getText();
}
Wie man sieht, holt sich die Methode ein neues HelloText Model und befüllt es mit dem aktuellen Produkt sowie einem Beispieltext. Wir müssen diese Methode jetzt noch im dazugehörigen Template aufrufen:
<p>
<?php echo __('Hello') ?> , <?php echo $block->getProductName(); ?>!
</p>
<p>
<a href="<?php echo $block->getHelloWorldUrl() ?>">
<?php echo $block->getText() ?>
</a>
</p>
Damit sind wir für heute fertig!
Dieses Mal ist der Artikel etwas länger geraten, aber ich denke, nach so langer Wartezeit ist das auch nur gut so. Nächstes Mal gibt es dann eine Übersicht über einige hilfreiche Objekte, die geladen werden können und die alten Mage:: Befehle ersetzen. Beispielsweise erkläre ich, wie Dinge aus der Registry gelesen und in sie geschrieben werden. In späteren Teilen werde ich mich dann auch noch mit Update Scripts, Resource Models und Collections befassen.
Die weiteren Beiträge der Serie
- Magento 2 Modulentwicklung – Teil 1: Vorbereitung
- Magento 2 Modulentwicklung – Teil 2: Frontend Entwicklung
- Magento 2 Modulentwicklung – Teil 3: Controller und Actions
- Magento 2 Modulentwicklung – Teil 5: Wichtige Objekte
- Magento 2 Modulentwicklung – Teil 6: Resource Models und Install-Scripts
- Magento 2 Modulentwicklung – Teil 7: Observer, Helper und Fazit