Comparable Interface für PHP

Vor etwa 5-6 Jahren hatte ich meine “PHP sollte mehr wie Java sein” Phase und habe viel mit Sachen wie String Objekten und Überladen von Methoden experimentiert, was meistens fiese Workarounds erforderte und die meisten Dinge stellten sich auf lange Sicht nicht als sehr praktikabel heraus.

Aber ein Package aus der Zeit gefällt mir immer noch ziemlich gut, und zwar ComparatorTools, was immerhin Platz 2 in den monatlichen PHPclasses.org Innovation Awards belegte. Es stellt Comparable und Comparator Interfaces zur Verfügung sowie Funktionen, analog zu den Core Array-Funktionen, die mit diesen arbeiten können.

Interfaces

Die Interfaces ähneln den entsprechenden Java interfaces, außer dass wir keine Generics in PHP haben, so dass nicht garantiert werden kann, dass die verglichenen Objekte den selben Typ haben. Dies muss zur Laufzeit in der Implementierung geprüft werden, sofern nötig. Ein Exception-Typ für diese Fälle ist verfügbar:

interface Comparable
{
	/**
	 * @param object $object
	 * @return numeric negative value if $this < $object, positive if $this > $object, 0 otherwise (if objects are considered equal)
	 * @throws ComparatorException if objects are not comparable to each other
	 */
	public function compareTo($object);
}
interface Comparator
{
	/**
	 * @param object $object1
	 * @param object $object2
	 * @return numeric Negative value if $object1 < $object2, positive if $object1 > $object2, 0 otherwise
	 * @throws ComparatorException if objects are not comparable to each other
	 */
	public function compare($object1, $object2);
}

Continue reading “Comparable Interface für PHP”

EcomDev_PHPUnit Tipp #5

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 #5: Secure Area

Problem: Testfälle, die EcomDev_PhpUnit_Test_Case_Controller extenden und eine customer Fixture nutzen, schlagen mit Cannot complete this operation from non-admin area bzw. Diese Aktion kann nicht außerhalb des Admin-Bereichs fertiggestellt werden. fehl weil Magento beim tearDown im area=frontend Modus ist und kein Löschen von Kunden erlaubt. Das selbe Problem tritt auf, wenn man versucht, Kunden im Test zu löschen, ohne im adminhtml Kontext zu sein.

Das Customer Model prüft, ob das isSecureArea Flag in der Magento Registry, um das Problem zu lösen, setzen wir den Flag also im Test. Es gibt zwei mögliche Wege dies zu tun:

1.) Wenn Du Kunden im Test selbst erstellst und löschst:

/*
 * @test
 * @registry isSecureArea
 */
public function testThatNeedsToDeleteCustomers()
{
    Mage::register('isSecureArea', true);
    // ...
}

(Beachte, dass die @registry Annotation den Flag nachher zurücksetzt, siehe Tipp #1)

2.) Wenn Du eine Fixture mit Kunden nutzt:

protected function setUp()
{
    Mage::register('isSecureArea', true);
    parent::setUp();
}
protected function tearDown()
{
    parent::tearDown();
    Mage::unregister('isSecureArea');
}

oder bei Fixtures auf Klassen-Ebene:

    public static function setUpBeforeClass()
    {
        Mage::register('isSecureArea', true);
    }
    public static function tearDownAfterClass()
    {
        Mage::unregister('isSecureArea');
    }

EcomDev_PHPUnit Tipp #4

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 #4: Benannte Parameter

Der Vorteil von YAML-Dateien für die Konfiguration soll einfache Lesbarkeit sein. Wenn aber ein Data Provider so aussieht, ist es nicht weit her mit der Lesbarkeit:

-
  - 7
  - 1
  -
    5: 7
    6: 9
-
  - 7
  - 1
  -
    5: 8
    6: 10

Technisch das selbe, aber deutlich besser wartbar:

Bundle_X_A1_B2:
  product_id: 7
  qty: 1
  bundle_selections_by_option:
    5: 7
    6: 9
Bundle_X_A2_B2:
  product_id: 7
  qty: 1
  bundle_selections_by_option:
    5: 8
    6: 10

Dass es hier um das in den Warenkorb legen von Bündelprodukten geht, kann man zumindest erahnen und wenn man es weiß, sind die Testdaten einfach zu verstehen, auch ohne in den Quelltext zu sehen.

Ein weiterer positiver Effekt, ist dass PHPUnit bei fehlgeschlagenen Tests nicht mehr TestCase::test() with data set #1 ausgibt, sondern z.B. TestCase::test() with data set "Bundle_X_A1_B2"

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”