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.

Was die Klasse kann

  • Lesen und schreiben von Arrays aus bzw. in CSV Datei über objektorientiertes Interface
  • Vollständiges einlesen in Array mit einer Spalte als Schlüssel
  • Eigene fputcsv Implementierung, die in jedem Fall “Enclosures” nutzt, also z.B. Anführungszeichen um Werte auch setzt, wenn diese keine Leerzeichen enthalten. Das kann die PHP-Funktion fputcsv() nicht

Was sie nicht kann

  • Sequentielles Lesen/Schreiben
  • Iterator/Generator Interfaces

Besonders das fehlende sequentielle Lesen hat mich dazu gebracht, ein Import-Skript für Kundendaten von Varien_CSV auf eine eigene Lösung umzustellen, was den Speicherverbrauch schlagartig um 95% reduziert hat. Hier ist der Code des CSV Iterarors:

class Int_ListImport_Model_Csv_Iterator implements Iterator
{
    const CSV_DELIMITER = '|';
    /**
     * @var string path to CSV
     */
    protected $_filename;
    /**
     * @var string[]
     */
    protected $_columns = array();
    /**
     * @var string
     */
    protected $_keyColumn;
    /**
     * @var string[]
     */
    protected $_currentRow = array();
    /**
     * @var int
     */
    protected $_currentRowNumber = 0;
    /**
     * @var resource
     */
    protected $_filePointer;

    public function __construct($filename, $keyColumn)
    {
        $this->_filename = $filename;
        $this->_keyColumn = $keyColumn;
    }

    public function open()
    {
        if (!$this->_filePointer) {
            $this->_filePointer = fopen($this->_filename, 'r');
        }
        return $this;
    }

    public function close()
    {
        if ($this->_filePointer) {
            fclose($this->_filePointer);
        }
        return $this;
    }

    public function current()
    {
        return $this->_currentRow;
    }
    public function next()
    {
        $this->_readRow();
    }
    public function key()
    {
        if (is_array($this->_keyColumn)) {
            return array_reduce($this->_keyColumn, function($carry, $keyPartColumn) {
                $carry .= $this->_currentRow[$keyPartColumn] . '|';
                return $carry;
            }, '|');
        } else {
            return $this->_currentRow[$this->_keyColumn];
        }
    }
    public function valid()
    {
        return $this->_currentRow !== false;
    }
    public function rewind()
    {
        $this->open();
        fseek($this->_filePointer, 0);
        $this->_currentRowNumber = 0;
        $this->_readHead();
        $this->_readRow();
    }
    protected function _readHead()
    {
        $this->_columns = fgetcsv($this->_filePointer, null, self::CSV_DELIMITER);
        if (strpos(join('', $this->_columns), "\r") !== false) {
            throw Mage::exception('Int_ListImport', 'The CSV file contains Windows or Mac line breaks (CR). Please convert it to Unix line breaks (LF)');
        }
        return $this;
    }
    protected function _readRow()
    {
        ++$this->_currentRowNumber;
        $this->_currentRow = fgetcsv($this->_filePointer, null, self::CSV_DELIMITER);
        if ($this->_currentRow === false) {
            if (feof($this->_filePointer)) {
                $this->close();
                return $this;
            } else {
                throw Mage::exception('Int_ListImport', 'Read error in row ' . $this->_currentRowNumber);
            }
        }
        $this->_currentRow = array_combine($this->_columns, $this->_currentRow);
        if ($this->_currentRow === false) {
            throw Mage::exception('Int_ListImport', 'Column count does not match in row ' . $this->_currentRowNumber);
        }

        return $this;
    }
}

Nutzung

$iterator = new Int_ListImport_Model_Csv_Iterator(
    'path/to/file.csv', 'name_of_key_column');
foreach ($iterator as $key => $row) {
    //...
}

Es ist auch möglich, ein Array von Spaltennamen anzugeben, um einen kombinierten Schlüssel zu erhalten. Wie man sieht, ist der Code noch an das zugehörige Magento-Modul gekoppelt, wer ihn also verwenden will, muss zumindest die Fehlerbehandlung anpassen.

Erst später habe ich entdeckt, dass die Klasse SplFileObject aus der Standard PHP Library (SPL) bereits als Iterator für CSV-Dateien fungieren kann, es hätte also gut als Basis-Klasse herhalten können.

$file = new SplFileObject("data.csv");
$file->setFlags(SplFileObject::READ_CSV);
$file->setCsvControl('|');
foreach ($file as $row) {
    list ($fruit, $quantity) = $row;
    // Do something with values
}

Quelle: http://de.php.net/manual/en/splfileobject.setcsvcontrol.php

Außerdem gibt es aus der Reihe der wirklich guten League of Extraordinary Packages ein Package für CSV-Verarbeitung, das auf SplFileObject aufbaut. Die Funktion aus Varien_FIle_Csv, eine Spalte als Array-Schlüssel auszuwählen, bringen allerdings beide nicht von Haus aus mit.

Vergleich

Steilen wir die Klassen von Varien und der PHP League einmal gegenüber, zusammen mit SPL only:

Varien_File_Csv SplFileObject League\Csv
Sequentielles Lesen x x
Sequentielles Schreiben x x
Spalte als Schlüssel definieren x
Enclosures erzwingen x
Iterator Interface x x
Generator Interface
Filter x

Fazit

Wer die Features von Varien_File_Csv benötigt, also erzwingen von Enclosures oder Spalten als Array-Schlüssel, ist besser damit bedient, sie selber zu implementieren und soweit möglich auf SplFileObject oder League\Csv zurückzugreifen.

One Reply to “CSV-Verarbeitung in Magento”

  1. Just stumble on your blog post. As the maintainer of League\Csv I should point that
    since version 7.0, League\Csv has been able to enforce enclosure. I’ve written a blog post about it on my blog.

    And with the release of version 8.0, the package has been update and should be able to accomplish everything you’ve noted in your table. Funny enough, the changes where made without me knowing about Varien_File_Csv since I’m not a Magento user to begin with.

Comments are closed.