Türchen 02: Fehler mit der Vergleichsliste vermeiden

Die Vergleichsliste in Magento ist ein gern verwendetes Feature. Bei meiner Arbeit mit Magento und der Vergleichliste bin ich auf einen Design Fehler der CompareCollection gestoßen. Jedes mal wenn der Google-Bot kommt, erzeugt das Shop-System Fehlerberichte in var/reports. Die Fehlermeldung lautet:

(Mage_Catalog_Model_Product) with the same id "XXX" already exist

Der Normale Shopbetrieb war davon nicht betroffen. Also muss es etwas mit Google (Fehlerzeit im Log stimmte mit den Zugriffslogs vom Apache überein.) zu tun haben.

Seit Version 1.4 ist Magento in der Lage Crawler zu erkennen und diese beim Tracking durch das Log Modul zu ignorieren. Siehe:

<config>
    <modules>
    <Mage_Log>
        <version>0.7.7</version>
    </Mage_Log>
    </modules>
    <global>
        <ignore_user_agents>
            <google1>Googlebot/1.0 (googlebot@googlebot.com http://googlebot.com/)</google1>
            <google2>Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)</google2>
            <google3>Googlebot/2.1 (+http://www.googlebot.com/bot.html)</google3>
        </ignore_user_agents>
    ...

Der Visitor wird einfach nicht erzeugt und erhält die ID 0. Soweit alles kein Problem. So lange man keine Collection der Vergleichsobjekte aufruft. Aber warum sollte das ein Problem sein? Werfen wir einen Blick auf die Tabelle

<table_prefix>catalog_compare_item
. Diese verlangt entweder eine
visitor_id
oder eine
customer_id
. Wenn eine Kunde eingeloggt ist, dann wird die
visitor_id
einfach auf 0 gesetzt. Erinnern wir uns an dass was passiert wenn ein Crawler den Shop aufruft. Genau er bekommt die
visitor_id = 0
. Wenn wir nun eine Collection der Produkte in der Vergleichliste holen, holt Magento automatisch alle Produkte von allen Kunden. Glaubt ihr nicht? Sehen wir uns den Helper (
Mage_Catalog_Helper_Product_Compare
) an:

/**
     * Retrieve compare list items collection
     *
     * @return Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Compare_Item_Collection
     */
    public function getItemCollection()
    {
        if (!$this->_itemCollection) {
            $this->_itemCollection = Mage::getResourceModel('catalog/product_compare_item_collection')
                ->useProductItem(true)
                ->setStoreId(Mage::app()->getStore()->getId());

            if (Mage::getSingleton('customer/session')->isLoggedIn()) {
                $this->_itemCollection->setCustomerId(Mage::getSingleton('customer/session')->getCustomerId());
            } else {
                $this->_itemCollection->setVisitorId(Mage::getSingleton('log/visitor')->getId());
            }

            Mage::getSingleton('catalog/product_visibility')
                ->addVisibleInSiteFilterToCollection($this->_itemCollection);

            /* Price data is added to consider item stock status using price index */
            $this->_itemCollection->addPriceData();

            $this->_itemCollection->addAttributeToSelect('name')
                ->addUrlRewrite()
                ->load();

            /* update compare items count */
            $this->_getSession()->setCatalogCompareItemsCount(count($this->_itemCollection));
        }

        return $this->_itemCollection;
    }

In der IF Abfrage und dem load() liegt das Problem. Mit einem Rewrite des Helpers können wir das Problem an zentraler Stelle beheben.

class Webguys_Catalog_Helper_Product_Compare extends Mage_Catalog_Helper_Product_Compare {
    public function getItemCollection() {
	$visitor = Mage::getSingleton('log/visitor');
	if ($visitor->getId() == 0) {
	     $visitor->setId(-1);
	}
	return parent::getItemCollection();
    }
}

Jetzt noch den Rewrite in einer config.xml hinterlegen und am besten in einem eigenen Modul unterbringen.

<helpers>
  <catalog>
    <rewrite>
      <product_compare>Webguys_Catalog_Helper_Product_Compare</product_compare>
    </rewrite>
  </catalog>

Ob der Rewrite funktioniert zeigt uns ein Unit-Test (Verbesserungen sind erwünscht! Bitte in die Kommentare!):

class Webguys_Model_CompareCollectionTest extends PHPUnit_Framework_TestCase {
	/** @var $_helper Mage_Catalog_Helper_Product_Compare */
	private $_helper = null;
	private $_session = null;

	public function setUp() {
		$this->_helper = Mage::helper('catalog/product_compare');
		$this->_session = Mage::getSingleton('catalog/session');
	}
	public function testVisitorNotDefinied() {
		$visitor = Mage::getSingleton('log/visitor');
		$this->assertArrayNotHasKey("id", $visitor->getData());
		$this->assertTrue($visitor->getId() == 0);
	}
	public function testCatalogSession()
	{
		$this->assertNotNull($this->_session, "Keine Session");
		$this->assertArrayNotHasKey("catalog_compare_items_count", $this->_session->getData());
	}
	public function testDuplicateEntryInCollection() {
		$collection = $this->_helper->getItemCollection();
		$this->assertEquals(-1, Mage::getSingleton('log/visitor')->getId());
		$this->assertEquals(0, count($collection));
	}
	public function testDuplicateEntryByCount() {
		try {
			$this->_helper->getItemCount();
		} catch (Exception $e) {
			$this->fail($e->getMessage());
		}
	}
	public function testCompareExceptionFix() {
		$visitor = Mage::getSingleton('log/visitor');
		if ($visitor->getId() == 0) {
			$this->_session->setData("catalog_compare_items_count", 0);
		}
		try {
			$this->_helper->getItemCollection();
		} catch (Exception $e) {
			$this->fail("Fix not working!");
		}

	}
}

Mein Fazit: Rechne mit den Schlimmsten beim Design von API's und testen, testen, testen und testen.



Ein Beitrag von Karl Spies
Karl's avatar

Karl Spies arbeitet seit 2009 mit Magento und ist seit 2011 durch Magento zertifizierter Entwickler. Er arbeitet für die SYNAXON AG. In seiner Freizeit ist er Nerd und ein Technologiejunkie. Ihr erreichet Ihn per E-Mail unter karl.spies@gmx.net.

Alle Beiträge von Karl

Kommentare
Waldemar am

Ich bin ein Magento-Newbie. Kann mir jemand die genaueren Schritte erklären? Danke!

Matthias Zeis am

Oder Fixgento.

Matthias Zeis am

Bugento ist super. ;)

