Magento Theme Refactoring

Ein sehr häufiges Problem in 2014 war „Ich habe auf Magento 1.8 aktualisiert und jetzt funktioniert das (Login|Warenkorb)-Formular nicht mehr“. Der Grund war, dass seit Magento 1.8 der form key in mehr Formularen genutzt wird, als Security Feature (verhindert CSRF-Angriffe). Aber in jedem Theme, das eine eigene Version der jeweiligen Templates enthielt, fehlte der Form Key, so dass die serverseitige Validierung fehlschlug, leider ohne jede Fehlermeldung.

Natürlich gibt es Themes, deren Markup so verschieden vom Default Theme ist, dass die meisten Templates aus gutem Grund überschrieben wurden. Aber ich sehe mindestens genauso viele Themes, insbesondere Eigenentwicklungen, bei denen erst einmal alle Templates aus base/default kopiert und dann angepasst wurden. Für das Überschreiben von Layout-XML-Dateien gibt es fast keine Entschuldigung, das Layout kann man in allen erdenklichen Möglichkeiten mit einer Theme-spezifischen local.xml Datei anpassen.

Das obengenannte Problem ist ein gutes Beispiel für die Gründe des „Achte auf Updatefähigkeit“ Mantras. Die Fehler hätten vermieden werden können, wenn nur Dateien kopiert worden wären, die wirklich Anpassung benötigten.

In diesem Artikel möchte ich meinen Prozess zum Theme Refactoring darlegen (nur den strukturellen Teil, der HTML-Quelltext wird anschließend exakt gleich aussehen wie vorher, abgesehen von Änderungen durch neuere Versionen der Default-Templates).

Weiterlesen auf integer-net.de

EcomDev_PHPUnit Tipp #3

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #3: Gemeinsame Data Provider

Hast Du dich jemals gefragt, warum man für Expectations und Fixtures den Dateinamen spezifizieren kann aber für Data Providers scheinbar auf den Standard “Testname punkt yaml” festgelegt ist? Der einfache Grund ist, dass @dataProvider ein natives Feature von PHPUnit ist und sein Parameter ein Methodenname sein muss.

Also bedeutet @dataProvider dataProvider, dass die Methode EcomDev_PHPUnit_Test_Case::dataProvider() als Data Provider genutzt wird:

    /**
     * Implements default data provider functionality,
     * returns array data loaded from Yaml file with the same name as test method
     *
     * @param string $testName
     * @return array
     */
    public function dataProvider($testName)
    {
        return TestUtil::dataProvider(get_called_class(), $testName);
    }

Aber EcomDev_PHPUnit bietet auch eine Möglichkeit, den Dateinamen für Data Provider explizit anzugeben, was dann z.B. gemeinsame Data Provider ermöglicht:

/**
 * @dataProvider dataProvider
 * @dataProviderFile customFileName.yaml
 */
public function testSomething($something)

EcomDev_PHPUnit Tipp #2

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #2: Expectation Keys

Das ist ein Kurzer: expected('works %s sprintf', 'like').

Welche Expectations geladen werden sollen, hängt üblicherweise von den Testdaten ab, wenn Deine Expectation-Datei also so aussieht:

- product-1-qty-10:
  - answer: 42
- product-2-qty-10:
  - answer: 42

kannst Du sie wie folgt im Test laden:

/**
 * @test
 * @loadExpectation
 * @loadFixture
 * @dataProvider dataProvider
 */
public function testSomething($productId, $qty)
{
  $expectedAnswer = $this->expected('product-%s-qty-%s', $productId, $qty);
}

EcomDev_PHPUnit Tipp #1

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #1: Globalen Zustand zurücksetzen

Ein Umstand, der das Testen mit Magento erschwert, ist die freizügige Anwendung von globalen Zuständen, in Form von Singletons und Registry. Diese werden auch über Tests hinweg nicht zurückgesetzt, EcomDev_PHPUnit ermöglicht aber das explizite Zurücksetzen mit Annotations.

/**
 * @singleton checkout/session
 * @helper tax
 * @registry current_product
 */
public function testSomething()

Die Parameter sind jeweils die selben wie für Mage::getSingleton(), Mage::helper() und Mage::registry().

Es ist zu empfehlen, alle Singletons und Registry-Werte, die im Test genutzt werden zurückzusetzen, nicht erst, wenn es zu Konflikten kommt. Insbesondere für Session-Singletons ist es wichtig, übrigens unabhängig davon ob sie im aktuellen Test gemockt werden oder nicht. Bei zustandslosen Helpern, also solchen ohne eigene Attribute, ist ein Zurücksetzen allerdings nicht notwendig,

CSV-Verarbeitung in Magento

