Erstellen eines Install-Scripts
Zunächst brauchen wir überhaupt einmal eine Tabelle in der Datenbank. Dafür benötigen wir ein Script, das beim Installieren unseres Moduls ausgeführt wird. Dieses Script befindet sich in einer Klasse, die wir unter Setup/InstallSchema.php anlegen. Wir füllen sie zunächst mit folgendem Inhalt:
<?php
namespace Tudock\HelloWorld\Setup;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
class InstallSchema implements InstallSchemaInterface {
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) {
$setup->startSetup();
/**
* Create table 'tudock_helloworld'
*/
$table = $setup->getConnection()->newTable(
$setup->getTable('tudock_helloworld')
)->addColumn(
'id',
Table::TYPE_INTEGER,
null,
['unsigned' => true, 'nullable' => false, 'default' => '0', 'primary' => true],
'ID of text'
)->addColumn(
'product',
Table::TYPE_INTEGER,
null,
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Product ID'
)->addColumn(
'text',
Table::TYPE_TEXT,
255,
['unsigned' => true, 'nullable' => false, 'default' => '0'],
'Text'
)->setComment(
'HelloWorld demo table'
)->addIndex(
$setup->getIdxName('tudock_helloworld', 'id',
\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE),
'id',
['type' =>\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_UNIQUE]
)->addIndex(
$setup->getIdxName('tudock_helloworld', 'product'),
'product'
)->addForeignKey(
$setup->getFkName(
'tudock_helloworld',
'product',
'catalog_product_entity',
'entity_id'
),
'product',
$setup->getTable('catalog_product_entity'),
'entity_id',
Table::ACTION_CASCADE
);
$setup->getConnection()->createTable($table);
$setup->endSetup();
}
}
Install-Schema-Scripts in Magento 2 sind PHP-Klassen, die im einfachsten Fall nur die install-Methode implementieren.
In unserem Beispiel generieren wir die Tabelle tudock_helloworld. Wir erstellen die Tabelle mit den Feldern id (der ID unseres Datensatzes), product (das verknüpfte Produkt) und text (Nachricht, die auf der Artikeldetailseite angezeigt werden soll). Die Namen der Felder entsprechen denen im Model.
Nachdem wir die einzelnen Felder definiert haben, legen wir zwei SQL Indizes an. Zunächst den Primärschlüssel id und außerdem einen Schlüssel für product. Diese verknüpfen wir dann über Foreign Keys mit der Produkt-Tabelle, so dass unser Datensatz zusammen mit den Produkten gelöscht wird. Die Syntax kann – so wie oben im Beispiel angegeben – gut als Schablone für eigene Projekte genutzt werden.
Vor Durchführung sämtlicher Befehle muss zunächst beginSetup() auf $setup aufgerufen werden, danach endSetup().
Zusätzlich zu der Install-Schema-Klasse gibt es auch noch eine Install-Data-Klasse. Diese wird unter Setup/InstallData.php angelegt:
namespace Tudock\HelloWorld\Setup;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
use Magento\Catalog\Model\Product;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class InstallData implements InstallDataInterface {
/**
* Hello World Model Factory
* @var EavSetupFactory
*/
private $_helloTextFactory;
/**
* Init
* @param \Tudock\HelloWorld\Model\HelloTextFactory $helloTextFactory
*/
public function __construct(
\Tudock\HelloWorld\Model\HelloTextFactory $helloTextFactory
)
{
$this->_helloTextFactory = $helloTextFactory;
}
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) {
$setup->startSetup();
$helloText = $this->_helloTextFactory->create();
$helloText
->setData('product',14)
->setText('Das ist nur ein Test')
->save();
$setup->endSetup();
}
}
In unser Install-Data-Script laden wir eine HelloTextFactory. Mit dieser erstellen wir ein neues HelloText Model für das Produkt mit der ID 14. Hierbei handelt es sich um das Produkt „Push It Messenger Bag“ im Demo-Magento 2-Shop.
Anmerkung: Wir nutzen hier setData, damit wir nicht erst ein Produkt-Model laden müssen – ist nicht so schön, hatte ich bei der Erstellung des Models aber leider nicht bedacht.
Danach speichern wir das Model. Folgende Idee steckt dahinter: Wenn wir fertig sind, präsentiert dieses Produkt den Text auf der Artikeldetailseite. Alle anderen Produkte zeigen aber erst Text, wenn wir auch ihnen Text hinzufügen.
save() speichert das Model in die Datenbank. Voraussetzung ist allerdings, dass wir ein Resource Model einrichten.
Um die Tabelle anzulegen, müssen wir unser Modul einmal aus unserem Demo-Shop deinstallieren und danach wieder installieren – sonst werden die Install-Scripte nicht ausgeführt:
bin/magento module:uninstall Tudock_HelloWorld
Achtung aufpassen: Das könnte je nach Art der Installation auch den Code komplett löschen, daher das Modul lieber vorab sichern.
Bevor wir das Modul wieder installieren, müssen wir das Resource Model anlegen. Nur so ist sichergestellt, dass unser Modul die Daten speichert, die wir im Install-Data-Script generieren.
Anlegen des Resource Models
Ein Model mit dem Namen „HelloText“ haben wir ja bereits erstellt. Wir benutzen dieses Model auch in dem Install-Data-Script. Das Resource Model für unser HelloText Model unter Model/HelloText.php wird angelegt unter Model/ResourceModel/HelloText.php:
namespace Tudock\HelloWorld\Model\ResourceModel;
class HelloText extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb {
/**
* Initialize resource model
*
* @return void
*/
protected function _construct() {
$this->_init('tudock_helloworld', 'id');
}
}
Das Beispiel zeigt das einfachste Resource Model. In der _construct Methode (nicht verwechseln mit __construct!) wird ähnlich wie in Magento 1 eine Verknüpfung mit der Tabelle und dem ID-Feld in der Tabelle (in unserem Fall id) angelegt.
ResourceModels können auch Hooks (_beforeSave, _afterSave) oder Code zur Validierung enthalten. Unser einfaches Resource Model verknüpft lediglich das Model mit der Datenbank.
Eine Collection für unser Model
Als Letztes müssen wir noch eine Möglichkeit finden, unser HelloText Model auf der Artikeldetailseite der Produkte zu orten. Denn wir wollen auf der ADS von einem Produkt nur den dazugehörigen Text.
Dafür kann man beispielsweise eine Collection nutzen. Collections sammeln alle Instanzen eines Models, die in der Datenbank stehen und bestimmten Kriterien entsprechen. Wir wollen damit genau den Eintrag suchen, zu dem das Produkt gehört.
Die Collection für Model\HelloText.php wird in Model\ResourceModel\HelloText\Collection.php angelegt:
namespace Tudock\HelloWorld\Model\ResourceModel\HelloText;
use Magento\Catalog\Api\Data\ProductInterface;
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
/**
* Initialization here
*
* @return void
*/
protected function _construct()
{
$this->_init('Tudock\HelloWorld\Model\HelloText', 'Tudock\HelloWorld\Model\ResourceModel\HelloText');
}
/**
* Add Product Filter to Collection
*
* @param int|array|ProductInterface $product
* @return $this
*/
public function addProductFilter($product){
$id = -1;
if($product instanceof ProductInterface){
$id = $product->getId();
} else if(is_numeric($product)){
$id = $product;
}
$this->addFieldToFilter('product',$id);
return $this;
}
}
Unsere Collection erlaubt es uns standardmäßig, alle verfügbaren Daten in eine Sammlung von Models zu laden. Durch addProductFilter und die internen Methode addFieldToFilter können wir die Collection nach einem Feld, hier der Produkt ID, filtern.
Im Konstruktor wird die Collection mit Model und ResourceModel über Klassenpfade verknüpft.
Collection und ResourceModel am Model registrieren
Als Letztes müssen wir unserem HelloText Model, das wir in Teil 4 dieser Blogserie erstellt haben, noch beibringen, dass wir jetzt ein ResourceModel und eine Collection haben.
Dafür müssen wir im Konstruktor die beiden Klassen laden und an die Superklasse übergeben. Das Ganze sieht dann so aus (neue Zeilen sind makiert):
/**
* @param \Magento\Framework\Model\Context $context
* @param \Magento\Framework\Registry $registry
* @param array $data
* @param \Magento\Catalog\Model\ProductFactory $productFactory
*/
public function __construct(
\Magento\Framework\Model\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Catalog\Model\ProductFactory $productFactory,
\Tudock\HelloWorld\Model\ResourceModel\HelloText $resource = null, // NEU
\Tudock\HelloWorld\Model\ResourceModel\HelloText\Collection $resourceCollection = null // NEU
) {
parent::__construct($context, $registry, $resource, $resourceCollection); // NEU: Die zwei hinteren Parameter
$this->_productFactory = $productFactory;
}
Außerdem muss das ResourceModel in der _construct Methode registiert werden:
/**
* Initialize resource model
*
* @return void
*/
protected function _construct()
{
$this->_init('Tudock\HelloWorld\Model\ResourceModel\HelloText');
}
Anzeigen auf der ADS
Jetzt sind wir soweit, die Infos aus der Datenbank zu lesen. Dafür ersetzen wir in dem HelloProduct Block die Methode getText mit einer Methode, die mittels Collection das HelloText Model zum Produkt sucht und bei Fehlschlag eine Standard-Meldung zurückgibt:
/**
* Get product-specifc text
* @return string
*/
public function getText() {
$helloTextCollection = $this->_helloTextCollectionFactory->create();
$helloTextCollection->addProductFilter($this->getProduct());
$entireText = "";
foreach($helloTextCollection as $helloText) {
$entireText .= $helloText->getText();
}
if ($entireText == "") {
$entireText = "Leider hat mich niemand lieb :(";
}
return $entireText;
}
Dabei sammeln wir die Texte aller HelloText Models zusammen und hängen sie aneinander. Wir haben zwar im Beispiel nur einen Text, aber ebenso könnten wir mehrere Texte anlegen.
$this->_helloTextCollectionFactory wird genutzt, um eine neue Instanz der Collection zu erstellen. Diese müssen wir im Block erst laden. Wir ändern den Konstruktor dementsprechend:
protected $_helloTextCollectionFactory;
/**
* @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\ResourceModel\HelloText\CollectionFactory $helloTextCollectionFactory
) {
parent::__construct($context, $arrayUtils, $data);
$this->_helloTextCollectionFactory = $helloTextCollectionFactory;
}
Testen
Jetzt wird es endlich Zeit, das Ganze zu testen! Wir versionieren die Änderungen, installieren das Modul wieder und führen danach bin/magento setup:upgrade aus, da dadurch unser Install-Script überhaupt erst gestartet wird:
composer require tudock/helloworld dev-master # Oder ähnlich, je nach Art der Installation
bin/magento setup:upgrade
Wenn es dabei zu keinem Fehler kam, können wir einen Blick in die Datenbank werfen und schauen, ob unser Eintrag da ist:
Gut, der Eintrag ist da! Gehen wir einmal auf die ADS des Produkts und sehen nach, ob unser Text geladen wird:
Super! Damit haben wir erfolgreich ein Model mit der Datenbank geladen.
Funktionsweise von Upgrade-Scripts
Upgrade-Scripts funktionieren genau wie Install-Scripte und kommen ebenfalls in zwei Varianten: Data und Schema.
Besonderheit bei Magento 2 ist allerdings, dass es jeweils nur eine Upgrade-Klasse für Data und Schema gibt. Die upgrade Methode der Klasse wird immer ausgeführt, wenn sich die Version des Moduls ändert. In der Klasse muss „per Hand“ überprüft werden, was für ein Versionssprung vorliegt. Hier eine Beispielklasse für Upgrade-Data:
use Magento\Framework\Setup\UpgradeDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class UpgradeData implements UpgradeDataInterface
{
public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();
if (version_compare($context->getVersion(), '2.0.1', '<')) {
$this->run201($setup);
}
$setup->endSetup();
}
}
Mittels version_compare wird die Version überprüft und die jeweilige Aktion ausgeführt. Die Version legen wir in der module.xml fest.
Anlegen von Attributen in Install/Upgrade-Scripts
Kleiner Tipp am Rande: Falls Attribute angelegt werden sollen, können dafür Install/Upgrade-Data-Scripts genutzt werden sowie eine EAV-Hilfsklasse. Hier ein Beispiel:
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
use Magento\Catalog\Model\Product;
class InstallData implements InstallDataInterface
{
/**
* EAV setup factory
*
* @var EavSetupFactory
*/
private $eavSetupFactory;
/**
* Init
*
* @param EavSetupFactory $eavSetupFactory
*/
public function __construct(EavSetupFactory $eavSetupFactory)
{
$this->eavSetupFactory = $eavSetupFactory;
}
public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
/** @var EavSetup $eavSetup */
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
/**
* Add attributes to the eav/attribute
*/
$eavSetup->addAttribute(
Product::ENTITY,
'tudock_product_document',
[
'type' => 'varchar',
'backend' => 'Tudock\ProductDocuments\Model\Product\Attribute\Backend\Document',
'frontend' => '',
'label' => 'Product Document',
'input' => 'document',
'class' => '',
'source' => '',
'global' => Attribute::SCOPE_GLOBAL,
'visible' => true,
'required' => false,
'user_defined' => true,
'default' => 0,
'searchable' => false,
'filterable' => false,
'filterable_in_search' => false,
'comparable' => false,
'visible_on_front' => false,
'visible_in_advanced_search' => false,
'used_in_product_listing' => false,
'unique' => false,
'is_configurable' => false,
'apply_to' => 'configurable,simple',
'used_for_sort_by' => false,
'input_renderer' => 'Tudock\ProductDocuments\Block\Adminhtml\Form\Document',
'group' => 'Documents'
]
);
}
}
Ausblick
Damit sind wir fast fertig mit den Basics der Magento 2 Modulentwicklung. Im nächsten und zunächst letzten Teil folgen noch Observer und Helper.
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 4: Models und Dependency Injection
- Magento 2 Modulentwicklung – Teil 5: Wichtige Objekte
- Magento 2 Modulentwicklung – Teil 7: Observer, Helper und Fazit