i am writing a magento product exporter, that writes a couple of attributes into a csv file. one attribute is called the "category string" and its method looks like:
...
foreach($products as $_product) {
...
$productId = $_product->getSku();
$productCategory = getCategoryString($_product['category_ids']);
...
}
...
function getCategoryString($numbers) {
$catString = '';
$catModel = Mage::getModel('catalog/category')->setStoreId(Mage_Core_Model_App::ADMIN_STORE_ID);
$ex = explode(',', $numbers);
foreach ($ex as $i => $e) {
if ($i > 0) {
$catString .= $catModel->load($e)->getName();
if ($i < (count($ex)-1))
$catString .= ' > ';
}
}
$ex = NULL;
$numbers = NULL;
$catModel->unsetData();
unset($catModel);
$catModel = NULL;
return $catString;
}
but after each iteration the method call costs about 1MB for each product and i have about 9000 products! i cannot clean up the $catModel variable! the $catModel = NULL and unset($catModel) lines have no effects. what am i doing wrong? how can i force to unset the object?!
We had the same problem with a cron for Magento, I know that it isn't the best way to do it but we needed to do it quickly.
Our solution was creating a new PHP file with the necessary code to do one single operation. From magento we get a product list and then call with exec() to this external PHP file product by product.
Something like this:
foreach($products as $_product) {
...
exec("do_the_work.php {$_product->getSku()}");
...
}
Hope it helps.
so your script would be so much cooler if you
get all catalog ids from all the products you have as an array
load the category collection with IN() clause with the category names you need
foreach your product collection one more time and select the neccecary category id's from preloaded collection with just the right elements
profit $$$$ cause you are not using memory like its unlimited for you
much much cooler thing would be joining the category names directly to product collection, that would consume even less resources
Related
I made a simple import script and I'm trying to programatically save 3 custom attributes (att1, att2, att3) together with all other info (name, description, price, category..).
So basically I have:
public function insert_product($data) {
$product = Mage::getModel('catalog/product');
try {
$sku = $data['code'];
if ($this->prodottiImportati[$sku]) {
$sku = $data['code'] . '-1';
}
$this->prodottiImportati[$sku] = true;
$product->setSku($sku);
$product->setName($data['name']);
$product->setDescription($data['desc']);
$product->setShortDescription($data['short_desc']);
$product->setManufacturer('');
$product->setPrice($data['price']);
$product->setTypeId('simple');
$product->setAttributeSetId($this->attributeSet);
$categorie = $this->get_categories($data);
$product->setCategoryIds($categorie);
$product->setWeight($data['peso']);
$product->setTaxClassId(2); // taxable goods
$product->setVisibility(4); // catalog, search
$product->setStatus(1); // enabled
$product->setWebsiteIds($data['store_id']);
$stockData = $product->getStockData();
$stockData['qty'] = $data['qty'];
if ($data['quantita'] > 0) {
$stockData['is_in_stock'] = 1;
} else {
$stockData['is_in_stock'] = 0;
}
$stockData['manage_stock'] = 1;
$stockData['use_config_manage_stock'] = 0;
$product->setStockData($stockData);
$product->setIsMassupdate(true)->setExcludeUrlRewrite(true);
$product->save();
$productID = $product->getId();
} catch(Exception $e) {
echo ($e->getMessage());
}
return $productID;
}
First thing I tryed was adding a
$productID = $this->insert_product($data);
Mage::getSingleton('catalog/product_action')->updateAttributes(
array($productID), array(
'att1' => $data['att1'],
), $data['store_id']);
So basically updating things after the insert function was called, using the ID got after the insert. store_id is the ID of the store in that given language. Didn't save anything.
Second attempt, I follwed this: Magento add custom options with programmatically importing products
I tryed that within the insert_product function and also outside after $productID = $this->insert_product($data); Neither worked.
Last I tryed a magical $product->setAtt1('value'); witin the insert_product function, not sure how Magento would understand how to set att1 that way, but...you know, I read it somewhere and I gave it a try ;)
att1, att2 and att3 are spelled lowercase, althoug they have an uppercase label (think that dosen't matter here), they are part of an attribute group (I'm passing it with $product->setAttributeSetId($this->setAttributi)) and they are all multiple selection attributes, so I could in teory pass multiple values to them.
I'm sure I'm missing something on the way. Can anyone help?
After 10 more minutes since I wrote here, I was able to find the way. I took me forever to solve it.
The clue of this is that you have to add attributes ID, not values. That happens at least for me with multiple selection attributes, not sure if it's true all the time.
Here is how I did:
In the function insert_product I added:
$optionId = $this->get_option_id_by_code('att1', 'Value of the attribute you need to add');
$product->setAtt1($optionId);
So if yor attribute is named, let's say "brand" it will be:
$optionId = $this->get_option_id_by_code('brand', 'Nike');
$product->setBrand($optionId);
If your attribute can have multiple values, you need to change the code above to:
$optionId = array();
foreach ($myAttributesArray as $someValues) {
$optionId[] = $this->get_option_id_by_code('att1', $someValues);
}
$product->setAtt1($optionId);
The foreach is just an example, you need to loop through your mutiple values and get the respective ID and add them all toghether with setAtt1 passing them as an array. I'm working on an improved version where the get_option_id_by_code function does all at once in a more efficient way. This is kust a "basic" version that works, feel free to make it fancy and smart.
Then I created an other function called get_option_id_by_code like this:
protected function get_option_id_by_code($attrCode, $optionLabel) {
$attrModel = Mage::getModel('eav/entity_attribute');
$attrID = $attrModel->getIdByCode('catalog_product', $attrCode);
$attribute = $attrModel->load($attrID);
$options = Mage::getModel('eav/entity_attribute_source_table')
->setAttribute($attribute)
->getAllOptions(false);
foreach ($options as $option) {
if ($option['label'] == $optionLabel) {
return $option['value'];
}
}
return false;
}
To be honest I found this with a collage of other sources / authors, so I need to be thankful to a bunch of smarter programmers here and there, since it took a while for me to strouggle with this simple task I wrote the solution here hoping to help you guys. Thanks!
I'm working on an app that has a product catalog with a n-level category tree.
I have the next structure:
Category 1
--Category 1.1
----Category 1.1.1
------Product 1.1.1.1
------Product 1.1.1.2
----Product 1.1.1
--Category 1.2
----Product 1.2.1
--Product 1.1
And I need something like a flat array:
Category 1
Category 1.1
Category 1.1.1
Product 1.1.1.1
Product 1.1.1.2
Product 1.1.1
Category 1.2
Product 1.2.1
Product 1.1
But the problem is that I need too much queries and too much time to generate this (I have 700 products and 200 categories more or less and it takes about 846 queries and 2100 ms to execute them). I can add more joins, but the execution time grows significantly. I'm working with translatable entities.
Any idea to minimize the queries number and the execution time? Categories has a self-relationship and a relationship with products.
I think that an option can be to get all categories and make a ordered tree and later redo the flat array again, but I don't know if it could be the best option...
Thank you in advance for any help!
EDIT:
This is my function to generate the "flat" array.
public function generateCategoriesTree($categories, $result = array()) {
$tempResult = array();
if(count($categories)) {
foreach($categories as $k => $v) {
$tempResult[] = $v;
if(count($v->getCategories())) {
$temp = $this->generateCategoriesTree($v->getCategories(), $result);
foreach($temp as $kk => $vv) {
$tempResult[] = $vv;
}
}
}
}
return array_merge($result, $tempResult);
}
But the problem is not here, the problem is when I trying to acces to the second level of the tree, when call to the method getcategories, that execute a query in the database.
UPDATE
With this solution, I reduced the number of queries from 700 to 100 and the execution time from 2100ms to 350ms. I think that it could be enough for now, becouse this actions is used a couple per hour.
if ($this->isGranted('ROLE_EMPLOYEE') || $currentUser->isXXXXXXSeller()) {
$XXXXXXCategories = $em->getRepository('CatalogBundle:Category')->getMainCategoriesByBrand(1);
if(is_array($XXXXXXCategories)) {
$categoriesXXXXXX = $this->get('core_tools')->generateCategoriesTree($XXXXXXCategories);
$categories = new ArrayCollection(
array_merge($categories->toArray(), $categoriesXXXXXX)
);
}
}
$categoriesToShow = array();
$products = $em->getRepository('CatalogBundle:Product')->getAllOrderedByPosition();
if(is_array($products) && is_array($categories)) {
foreach($categories as $category) {
$categoriesToShow["c_".$category->getId()] = array(
"category" => $category,
"products" => array(),
);
}
foreach($products as $product) {
if($product->getCategories()) {
foreach($product->getCategories() as $productCategory) {
if(array_key_exists("c_".$productCategory->getCategory()->getId(), $categoriesToShow)) {
$categoriesToShow["c_".$productCategory->getCategory()->getId()]['products'][] = $product;
}
}
}
}
}
If the catalog doesn't change very often (ie: more than every few seconds) - then do the hard work once, and then cache the output ready to be used instantly.
When the catalog changes - rebuild the cache.
If you are using MySQL you might like to consider the "Nested Sets" model to store your category hierarchy. The complexity of your SELECTs will usually stay constant no matter how many levels your category tree has. There are Doctrine extensions available that help you setting it up (e.g. https://github.com/blt04/doctrine2-nestedset).
If you are using a DBS supporting hierarchical queries you can consider using them (https://en.wikipedia.org/wiki/Hierarchical_and_recursive_queries_in_SQL).
I need to gather all of the available attributes for the given product and then create a multidimensional array with them. Hopefully you can create a multidimensional array with more than two dimensions? The resulting array declarations should look like this:
$simpleArray[$child->getVendor()][$child->getColor()]=$child->getPrice();
First I'm gathering all the attributes then adding them to a string where I can call each one later:
$_product = $this->getProduct();
$_attributes = Mage::helper('core')->decorateArray($this->getAllowAttributes());
//Gather all attribute labels for given product
foreach($_attributes as $_attribute){
$attributeString .= '[$child -> get' . ucfirst($_attribute->getLabel()) . '()]';
}
Then I'm attempting to append that string to the array to declare it:
foreach($childProducts as $child) { //cycle through simple products to find applicable
//CAITLIN you are going to need way to search for other attributes, GET list of attributes
$simpleArray. $attributeString =$child->getPrice();
}
Mage::log('The attributeString is '. $simpleArray. $attributeString, null, 'caitlin.log'); //This is logging as "The attributeString is Array74"
Any suggestions?
You'll need to use recursion to do what you're requesting without knowing the attribute names while writing the code.
This will loop through and provide all of the child product prices, in a multi dimensional array based on the configurable attributes. It assumes that $_product is the current product.
$attrs = $_product->getTypeInstance(true)->getConfigurableAttributesAsArray($_product);
$map = array();
foreach($attrs as $attr) {
$map[] = $attr['attribute_code'];
}
$childPricing = array();
$childProducts = $_product->getTypeInstance()->getUsedProducts();
foreach($childProducts as $_child) {
// not all of the child's attributes are accessible, unless we properly load the full product
$_child = Mage::getModel('catalog/product')->load($_child->getId());
$topLevel = array($child->getData($map[sizeof($map)]) => $_child->getPrice());
array_pop($map);
$childProducts = array_merge($childProducts,$this->workThroughAttrMap($map,$_child,$topLevel));
}
//print_r childProducts to test, later do whatever you were originally planning with it.
In the same controller include this:
protected function workThroughAttrMap(&$map,$child,$topLevel) {
$topLevel = array($child->getData($map[sizeof($map)]) => $topLevel);
array_pop($map);
if(sizeof($map) > 0) return workThroughAttrMap($map,$child,$topLevel);
else return $topLevel;
}
I haven't tested this code so there may be a few minor bugs.
There are a few things you could do to make the code a bit cleaner, such as moving the first $topLevel code into the function, making that an optional parameter and initializing it with the price when it doesn't exist. I also haven't included any error checking (if the product isn't configurable, the child product doesn't have its price set, etc).
i want to filter and paginate a product collection. everything is fine - except pagination. im just getting the whole collection back, instead of 3 items for the first page.
//fetch all visible products
$product_collection = Mage::getModel('catalog/product')->getCollection();
//set wanted fields (nescessary for filter)
$product_collection->addAttributeToSelect('name');
$product_collection->addAttributeToSelect('description');
$product_collection->addAttributeToSelect('price');
$product_collection->addAttributeToFilter('visibility', array('neq' => 1));
//filter by name or description
$product_collection->addFieldToFilter(array(
array('attribute'=>'name','like'=>$sTerm),
array('attribute'=>'description','like'=>$sTerm)
));
//filter for max price
foreach ($product_collection as $key => $item) {
if($item->getPrice() >= $priceTo){
$product_collection->removeItemByKey($key);
}
}
//pagination (THIS DOESNT WORK!)
$product_collection->setPageSize(3)->setCurPage(1);
//TEST OUTPUT
foreach ($product_collection as $product) {
echo $product->getName().'<br />';
}
thanks for your support!
You are so close! Try moving that $product_collection->setPageSize(3)->setCurPage(1); line before the first foreach() iteration over the collection.
Magento collections are lazy-loaded. Until you directly load() them (or implicitly load them via a call to count() or foreach()) you can modify the collection properties which affect the underlying query (EDIT: see note below). Once the collection has been loaded explicitly or implicitly though you will only get the members of the _items property that have been set.
FYI you can call clear() to leave the original query-affecting properties (filters, sorters, limits, joins, etc) in place and then add further properties.
HTH
EDIT: Actually, adjusting query properties is always possible regardless of _items load state, but the effect won't be visible until the collection is regenerated.
Thanks #Ben! You gave me the right hint. Now it does work! Basically I'm creating another collection and filter this one by the ids of the already filtered items. Afterwards its easy to add pagination to that new collection. That's the working code:
//fetch all visible products
$product_collection = Mage::getModel('catalog/product')->getCollection();
//set wanted fields (nescessary for filter)
$product_collection->addAttributeToSelect('name');
$product_collection->addAttributeToSelect('description');
$product_collection->addAttributeToSelect('price');
$product_collection->addAttributeToFilter('visibility', array('neq' => 1));
//filter by name or description
$product_collection->addFieldToFilter(array(
array('attribute'=>'name','like'=>$sTerm),
array('attribute'=>'description','like'=>$sTerm)
));
//filter for max price
foreach ($product_collection as $key => $item) {
if($item->getPrice() >= $priceTo){
$product_collection->removeItemByKey($key);
}
}
//build id array out of filtered items (NEW!)
foreach($product_collection as $item){
$arrProductIds[]=$item->getId();
}
//recreate collection out of product ids (NEW)
$product_filtered_collection = Mage::getModel('catalog/product')->getCollection();
$product_filtered_collection->addAttributeToFilter('entity_id', array('in'=>$arrProductIds));
//add pagination (on new collection) (NEW)
$product_filtered_collection->setPageSize(3)->setCurPage(1);
//TEST OUTPUT
foreach ($product_filtered_collection as $product) {
echo $product->getName().'<br />';
}
I'm in the progress of making a shopping cart in PHP. To check if a user has selected multiple products, I put everything in an array ($contents). When I output it, I get something like "14,14,14,11,10". I'd like to have something like "3 x 14, 1 x 11, 1 x 10". What is the easiest way to do that? I really have no clue how to do it.
This is the most important part of my code.
$_SESSION["cart"] = $cart;
if ( $cart ) {
$items = explode(',', $cart);
$contents = array();
$i = 0;
foreach ( $items as $item ) {
$contents[$item] = (isset($contents[$item])) ? $contents[$item] + 1 : 1;
$i++;
}
$smarty->assign("amount",$i);
echo '<pre>';
print_r($contents);
echo '</pre>';
Thanks in advance.
Why not build a more robust cart implementation?
Consider starting with a data-structure like this:
$cart = array(
'lines'=>array(
array('product_id'=>14,'qty'=>2),
array('product_id'=>25,'qty'=>1)
)
);
Or similar.
Then you could create a set of functions that operate on the cart structure:
function addToCart($cart, $product_id, $qty){
foreach($cart['lines'] as $line){
if ($line['product_id'] === $product_id){
$line['qty'] += $qty;
return;
}
}
$cart['lines'][] = array('product_id'=>product_id, 'qty'=>$qty);
return;
}
Of course, you could (and perhaps should) go further and combine this data structure and functions into a set of classes. Shopping carts are a great place to start thining in an object-oriented way.
The built-in array_count_values function might does the job.
E.g:
<?php
$items = array(14,14,14,11,10);
var_dump(array_count_values($items));
?>
Outputs:
array(3) {
[14]=>
int(3)
[11]=>
int(1)
[10]=>
int(1)
}
You would benefit from using a multi dimensional array to store your data in a more robust structure.
For example:
$_SESSION['cart'] = array(
'lines'=>array(
array('product_id'=>14,'quantity'=>2, 'item_name'=>'Denim Jeans'),
...
)
);
Then to add new items to the cart you can simply do this:
$_SESSION['cart'][] = array('product_id'=45,'quantity'=>1, 'item_name'=>'Jumper');
When you let a user add an item you need to add it in the right position in the array. If the product id already exist in the array, you need to update it. Also always be careful of users trying to enter zero or minus numbers!