Nützliche Codeschnipsel – Arbeitstage zwischen zwei Datumsangaben mit Zend_Date berechnen

Hier mal eine eine kleine Klasse, um die Anzahl zwischen zwei Datumsangaben zu berechnen.
Es werden allerdings nur bundesweite Feiertage berücksichtigt. Wenn man auch Feiertage in einzelnen Bundesländern berücksichtigen möchte, kann man einfach die Variable $_nationalHolidays entsprechend anpassen.
Achtung: Die Klasse setzt eine Umgebung voraus, in der das Zend Framework per Autoload geladen werden kann.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class MyCalenderUtils
{
 
    const DATE_FORMAT_DAY = 'dd.MM';
 
    /**
     * @var array
     */
    protected $_holidays = array();
 
    /**
     * @var array
     */
    protected $_nationalHolidays = array(
        '01.01', // Neujahr
        '01.05', // 1. Mai
        '03.10', // Tag der Einheit
        '25.12', // Weihnachten
        '26.12', // Weihnachten
    );
 
    /**
     * @param $year
     * @return $this
     */
    public function setHolidays($year)
    {
        $this->_holidays[$year] = array_merge($this->_nationalHolidays, $this->getEasterDays($year));
        return $this;
    }
 
    /**
     * @param $year
     * @return array
     */
    public function getHolidays($year)
    {
        if (!isset($this->_holidays[$year])) {
            $this->setHolidays($year);
        }
        return $this->_holidays[$year];
    }
 
    /**
     * @param $year
     * @return array
     */
    public function getEasterDays($year)
    {
        // ab PHP 5.3. könnte man auch die DateTime API von PHP nutzen
        $eastern          = new Zend_Date(easter_date($year));
        $a_eastern_days   = array();
        $a_eastern_days[] = $eastern->copyPart(null)->subDay(2)->toString(self::DATE_FORMAT_DAY); // Karfreitag
        $a_eastern_days[] = $eastern->copyPart(null)->addDay(1)->toString(self::DATE_FORMAT_DAY); // Ostermontag
        $a_eastern_days[] = $eastern->copyPart(null)->addDay(39)->toString(self::DATE_FORMAT_DAY); // Himmelfahrt
        $a_eastern_days[] = $eastern->copyPart(null)->addDay(50)->toString(self::DATE_FORMAT_DAY); // Pfingstmontag
        return $a_eastern_days;
    }
 
    /**
     * @param Zend_Date $date
     * @return bool
     */
    public function isHoliday(Zend_Date $date)
    {
        if (in_array($date->toString(self::DATE_FORMAT_DAY), $this->getHolidays($date->toString(Zend_Date::YEAR)))) {
            return true;
        }
        return false;
    }
 
    /**
     * @param $date
     * @return bool
     */
    public function isWeekend($date)
    {
        // nur Samstag und Sonntag beginnen mit einen 'S' ;-)
        if ($date->toString(Zend_Date::WEEKDAY_NARROW) == 'S')
        {
            return true;
        }
        return false;
    }
 
    /**
     * @param Zend_Date $date
     * @return bool
     */
    public function isWorkday(Zend_Date $date)
    {
        // prüfen ob Wochenende oder Feiertag
        if ($this->isWeekend($date) || $this->isHoliday($date)) {
            return false;
        }
        return true;
    }
}

Beispiel für die praktische Anwendung der Klasse:

1
2
3
4
5
6
7
8
9
10
$dateFrom = new Zend_Date('2014-03-10');
$dateTo   = new Zend_Date('2014-04-30');
$dayCounter = 0;
$myCalenderUtils = new MyCalenderUtils();
while ($dateTo->isLater($dateFrom->addDay(1))) {
    if ($myCalenderUtils->isWorkday($dateFrom)) {
       $dayCounter++;
    }
};
print "Arbeitstage: " . $dayCounter . "\n";
Veröffentlicht unter PHP | Verschlagwortet mit , , , , | Hinterlasse einen Kommentar

Magento – fehlende Übersetzungen im Developer Mode

In Magento gibt es das Problem/Feature, das bei eingeschaltetem Developer Mode einige Übersetzungen nicht funktionieren (das ist natürlich nur relevant, sofern man keinen englischsprachigen Shop betreibt).
Den Developer Mode kann man aktivieren, in dem man z.B. beim Apache in der Vhost Datei die Environment Variable wie folgt setzt:

1
SetEnv MAGE_IS_DEVELOPER_MODE "1"