Tobias Vogt am

Bugento? :)

Karl Spies am

Hey Tobi, Hey Rico,

ich werde das mal in Angriff nehmen und mit dem CompareList-Feature anfangen. Gibt es einen Wunsch für den Namespace? Ich finde hier Firegento nicht schlecht.

Viele Grüße

Karl

Tobias Vogt am

Hey Rico,

gleiche Idee hatten Karl und ich auch schon. Wir hatten sogar überlegt bei Github unter Firegento oder Fodcamp einen Schritt weiterzugehen und für eine definierte Version über einen längeren Zeitraum Bugfixes zu sammeln.

Eine Community Version die über ein zusätzlich Modul LTS-Support erhält quasi :)

Leider fehlt mir im Moment, bedingt durch den Adventskalender, die Zeit da irgendwas auf die Beine zu stellen :(

Liebe Grüße

Tobi

Rico Neitzel am

Hi Jungs,

was haltet Ihr davon einen "BugFix" Namespace zu bauen, in dem solche Fehler veröffentlicht werden und auf MagentoConnect unter einem eigenen Benutzer zur Verfügung gestellt werden? Dann könnte jeder, der möchte, völlig ohne Gewähr, die BugFixes runterladen und findet sie gleich im richtigen Ordner und funktionsfähig in seiner Instanz?

Das ganze ließe sich bestimmt super per Git vermangeln und alle sind happy?

Liebe Grüße Rico

Tobias Vogt am

Mh Torsten. Deinem Kommentar fehlt der wirkliche Inhalt. Leider bleiben Fragen welches System besser wäre und aus welchem Gründen man dort mit weniger Fehlern zu kämpfen hat ungeklärt.

Wenn wir jedoch zeigen wollen wie man Probleme löst ohne den Magento-Core zu verändern schreiben wir eigene Module die den Job übernehmen. Ich finde das gut weil es jeden Bugfix dokumentiert. Das ist gut, richtig gut.

Das du einige Kunden von Agenturen erhalten hast die sich an Magento versucht haben kann ich verstehen. Magento ist nicht einfach - für Magento braucht es Struktur und natürlich einiges an Wissen. Gerade bei Agenturen die ihr erstes Projekt mit Magento machen liegt das selten vor - das ging mir damals auch nicht anders. Kunden sind damit unzufrieden und wechseln die Agentur. Je nach Kompetenz der Agentur kommt dann entweder wieder Magento oder eben ein anderes System zum Einsatz. Das das Problem am System liegt ist aber, wie ich finde, eher albern. Es liegt in der Regel an der jeweiligen Kompetenz.

Das Magento kostenlos ist behaupten wir zumindest nicht. Wie sagen nur es kostet keine Lizenzgebühren aber erfordert natürlich trotzdem Arbeit. Lange Projektlaufzeiten kann ich aber nicht unbedingt bestätigen. Verzögerungen treten in der Regel durch Probleme im Produktsortiment oder im Workflow des Kunden auf. Die Erfahrung habe ich aber auch unabhängig von der Technologie machen dürfen.

Dennoch bin ich Stolz das wir unseren ersten Troll-Post erhalten haben. Das ist schon etwas besonderes :)

Torsten Ploigt am

Danke! Das passt hervorragend für einen Freitag Nachmittag. ;-) Ist wirklich sehr unterhaltsam zu sehen, mit welchen Bugs und Designproblemen doch Magento-Entwickler ständig so zu kämpfen haben. Statt die Bugs zu beheben, werden dann ganze Module entwickelt. Ganz so, als wenn noch mehr Komplexität die Sache stabiler gestalten würde. Naja, mir bringt das zumindest neue Kunden, ist doch so mancher schon an seiner Magento-Agentur bzw. der langen Zeit für Projektumsetzungen verzweifelt. Und an den damit verbundenen Kosten, obwohl Magento doch "kostenlos" sei. Höhö.

Vinai am

Klasse Fund - danke für dieses Türchen!

Tobias Vogt am

Hey Karl,

sehr spannende Problematik und ganz klar doch ein Bug in Magento. Toll finde ich auch den Unit-Test am Ende.

Hast du deinen Fix in Form eines Patches Magento bereitgestellt?

Liebe Grüße

Tobi

Dein Kommentar