Email Translations

When working with email translations, they’re taken from the following place:

app/locale/<<lang_COUNTRY>>/template/email/

So, depending on the set locale for the store (set in System>Configuration>General>Locale) depends on where it gets its translation files from. Inspect the dropdown to see the list of language / country combinations.

Copying Product Attributes to Quote & Order Items

Add this to your XML;

<global>
    <sales>
        <quote>
            <item>
                <product_attributes>
                    <location/>
                </product_attributes>
            </item>
        </quote>
    </sales>
</global>

  • This makes the product attributes accessible to the Mage_Sales_Model_Quote_Config class’ getProductAttributes() method. This reads in the sales/quote/item/product_attributes node.

  • This is called by the _assignProducts method of the Mage_Sales_Model_Resource_Quote_Item_Collection‘s _assignProducts() method, where it adds all of the attributes to the product collection of the quote item collection.

Add the columns to the quote and order item tables

  • Create an installer with the following:

    $installer = new Mage_Sales_Model_Resource_Setup('core_setup');
    
    $entities = array(
        'quote_item',
        'order_item'
    );
    
    $options = array(
        // For some reason, VARCHAR works here, whereas elsewhere it must be TYPE_TEXT with a length of 255.
        'type'     => Varien_Db_Ddl_Table::TYPE_VARCHAR,
        'visible'  => true,
        'required' => false
    );
    
    foreach ($entities as $entity) {
        $installer->addAttribute($entity, 'location', $options);
    }
    

  • The addAttribute() method of the Mage_Sales_Model_Resource_Setup creates the columns on the quote_item and order_item tables.

Copy the attributes to the quote item

Unlike copying from quote items to order items, there isn’t an XML method to do this – it has to be accomplished through an observer. Add the following to the config (with the observer / method substituted):

<frontend>
    <events>
        <sales_quote_item_set_product>
            <observers>
                <observer_name>
                    <class>model/observer</class>
                    <method>addAttributesToQuoteItem</method>
                </observer_name>
            </observers>
        </sales_quote_item_set_product>
    </events>
</frontend>

Then create the following method for the observer above:

    public function addAttributesToQuoteItem($observer){
        $quoteItem = $observer->getQuoteItem();
        $product = $observer->getProduct();
        $quoteItem->setLocation($product->getLocation());
    }

Note: Values Don’t have to come from the product item

Any arbitrary information could be set on the quote or quote item; it does not have to come from the product itself. E.g. A different type of ship note could be set by a different observer. The associated columns just need to exist on the quote or quote item tables.

Copying quote item attributes to the order item

  • This can be achieved with a small amount of XML:
  <global>
    <fieldsets>
        <sales_convert_quote_item>
            <location>
                <to_order_item>*</to_order_item>
            </location>
        </sales_convert_quote_item>
    </fieldsets>
</global>

Note: This will not copy the data to the sales_flat_shipment_item table.

Converting from an order item to other types of item

The following will copy the object’s attributes when converting the quote item back to a quote item, an invoice item, and a credit memo item

<global>
    <fieldsets>
        <sales_convert_order_item>
            <location>
                <to_quote_item>*</to_quote_item>
                <to_invoice_item>*</to_invoice_item>
                <to_cm_item>*</to_cm_item>
            </location>
        </sales_convert_order_item>
    </fieldsets>
</global>

These are used in the Mage_Sales_Model_Convert_Order class’ itemToQuoteItem, itemToInvoiceItem, and itemToCreditmemoItem methods. Note: In default Magento, it does not appear that the itemToQuoteItem is used, so this should be included for third party extensions which may rely on this method.

Magento 2.0: Fixing a failed install

Seemingly due to some issues with my MySQL Server, Magento 2’s installer failed before it could complete. It left me with the following error in my browser:

Unable to proceed: the maintenance mode is enabled. 

Aha! I can turn maintenance mode off – that’s easy! Just watch as this error falls like that gummy bear tower you tried to build in junior school! Let’s hop over to Magento’s bin folder…

./magento maintenance:disable

Woohoo, and all is… erm.. yup. Still broken:

