SVG Bilder in Magento Uploader erlauben

Wer die erlaubten Bilddatei-Typen für Produktbilder in Magento ändern will, wird feststellen, dass sie hart kodiert sind in Mage/Catalog/Model/Product/Attribute/Backend/Media.php und Mage/Adminhtml/controllers/Catalog/Product/GalleryController.php

Ich habe allerdings einen Observer gefunden, mit dem sie zusammen mit anderen Uploader-Konfigurationen angepasst werden können. Dieser Beitrag beschreibt, wie eine Extension für diesen Zweck erstellt werden kann, Grundlagen der Magento Extension-Programmierung werden vorausgesetzt.

Continue reading “SVG Bilder in Magento Uploader erlauben”

Zugriff auf Magento Admin Session aus Frontend

Das nächste mal, wenn ich etwas im Magento Frontend nur für Admin-User zeigen möchte, wird Alan Storms Modul Magento_CrossAreaSessions nützlich sein! Das Thema hat mir in der Vergangenheit schon Kopfschmerzen bereitet. Mehr auf Magento Quickies oder GitHub: Magento_CrossAreaSessions

Hintergrund: Magento trennt adminhtml und frontend Sessions strikt, daher ist es kein trivialer Task, auf einer Frontend-Seite auf die Backend-Session zuzugreifen.

Das Modul erlaubt das Lesen von rohen Session-Daten und Verarbeitung von ACL-Regeln, was für die meisten Fälle ausreichen sollte.

Magento-Entwicklung mit Vagrant – Teil 1: Vagrant-Einführung

Vagrant + Magento

Vagrant ist ein Tool, das portable virtuelle Entwicklungsumgebungen ermöglicht. Wenn an verschiedenen Rechnern und/oder von verschiedenen Team-Mitgliedern an einem Projekt gearbeitet wird, ist somit eine einheitliche Umgebung sichergestellt. Die Einrichtung muss nur ein einziges Mal erfolgen und kann beliebig oft reproduziert werden.

Aber es ist auch für einzelne Entwickler interessant, die an unterschiedlichen Projekten arbeiten. Häufig sind z.B. auf den Produktivsystemen unterschiedliche PHP-Versionen installiert, es werden bestimmte Extensions benötigt oder unterschiedliche Systemkonfigurationen. Gerade bei der Entwicklung mit PHP, dessen Verhalten an vielen Stellen von globaler Konfiguration abhängt, gibt es so oft Fehler aufgrund unterschiedlicher Systeme.

Mit Vagrant lässt sich die gesamte Umgebung für jedes Projekt einzeln definieren und auch versionieren (Konfigurationsdateien im Git-Repository). Auch wird das eigene System weniger „vollgemüllt“, mit Software die man für irgendein Projekt mal benötigt hat. Im Idealfall spiegelt die Umgebung das Produktivsystem 1:1 wieder.

Say goodbye to “works on my machine” bugs. – http://docs.vagrantup.com/v2/why-vagrant/

Diese Artikelserie soll insbesondere Magento-Entwickler Schritt für Schritt an Vagrant heranführen, am Ende steht ein Basis-Setup für Magento-Projekte.

Weiterlesen auf integer-net.de

Das Magento buyRequest Objekt – eine Referenz

Wer im Zusammenhang mit Bestellungen schon einmal im Magento-Quelltext oder der Datenbank gestöbert hat, dem ist vermutlich der Parameter $buyRequest in vielen Methoden bzw. die in quote items und order items gespeicherte Option info_buyRequest aufgefallen, deren Sinn nicht direkt offensichtlich ist, da scheinbar redundante Informationen enthalten sind. Es gibt dazu auch keine eigene Model-Klasse, es ist einfach ein Varien_Object bzw. in der Datenbank ein serialisiertes Array.

Beispiel:

mysql> select code,value from sales_flat_quote_item_option where option_id=2359;
+-----------------+------------------------------------------------------------------------------------------------------------+
| code            | value                                                                                                      |
+-----------------+------------------------------------------------------------------------------------------------------------+
| info_buyRequest | a:4:{s:4:"uenc";s:140:"[...],,";s:7:"product";s:4:"5000";s:15:"related_product";s:0:"";s:3:"qty";s:1:"1";} |
+-----------------+------------------------------------------------------------------------------------------------------------+

Kurz gefasst: Das $buyRequest Objekt repräsentiert die Kundenanfrage, die durch Klick auf „In den Warenkorb“ zustandekommt, alle weiteren Daten sind von diesem Objekt ableitbar. Es dient sozusagen als Generator für Quote Items. Die minimal notwendigen Daten sind also die Produkt ID (product) und Menge (qty).