Wenn der Developer Mode eingeschaltet ist, werden z.B. die Exceptions nicht geloggt, sondern direkt „geworfen“, da Magento einen eigenen Error Handler definiert hat.
Siehe Datei: app/code/core/Mage/Core/functions.php

1
2
3
4
5
6
$errorMessage .= ": {$errstr}  in {$errfile} on line {$errline}";
if (Mage::getIsDeveloperMode()) {
   throw new Exception($errorMessage);
} else {
   Mage::log($errorMessage, Zend_Log::ERR);
}

Beim Entwickeln ist es natürlich von Vorteil, wenn man Fehler direkt sieht und nicht immer erst im Logfile nachschauen muss.
Wer wissen möchte welche Auswirkungen der Devolper Mode noch hat, sollte einfach mal im Magento Code nach „Mage::getIsDeveloperMode“ suchen.

Allerdings führt das Einschalten des Developer Modes auch zu einem unschönen Nebeneffekt, und zwar werden selbst definierte Übersetzungstexte nicht mehr angezeigt, sofern sie bereits in einem anderen Modul definiert wurden.

Funktionsweise des Translation Moduls

Magento speichert alle Übersetzungstexte im Model Mage_Core_Model_Translate. Das Einlesen der CSV Files mit den Übersetzungen für die einzelnen Module funktioniert wird folgt:
Beim Laden der Module wird in der Init Funktion der Klasse Mage_Core_Model_Translate die Funktion _loadModuleTranslation aufgerufen.
Siehe: Mage_Core_Model_Translate

1
2
3
4
5
6
7
8
9
10
11
public function init($area, $forceReload = false)
{
    ...
    $this->_data = array();
 
    foreach ($this->getModulesConfig() as $moduleName=>$info) {
        $info = $info->asArray();
        $this->_loadModuleTranslation($moduleName, $info['files'], $forceReload);
    }
    ...
}

In dieser Funktion wird dann das jeweilige CSV File eingelesen

1
2
3
4
5
6
7
8
protected function _loadModuleTranslation($moduleName, $files, $forceReload=false)
{
    foreach ($files as $file) {
        $file = $this->_getModuleFilePath($moduleName, $file);
        $this->_addData($this->_getFileData($file), $moduleName, $forceReload);
    }
    return $this;
}

In der Funktion _addData werden alle Übersetzungstexte aus dem unterschiedlichen Quellen (CSV, Files, Datenbank, XML Files) als Array in der Variable $_data gespeichert.
In $data werden die zu übersetzenden Texte als Array übergeben, Scope enthält standardmäßig den Namespace und den Namen des Moduls (Namespace_Modulename).
Siehe Datei: app/code/core/Mage/Core/Model/Translate.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
protected function _addData($data, $scope, $forceReload=false)
{
	foreach ($data as $key => $value) {
	    if ($key === $value) {
		continue;
	    }
	    $key    = $this->_prepareDataString($key);
	    $value  = $this->_prepareDataString($value);
 
	    if ($scope && isset($this->_dataScope[$key]) && !$forceReload ) {
		/**
		 * Checking previos value
		 */
		$scopeKey = $this->_dataScope[$key] . self::SCOPE_SEPARATOR . $key;
		if (!isset($this->_data[$scopeKey])) {
		    if (isset($this->_data[$key])) {
		        $this->_data[$scopeKey] = $this->_data[$key];
		        /**
		         * Not allow use translation not related to module
                         */
		        if (Mage::getIsDeveloperMode()) {
		            unset($this->_data[$key]);
		        }
		    }
		}
		$scopeKey = $scope . self::SCOPE_SEPARATOR . $key;
		$this->_data[$scopeKey] = $value;
	    }
	    else {
		$this->_data[$key]     = $value;
		$this->_dataScope[$key]= $scope;
	    }
	}
	return $this;
}

Die Übersetzungen in den Templates werden über die Funktion Mage_Core_Block_Abstract::__() geladen:
Mage_Core_Block_Abstract

1
2
3
4
5
6
7
public function __()
{
    $args = func_get_args();
    $expr = new Mage_Core_Model_Translate_Expr(array_shift($args), $this->getModuleName());
    array_unshift($args, $expr);
    return Mage::app()->getTranslator()->translate($args);
}

Mage_Core_Model_Translate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected function _getTranslatedString($text, $code)
{
    $translated = '';
    if (array_key_exists($code, $this->getData())) {
        $translated = $this->_data[$code];
    }
    elseif (array_key_exists($text, $this->getData())) {
        $translated = $this->_data[$text];
    }
    else {
        $translated = $text;
    }
    return $translated;
}