SQLSTATE[42S02]: Base table or view not found: 1146 Table &#039;magentorc.store_website&#039; doesn&#039;t exist, query was: SELECT `main_table`.* FROM `store_website` AS `main_table` WHERE (`is_default` = &#039;1&#039;) AND (main_table.website_id &gt; 0) ORDER BY main_table.sort_order ASC, main_table.name ASC

Hmm, a quick look at the magentorc table I'd previously created shows it's completely empty – looks like the MySQL issue was the dastardly culprit! After trying to get it to perform the setup again using a variety of cli commands (and a lot of swearing), I instead opted to remove the following file:

app/etc/env.php

This is where Magento keeps its database details. Removing this started the install process once more – voilà!

Debugging MySQL installed through Homebrew on Yosemite

I recently came across an annoying issue where MySQL would not start on Mac OS X Yosemite, exiting with the following:

ERROR 2002 (HY000): Can&#039;t connect to local MySQL server through socket &#039;/tmp/mysql.sock&#039; (2)

Join me on a journey which makes putting a greased ape on a waltzer and getting him to rate the experience seem like an easy task.

Finding the log files

To fix what’s going wrong, we need to know what’s going wrong. Unfortunately, finding log files (for me at least) can often be a bigger job than fixing the issue at hand. My version of MySQL is installed through Brew. After six days of Googling, I eventually found MySQL’s error log here:

tail -f /usr/local/var/mysql/Daves-MacBook-Pro.local.err

Four and a half days trawling through something which makes War and Peace look like a children’s book, I stumbled on a possible cause for the error:

2015-11-12T15:35:05.354808Z 0 [Warning] InnoDB: Ignoring tablespace `defender/wishlist_item_option` because it could not be opened.

Aha! It looks like one of the databases has become corrupted, MySQL can't open it, and it's failing! Great. Let's try moving the database elsewhere and see how we get on.

ERROR 2002 (HY000): Can&#039;t connect to local MySQL server through socket &#039;/tmp/mysql.sock&#039; (2)

No dice. Right, looking through the log shows up another error:

2015-11-12T15:48:13.051616Z 0 [ERROR] unknown variable &#039;key_buffer=32M&#039;

Variable variables

So, it looks like the variable key_buffer has been replaced with key_buffer_size in a newer version of MySQL. This exists in my MySQL config file:

/usr/local/etc/my.cnf

Changing this sorted my headache, and allowed me to get back to those yummy scrummy databases once more.

Collections, Models and Queries

Models

Loading an item:

<?php Mage::getModel('catalog/category')->load($id);

Loading by attribute:

<?php Mage::getModel('catalog/category')->load(‘url_key’, $key);

Collections

Adding an item collection

<?php $collection->addItem($item);

Eav’s AddAttributeToFilter

The addAttributeToFilter for EAV is interchangeable with addFieldToFilter (which non-eav collections use)

Less Than

<?php $products->addAttributeToFilter('price', array('lt' => 100));

Greated Than

<?php $products->addAttributeToFilter('price', array('gt' => 100));

In

<?php $products->addAttributeToFilter('sku', array('in' => array('1222333', 'ABC 456')));

String Equal

<?php $products->addAttributeToFilter('sku', array('in' => array('1222333', 'ABC 456')));

List of methods from Varien_Db_Adapter_PDO

<?php
$conditionKeyMap = array(
            'eq'            => "{{fieldName}} = ?",
            'neq'           => "{{fieldName}} != ?",
            'like'          => "{{fieldName}} LIKE ?",
            'nlike'         => "{{fieldName}} NOT LIKE ?",
            'in'            => "{{fieldName}} IN(?)",
            'nin'           => "{{fieldName}} NOT IN(?)",
            'is'            => "{{fieldName}} IS ?",
            'notnull'       => "{{fieldName}} IS NOT NULL",
            'null'          => "{{fieldName}} IS NULL",
            'gt'            => "{{fieldName}} > ?",
            'lt'            => "{{fieldName}} < ?",
            'gteq'          => "{{fieldName}} >= ?",
            'lteq'          => "{{fieldName}} <= ?",
            'finset'        => "FIND_IN_SET(?, {{fieldName}})",
            'regexp'        => "{{fieldName}} REGEXP ?",
            'from'          => "{{fieldName}} >= ?",
            'to'            => "{{fieldName}} <= ?",
            'seq'           => null,
            'sneq'          => null
        );
  