Wozu wird das $buyRequest Objekt benötigt?

Es wird zum Beispiel beim hinzufügen eines Produkts zur Merkliste (Wishlist) angelegt, so dass es von dort mit der selben Konfiguration einfach in den Warenkorb gelegt werden kann. Beim „rekonfigurieren“, also dem Bearbeiten eines Items vom Warenkorb aus wird der buyRequest dem Produkt View übergeben, um alle Optionen vorauszuwählen.

Auch für Wiederkehrende Profile (recurring profile) und Nachbestellen (reorder) werden die $buyRequest Objekte „wiederverwendet“, um eine neue Bestellung zu generieren.

Anwendungsfälle

Einige Beispiele, wann man sich mit dem buyRequest beschäftigen sollte:

Continue reading “Das Magento buyRequest Objekt – eine Referenz”

Magento Layout: getChildHtml() mit leerem Ergebnis

Das Problem: Mysteriöserweise liefert getChildHtml() im Template ein leeres Ergebnis, obwohl die Kind-Blöcke offenbar erzeugt wurden.

Die Lösung: Blöcke im Magento-Layout sollten immer einen Namen haben. Ohne name-Attribut werden sie zwar angezeigt, Unter-Elemente können ihnen aber nicht zugeordnet werden und bleiben “verwaist”.

Verantwortlich ist die Methode Mage_Core_Model_Layout::_generateBlock(). Wie man sieht, wird der Parent Block nur zugewiesen, wenn er einen Namen hat:

            $parentName = $parent->getBlockName();
            if (!empty($parentName)) {
                $parentBlock = $this->getBlock($parentName);
            }

Man beachte, dass $parent hier ein XML-Knoten ist und kein Block-Objekt. Es hilft also nichts, dass Blöcke ohne Namen automatisch einen Namen wie ANONYMOUS_1 erhalten. Die Zuordnung geschieht über das name-Attribut des Knotens, so dass sie gleichermaßen für <block> wie für <reference> Knoten funktioniert.

Magento Testing: Formulare mit einem Klick ausfüllen

Wer kennt es nicht: Beim manuellen Testen von Funktionen wie dem Checkout als Gast müssen jedes Mal mühsam alle Formularfelder ausgefüllt werden. Mit Chrome Autocomplete oder “test”, Strg + C, Strg + V geht das noch einigermaßen schnell, nervt aber immer noch ein wenig. Und was, wenn die Testdaten noch einigermaßen sinnvoll sein sollen und nicht jedes Mal gleich?

Inspiriert von diesem Artikel auf css-tricks.com habe ich ein kleines Magento-Modul entwickelt, das das Ausfüllen von Magento-Formularen mit Dummy-Daten mit einem Mausklick ermöglicht. Aktuell implementiert ist es für Rechnungsadresse und Versandadresse.

Hier geht es zum Github-Repository: SSE_FormFiller

Und so sieht es aus:

Screenshot: Formulare im Checkout

Konfiguration

Continue reading “Magento Testing: Formulare mit einem Klick ausfüllen”

Magento: Integrationstest für Checkout

Gelegentlich funktioniert die Weiterleitung beim One Page Checkout nicht mehr, nachdem irgendein neues Modul Debug-Ausgaben macht oder Fehler wirft. Exceptions beim Versand der Bestätigungs-Mail sind auch eine häufige Ursache.
Das ist ein ziemlich gravierender Fehler, der aber leicht unbemerkt bleibt. Nachdem ich das nun ein paar Mal hatte, war klar: Der Checkout muss beim Regressionstest automatisch getestet werden, so dass ich über so einen Fehler rechtzeitig informiert werde. Alistair Steads hat glücklicherweise beispielhaft einen Checkout Integration Test online gestellt, seine Bibliothek Mage-Test benutze ich allerdings nicht mehr, habe den Test also für EcomDev PHPUnit 0.2 umgeschrieben. Am Code der Test-Methode ändert sich zunächst nicht viel, nur ein paar Methoden heißen anders. Das Setup ist allerdings mal wieder etwas kniffelig, daher will ich es hier mal etwas näher beleuchten. Links zum fertigen Test Case folgen unten.

Zunächst mal wird mindestens ein Produkt für den Warenkorb benötigt, die Unit Tests laufen ja auf ihrer eigenen Datenbank, die für jeden Test mittels Fixtures entsprechend vorbereitet werden muss (mehr dazu im EcomDev PHPUnit Manual). Hier die Minimal-Daten für unsere Zwecke:

eav:
  catalog_product:
    -
      entity_id: 1
      stock:
        qty: 100
        is_in_stock: 1
      website_ids:
        - default
      sku: test
      name: Test
      status: 1 # enabled
      type_id: simple
      price: 1.00

Um sicherzustellen, dass die Bestellung akzeptiert wird, sollte das Empfängerland explizit erlaubt sein und die Funktion “Terms & Conditions” deaktiviert werden:

config:
  default/general/country/allow: DE,GB
  stores/default/checkout/options/enable_agreements: 0

Außerdem muss die Zahlungsmethode konfiguriert sein. Ich habe mich dazu entschieden, für den Test “Scheck / Zahlungsanweisung” statt Paypal zu verwenden, die Standard-Methode, die keine Konfiguration benötigt. Wenn explizit verschiedene Zahlungsmethoden getestet werden sollen, ist ein aufwändigeres Setup nötig, das macht meiner Meinung nach in der isolierten Testumgebung aber wenig Sinn.

Ganz wichtig ist, dass nachdem der Warenkorb gefüllt wurde, der Session-Wert cart_was_updated auf false gesetzt wird, ansonsten bricht der Test mit einer wenig aussagekräftigen “Headers already sent”-Exception ab (mehr dazu unten).

    protected function _fixCheckout()
    {
        Mage::getSingleton('checkout/session')->setCartWasUpdated(false);
    }

Eine weitere Überraschung gab es, nachdem der Test das erste Mal erfolgreich durchlief und beim nächsten Mal ohne ersichtlichen Grund einen Fehler in der JSON-Ausgabe meldete. Gut, wenn man Exception-Logging aktiviert hat: Grund war eine PDOException aufgrund doppeltem Schlüssel in der Order-Tabelle. Das interessante: Das EcomDev Test-Framework räumt die Datenbank eigentlich nach jedem Testlauf ab (wenn nicht gerade ein Fatal Error der Ausführung der tearDown-Methoden zuvorkommt) aber für die beim Checkout generierte Bestellung (und nur die) funktionierte das nicht. Als Ursache vermute ich die zusätzliche Kontrolle im Order-Model, die verhindern soll, dass Bestellungen von irgendeinem Modul außerhalb des Admin-Bereichs gelöscht werden können. Jedenfalls hat das setzen des isSecureArea-Flags geholfen, generierte Bestellungen wieder zu löschen:

    /**
     * Deletes any created order
     * 
     * @return void
     */
    protected function _deleteOrders()
    {
        Mage::register('isSecureArea', true);
        /* @var $orders Mage_Sales_Model_Mysql4_Order_Collection */
        $orders = Mage::getModel('sales/order')->getCollection()->load();
        $orders->walk('delete');
    }

Dinge, die ich beim Debuggen gelernt habe

  1. Der One Page Checkout Controller reagiert auf diverse Fehler mit einem 403 Session Expired Header, was gemeinerweise vom Test-Framework nur mit einer “Headers already sent”-Exception beim verarbeiten des Response-Objekts quittiert wird. Den Fehler habe ich natürlich erst bei mir selbst gesucht, aber das Response-Objekt wurde überall sauber resettet.
    Zu den verursachenden Fehlern dieses Headers gehören ein leerer Warenkorb und jegliche Fehler aus dem Quote-Model (siehe dazu die Methode OnepageController::­_expireAjax()). Um so etwas zu finden, empfiehlt es sich, die Header direkt an der Stelle, wo die Exception geworfen wird zu analysieren und dann im Magento-Source nach dem Header-Text zu suchen.
  2. Um aufzudecken, welche Exceptions zu (oft wenig hilfreichen) Magento-Fehlermeldungen geführt haben, ist das Exception-Log bei Integrationstests unentbehrlich. Die Fixture dazu sieht so aus:
     stores/default/dev/log/active: 1
      stores/default/dev/log/file: tests-system.log
      stores/default/dev/log/exception_file: tests-exception.log
    

Quelltext

Wie versprochen, stehen Fixture & Test Case als github:gist online zur Verfügung.

Alle SQL Queries in Magento anzeigen

Aktiviere den Zend SQL Profiler mit dem folgenden Knoten in local.xml:

    <resources>
     <default_setup>
      <connection>
       <profiler>1</profiler>

Dann kannst Du auf den Profiler an beliebiger Stelle im Code zugreifen und einiges an Informationen über die ausgeführten Queries erhalten:

$profiler = Mage::getSingleton('core/resource')
    ->getConnection('core_write')->getProfiler();

Um alle Queries auszugeben genügt z.B.:

print_r($profiler->getQueryProfiles());

Frage und Antwort auf StackOverflow