Das Problem mit der fehlenden Übersetzung

Das oben beschriebene Problem tritt bei den Übersetzungsvariablen auf, die in mehrere Modulen definiert sind.
Das heißt im Developer Mode werden die Übersetzungen von Texten, die in mehreren CSV Files von unterschiedlichen Modulen enthalten sind, unter einem anderen Namensraum (Scope) gespeichert.
Beim Parsen der CSV Datei des ersten gefundenen Moduls wird der Wert im Array $_data gespeichert. Beim Verarbeiten der CSV Datei des zweiten Moduls wird der Wert wieder gelöscht:

if (Mage::getIsDeveloperMode()) {
   unset($this->_data[$key]);
}

Anschließend wird es mit unter einem anderen Scope im Array $_data gespeichert.

$scopeKey = $scope . self::SCOPE_SEPARATOR . $key;
$this->_data[$scopeKey] = $value;

Beispiel:

Um das Ganze mal zu verdeutlichen, hier mal ein Beispiel anhand des Textes „Loading next step…“, der sowohl in einem Core Modul, als auch in einem eigenen Modul übersetzt wurde:

1. Aufruf der Funktion Mage_Core_Model_Translate::_addData beim Laden des Core Moduls Mage_Checkout
Übersetzungfile: „/var/www/VaaTrunk/app/locale/de_DE/Mage_Checkout.csv“
Wert der einzelnen Variablen in der Funktion _addData

$key:      "Loading next step..."		
$scope:    "Mage_Checkout"
$value:    "Nächster Schritt wird geladen ..."

Nach dem Speichern im Translation Array

$scopeKey: "Mage_Checkout::Loading next step..."
$this->_data["Loading next step..."]  = "Nächster Schritt wird geladen ...";
$this->_dataScope["Loading next step..."]= "Mage_Checkout";

2. Aufruf der Funktion Mage_Core_Model_Translate::_addData beim Laden des eigenen Moduls Namespace_Modulname
Übersetzungfile: „/var/www/VaaTrunk/app/locale/de_DE/Namespace_Modulname.csv“

$key:      "Loading next step..."		
$scope:    "Namespace_Modulname"
$scopeKey: "Mage_Checkout::Loading next step..."

Da der Wert bereits unter einen anderen Scope vorhanden ist, wird er wieder gelöscht:

unset($this->_data["Loading next step..."]);

Werte nach dem Löschen

$scopeKey: "Namespace_Modulname::Loading next step..."
$value:    "Lade nächsten Schritt..."

Anschließend wird der Wert mit dem neuen Scope Key im Übersetzungs Array gespeichert

$this->_data["Mage_Checkout::Loading next step..."] = "Lade nächsten Schritt...";

Wenn man jetzt also im Template den String mit der Übersetzungsfunktion $this->__(‚Loading next step…‘) laden will, kann die Übersetzung nicht geladen werden, da der Text ja jetzt mit dem Scope Key „Namespace_Modulname::Loading next step…“ gespeichert ist.
Wird ein Wert im _data Array nicht gefunden, so wird der original String, der an die Übersetzungfunktion übergeben wurde, verwendet.
Mann müsste ihn also wie folgt laden, damit es funktioniert:

$this->__('Namespace_Modulname::Loading next step...')

Ob das jetzt ein Bug oder ein Feature ist, muss jeder selbst entscheiden.

Veröffentlicht unter Magento | Verschlagwortet mit , , , | Hinterlasse einen Kommentar

Magento – Einen Block cachen

Magento enthält bereits in der Commmunity Version ein sehr umfangreiches Cache Modul.
So werden standardmäßig Konfigurationsparameter, Layout Xmls, Übersetzungen, Daten von Collections oder die Ausgabe einzelner Blöcke gecachet, sofern der Cache einschaltet ist.
Als Cache Backend kann dabei z.B. einfach das Filesystem, APC oder Memcache dienen. Um den Nutzen zu optimieren, sollte man natürlich versuchen, den Cache komplett im Hauptspeicher vorzuhalten.