Performing OR Queries

ORs with the same attributes

<?php $products->addAttributeToFilter('sku', array(array('eq' => '1222333'), array('eq' => 'LLAPGOCH-SIMPLE' ));

ORs with different attributes

<?php
$collection->addAttributeToFilter(
    array(
        array('attribute'=> 'sku','like' => 'value'),
        array('attribute'=> 'otherattribute','nin' => array(1, 2, 3)),
        array('attribute'=> 'anotherattribute','like' => 'value'),
    )
);

Ordering

<?php
$collection->setOrder('attribute', Varien_Data_Collection::SORT_ORDER_DESC);
$collection->setOrder('attribute', Varien_Data_Collection::SORT_ORDER_ASC);

Useful Methods

<?php
// Gets a list of all Ids within a collection
$collection->getAllIds();

// Gets the last page number of a collection (when using the page functions)
$collection->getLastPageNumber();

// Get the pagesize of a collection
$collection->getPageSize();

// Get the size of the collection (similar to count, but the result is cached)
$collection->getSize(0);

// Getting a specific item(s)
$collection->getFirstItem();
$collection->getLastItem();
$collection->getItems();

// Clearing a collection (allows reloading)
$collection->clear();

// Converts the collection to an XML String
$collection->toXML();

Iterating Collections

Sometimes, large collections will cause memory issues in PHP. One solution, is to use Magento’s core/resource_iterator. This is the only purpose of this class:

<?php
public function output(){
        $products = Mage::getResourceModel('catalog/product_collection')

                // When adding attributes to the collection, make sure to use the the second parameter 'inner', otherwise the resource iterator won't pull them out.
        $products->addFieldToFilter('price', array('gt' => 300))
            ->addAttributeToSelect(array('name', 'image', 'url_key', 'price', 'visibility'), 'inner');

        Mage::getSingleton('core/resource_iterator')->walk(
            $products->getSelect(),
            array(array($this, 'walkCallback'))
        );

    }

    public function walkCallback($args) {
        $product = Mage::getModel('catalog/product');
        $product->setData($args['row']);

        var_dump($args['row']);
    }

  • The second join parameter must be ‘inner’ to pull the required data out using this method.

Joining Tables in a Collection

<?php
$collection->join(
	array('banner_item' => 'llapgoch_banners/banner_item'),
	'banner_item.banner_id=main_table.banner_id',
        array('banner_item_title' => 'title')
);

– The first parameter of the join, is the table to join onto. It’s an array, the key of the array being the alias that we’ll use for the table.
– The second parameter is the ON part of the query, I.e. the query to join the two tables together. Use the alias name given in the first parameter to reference the table that we’re joining on, and Magento uses the alias main_table for the table to which this collection refers.
– The third parameter is the fields which should be selected from the table we’re joining on. Leaving this parameter as null will cause Magento to select everything from the table; **This could overwrite variables with the same name from the main table if they clash **. As in the example, this parameter can be used as key value pairs, the key becoming the field’s alias in the query.

Left Joining

  • The collection object doesn’t have a left join method itself, so the collection’s select object needs to be used:
    <?php
    $collection->getSelect()->joinLeft(
    	array('banner_item' => $collection->getTable('llapgoch_banners/banner_item')),
    	'banner_item.banner_id=main_table.banner_id',
    	array('banner_item_title' => 'title')			
    );
    

  • The select object is an instance of Varien_Db_Select, this extends Zend_Db_Select, which is where this method is defined.

  • The first parameter is the table name (which needs to be resolved by the getTable method, either on the collection or connection object), this needs to be done here and not in the collection object’s join method because the collection object performs this operation for us.
  • In the example, we use an array as the first parameter instead of just the table name. The key of this array becomes the alias for the table.
  • The second parameter is the ON query. Use the alias (if defined) from the first parameter. Main table is the table for which the collection refers to.
  • The third parameter is the columns is the columns we would like to select from the joined table. Leaving this as null (default) will cause all columns to be selected. If no columns are required (E.g. we’re just joining to use the data for an aggregate query), then pass an empty array in as this parameter.

Inner Join

  • The select object also includes an inner join method, if the collection’s join method can’t be used:
    <?php
    $collection->getSelect()->joinInner(
    	array('banner_item' => $collection->getTable('llapgoch_banners/banner_item')),
    	'banner_item.banner_id=main_table.banner_id',
    	array('banner_item_title' => 'title')			
    );
    

Grouping

<?php
$collection->getSelect()->group('main_table.banner_id');

– The collection’s select object needs to be employed to group by a column. main_table references the table to which the collection refers to.

Adding aggregate queries

Method One

<?php
$collection->addExpressionFieldToSelect('num_banner_items', 'COUNT(*)', null);

– The first parameter is the fieldset name the value will be assigned to
– The second parameter is the query, aggregate or otherwise.
– The third parameter can be null, or an array of key values used in replacing values specified in the query between double curly brackets: {{value_to_be_replaced}}

Method Two 

<?php
$collection->getSelect()->columns('COUNT(*) as num_banner_items');

– This will add the parameter ‘num_banner_items’ to the collection object’s items.

Randomly Ordering a Collection

Method One

<?php
$collection->getSelect()->orderRand();

Method Two

<?php
$collection->getSelect()->order(new Zend_Db_Expr('RAND()'));

– See the note later on as to what the Zend_Db_Expr object actually is.

Adding a Straight Where Query

<?php
$collection->getSelect()->where('main_table.banner_id <= 1');

Limiting using the Select Object

  • Instead of setting the and page size on the collection, a limit can be called directly on the select object:
    <?php
    $collection->getSelect()->where('main_table.banner_id <= 1');
    

### Getting a collection’s full query ###

<?php
$collection->getSelect()->assemble()

Having

  • Use having to filter on aliases that have been set up either in the $collection->getSelect()->columns method or the $collection->addExpressionFieldToSelect method.
    <?php
    $collection->getSelect()->having('COUNT(*) = ?', 2);
    
    • Note: When using havings or aggregate queries in an admin grid, be careful as Magento will strip off all column names when it performs its pagination queries. You won’t be able to use aggregate aliases in the having, so use the aggregate function again.

The Zend_Db_Expr Object

This is an object which accepts an expression as its parameter. All the object does is cast the value passed in to a string, and then give it back in its _toString() method:

<?php
class Zend_Db_Expr
{
    protected $_expression;
    
    public function __construct($expression)
    {
        $this->_expression = (string) $expression;
    }
    public function __toString()
    {
        return $this->_expression;
    }
}

Flat Catalog

More complex queries may fail id flat catalog is toggled. These can be adapted and checked for using the following check:

  public function isProductFlatCatalogOn()
    {
        $flatHelper = Mage::helper('catalog/product_flat');
        return $flatHelper->isAvailable() && !Mage::app()->getStore()->isAdmin() && $flatHelper->isBuilt(true);
    }

Adding Values For Multiselect Attributes in Magento

Add this to your XML;

<global>
 <sales>
            <quote>
                <item>
                    <product_attributes>
                        <location />
                    </product_attributes>
                </item>
            </quote>
        </sales>
	</global>
  • This makes the product attributes accessible to the Mage_Sales_Model_Quote_Config class’ getProductAttributes() method. This reads in the sales/quote/item/product_attributes node.

  • This is called by the _assignProducts method of the “`Mage_Sales_Model_Resource_Quote_Item_Collection’s _assignProducts() method, where it adds all of the attributes to the product collection of the quote item collection.

Copy the attributes to the quote item

Unlike copying from quote items to order items, there isn’t an XML method to do this – it has to be accomplished through an observer. Add the following to the config (with the observer / method substituted):

<frontend>
    <events>
         <sales_quote_item_set_product>
             <observers>
                 <observer_name>
                        <class>model/observer</class>
                        <method>addAttributesToQuoteItem</method>
                 </observer_name>
             </observers>
         </sales_quote_item_set_product>
    </events>
</frontend>

Then create the following method for the observer above:

    public function addAttributesToQuoteItem($observer){
        $quoteItem = $observer->getQuoteItem();
        $product = $observer->getProduct();
        $quoteItem->setLocation($product->getLocation());
    }