Ein Grundsatz bei der Entwicklung, nicht nur mit Magento, ist dass man nicht versuchen sollte, das Rad neu zu erfinden und insbesondere auf die Funktionen des verwendeten Frameworks zurückzugreifen, soweit möglich. Magento hat viele mehr oder weniger bekannte universelle Helfer, in den Helper-Klassen aus Mage_Core sowie unter lib/Varien, und natürlich im Zend Framework.

Ein Klassiker ist z.B. JSON Kodierung. PHP hat zwar built-in die Funktionen json_encode und json_decode, die haben aber einige Unzulänglichkeiten, die in der Implementierung von Zend_Json ausgebügelt wurden. So gibt es in Zend_Json::encode() einen Zyklen-Check. Magento hat in Mage_Core_Helper_Data::jsonEncode() noch Support für Inline-Translations innerhalb von JSON hinzugefügt.
In Magento sollte man also immer Mage::helper('core')->jsonEncode() (bzw. jsonDecode) benutzen.

Varien_File_Csv

Und wie sieht es bei der Verarbeitung von CSV Dateien aus? Da der import und Export im Standard mit CSV Dateien funktioniert, sollte Magento doch etwas haben… Vorhang auf für Varien_File_Csv. Naja, ich nehme das Ergebnis mal vorweg: außer bei ganz einfachen Aufgaben mit kleinen Dateien ist die Klasse nicht zu gebrauchen.

Continue reading “CSV-Verarbeitung in Magento”

Magento Indexer-Fortschritt visualisieren

Magento Reindex Progress

Beim Magento Hackathon Zürich 2014 haben Dima Janzen von Mash2 und ich uns ein altes Thema aus der „Projektideen“-Liste ausgesucht, „Visualize reindexing“ (Danke an Tim Bezhashvyly für den Vorschlag!). Das Team, das es letztes Mal angefangen hatte, zu implementieren, sagte uns, dass es keinen vernünftigen Weg gibt, den Fortschritt von verarbeiteten Index-Events zu bestimmen, was auch unser erster Ansatz gewesen wäre. Also kamen wir auf einen anderen Ansatz: Schätzen der Gesamtlaufzeit pro Indexer, basierend auf vorherigen Laufzeiten, so wie es zum Beispiel auch Buildserver machen.

Glücklicherweise sind diese Daten einfach auszulesen, Magento speichert bereits Startzeit und Endzeit des aktuellen/letzten Laufs in der Datenbank-Tabelle “index_process”, wir mussten sie nur in einer zweiten Tabelle persistieren, um den Verlauf zu sehen.

Dann war der Großteil der Arbeit, ein schönes User Interface drumherum zu bauen. Die wichtigen Ziele dabei waren:

  • Unaufdringliche Integration ins Indexer Grid
  • Information in Echtzeit

Weiterlesen auf integer-net.de

Ein JSON-RPC Adapter für die Magento API

Beim Durchsehen meiner alten Antworten auf Magento StackExchange bin ich auf diese Frage zum Ansprechen der Magento API via JavaScript gestoßen und musste feststellen dass der Link auf GitHub, der einen wesentlichen Teil der Lösung enthielt, nämlich die Implementierung eines JSON-RPC Adapters für die Magento-API mittlerweile tot ist.

Also habe ich kurzerhand das komplette daraus entstandene Modul selbst veröffentlicht (der originale Link war ein Core Hack):

GitHub: SGH_JsonRpc

Das ganze Modul sind weniger als 100 Zeilen Code. In config.xml wird unser Controller der api Route hinzugefügt:

    <frontend>
        <routers>
            <api>
                <args>
                    <modules>
                        <sgh_jsonrpc before="Mage_Api">SGH_JsonRpc_Api</sgh_jsonrpc>
                    </modules>
                </args>
            </api>
        </routers>
    </frontend>

Der neue API Adapter wird in api.xml definiert:

Continue reading “Ein JSON-RPC Adapter für die Magento API”

Magento Attribute effizient inkrementieren & dekrementieren

Magento.SE Screenshot

Diese Frage tauchte auf Magento StackExchange auf:

I need to decrement a value with an atomic database operation, is it possible using Magento models?

Es ist tatsächlich möglich, Attribute mit einem Update zu inkrementieren und dekrementieren und zwar mit einer weniger bekannten Technik mittels Zend_Db_Expr. Ich teile es auch hier:

$object->setNumber(new Zend_Db_Expr('number-1'));

Als Referenz:

Die Methode Mage_Core_Model_Resource_Abstract::_prepareDataForSave() enthält folgenden Code:

if ($object->hasData($field)) {
    $fieldValue = $object->getData($field);
    if ($fieldValue instanceof Zend_Db_Expr) {
        $data[$field] = $fieldValue;
    } else {
        ... [normale Verarbeitung folgt]

EAV Models:

Beachte, dass man das Attribut nur mit seinem Namen referenzieren kann (“number” im Beispiel) wenn es eine echte Spalte der Haupt-Tabelle ist, kein EAV-Attribut.

Obwohl die obengenannte Methode nur von Models mit flachen Tabellen benutzt wird, kann Zend_Db_Expr aber auch für EAV Attribute benutzt werden, die Methode die den Parameter verarbeitet, ist Varien_Db_Adapter_Pdo_Mysql::prepareColumnValue().

ABER man muss dann immer den Spaltennamen “value” verwenden:

$product->setNumber(new Zend_Db_Expr('value-1'));

Es ist nicht notwendig, einen Alias für die Tabelle anzugeben, weil jedes Attribue einzeln mit eigenem Query verarbeitet wird, so dass “value” nicht mehrdeutig ist.

Magento: Direktlink auf Tab in Adminhtml Tab Widgets

Für eine Extension an der ich neulich gearbeitet habe, habe ich mich gefragt, ob es eine eingebaute Möglichkeit gibt, direkt auf einen Tab einer Seite im Backend zu linken. Meine Nachforschungen haben nichts ergeben (sprich: Mein Google Foo hat versagt), also habe ich mir den Core Code vorgenommen um einen Ansatz zu finden. Was ich herausgefunden habe:

Das Problem

Im speziellen wollte ich einen Link auf den “Bezeichnungen / Optionen verwalten” Tab der “Produktattribut bearbeiten” Seite setzen:

Screenshot

Die Lösung

Tatsächlich ist es mit einem URL-Parameter möglich: ?active_tab=$id.

Wie finde ich die Tab-ID heraus?

Finde den verantwortlichen Tab Container. Dies ist eine Unterklasse von Mage_Adminhtml_Block_Widget_Tabs, in meinem Fall Mage_Adminhtml_Block_Catalog_Product_Attribute_Edit_Tabs.

Du wirst Aufrufe von $this->addTab() finden, üblicherweise in den Methoden _beforeToHtml(), oder _construct(). Der erste Parameter von addTab() ist die Tab ID:

    $this->addTab('labels', array(
        'label'     => Mage::helper('catalog')->__('Manage Label / Options'),
        'title'     => Mage::helper('catalog')->__('Manage Label / Options'),
        'content'   => $this->getLayout()->createBlock('adminhtml/catalog_product_attribute_edit_tab_options')->toHtml(),
    ));

Die URL ist also /admin/catalog_product_attribute/edit/attribute_id/123/?active_tab=labels, generiert mit diesem Code (innerhalb eines Adminhtml Blocks):

    $this->getUrl('adminhtml/catalog_product_attribute/edit',
        array('attribute_id' => 123, '_query' => array('active_tab' => 'labels'));

Wie es funktioniert

Schauen wir uns den verantwortlichen Code an:
Continue reading “Magento: Direktlink auf Tab in Adminhtml Tab Widgets”

Magento ACL: Kein “Ausloggen und wieder einloggen” mehr nach Extension-Installation

In fast jeden Installationsanweisungen für Magento Extensions findet man den Schritt “Ausloggen und wieder einloggen”, der notwendig ist um Zugriff auf neue Bereiche im Admin Menü oder der Systemkonfiguration zu erhalten. Aber warum müssen wir das hinnehmen? Das Problem ist, dass die Access Control List (ACL) nur bei Login geladen wird und dann in der Session gecached bleibt. Sie bei jedem Zugriff zu laden ist keine brauchbare Alternative, das würde das Backend zu sehr verlangsamen.

Aber mit nur wenigen Zeilen Code können wir das Neuladen der ACL ein wenig komfortabler gestalten:

Der Code

Diese Controller Action lädt die ACL auf Anfrage neu:

class SSE_AclReload_Adminhtml_Permissions_AclReloadController
    extends Mage_Adminhtml_Controller_Action
{
    public function indexAction()
    {
        $session = Mage::getSingleton('admin/session');
        $session->setAcl(Mage::getResourceModel('admin/acl')->loadAcl());

        Mage::getSingleton('adminhtml/session')->addSuccess(
            $this->__('ACL reloaded'));
        $this->_redirectReferer();
    }
}

Dieses Template stellt einen Button zur Verfügung, der zu dem neuen Controller linkt:

<?php
$request = Mage::app()->getRequest();
?>
<a href="<?php echo $this->getUrl('adminhtml/permissions_aclReload/index'); ?>">
<?php echo $this->__('Reload ACL'); ?>
</a>

Und dieses Layout Update fügt den Button zu “404” Fehlerseiten im Backend hinzu (die, die man sieht, wenn man keinen Zugriff auf eine Seite hat):

    <adminhtml_noroute>
        <reference name="content">
            <block type="adminhtml/template" name="content.aclReload"
                after="content.noRoute" template="sse_aclreload/button.phtml" />
        </reference>
    </adminhtml_noroute>

Das Ergebnis

Screenshot Magento Admin 404

Continue reading “Magento ACL: Kein “Ausloggen und wieder einloggen” mehr nach Extension-Installation”