In der Enterprise Version ist außerdem ein Full Page Cache Modul enthalten, mit dem nochmal erhebliche performance Steigerungen erzielt werden können, da im Idealfall die komplette Seite (bis auf wenige Platzhalter, wie z.B. die Anzeige der Anzahl der Artikel im Warenkorb) aus dem Cache geladen wird. Das heißt, es muss nicht mehr der komplette Dispatch Prozess durchlaufen werden, was die gecachten Seiten natürlich wesentlich schneller macht.
Die Funktionsweise des Fullpage Cache ist sicherlich einen eigenen Artikel wert und soll an dieser Stelle nicht weiter erwähnt werden.

Aber zurück zum Cache Modul der Community Version, insbesondere dem „Blocks HTML output“ Cache.
Auch wenn man es vermuten könnte, standardmäßig wird die HTML Ausgabe eines Blocks in Magento nicht gecachet. Man muss explixit angeben, dass ein Block gecachet werden soll.
In der Community Version von Magento werden in der in der aktuellen Version (1.7.) lediglich folgende Blöcke gecachet:

  • Mage_Catalog_Block_Navigation
  • Mage_Catalog_Block_Product_New
  • Mage_Page_Block_Html_Footer

Um die HTML Ausgabe eines Blocks zu cachen, gibt es zwei Vorgehensweisen (siehe Magento Wiki):

1. Cache für einen Block per Layout XML aktivieren

1
2
3
4
5
6
7
8
9
<layout version="0.1.0">
    <modulname-blockname>
        <reference name="content">
            <block name="modulname-blockname" type="namespace_modulname/blockclass" />
            <action method="setCacheLifetime"><sec>86400</sec></action>
            <!--<action method="setCacheKey"><key>mycachekey</key></action>-->
        </reference>
    </modulname-blockname>
</layout>

2. Cache im Konstruktor des Blocks aktivieren

