Magento-Erweiterungen im Eigenbau: Module für das Open-Source-Shop-System programmieren
Bestellungen nach Produkt-Lieferanten aufteilen und sie an diese weiterleiten – eine Funktionalität, die in vielen Magento-Projekten benötigt wird. Dieser Workshop nimmt sich des Themas an und zeigt exemplarisch, wie sich solch eine Funktion als Extension abbilden lässt. Folgende Schritte werden dabei behandelt:
- Erweiterung anlegen
- Modul-Installations-Skripte erstellen
- Modelle aus dem Magento-Core erweitern
- Event-Observer benutzen
Der Begriff „Modul“ wird im Artikel synonym zu „Erweiterung“ genutzt, auch wenn genau genommen nicht jede Erweiterung ein eigenes Modul ist.
Wie fange ich an?
Zuerst müssen Sie drei Entscheidungen treffen:
- Soll die Erweiterung nur für das aktuelle Projekt dienen oder wollen Sie sie veröffentlichen? Die Antwort entscheidet über das Verzeichnis, in dem die Erweiterung angelegt wird. Lokale Module (wie in diesem Artikel) liegen in „app/code/local“, Module, die Sie veröffentlichen wollen, in „app/code/community“.
- Welchen Namensraum („Namespace“) wollen Sie verwenden? Der Namespace dient der Vermeidung von Konflikten zwischen Modulen und ist nichts weiter als der Name des Verzeichnisses, in dem Sie Ihre Module sammeln. Der Name sollte nicht zu lang sein, da er häufig getippt werden muss. Außerdem muss der erste Buchstabe groß geschrieben sein. Überhaupt lohnt es sich bei Magento-Modulen immer peinlich genau auf eine konsistente Groß-/Kleinschreibung zu achten. Für die Beispielanwendung benutzen Sie den Namespace „T3nBsp“.
- Wie soll das Modul heißen? Am besten sind kurze Namen, die etwas über das Modul aussagen. Auch hier muss der erste Buchstabe groß geschrieben werden. Für das Beispiel wurde „OrderSplitter“ gewählt.
Wie geht es weiter?
Aus den Antworten auf die drei Fragen ergeben sich die nächsten Schritte. Zuerst erstellen Sie das Verzeichnis „app/code/local/T3nBsp/OrderSplitter“. Noch erkennt Magento das Modul nicht – es muss erst angemeldet werden. Registrieren Sie dazu Ihre Erweiterung mit der Datei „app/etc/modules/T3nBsp_OrderSplitter.xml“:
Die Modulregistrierung unter app/etc/modules/
<?xml version="1.0"?> <config> <modules> <T3nBsp_OrderSplitter> <active>true</active> <codePool>local</codePool> </T3nBsp_OrderSplitter> </modules> </config>
Listing 1
Sollte ein Modul in „app/code/community“ liegen, gehört in den Knoten „<codePool>“ der Wert „community“ statt „local“. Ob alles stimmt, können Sie überprüfen, indem Sie im Magento-Backend die Seite „System -> Konfiguration -> Erweitert -> Erweitert -> Modulausgaben deaktivieren“ aufrufen.
Sollte das Modul dort nicht gelistet sein, ist vermutlich der System-Cache noch an (System -> Cache Verwaltung). Beim Entwickeln sollten Sie den Cache am besten komplett deaktivieren.
Das Herz des Moduls
Nachdem Ihr Modul nun existiert, müssen Sie es mit Leben füllen. Alle weiteren Schritte nehmen ihren Anfang in der Datei „etc/config.xml“ des Moduls (der komplette Pfad ist app/code/local/T3nBsp/OrderSplitter/etc/config.xml).
Skelett für die config.xml
<?xml version="1.0"?> <config> <modules> <T3nBsp_OrderSplitter> <version>0.1.0</version> </T3nBsp_OrderSplitter> </modules> <global> <models> <OrderSplitter> <class>T3nBsp_OrderSplitter_Model</class> </OrderSplitter> </models> </global> </config>
Listing 2
Attribute per Skript anlegen
Jedem Produkt soll eine Distributor-E-Mail-Adresse zugeordnet werden können. Damit das Modul wiederverwendbar ist, soll Magento das Produkt-Attribut automatisch bei der Installation der Erweiterung anlegen. Magento bietet dafür Setup-Skripte. Welche Setup- Skripte ausgeführt werden, bestimmen Einträge in der Datei „config.xml“. Zunächst brauchen Sie eine <resource>-Sektion in Ihrer Konfiguration, direkt unter dem schließenden </models>-Tag:
Resourcen für das Modul definieren
<resources> <OrderSplitter_setup> <setup> <module>T3nBsp_OrderSplitter</module> <class>Mage_Sales_Model_Mysql4_Setup</class> </setup> <connection><use>default_setup</use></connection> </OrderSplitter_setup> <OrderSplitter_write> <connection><use>default_write</use></connection> </OrderSplitter_write> <OrderSplitter_read> <connection><use>default_read</use></connection> </OrderSplitter_read> </resources>
Listing 3
Der Name der Setup-Resource (<OrderSplitter_setup>) und der Wert des <version>-Knotens (0.1.0) bestimmen den Pfad und den Dateinamen des Setup-Skripts. Für Ihr Modul ist das „sql/OrderSplitter_setup/mysql4-install-0.1.0.php“. Der Kontext des Skripts, also die Klasse, die das Setup-Skript ausführt, steht im <class>-Knoten, hier also „Mage_Sales_Model_Mysql4_Setup“ (der Zend-Autoloader übersetzt den Klassennamen in einen Dateipfad und lädt dann automatisch „app/code/core/Mage/Sales/Model/Mysql4/Setup.php“). Hier der Inhalt Ihres Installations-Skripts:
Ein Attribut per Installations-Skript anlegen
<?php $this->startSetup(); $this->addAttribute( 'catalog_product', 'distributor_email', array( 'group' => 'General', 'type' => 'varchar', 'label' => 'Distributor Email Adresse', 'input' => 'text', 'global' => Mage_Catalog_Model_Resource_Eav_Attribute::SCOPE_GLOBAL, 'required' => 1, 'user_defined' => 1, 'is_configurable' => 0, ) ); $this->endSetup();
Listing 4
Weitere mögliche Parameter für die Methode „addAttribute()“ können Sie in „app/code/core/Mage/Eav/Model/Entity/Setup.php“ nachlesen. Setup-Skripte führt Magento beim ersten Reload aus.
Tipp: |
Magento speichert die aktuell installierte Version eines Moduls in der Tabelle „core_resource“. Wenn ein Setup-Skript nicht ausgeführt wird, lohnt ein Blick in die Datenbank, um den Datensatz Ihres Moduls zu löschen (Achtung: nur den Datensatz von Ihrem Modul!) oder die Versionsnummer manuell herunter zu setzen. So kann man das erneute Ausführen eines Setup-Skripts erzwingen. |
Jetzt brauchen Sie ein paar Produkte mit unterschiedlichen Distributor-E-Mail-Adressen, um testen zu können.
Mage Core Klassen überschreiben
Anschließend kommt das eigentliche Aufteilen der Bestellungen nach Distributoren an die Reihe. Das Schwierigste dabie ist, den richtigen Ansatzpunkt zu finden. Da hilft leider nur Suchen und Code verfolgen, bis nach einiger Zeit genug Erfahrung vorhanden ist, um gezielt ansetzen zu können. In diesem Beispiel benutzen Sie die Klasse „Mage_Checkout_Model_Type_Onepage“. Sie befindet sich in der Datei „app/code/core/Mage/Checkout/Model/Type/Onepage.php“ im Modul „Mage_Checkout“.
Durch einen Eintrag in die „config.xml“ teilen Sie Magento mit, dass immer, wenn ein Objekt dieser Klasse gebraucht wird, stattdessen ein Objekt Ihrer erweiterten Klasse erzeugt werden soll.
Dafür ergänzen Sie die <models>-Sektion aus Ihrer config.xml:
Die komplette <models> Sektion
<models> <OrderSplitter> <class>T3nBsp_OrderSplitter_Model</class> </OrderSplitter> <checkout> <rewrite> <type_onepage>T3nBsp_OrderSplitter_Model_Checkout_Type_Onepage</type_onepage> </rewrite> </checkout> </models>
Listing 5
Die Klasse, die Sie erweitern wollen, ist ein Modell im Modul „Mage_Checkout“. Welchen Tag-Namen Sie nehmen müssen, können Sie in der <models>-Sektion der „etc/config.xml“ des jeweiligen Moduls nachschlagen. Für das Mage_Checkout-Modul benutzen Sie also ein <checkout>-Tag. Auf dieselbe Weise können Sie auch Block- und Helfer-Klassen erweitern; in dem Fall würde das <checkout> in die <blocks>- oder die <helpers>-Sektion gehören.
In die Sektion, die das Modul bestimmt (hier <checkout>), kommt ein <rewrite>, dann der zu ersetzende Klassenname (<type_onepage>) und anschließend die neue Klasse (T3nBsp_OrderSplitter_Model_Checkout_Type_Onepage), die statt dessen benutzt werden soll. Die zu ersetzende Klasse wird ohne Namespace und Modulnamen angeben (also „type_onepage“). Das Ganze ist Anfangs etwas gewöhnungsbedürftig, wird aber mit etwas Übung schnell leichter.
Für die erweiterte Onepage-Checkout-Klasse legen Sie die Datei „Model/Checkout/Type/Onepage.php“ in Ihrem Modul an:
Eigene Klasse erweitert die Magento Core Klasse
<?php class T3nBsp_OrderSplitter_Model_Checkout_Type_Onepage extends Mage_Checkout_Model_Type_Onepage { }
Listing 6
Das Schwerste ist jetzt geschafft. Denn der aufwändigste Teil ist meist, die richtige Stelle zu finden, an der man ansetzt, um die gewünschte Funktionalität zu integrieren.
Jetzt weiter im Modul: Die Bestellung wird in der Methode „saveOrder()“ erzeugt. Der Plan sieht wie folgt aus: Vor dem Aufruf der eigentlichen Methode werden zuerst alle Produkte im Warenkorb (dem „Quote“-Objekt bei Magento) nach Distributoren sortiert. Dann werden für jeden Distributor die passenden Produkte im Warenkorb gesammelt, alle nicht passenden entfernt und eine Bestellung abgeschickt.
Das Aufteilen der Bestellung
public function saveOrder() { // Array zum Sammeln der Produkte sortiert nach Distributor $sorted = array(); // Dieses Collection Objekt ist eine Sammlung aller Produkte im Warenkorb $quoteItems = $this->getQuote()->getItemsCollection(); // Aufteilen der Produkte foreach ($quoteItems as $item) { // Distributor Email holen (das load() ist nötig damit alle Produkt-Attribute geladen sind) $distributorEmail = $item->getProduct() ->load($item->getProduct()->getId()) ->getDistributorEmail(); $sorted[$distributorEmail][] = $item; } // Für jeden Distributor eine Bestellung erzeugen foreach ($sorted as $distributorEmail => $items) { // Warenkorb leeren foreach ($quoteItems as $item) { $quoteItems->removeItemByKey($item->getId()); } // Alle Produkte des aktuellen Distributors in den Warenkorb legen foreach ($items as $item) { $quoteItems->addItem($item); } // Zwischensumme, Steuern usw für aktuellen Inhalt des Korbes berechnen $this->getQuote()->collectTotals(); // Aktuelle Distributor Email zur späteren Verwendung speichern $this->getQuote()->setDistributorEmail($distributorEmail); // Bestellung anlegen parent::saveOrder(); } return $this; }
Listing 7
Fast am Ziel
Jetzt fehlt nur noch das Verschicken der E-Mails an die Distributoren. Dabei hilft ein Event-Observer. Events sind in Magento Hooks, an denen eigener Code ausgeführt werden kann. Diese sollten Sie, wenn möglich, dem Erweitern von Core-Klassen vorziehen. Leider stehen sie nicht immer zur Verfügung, wenn sie gebraucht werden. Fangen Sie wieder in der Datei „config.xml“ an.
Direkt nach </global> einfügen:
<frontend> <events> <checkout_type_onepage_save_order_after> <observers> <OrderSplitter> <type>singleton</type> <class>OrderSplitter/observer</class> <method>checkoutTypeOnepageSaveOrderAfter</method> </OrderSplitter> </observers> </checkout_type_onepage_save_order_after> </events> </frontend>
Listing 8
Eine Excel-Datei mit einer (fast vollständigen) Liste aller Events finden Sie im Magento-Wiki [1]. Um Ihr Modul zu vollenden, braucht es noch die Observer-Klasse:
Die Datei „Model/Observer.php“ im Modulverzeichnis
<?php class T3nBsp_OrderSplitter_Model_Observer { public function checkoutTypeOnepageSaveOrderAfter($observer) { // Übergebenes Quote und Order Objekt holen $order = $observer->getEvent()->getOrder(); $quote = $observer->getEvent()->getQuote(); // Nur schicken wenn tatsächlich eine Adresse zugewiesen wurde if ($quote->getDistributorEmail()) { // Lieferadresse als Text formatiert holen $shipTo = $order->getShippingAddress()->format('text'); $itemsTxt = "Items\n-----------------------\n"; foreach ($order->getAllVisibleItems() as $item) { $itemsTxt .= 'SKU: ' . $item->getSku() . ' QTY: ' . $item->getQtyOrdered() . "\n"; } $mail = Mage::getModel('core/email') ->setFromEmail('shop@example.com') ->setSubject('Neue Bestellung') ->setToEmail($quote->getDistributorEmail()) ->setBody("Lieferadresse:\n\n" . $shipTo . "\n" . $itemsTxt) ->send(); } } }
Listing 9
Damit ist Ihr Modul komplett. Ihm fehlt zwar noch einiges an Sonderfall- und Fehlerbehandlung, kann aber schon jetzt als Grundlage für Ihre ersten eigenen Erweiterungen dienen. Die vorgestellten Techniken bilden die Basis, auf der Sie bei der Magento-Modulprogrammierung aufbauen können.
Super Beitrag!!!
Gerade die Sache mit dem Order Split hat mir eine Herausforderung weniger beschert!
Vielen Dank und weiter so.
Christian
Hallo Vinai,
kann mich nur anschließen – super Beitrag.
Hat bei 1.3.x super funktioniert.
Kann es sein das bei 1.4.1.1 es nicht mehr funktioniert ?
Es kommt nur noch ein popup „undefined“.
Viele Grüße,
Michael
Hallo Vinai,
Problem ist gelöst. Deine Anleitung funktioniert nach wie vor perfekt.
Ich habe den Rechnungsprozess und Lieferprozess übersprungen.
Das verträgt sich nicht ohne weiteres mit dem Modul.
Vielleicht hat jemand einen Tip, was in diesem Fall zu tun ist ?
Viele Grüße,
Michael
hallo,
ich versuche mich an dem Tutorial mit Magento 1.5 (frisch installiert, einzig Market Ready Germany installiert)
Leider komme ich nur bis Seite 3/5: Das Feld: ‚Distributor Email‘ taucht einfach nicht auf..
Davor hat alles noch geklappt und ich habe eigentlich alles genauso gemacht wie beschrieben..
woran kann das liegen? Inkompatible Version?
Hallo Mario,
seit Magento 1.4 muss man bei addAttribute() auch das Attribut Set und die Gruppe angeben wenn user_defined auf 1 gesetzt ist, ansonsten musst du das Attribut von Hand im Backend dem Set und einer Gruppe zuweisen.
Am einfachsten ist es allerdings im Setup Script user_defined auf 0 zu setzen, dann ist das ehemalige Verhalten wieder hergestellt und das Attribut wird automatisch allen Attribut Sets in der General Gruppe hinzugefügt.
Vinai
Hallo,
ich nutze Version 1.4.2 und bekomme die Extension nicht zum laufen.
Wenn ich alles erstellt habe, kommt beim Aufruf des Checkout folgende Fehler Meldung:
Fatal error: Call to a member function getQuote() on a non-object in Pfad zu Magento\app\code\core\Mage\Checkout\controllers\OnepageController.php on line 151
Kann mir jemand weiter helfen? Oder die fertige Extension hochladen?
Danke
Dieter
Hallo Dieter,
ich denke Du hast einen Fehler in der Klasse T3nBsp_OrderSplitter_Model_Checkout_Type_Onepage. Entweder einen PHP Syntax Fehler (PHP Error log checken?), oder vielleicht extended deine Klasse nicht Mage_Checkout_Model_Type_Onepage.
Viele Grüße,
Vinai
Hallo Vinai,
danke dir. Ein kompletter Neustart und es läuft. Wirklich schöne Extension.
Gruß
Dieter
Hallo,
kann es sein, dass die Berechnung der Zwischensumme, Steuern usw für aktuellen Inhalt des Korbes nicht funktioniert?
Also: $this->getQuote()->collectTotals();
Gruß
Dieter
Hallo Dieter,
in der aktuellen Version sollte das so aussehen:
$this->getQuote()->setTotalsCollectedFlag(false)->collectTotals();
Hallo Vinai,
ich nutze die Version 1.4.2, deine Lösung ist wohl für 1.5, denn leider funktioniert es nicht.
Scheinbar wird die Funktion nicht aufgerufen, ich habe in app/code/core/Mage/Sales/Model/Quote.php mal ein Mage::log Eintrag eingefügt und dieser wird nicht nach dem zusammenstellen der einzelnen Warenkörbe aufgerufen.
Hast du eine Idee?
Danke für deine Hilfe.
Gruß
Dieter
Hall Dieter,
das $this->getQuote()->setTotalsCollectedFlag(false)->collectTotals(); passt auch für Magento in der Version 1.4.2 (vergleiche Zeile 946 in der Datei app/code/core/Mage/Sales/Model/Quote.php).
Es geht in Deinem Code um die Zeile zum Berechnen der Zwischensumme, Steuern usw für den aktuellen Inhalt des Korbes.
Viele Grüße,
Vinai
Habe es leider bisher nicht zum laufen gebracht.
Ich werde demnächst mal eine neue Installation aufsetzen und schauen ob ich es dort hin bekomme
Danke für deine Hilfe Vinai.
Hallo,
funktioniert diese Lösung auch für 1.6.1.1 und höher?
Danke