The reason you're getting that error is the collection's query returns two rows with the same ID, and Magento collections are programmed to only allow one unique ID for each item
#File: lib/Varien/Data/Collection.php
public function addItem(Varien_Object $item)
{
$itemId = $this->_getItemId($item);
if (!is_null($itemId)) {
if (isset($this->_items[$itemId])) {
throw new Exception('Item ('.get_class($item).') with the same id "'.$item->getId().'" already exist');
}
$this->_items[$itemId] = $item;
} else {
$this->_addItem($item);
}
return $this;
}
As for the why, you're extending Mage_Reports_Model_Resource_Order_Collection
which already extends the Mage_Sales_Model_Resource_Order_Collection
class. This later class initialized the collection with the class alias sales/order
.
#File: app/code/core/Mage/Sales/Model/Resource/Order/Collection.php
protected function _construct()
{
$this->_init('sales/order');
$this
->addFilterToMap('entity_id', 'main_table.entity_id')
->addFilterToMap('customer_id', 'main_table.customer_id')
->addFilterToMap('quote_address_id', 'main_table.quote_address_id');
}
If you look at what the _init
method actually does for a collection
#File: app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php
protected function _init($model, $resourceModel = null)
{
$this->setModel($model);
if (is_null($resourceModel)) {
$resourceModel = $model;
}
$this->setResourceModel($resourceModel);
return $this;
}
It sets the model and resource model. Why is this important? Because the abstract db collection that most collections inherit from calls an _initSelect
method at the end of its __construct
method (which is also after _constrct
has been called).
#File: app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php
public function __construct($resource = null)
{
parent::__construct();
$this->_construct();
$this->_resource = $resource;
$this->setConnection($this->getResource()->getReadConnection());
$this->_initSelect();
}
The _initSelect
method sets the main table of the collection's select query.
#File: app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php
protected function _initSelect()
{
$this->getSelect()->from(array('main_table' => $this->getMainTable()));
return $this;
}
And getMainTable
works by asking the resource model for a table name,
public function getMainTable()
{
if ($this->_mainTable === null) {
$this->setMainTable($this->getResource()->getMainTable());
}
return $this->_mainTable;
}
...
public function getResource()
{
if (empty($this->_resource)) {
$this->_resource = Mage::getResourceModel($this->getResourceModelName());
}
return $this->_resource;
}
So, all that happens when the constructor is called, irrespective of whether you call it explicitly or not.
Now, in your code, after calling the constructor, you recall _init
public function __construct()
{
parent::__construct();
$this->_init('sales/order_item');
}
As a reminder, here's what _init
looks liks
#File: app/code/core/Mage/Core/Model/Resource/Db/Collection/Abstract.php
protected function _init($model, $resourceModel = null)
{
$this->setModel($model);
if (is_null($resourceModel)) {
$resourceModel = $model;
}
$this->setResourceModel($resourceModel);
return $this;
}
So, you're changing the model and (more importantly) the resource model to use sales/order_item
. However, the query's main_table
was still set with the sales/order
resource model derived table name.
This means in your custom collection, post __construct
, anytime the Magento code calls
$this->getMainTable();
It will get the main table for the sales/order_item
resource model. With your extra _init
in the constructor, calls to getMainTable
would have returned the main table for the sales/order
resource model. Any other query generating code that relies on resource model behavior would also act differently.
Without tracing the code further, I'm sure you can see how this creates the opportunity for subtle but significant differences in the generated queries.
Re: the question in the comments.
No, it's not a sensible thing to do. The collection system in Magento is designed to load an array of models which have one main table, and several optional support tables (either via joins or extra selects). When you call _init
a second time in the PHP __construct
method, you're creating a collection with two main tables, and while that might allow you (or your tutorial writer) a way to quickly and cleverly implement a feature, it seems bound to cause confusion, and possible fall afoul of a future update.
Also, you're already heading into not-sensible territory since you're extending a class which already extends a concrete Magento collection. While this is standard OOP, it does fall outside general usage patterns, which means you're on your own when it comes to tracking down weird bugs or behavior you don't understand.
Like anything though, it's all about time/benefit ratios and tradeoffs. Sensible for one team isn't sensible for a different team.
I've found a workaround which looks feasible in my case. But I guess what's happening here is not exactly the intended behaviour of Magento collections in (custom) layered models with flat tables enabled. The workaround is far away from fixing the general problem but it works in the specific situation:
I overwrote Mage_Catalog_Model_Resource_Layer_Filter_Price
by placing a copy of it in app\code\local\Mage\Catalog\Model\Resource\Layer\Filter.
In _replaceTableAlias()
right before the return I added the following code
if (Mage::helper('catalog/product_flat')->isEnabled()) {
$activePrefix = "";
if (strpos($conditionString, 'AND') !== false) {
$activePrefix = "AND ";
}
if (strpos($conditionString, 'special_price') !== false) {
return $activePrefix."1 = 1";
}
if (strpos($conditionString, 'special_to_date') !== false) {
return $activePrefix."1 = 1";
}
if (strpos($conditionString, 'special_from_date') !== false) {
return $activePrefix."1 = 1";
}
}
Kind of ugly but it does what it should and I don't expect complications as I don't think 'special_price' is something that should ever come up in the condition string other than in the case where I want to replace it. First tests seem to confirm that.
Best Answer
Your question doesn't have enough context to provide a concrete answer, so I'm going to explain why Magento is throwing an exception with the message
Hopefully that will give you enough information to track down the problem.
In a stock Magento system, the only place the error "Cannot determine the field name" appears is
So, if the
_attributeToField
method is passed an attribute value that'sAn Empty string
An attribute object where
getAttributeCode
returns false (or one of the many values PHP considers false-ish)Something that's not an
Mage_Eav_Model_Entity_Attribute
or stringthen the exception "Cannot determine the field name" is thrown.
In a standard system Magento calls this method when you call a collection object's
addAttributeToSelect
,addAttributeToFilter
,addAttributeToSort
, oraddAttributeToSearchFilter
methods.So, something about your code causes an invalid value to be passed to
_attributeToField
. This means the problem isn't your sub-query, or the sub-query may alter something that causes an invalid value to be passed.