1
2
3
4
5
6
7
8
9
10
class Namespace_Modulname_Block_Blockname extends Mage_Core_Block_Template
{
    protected $_template = 'namespace/modulname/blockname.phtml';
    protected function _construct()
    {
      $this->addData(array(
            'cache_lifetime'    => 64000,
            'cache_tags'        => array(Mage_Cms_Model_Page::CACHE_TAG), 
       ));
}

Die Cache Lifetime wird dabei in Sekunden angegeben. Wenn man keinen Cache Tag angibt, wird standardmäßig „block_html“ verwendet.
Siehe: Mage_Core_Block_Abstract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Mage_Core_Block_Abstract extends Varien_Object
{
    /**
     * Cache group Tag
     */
    const CACHE_GROUP = 'block_html';
 
    ...
    public function getCacheTags()
    {
        if (!$this->hasData('cache_tags')) {
            $tags = array();
        } else {
            $tags = $this->getData('cache_tags');
        }
        $tags[] = self::CACHE_GROUP; // Standard Cache Tag hinzufügen
        return $tags;
    }

Die Angabe des Cache Tags ist entscheidend, wenn es darum geht, wann der Cache geleert wird.
Im Magento Backend kann man entweder den kompletten Cache oder einzelne Caches leeren.

Folgende Cache Tags sind standardmäßig vorhanden:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Mage_Api_Model_Config::CACHE_TAG                 = 'config_api';
Mage_Adminhtml_Block_Page_Menu::CACHE_TAGS       = 'BACKEND_MAINMENU';
Mage_Api2_Model_Config::CACHE_TAG 		 = 'CONFIG_API2';
Mage_Catalog_Model_Category::CACHE_TAG           = 'catalog_category';
Mage_Catalog_Model_Product_Type_Price::CACHE_TAG = 'PRODUCT_PRICE';
Mage_Catalog_Model_Product::CACHE_TAG            = 'catalog_product';
Mage_CatalogSearch_Model_Query::CACHE_TAG        = 'SEARCH_QUERY';
Mage_Cms_Model_Block::CACHE_TAG     		 = 'cms_block';
Mage_Cms_Model_Page::CACHE_TAG                   = 'cms_page';
Mage_Core_Model_App::CACHE_TAG                   = 'MAGE';
Mage_Core_Model_Config::CACHE_TAG                = 'CONFIG';
Mage_Core_Model_Resource_Db_Collection_Abstract::CACHE_TAG = 'COLLECTION_DATA';
Mage_Core_Model_Store_Group::CACHE_TAG           = 'store_group';
Mage_Core_Model_Store::CACHE_TAG                 = 'store';
Mage_Core_Model_Translate::CACHE_TAG             = 'translate';
Mage_Core_Model_Website::CACHE_TAG               = 'website';
Mage_Eav_Model_Entity_Attribute::CACHE_TAG       = 'EAV_ATTRIBUTE';
Mage_Rss_Block_Catalog_NotifyStock::CACHE_TAG    = 'block_html_rss_catalog_notifystock';
Mage_Rss_Block_Catalog_Review::CACHE_TAG         = 'block_html_rss_catalog_review';
Mage_Rss_Block_Order_New::CACHE_TAG              = 'block_html_rss_order_new';

Verwendet man also das Standard Cache Tag für die Blöcke, so wird der Cache Eintrag des Blocks geleert, wenn der Block Cache geleert wird, oder natürlich wenn der gesamte Cache geleert wird.

Laden des Blocks aus dem Cache

Ob ein Block aus dem Cache geladen werden kann, wird dann hier überprüft:
Mage_Core_Block_Abstract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final public function toHtml()
{
    Mage::dispatchEvent('core_block_abstract_to_html_before', array('block' => $this));
    if (Mage::getStoreConfig('advanced/modules_disable_output/' . $this->getModuleName())) {
        return '';
    }
    $html = $this->_loadCache();
    if ($html === false) {
        // Der Block wurde nicht im Cache gefunden
        $translate = Mage::getSingleton('core/translate');
        /** @var $translate Mage_Core_Model_Translate */
        if ($this->hasData('translate_inline')) {
            $translate->setTranslateInline($this->getData('translate_inline'));
        }
 
        $this->_beforeToHtml();
        $html = $this->_toHtml();
        $this->_saveCache($html);
 
        if ($this->hasData('translate_inline')) {
            $translate->setTranslateInline(true);
        }
    }
    $html = $this->_afterToHtml($html);

Cache Inhalt laden
Mage_Core_Block_Abstract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected function _loadCache()
{
    if (is_null($this->getCacheLifetime()) || !Mage::app()->useCache(self::CACHE_GROUP)) {
        return false;
    }
    $cacheKey = $this->getCacheKey();
    /** @var $session Mage_Core_Model_Session */
    $session = Mage::getSingleton('core/session');
    $cacheData = Mage::app()->loadCache($cacheKey);
    if ($cacheData) {
        $cacheData = str_replace(
            $this->_getSidPlaceholder($cacheKey),
            $session->getSessionIdQueryParam() . '=' . $session->getEncryptedSessionId(),
            $cacheData
        );
    }
    return $cacheData;
}

Das Cache Modul ist also sehr einfach zu verwenden und da Magento nicht unbedingt für seine gute Performance bekannt ist, sollte man den Cache eigentlich überall nutzen wo es möglich ist.
Und immer dran denken: Cache me if you can !

Veröffentlicht unter Magento | Verschlagwortet mit , , | Hinterlasse einen Kommentar

Magento: Ein einzelnes Produkt laden

Bei Magento gibt es mehrere Möglichkeiten ein einzelnes Produkt Objekt zu laden.

1. Laden anhand der ID

1
2
3
$product = Mage::getModel('catalog/product')
                        ->setStoreId(Mage::app()->getStore()->getId())
                        ->load($productId);

2. Laden anhand eine Attributs

1
2
3
4
$product = Mage::getModel('catalog/product')
                        ->setStoreId(Mage::app()->getStore()->getId())
                        ->loadByAttribute($attributeName, $attributeValue)
						->load();

Hierbei ist zu beachten, das eigentlich die Product Collection geladen wird und einfach das erste Objekt der Collection zurückgegeben wird.
Siehe: Mage_Catalog_Model_Abstract

1
2
3
4
5
6
7
8
9
10
11
public function loadByAttribute($attribute, $value, $additionalAttributes = '*')
{
    $collection = $this->getResourceCollection()
        ->addAttributeToFilter($attribute, $value)
        ->setPage(1,1);
 
    foreach ($collection as $object) {
        return $object;
    }
    return false;
}

Wichtig: bei der zweiten Variaten werden nur die reinen Produktdaten geladen.
Bei der ersten Variaten wird das Objekt von diversen Observern mit weiteren Daten angereichert, da die _beforeLoad und die _afterLoad Funktion automatisch aufgerufen werden.

Siehe: Mage_Catalog_Model_Abstract

1
2
3
4
5
6
7
8
9
public function load($id, $field=null)
{
    $this->beforeLoad($id, $field);
    $this->_getResource()->load($this, $id, $field);
    $this->_afterLoad();
    $this->setOrigData();
    $this ->_hasDataChanges = false;
    return $this;
}

In der Funktion _afterLoad werden dabei folgende Events gefeuert.

1
2
3
4
5
6
protected function _afterLoad()
{
    Mage::dispatchEvent('model_load_after', array('object'=>$this));
    Mage::dispatchEvent($this ->_eventPrefix.'_load_after', $this->getEventData());
    return $this;
}

Das Event „catalog_product_load_after“ wird z.B. vom CatalogInventory Observer überwacht
Siehe: /app/code/core/Mage/CatalogInventory/etc/config.xml

1
2
3
4
5
6
7
8
9
10
11
<events>
    <catalog_product_load_after>
        <observers>
            <inventory>
                <class>cataloginventory/observer</class>
                <method>addInventoryData</method>
            </inventory>
        </observers>
    </catalog_product_load_after>
...
</events>

Siehe: Mage_CatalogInventory_Model_Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
public function addInventoryData($observer)
{
    $product = $observer->getEvent()->getProduct();
    if ($product instanceof Mage_Catalog_Model_Product) {
        $productId = intval($product->getId());
        if (!isset($this->_stockItemsArray[$productId])) {
            $this->_stockItemsArray[$productId] = Mage::getModel('cataloginventory/stock_item');
        }
        $productStockItem = $this->_stockItemsArray[$productId];
        $productStockItem->assignProduct($product);
    }
    return $this;
}

Dabei wird in der Funktion „assignProduct“ das StockItem Objekt im Produkt Objekt gespeichert:

Siehe: Mage_CatalogInventory_Model_Stock_Item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function assignProduct(Mage_Catalog_Model_Product $product)
{
    if (!$this->getId() || !$this->getProductId()) {
        $this->_getResource()->loadByProductId($this, $product->getId());
        $this->setOrigData();
    }
 
    $this->setProduct($product);
    $product->setStockItem($this);
 
    $product->setIsInStock($this->getIsInStock());
    Mage::getSingleton('cataloginventory/stock_status')
        ->assignProduct($product, $this->getStockId(), $this->getStockStatus());
    return $this;
}

Fazit

Man sollte also immer daran denken, dass wenn man ein Produkt per „loadByAttribute“ lädt, eventuell nicht alle Daten im Produkt Objekt geladen werden, die man im jeweiligen Kontext benötigt.
Das gilt übrigens generell, wenn man ein Objekt per Collection lädt. Wenn man über Objekte einer Collection iteriert, werden die Funktionen _beforeLoad und _afterLoad der geladenen Objekte nicht aufgerufen.

Veröffentlicht unter Magento | Verschlagwortet mit , , | Hinterlasse einen Kommentar

Magento – Zeitgeist

Da am Ende eines Jahres des öfteren der Google Zeitgeist durch die Presse wandert, habe auch ich mir mal einen Trend angesehen und zwar den für den Suchbegriff „Magento“.
Interessant ist dabei, das weltweit die meisten Anfragen aus Indien kommen. Eine Interpretation spare ich mir jetzt mal an dieser Stelle.

Magento Zeitgeist

Veröffentlicht unter Magento | Verschlagwortet mit , | Hinterlasse einen Kommentar

Magento – SEO Optimierung – Suchergebnisseite

Aus Gründen der Suchmaschinenoptimierung ist es besser, wenn man das Robots Tag auf der Suchergebnisseite auf „noindex,follow“ setzt. Man kann bei Magento zwar das Robots Tag über das Backend einstellen (System->Konfiguration->Gestaltung->HTML Kopf->Standard-Robots), allerdings gilt das für den gesamten Shop.
Um auf einzelnen Unterseiten das Robots Tag zu ändern, kann man aber die Einstellung bequem per Layout XML machen.

Beispiel: Anpassung des Robots Tags auf der Suchergebnisseite im File catalogsearch.xml

1
2
3
4
5
<catalogsearch_result_index translate="label">
   <reference name="head">
      <action method="setRobots"><value>NOINDEX,FOLLOW</value></action>
   </reference>
</catalogsearch_result_index>
Veröffentlicht unter Magento | Verschlagwortet mit , , , | Hinterlasse einen Kommentar

Magento – Nützliche Codeschnipsel – Http oder Https ?

Mit folgendem Code kann man in Magento überprüfen, ob der aktuelle Seitenaufruf per SSL erfolgt ist:

1
2
3
4
5
if (Mage::app()->getStore()->isCurrentlySecure()) {
   // Aufruf per https://
} else {
   // Aufruf per http://
}
Veröffentlicht unter Magento | Verschlagwortet mit , , , , | Hinterlasse einen Kommentar

Magento – Volltextsuche optimieren

In Magento gibt es einen ziemlich fiesen Bug bei der Volltextsuche.
Das Problem ist, dass die Ergebnisse bei der Volltextsuche zwar nach der Relevanz der einzelnen Suchergebnisse sortiert wird, allerdings ist die Relevanz der Ergebnisse immer ‚1‘ und somit die Sortierung ziemlich unnütz. Das führt dazu, das häufig Produkte am Anfang der Suchergebnisseite stehen, nach denen der User gar nicht direkt gesucht hat.
Die Ursache für das Problem liegt in der SQL-Abfrage, mit der in Magento der Volltextindex abgefragt wird.
Die SQL Abfrage, die Magento bei der Volltextsuche ausführt, sieht wie folgt aus (wobei der relevante Teil das Select Statement ist):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSERT INTO `catalogsearch_result` 
 SELECT 
 12345 AS `query_id`, 
 `s`.`product_id`, 
 MATCH (s.data_index)
 AGAINST ("suchbegriff1 suchbegriff2" IN BOOLEAN MODE) AS `relevance` 
 FROM `catalogsearch_fulltext` AS `s`
 INNER JOIN `catalog_product_entity` AS `e` 
 ON e.entity_id = s.product_id 
 WHERE (s.store_id = 1) 
 AND 
 (MATCH (s.data_index) 
 AGAINST ("suchbegriff1 suchbegriff2" IN BOOLEAN MODE)) 
 ON DUPLICATE KEY UPDATE `relevance` = VALUES(`relevance`)

Wenn man diese Abfrage mal direkt in der Magento Datenbank ausführt sieht man, dass der Wert „relevance“ für jede Ergebniszeile „1“ ist.
Das liegt daran, dass der Wert „relevance“ wie folgt ermittelt wird:

1
2
3
4
...
MATCH (s.data_index)
 AGAINST ("suchbegriff1 suchbegriff2" IN BOOLEAN MODE) AS `relevance` 
...

Das Problem hierbei ist der Zusatz „IN BOOLEAN MODE“. Wenn man diesen Zusatz in der Spaltendefinition der Datenbankabfrage weglässt, so wird die Relevanz richtig ermittelt.
Also muss man die Abfrage wie folgt ändern:

1
2
3
4
...
MATCH (s.data_index)
 AGAINST ("suchbegriff1 suchbegriff2") AS `relevance` 
...

Weitere Infos dazu findet man in der offiziellen Mysql Doku.
Leider befindet sich der entscheidende Hinweise zu obigem „Phänomen“ nur in dem Kommtentar von Patrick O’Lone.

Die Ursachen für das Problem liegt an folgender Stellen im Magento Core:
Datei: Mage_CatalogSearch_Model_Resource_Helper_Mysql4

1
2
3
4
5
6
    public function chooseFulltext($table, $alias, $select)
    {
        $field = new Zend_Db_Expr('MATCH ('.$alias.'.data_index) AGAINST (:query IN BOOLEAN MODE)');
        $select->columns(array('relevance' => $field));
        return $field;
    }

Hier wird in der Variable $field der "Match Against" Ausdruck für die Volltextsuche definiert und sowohl in der „Column“-Definition als auch in der „Where“-Bedingung genutzt.
Um das Problem zu lösen, muss man den Code nur wie folgt ändern, so dass in der „Column“-Definition der Abfrage der Relevance ohne Boolen Mode berechnet wird:

1
2
3
4
5
6
// Use 'column' part without boolean mode
$field = new Zend_Db_Expr('MATCH ('.$alias.'.data_index) AGAINST (:query)');
$select->columns(array('relevance' => $field));
// Use 'where' part with boolean mode
$field = new Zend_Db_Expr('MATCH ('.$alias.'.data_index) AGAINST (:query IN BOOLEAN MODE)');
return $field;

Nun wird die Relevanz der einzelnen Suchergebnisse richtig berechnet.

Achtung: Die Änderungen sollten natürlich wie immer nicht direkt im Magento Core, sondern in einem eigenen Modul gemacht werden, dass das CatalogSearch Modul erweitert.

Es kann sein, dass das bei aktuellen Mysql Versionen nicht mehr auftritt, feststellen konnte ich den Bug bei folgendem Setup:

  • Magento Community Version: 1.7.0.2
  • Magento Enterprise Version: 1.12.0.2
  • Mysql Version: 5.5.25
Veröffentlicht unter Magento | Verschlagwortet mit , , , , , , | Hinterlasse einen Kommentar

Magento – Alle Datenbankabfragen loggen

Spätestens wenn man anfängt mit Magento zu arbeiten, sollte man sich intensiv mit einem Debugger, z.B. Zend Debugger oder XDebug befassen.
Mit dem guten alten Debugging per DIE('hier kommt meißtens eine total lustige Debug Nachricht...') oder der Ausgabe von Debug Infos in Logfiles wird man bei Magento auf lange Sicht keine große Freude haben.

Allerdings ist es manchmal auch nützlich, wenn man bestimmte Infos im Hintergrund in eine Logdatei schreibt, z.B. wenn man wissen möchte, wie viele und vor allem welche Datenbankabfragen Magento auf einer bestimmten Seite macht. Da wird sich der ein oder andere sicher wundern, es können nämlich schon mal schnell über hundert Datenbankabfragen pro Seite sein. Da hilft einem ein Debugger oder der Magento Profiler nur bedingt weiter.

Es gibt aber eine einfache Methode, um alle Queries mitzuschreiben, da Magento die Möglichkeit bereits integriert hat.
Man muss einfach nur in der Datei lib\Varien\Db\Adapter\Pdo\Mysql.php folgende Anpassungen machen.
1. Debug Modus einstellen

1
protected $_debug               = true;

2. Einstellen, dass wirklich alles Quries geloggt werden sollen

1
protected $_logAllQueries       = true;

Würde man den zweiten Wert nicht auf true stellen, so würden nur die Queries geloggt, die eine bestimmte Ausfürungszeit überschreiten.
Die minimale Ausführungszeit für das Logging ist dabei in folgender Variable gespeichert:

1
protected $_logQueryTime        = 0.05;

Sind diese Einstellungen gemacht, so werden alle Datenbankabfragen in die Datei geschrieben, die in folgender Variable definiert ist:

1
protected $_debugFile           = 'var/debug/pdo_mysql.log';

Standardmäßig wird die Datei also in diesem Verzeichnis unterhalb des Magento Root Pfads angelegt, man kann natürlich auch einen eigenen Pfad definieren.

Sind die Einstellungen entsprechend gemacht, so werden dann alle Datenbankabfragen in folgenden Format in der angegeben Datei gespeichert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## 2012-10-18 12:01:29
## 14485 ## QUERY
SQL: INSERT INTO `log_visitor` (`session_id`, `first_visit_at`, `last_visit_at`, `last_url_id`, `store_id`) VALUES (?, ?, ?, ?, ?)
BIND: array (
  0 => 'ncqvbv4ibmtom536p9p6egbv75',
  1 => '2012-10-18 12:01:29',
  2 => '2012-10-18 12:01:29',
  3 => 0,
  4 => '1',
)
AFF: 1
TIME: 0.0007
 
## 2012-10-18 12:01:29
## 14485 ## QUERY
SQL: DESCRIBE `log_visitor_info`
AFF: 7
TIME: 0.0068

Das heißt man sieht genau, wann die Abfrage ausgeführt, wie lange sie gedauert hat und natürlich den eigentlichen SQL Befehl inklusive der jeweiligen Paramter.

ACHTUNG: Auf dem Live System sollte man das Debugging natürlich nicht längere Zeit angeschaltet lassen, da die Datei mitunter recht schnell sehr groß wird, ja nachdem wie viel Traffic man auf der Seite hat.

Veröffentlicht unter Magento | Verschlagwortet mit , , , , , | Hinterlasse einen Kommentar

Magento – Nützliche Codeschnipsel

Es gibt mehrere Möglichkeiten, um die im Backend definierten erlaubten Länder auslesen. Hier drei einfache Beispiel:

  • 1. Variante: Auslesen der Config Variable
  • 1
    
    $allowedCountries = explode(',', (string)Mage::getStoreConfig('general/country/allow'));
  • 2. Variante: Per Directory Helper
  • 1
    2
    
    $countrys = Mage::helper('directory')->getCountryCollection()
          ->toOptionArray();
  • 3. Variante: Per Collection
  • 1
    2
    3
    
    $countryCollection = Mage::getSingleton('directory/country')
          ->getResourceCollection()
          ->loadByStore();
    Veröffentlicht unter Magento | Verschlagwortet mit , , , , | Hinterlasse einen Kommentar