Magento Collection Catch-22 - magento

I don't really understand how Magento collections work, so hopefully this is a simple question...
As the old adage goes, you can't observe an experiment without somehow altering it. This seems to hold true for Magento collections. I have a Featured Products module that I've written that works quite well. We have recently added Customer Reviews to our store. When viewing a category page it shows a random review of products in that category. This also works great. I added the review block to my Featured Products page, which was easy to do, but since those products aren't in a specific category, it just pulls a random, usually unrelated, review. To fix this, I modified my getProductCollection function in my Featured module and added the following to the end, after the collection has been created/saved:
$_product = $this->_productCollection->getFirstItem();
$_catIDs = $_product->getCategoryIds();
if(count($_catIDs) > 0)
{
$_cat = Mage::getModel('catalog/category')->load($_catIDs[0]);
Mage::register('current_category', $_cat);
}
Unfortunately, the mere act of looking at the first item in the collection breaks the pager in the toolbar. No matter which of the paging options I choose, it always shows all items when the above code is in place. If I comment out that section it works fine.
So my question is this: How can I get any information about the products in a collection without somehow changing the collection or breaking the paging?
Adding my code to help explain the problem more:
class VPS_Featured_Block_List extends Amasty_Sorting_Block_Catalog_Product_List//Mage_Catalog_Block_Product_List
{
protected function _getProductCollection()
{
if (is_null($this->_productCollection))
{
$_attributeNames = 'featured';
if($this->getAttributeName() != '')
$_attributeNames = $this->getAttributeName();
$_attrArray = explode(',', $_attributeNames);
$this->_productCollection = Mage::getModel('catalog/product')->getCollection();
$this->_productCollection->addAttributeToSelect('*');
$_filters = array();
foreach($_attrArray as $_attr)
$_filters[] = array('attribute' => $_attr, 'eq' => true);
$this->_productCollection->addFieldToFilter($_filters);
//$this->_productCollection->addFieldToFilter(array(array('attribute' => $_attr, 'eq' => true),));
Mage::getSingleton('cataloginventory/stock')->addInStockFilterToCollection($this->_productCollection);
Mage::getSingleton('catalog/product_status')->addVisibleFilterToCollection($this->_productCollection);
Mage::getSingleton('catalog/product_visibility')->addVisibleInCatalogFilterToCollection($this->_productCollection);
//Get category of first product in collection (used for featured review)
$_catIDs = array();
$_product = $this->_productCollection->getFirstItem();
$_catIDs = $_product->getCategoryIds();
if(count($_catIDs) > 0)
{
$_cat = Mage::getModel('catalog/category')->load($_catIDs[0]);
Mage::register('current_category', $_cat);
}
}
return $this->_productCollection;
}
}

Thanks to #clockworkgeek for this answer he posted in my followup question:
The reason is because when you call getFirstItem() (or just about any other retrieval method) on a collection that collection is loaded. Any subsequent operation ignores the database and uses only the loaded data, filters have no effect because they are SQL only, ditto for pagination and selected columns. The workaround is to use a second collection based on the first.
$secondCollection = clone $firstCollection;
$secondCollection->clear();
$_foo123 = $secondCollection->getFirstItem();
The clear() method unloads the data for that collection, forcing it to access the database again next time

Hard to say what's going on without more context (magento version, which class you're in, etc.) BUT, what I think (60% confidence) it happening is you're getting a reference to the product collection before its filters have been added. Magento collections lazy load, which means database queries aren't run until you explicitly call load or you attempt to access an item. My guess (again, a guess) is that when you access the item above, the collection loads (with no filter). Then, other parts of the system add the filter but they're ignored because the collection is already loaded.
Answering your larger question is impossible without more context.

Related

Add Child SKUs to Magento Fulltext Search Index

I'm not sure why this isn't part of the stock functionality of Magento, but I want customers to be able to search for a configurable product by a child SKU. For some reason, Magento doesn't index the child SKUs.
I found in app/code/core/Mage/CatalogSearch/Model/Resource/Fulltext.php
$dynamicFields = array(
'int' => array_keys($this->_getSearchableAttributes('int')),
'varchar' => array_keys($this->_getSearchableAttributes('varchar')),
'text' => array_keys($this->_getSearchableAttributes('text')),
'decimal' => array_keys($this->_getSearchableAttributes('decimal')),
'datetime' => array_keys($this->_getSearchableAttributes('datetime')),
);
I've tried several variations, without success. SKU is a "static" attribute, accessible through $this->_getSearchableAttributes('static'). I'm fine with getting all static attributes, but it's not working. Depending on what a try I either get no change in results or an error that the static attribute table doesn't exist (which make sense, because static attributes are in the product entity table).
Does anybody have a suggestion to solve this problem?
Online research has found suggestions to add a hidden attribute with these values, but that shouldn't be needed. I would rather solve the problem properly.
So many ways to do this..
However, I will lead with something I believe would be much cleaner rather than screwing around with the actual Fulltext in app/code/core/Mage/CatalogSearch/Model/Resource/Fulltext.php.
The problem is the lines AFTER that snippet you included:
// status and visibility filter
$visibility = $this->_getSearchableAttribute('visibility');
$status = $this->_getSearchableAttribute('status');
$statusVals = Mage::getSingleton('catalog/product_status')->getVisibleStatusIds();
$allowedVisibilityValues = $this->_engine->getAllowedVisibility();
which ultimately leads to:
core/Mage/CatalogSearch/Model/Resource/Fulltext/Engine.php
/**
* Retrieve allowed visibility values for current engine
*
* #return array
*/
public function getAllowedVisibility()
{
return Mage::getSingleton('catalog/product_visibility')->getVisibleInSearchIds();
}
then:
core/Mage/Catalog/Model/Product/Visibility.php
public function getVisibleInSearchIds()
{
return array(self::VISIBILITY_IN_SEARCH, self::VISIBILITY_BOTH);
}
So what you need to do is go to your simple product associated with your configurable and change the visibility to 'Search'.
However, here it is in action now:
However, yes, that doesn't look pretty at all. The next order of business is now modifying the search results so that when it lists a simple product item that's a.) associated to a configurable product b.) visibility ID is explicitly set to Mage_Catalog_Model_Product_Visibility::VISIBILITY_IN_SEARCH, you need to render the item in search results to the actual configurable product parent (and the rest of its details, such as images).
(I'll continue this later. my computer decided to crap out on me, and I lost over 50% of what I was writing. Even the part where we modify the fulltext class! Darn. see you later.)
I've used the method of setting simple product visibility to search. Then instead of displaying the simple product load/display its parent product.
Beside the frontend query in loop performance issues I never found this solution very satisfying. So after another whack at it I found another option.
I'm adding simple product SKUs to the fulltext index for grouped products but the same approach should work for configurable products.
Here is the idea:
Mage_CatalogSearch_Model_Resource_Fulltext->_prepareProductIndex
contains
$typeInstance = $this->_getProductTypeInstance($productData['type_id']);
if ($data = $typeInstance->getSearchableData($product)) {
$index['options'] = $data;
}
So by overriding the getSearchableData method in the product type class you can add data to the fulltext index. For grouped product my updated looked like:
... extends Mage_Catalog_Model_Product_Type_Grouped
{
/**
* all child SKUs to search data
*/
public function getSearchableData($product=null)
{
$searchData = parent::getSearchableData();
$adapter = Mage::getSingleton('core/resource')->getConnection('core_read');
$select = $adapter->select()
->from(
array('p' => Mage::getSingleton('core/resource')->getTableName('catalog/product')),
array('sku')
)
->joinLeft(
array('r' => Mage::getSingleton('core/resource')->getTableName('catalog/product_relation')),
'r.child_id = p.entity_id',
array()
)
->where('r.parent_id = ?', $this->getProduct($product)->getId());
$query = $adapter->query($select);
while ($row = $query->fetch()) {
$searchData[] = $row['sku'];
}
return $searchData;
}
Reindexed search and catalogsearch_fulltext.dataindex should contain the additional data.
There is a solution for this but it is a paid extension (we license it), it is not build in to Magento standard so you cannot search by child attributes and just return the parent configurable/grouped/bundle product.

Magento dmin Checkboxes Save magento categories to custom database table

I'm currently developing a custom module for Magento and theres one thing I´m not able to get working.
I have a front-end display of employees and an backend so I can add employees.
I save the employees in a regular mysql table(so not EAV). Just to add employees to the database is no problem but now I want to add a different table so that the employees can be part of more than one category. I want to display the magento categories, and that I get working, but next I want to save that value along with the id of my employee in my own table in the database. Thats the problem i'm having.
I've tried using the magento admin grid and have a tab for adding and editing. I´ve tried to add a new tab and adding checkboxes there to check and save but can get it to work
Maybe I'm completely of, if that so youre free to suggest a different approach.
add this to save action
if (isset($data['categories'])) {
$data['categories'] = explode(',', $data['categories']);
if (is_array($data['categories'])) {
$data['categories'] = array_unique($data['categories']);
}
}
and this to collection
$this->getSelect()->join(
array('category_table' => $this->getTable('qbanner/qbanner_category')), 'main_table.qbanner_id = category_table.qbanner_id', array()
)
->where('category_table.category_id = ?', $categoryId);
return $this;
hope this will help you

Replacing "product name" with "Item + productId" when product is added in Magento via admin panel

I want to auto generate product names in Magento.
When I'm going to add a product, for the product name I will type some string.
When I save the product, I want the product name to be automatically generated such that the product name becomes Item."productId".
Answering assuming that OP wants to incorporate the auto-increment value from the entity table into business data. This is generally not a great idea.
This is an interesting task which can be easily accomplished with Magento's EAV implementation - particularly when working in the catalog module. First, some background.
When an EAV entity is saved, it has a nice, neat array of key => value pairs which represent the attributes and attribute values for that entity:
Mage_Catalog_Model_Product->_data['attribute_code'] = 'attribute value';
During the save process, the EAV resource model takes this array and iterates over it. For each attribute, identified by its code (attribute_code in the above example) and its entity (catalog_product in the case of products), the configuration for the attribute itself is loaded. Of particular importance is the "backend model" for an attribute, as it is invoked to do pre- and post-processing of/relating to the value.
In the current case, there is a piece of information which will not be present when we are saving the attribute, at least, not in a way in which we can use it: the new product id. This can be used to adjust the original value as part of the save process.
It's always nice to have an example from the core, so, refer to the price attribute and its backend model, Mage_Catalog_Model_Product_Attribute_Backend_Price which can be seen in the eav_attribute table:
SELECT `attribute_code`, `backend_model`
FROM `eav_attribute`
LEFT JOIN `eav_entity_type` USING (`entity_type_id`)
WHERE `attribute_code` = 'price';
#+----------------+-----------------------------------------+
#| attribute_code | backend_model |
#+----------------+-----------------------------------------+
#| price | catalog/product_attribute_backend_price |
#+----------------+-----------------------------------------+
#1 row in set (0.00 sec)
When a product is saved, the price attribute's backend_model is instantiated and (in this case) the afterSave() method is called. Incidentally, this method is what updates pricing by conversion rate for website-scoped pricing. This same approach can be used to modify the name attribute.
The following setup script will add the backend model:
<?php
$installer = Mage::getResourceModel('catalog/setup','default_setup');
$installer->startSetup();
$installer->updateAttribute(
'catalog_product',
'name',
'backend_model',
'custom/product_attribute_backend_name'
);
$installer->endSetup();
The corresponding afterSave() method should do the trick:
public function afterSave($object)
{
$value = $object->getData($this->getAttribute()->getAttributeCode());
$origData = $object->getOrigData();
$origValueExist = $origData && array_key_exists($this->getAttribute()->getAttributeCode(), $origData);
//don't do this in case of update
if ($object->getStoreId() != 0 || !$value || $origValueExist) {
return $this;
}
//append autoinc id
$newValue = $value .'.'. $object->getId(); // or whatever
//assume global store, otherwise the stated need is getting weird!
$object->addAttributeUpdate($this->getAttribute()->getAttributeCode(), $newValue, 0);
return $this;
}
If you're doing this from the admin panel product edit screen, you're going to have to remove the "Required" class from the "Name" field so you can save it without the name. This means overriding the Edit form to replace that field specifically. Then you'll have to overload the save-related methods on the product model (or you can do it from the controller), but the child will have to generate the name on save before it goes to the database.
For example:
class Module_Catalog_Model_Product extends Mage_Catalog_Model_Product
{
protected function _beforeSave()
{
parent::_beforeSave();
$productName = 'Item' . $this->getId();
if (!$this->getId() && !$this->getName())
{
$this->setName('Item Unnamed');
} elseif ($this->getId()) && strcasecmp($this->getName(), $productName) <> 0)
{
$this->setName($productName);
}
}
}
The only problem with this is that it requires two saves. If you want to have it work on the fly, you'll have to do a second save using the _afterSave() method. Or, once again, you can do it from the controller.
I would use a Magento Event to do this:
Since Models in Magento have an event prefixes (just take a look at Mage_Catalog_Model_Product and look for $_eventPrefix, for our current model the eventPrefix is set to catalog_product.
If you now take a look at Mage_Core_Model_Abstract and search for _eventPrefix. You see that eventPrefix are found in _beforeLoad, _afterLoad, _beforeSave, _afterSave and a few others. In these methods you can see an event is dispatched using something as below:
Mage::dispatchEvent($this->_eventPrefix.'_save_before', $this->_getEventData());
This means you have an event available called catalog_product_save_before. With this event you can hook into Magento at that time and do your thing, change the field in this case, and Magento handles the rest.
Take a look at http://www.magentocommerce.com/wiki/5_-_modules_and_development/0_-_module_development_in_magento/customizing_magento_using_event-observer_method for more information how to use these events and turn them into a module. If you don't know how to build modules for Magento and want to learn, there are some awesome on-demand video's for free: http://www.magentocommerce.com/training/on-demand
First I want to thanks to all users which write in the topic. Thanks a lot of guys!
I did it, but I make it easier. (because I have very basic knowledge in Magento and it would toke more time)
So... With my colegues decided to make it with php/jquery/ajax.
First we create one single php file, which return the last id:
<?php
header('Access-Control-Allow-Origin: *');
require_once 'app/Mage.php';
umask(o);
Mage::app('default');
Mage::getSingleton('core/session', array('name'=>'frontend'));
$model = Mage::getModel('catalog/product'); //getting product model
$collection = $model->getCollection(); //products collection
foreach ($collection as $product) //loop for getting products
{
$id=$product->getId();
}
if($id)echo $id+1; //id of product
else{
echo 0;
}
?>
After step one I set the value of input (i.e. I auto generate the name):
if($j('#name').val()=='' && window.location.href.indexOf("admin/catalog_product/new/") > -1) {
$j.post('http://www.website.com/file.php', function(data) {
$j('#name').val('Item №'+data);
});
}
Thanks again for help.
Best Regards,
Jordan!

Storing product attributes with orders in Magento

I am looking for a solution to storing product attributes with each order. Essentially I have a need for storing a unique item lot number for each product that can then be searched on the front end to find out which orders contained products from a specific lot. My initial thought was to do this with product attributes and store the attribute with each product in an order.
Does anybody have a better solution or can point me in the right direction for implementing this solution?
Edit: Still looking for a solution to this
I agree the lot number should be a product attribute but you don't need to store it twice. Just join it with order table when you later need it - That way the information is kept up to date (unless you want to know what it was at the time of ordering, in which case this is all wrong).
Unfortunately the order tables are flattened, not EAV, so don't handle attributes so well. The collection's joinAttribute() method is a stub. You can get around that with this query patterns library (self promotion disclaimer; I'm using it here because I wrote the attribute functions and don't want to redo the work) and then extending it with a class specific to you.
class Knectar_Select_Product_Lot extends Knectar_Select_Product
{
public function __construct()
{
parent::__construct();
$this->joinAttribute('lots', 'catalog_product', 'lot_number',
'products.entity_id', 0, 'lot_number'
);
}
public static function enhance(Varien_Db_Select $select, $tableName, $condition, $columns = null, $type = self::LEFT_JOIN)
{
$select->_join(
$type,
array($tableName => new self()),
$condition,
$columns ? $columns : 'lot_number'
);
}
}
The enhance() function does the hard bit, you only need to call it and filter by it's column.
$orderItems = Mage::getResourceModel('sales/order_item_collection');
Knectar_Select_Product_Lot::enhance($orderItems->getSelect, 'lots', 'lots.product_id = main_table.product_id');
$orderItems->addFieldToFilter('lot_number', 'ABC123');
If you are using the collection in an admin grid then it's column filters will do addFieldToFilter() for you.

How to select from magento EAV tables when flat catalog product data is on

I have this code for selecting best selling products from Magento:
$productCollection = Mage::getResourceModel('reports/product_collection')
->addOrderedQty($startTime, $currentTime)
->addAttributeToSelect('*')
->setStoreId($storeId)
->addStoreFilter($storeId)
->setOrder('ordered_qty', 'desc')
->setPageSize($this->limit());
}
and it works fine, until I set "use flat catalog product" in backend to yes.
Is there any way to tell magento to not use flat tables, and use EAV instead?
Can any one help me with this.
Create a new model class ('mycatalog/product') that extends the original product class but hard code it to use the EAV resource model and EAV resource collection, and then use that model in your query code.
I'd been running my code from a stand alone php file, as soon as i moved my code into an admin module it stopped using the flat_file and went back to eav.
If you look at: Mage_Catalog_Model_Resource_Eav_Mysql4_Product_Collection
There's a method:
public function isEnabledFlat()
{
if (Mage::app()->getStore()->isAdmin()) {
return false;
}
if (!isset($this->_flatEnabled[$this->getStoreId()])) {
$this->_flatEnabled[$this->getStoreId()] = $this->getFlatHelper()
->isEnabled($this->getStoreId());
}
return $this->_flatEnabled[$this->getStoreId()];
}
You could modify this to add an extra condition that returns false based on your own criteria.
BTW, The reports collection mentioned in the first post by Blazo is an extension of this collection.
To expand on Alan's answer:
class Namespace_Module_Model_Category extends Mage_Catalog_Model_Category
{
protected function _construct()
{
$this->_init('catalog/category');
}
}
The above removes the check to see if flat was enabled and only inits the standard eav verson of the catalog/category resource.
And then when you wish to load your model ensuring that you get the eav model regardless of wither the flat data is enabled:
$category = Mage::getModel('namespace_module/category')->load($id)
I have use
Mage::app()->setCurrentStore(Mage_Core_Model_App::ADMIN_STORE_ID);
before
Mage::getModel('catalog/product')->getCollection()
And it start fetching data from eav based system.
This is an old post but I thought one important point was not stated.
1. Once you set flat catalog to on you need to run indexer via cron or via admin/shell so that flat catalog tables get populated.
If you do have many products in your search bypassing flat catalog table will slow down your site and each search code will consume lots of resources.
I found that the easiest solution was to turn off the flat tables and then get the SQL query that magento executes using the ->load(true) parameter
e.g.
$collection = Mage::getModel('catalog/category')->getCollection();
$collection
->setStoreId($store->getId())
->addAttributeToSelect('*')
->addAttributeToFilter(array(array('attribute'=>'ig_unifeed_ids', 'like'=>"%:".$this->getId().":%")))
->load(true);
then turn flat tables back on and replace this code with:
$resource = Mage::getSingleton('core/resource');
$readConnection = $resource->getConnection('core_read');
$query = "SELECT `e`.*, `at_ig_unifeed_ids`.`value` AS `ig_unifeed_ids` FROM `catalog_category_entity` AS `e` INNER JOIN `catalog_category_entity_varchar` AS `at_ig_unifeed_ids` ON (`at_ig_unifeed_ids`.`entity_id` = `e`.`entity_id`) AND (`at_ig_unifeed_ids`.`attribute_id` = '139') AND (`at_ig_unifeed_ids`.`store_id` = 0) WHERE (`e`.`entity_type_id` = '3') AND ((at_ig_unifeed_ids.value LIKE '%:".$this->getId().":%'))";
$collection = $readConnection->fetchAll($query);
From this point on you will probably need to change other code like replacing
$category->getId()
with
$category["entity_id"]
I hope this helps a bit...
NOTE: this is a real solution for the IG_Unifeed module magento bug that disregards category filtering when using flat tables.

Resources