Codegento Who Let Mage Out Of The Cage?

4Apr/1114

Observers and Dispatching Events

Posted by Ben Robie

As stated in many previous posts, it is not a wise idea to modify core code. There are basically three ways to extend/change the behavior of Magento and since copying Mage files down into the local code pool is the last resort, and we have already talked about rewriting, I thought I would try my hand at explaining the Event Dispatching/Observer system.

What is this Event Dispatching system I speak of?

Magento has picked places in their code where they feel people might want to extend the functionality of the core. In those places, they "dispatch an event" and pass along any objects that might be related to that event. You can assign a function(s) to "listen" to these events and in the functions you can act upon the objects that are passed in.

For instance, if you needed to send some order information to another system after an order has been placed, you could rewrite Mage_Checkout_Model_Type_Onepage's saveOrder() function and add the necessary code, but you will quickly find out that there are other models that you would have to alter as well. If you look closely at that function, at the end you will find:

        Mage::dispatchEvent(
            'checkout_submit_all_after',
            array('order' => $order, 'quote' => $this->getQuote(), 'recurring_profiles' => $profiles)
        );

This gives us the opportunity to act on the order (or quote, or recurring profile) object in a completely "decoupled" way. Not only that, but this same event is fired at the end of every checkout, so you only have to have your code in ONE place.

So how do you hook into this event that is dispatched?

  1. Create a new Model. This model doesn't need to extend anything specific and to be honest, most "observer models" don't extend anything.
  2. Create a new function in the new model. This function should be public and take in a variable called $observer (actually, it can be called anything, but convention calls it $observer)
  3. Register this model/function to be called when the event is dispatched. Below is an example of how do do that.
  4. Clear the cache.
<global>
	<events>
		<checkout_submit_all_after>
		    <observers>
		        <awesome_example>
		            <type>singleton</type>
		            <class>awesome/observer</class>
		            <method>doSomething</method>
		        </awesome_example>
		    </observers>
		</checkout_submit_all_after>
   	</events>
</global>
  • <global> - If you want your observer to listen no matter where the event is dispatched from, put it here. You can also put it in "frontend" or "adminhtml".
  • <events> - This is the element that stores all of the events that are registered.
  • <checkout_submit_all_after> - This is the "event" that you are listening to.
  • <observers> - This is the type of event. I don't think there are others.
  • <awesome_example> - This is a unique string that defines this configuration. It can be anything, and just needs to be unique.
  • <type> - I have always used singleton, but other options can be "model" or "object". The "singleton" will create the object as Mage::getSingleton() while both "object" and "model" will use Mage::getModel() when creating the observer object.
  • <class> - This is the observer class.
  • <method> - This is the function to be called in the observer class.

Here is the contents of our observer model:

<?php

class Super_Awesome_Model_Observer
{
	public function doSomething($observer){
		if (isset($observer['orders'])) {
			$orders = $observer['orders'];
			foreach ($orders as $order) {
				//Do something with the order
			}
		} else {
			//Do something with the order
		}
	}
}

Because the $observer variable can be anything and contain anything we have to make sure our method can handle what is being passed in. In this case it can either have "orders" or "order" passed in, depending on what checkout you go through.

So now, when you go through ANY checkout, this observer will be used to do something with your order(s).

What happens when you have multiple observers listening to the same event?

That is a fun problem. You can affect the order of your observers by implementing the element in your module config, but this might become tricky to remember/maintain. If you can, you should code your observers as if there are ASYNCHRONOUS even though they are SYNCHRONOUS.

Example: If I have two modules (Super_Awesome and Super_Rad) that both listen to the "controller_action_predispatch" event. Super_Awesome will be dispatched to first because it will be loaded in the config first by alphabetical order. However, if I make Super_Awesome "depend" on Super_Rad, Super_Rad will be loaded into the config first and it's observer will be used before Super_Awesome's.

Can you do anything else with the event observers?

This event dispatching stuff shouldn't only be for the Mage team to implement. In any of your development, consider using this pattern to make your code cleaner and more maintainable/extendable.

Also, check out MageDev's blog to find out how to Add Event Observers On The Fly.

Happy Observing!

28Mar/1115

Debugging Magento Step by Step

Posted by Ben Robie

Although these debugging tips can be applied towards any LAMP application (for the most part), we wanted to give a more narrowly focused step by step debugging checklist for Magento.

Get XDebug or ZendDebugger Working
I, personally, don't know how anyone can develop effectively outside of an IDE (mine being Eclipse PDT), or without a debugging environment. When all else fails, putting a breakpoint in the beginning of your backtrace and stepping line by line is the best way to debug and/or learn what Magento is doing.
Articles about setting up XDebug with PDT
Articles about setting up ZendDebugger with PDT

Clear the Cache
If you are using caching at all within Magento, one of the first steps to answering the question - "Why is this not working?" - is to clear the Magento cache. There are two ways to clear the cache:
System > Cache Management > Select All > "Refresh" Massaction > Submit
Delete the "cache" directory under BASE_MAGENTO_DIR/var

Clear the Browser Cache
More than once, I have wasted 10 minutes trying to figure out why something wasn't rendering to the browser correctly, only to find that my browser was caching old responses. If you don't know how to clear your browser cache, see the following links:
Firefox
Internet Explorer
Safari
Google Chrome

Make Sure Your Configurations Are Set For the Proper Scope
Because the System > Configurations can be set for specific scopes (Default / Website / Store View), we have to make sure that we have set the configurations for the proper scope. To check this, use the Magento Admin Panel -
System > Configuration > Upper Left Corner (Change to the scope/store view you are working with)

Turn On Logging
This one is a big one. Almost EVERY time, if there is a problem in Magento, something will appear in either the system.log or the exception.log. Checking these logs should become second nature to you. To turn on logging, use the Magento Admin Panel -
System > Configuration > Advanced > Developer > Log Settings > Enable
Check the magento exception.log under BASE_MAGENTO_DIR/var
Check the magento system.log under BASE_MAGENTO_DIR/var

Use Template Path Hints
Template Hints are pretty amazing. When you want to figure out which Block or phtml file a problem is occurring in, you can turn on template hints and see the information right on the browser. It's awesome, so if you haven't tried it, try it right now.
To turn on template path hints, use the Magento Admin Panel -
First, you NEED to select the store view you want to show the hints on. If you don't, the configuration option won't appear.
System > Configuration > Advanced > Developer > Debug > Template Path Hints and/or Add Block Names to Hints
By default, there is no way to turn on hints for the Admin Panel, so take a look at this article for help: Enable Template/Block Hints in Admin Panel

Tracing
If you have implemented a home-grown tracing mechanism, turn it on and look at the log. A common tracing mechanism consists of coding tracing statements within the code that log ENTRY into a function, important information within the function, and EXIT from the function. This information can be crucial in debugging problems in a production environment.

XDebug Tracing
XDebug provides some pretty kick-butt tracing ability. If you turn it on, get ready for a lot of data, but it can be very helpful when trying to pinpoint a problem. In the article "Tracing PHP Applications with xdebug" it gives some really good information about xdebug tracing.

Check the Apache error_log
When all else fails, take a look at the apache error_log. It could be a problem with the setup of your webserver.

Google It
It may come as a surprise, but there are still people that don't turn to Google as a "troubleshooting option". They should. When you want to ask a developer something, first ask it to Google. For instance, if you want to know how to do a rewrite of a model, just Google: How do I rewrite a model in Magento. If you want to know why you are getting an error like "HEADERS ALREADY SENT", Google: headers already sent magento. Chances are, another developer will give you a response like THIS if you ask them the question anyway.

Search Or Ask The Magento Forums
One of the big pros to Magento is the active community members. Many are willing to help answer your questions for free, and quickly. Before you ask your question, search the forums. If you don't find your answer, ask your question.

Ask Your Questions in IRC Chat
(irc.freenode.net) #magento
The guys in here are cranky but some of them know there stuff, so ask your question, wish upon a star, and hope they answer.

Use the Varien_Profiler
If you are having performance issues, turn on and use the Varien_Profiler. Do this in the Admin Panel -
System > Configuration > Advanced > Developer > Debug > Profiler
http://inchoo.net/ecommerce/magento/keeping-your-magento-fit-with-built-in-profiler/
http://activecodeline.net/extending-default-magento-profiler
http://www.neptuneweb.com/blog/?id=24

Then, if you still can't figure it out, start all over again. :)

26Mar/1123

How Blocks And PHTML Work Together

Posted by Ben Robie

Early on in my Magento-coding days, I struggled with the relationship between the block classes and the phtml files. Which came first? Who owned who? Do you need blocks? Do you need phtml? First and foremost, we need to understand that the complexity of the layout/block/phtml relationship is necessary, powerful, and flexible. Those three pieces allow for the ability to "theme" Magento and to easily override pieces of the store's look and feel without messing with the default layout/theme.

So where do we start? How about with this statement: Phtml files are owned and used by blocks. In the code, however, they are called "templates", not "phtml". Each block instance has 0-1 templates assigned to it (either through the layout XML files or hard coded in the block code itself).

Assigned in the layout file:

<block type="catalog/product_view" name="product.info" template="catalog/product/view.phtml">

Assigned in the code:

public function __construct()
{
        parent::__construct();
        $this->setTemplate('catalog/product.phtml');
}

So how does the flow work? It all starts with the layout files. Calling them layout files make sense in some cases, but the layout files don't always define the "layout" of the page. Actually, more often than not, the phtml does that. With that said however, the layout files define WHAT can be blocks/phtml are AVAILABLE. For example, lets say you have a snippet of your layout file that looks like this:

<reference name="content">
    <block type="awesome/outer_view" name="outer.view" template="awesome/outer/view.phtml">
        <block type="awesome/inner_view" name="inner.view" template="awesome/inner/view.phtml"/>
    </block>
</reference>

What this IS saying is:

  1. We are going to add the "outer.view" block to the already existing "content" block (aka: outer.view is now a child of content).
  2. outer.view's phtml WILL be rendered automatically (because content's block renders ALL of it's child blocks)
  3. Because "inner.view" is within the ""outer.view" block, the "inner.view" is now a child of "outer.view"

What this IS NOT saying:

  1. inner.view's phtml will automatically be rendered (This depends on it's parent - outer.view).

So how do we know what phtml will be rendered, and what won't? The answer is - it ALWAYS depends on the parent block. The initial loading and rendering of the block(s) is done in the controller action. The action will generally contain something like:

$this->loadLayout();
$this->renderLayout();

The loadLayout() looks through all of the appropriate layout files and creates one big layout of the page. Open up page.xml right now to aid in the understanding of the rest of this article. The block "root" is the Great Great Great Great Great Grandparent block. It all starts with him.

The renderLayout() begins by

  1. Calling the toHtml() function on the "root" block (Mage_Page_Block_Html)
  2. The toHtml() ends up rendering the phtml that was assigned to the "root" block (page/3columns.phtml)
    • While rendering the page/3columns.phtml, the rendering agent comes across the line: echo $this->getChildHtml('head'). Whenever we see $this inside phtml, it is always referring to the block that the phtml has been assigned to.
  3. So now we need to see if "head" is a child block of $this (which is "root"). If we look at the page.xml we see that "head" IS a child, so we will now call the toHtml() function on the "head" block (Mage_Page_Block_Html_Head)
  4. The toHtml() ends up rendering the phtml file that is hard-coded in the block's constructor which is (page/html/head.phtml).
  5. This type of flow goes ON and ON until we pick it up again at the "content" block which happens to be a Mage_Core_Block_Text_List block.
  6. The toHtml() function will be called on this block, but there is no phtml assigned to it anywhere. Instead, the toHtml() function just says "loop through all of my children and render them one right after another".
  7. If we are sticking with our example up above, we have added 1 child block to "content": "outer.view". We will assume that "awesome/outer_view" maps to Super_Awesome_Block_Outer_View and that Super_Awesome_Block_Outer_View extends Mage_Core_Block_Template.
  8. So now the toHtml() gets called on Super_Awesome_Block_Outer_View so as long as we haven't overridden the _toHtml() function it will render the phtml that is assigned to it (awesome/outer/view.phtml).

Assuming awesome/inner/view.phtml looks like this:

<div>Inner View</div>

If the awesome/outer/view.phtml looks like this:

<div>Start Outer</div>
<div>End Outer</div>

It will NEVER render the inner.view and will look like this:

Start Outer
End Outer

If the awesome/outer/view.phtml looks like this:

<div>Start Outer</div>
<?php echo $this->getChildHtml('inner.view') ?>
<div>End Outer</div>

It WILL render the inner view in between the two Start and End divs like this:

Start Outer
Inner View
End Outer

If the awesome/outer/view.phtml looks like this:

<div>Start Outer</div>
<?php echo $this->getChildHtml() ?>
<div>End Outer</div>

It WILL render the inner view (and any other child blocks) in between the two Start and End divs like this:

Start Outer
Inner View
End Outer

We can't assume that just because the block is defined in the layout that it will be rendered. Either the toHtml() needs to explicitly render the child blocks, or the phtml assigned to the block must render the child blocks via the getChildHtml() function.

So, recapping what we learned:

  • Block are what drive the output to the browser.
  • Templates/Phtml are assigned to blocks
  • The parent/child relationships of blocks are defined in the layout files (but can be in the code to if a block calls the setChild() function in itself).
  • The parent block determines how/when/if the child block is rendered (generally through the "echo $this->getChildHtml()" functionality.
  • Any time you see $this in a phtml file, the $this object is the instance of the block you are currently rendering.
15Mar/119

Joining An EAV Table (With Attributes) To A Flat Table

Posted by Ben Robie

If you have created a "custom" flat table that has a foreign key to an existing Magento EAV table, you might come across the need to join the two together. If you main table is the flat table, you will find it difficult to join the two using existing Magento code.

Below is a snippet of code that joins an EAV table (and all of its attributes) to a flat table. I have no idea about the performance of this bad boy, but I know it works.

/**
 * This assumes that the foreign key is the entity id of the eav table.
 * $collection is a collection object of a flat table.
 * $mainTableForeignKey is the name of the foreign key to the eav table.
 * $eavType is the type of entity (the entity_type_code in eav_entity_type)
 */
public function joinEavTablesIntoCollection($collection, $mainTableForeignKey, $eavType){

		$entityType = Mage::getModel('eav/entity_type')->loadByCode($eavType);
		$attributes = $entityType->getAttributeCollection();
		$entityTable = $collection->getTable($entityType->getEntityTable());

		//Use an incremented index to make sure all of the aliases for the eav attribute tables are unique.
		$index = 1;
		foreach ($attributes->getItems() as $attribute){
			$alias = 'table'.$index;
			if ($attribute->getBackendType() != 'static'){
				$table = $entityTable. '_'.$attribute->getBackendType();
				$field = $alias.'.value';
				$collection->getSelect()
				->joinLeft(array($alias => $table),
					   'main_table.'.$mainTableForeignKey.' = '.$alias.'.entity_id and '.$alias.'.attribute_id = '.$attribute->getAttributeId(),
				array($attribute->getAttributeCode() => $field)
				);
			}
			$index++;
		}
		//Join in all of the static attributes by joining the base entity table.
		$collection->getSelect()->joinLeft($entityTable, 'main_table.'.$mainTableForeignKey.' = '.$entityTable.'.entity_id');

		return $collection;

	}

The example below is joining the wishlist table to the customer's eav tables:

$wishlistCollection = Mage::getModel('wishlist/wishlist')->getCollection();
$wishlistCollection = Mage::helper('awesome')->joinEavTablesIntoCollection($wishlistCollection, 'customer_id', 'customer');
echo $wishlistCollection->getSelect()->__toString();

This produces the following SQL:

SELECT `main_table`.*, `table1`.`value` AS `confirmation`, `table3`.`value` AS `created_in`, `table4`.`value` AS `default_billing`, `table5`.`value` AS `default_shipping`, `table6`.`value` AS `dob`, `table8`.`value` AS `firstname`, `table9`.`value` AS `gender`, `table11`.`value` AS `lastname`, `table12`.`value` AS `middlename`, `table13`.`value` AS `password_hash`, `table14`.`value` AS `prefix`, `table16`.`value` AS `suffix`, `table17`.`value` AS `taxvat`, `customer_entity`.* FROM `wishlist` AS `main_table` LEFT JOIN `customer_entity_varchar` AS `table1` ON main_table.customer_id = table1.entity_id and table1.attribute_id = 16 LEFT JOIN `customer_entity_varchar` AS `table3` ON main_table.customer_id = table3.entity_id and table3.attribute_id = 3 LEFT JOIN `customer_entity_int` AS `table4` ON main_table.customer_id = table4.entity_id and table4.attribute_id = 13 LEFT JOIN `customer_entity_int` AS `table5` ON main_table.customer_id = table5.entity_id and table5.attribute_id = 14 LEFT JOIN `customer_entity_datetime` AS `table6` ON main_table.customer_id = table6.entity_id and table6.attribute_id = 11 LEFT JOIN `customer_entity_varchar` AS `table8` ON main_table.customer_id = table8.entity_id and table8.attribute_id = 5 LEFT JOIN `customer_entity_int` AS `table9` ON main_table.customer_id = table9.entity_id and table9.attribute_id = 32 LEFT JOIN `customer_entity_varchar` AS `table11` ON main_table.customer_id = table11.entity_id and table11.attribute_id = 7 LEFT JOIN `customer_entity_varchar` AS `table12` ON main_table.customer_id = table12.entity_id and table12.attribute_id = 6 LEFT JOIN `customer_entity_varchar` AS `table13` ON main_table.customer_id = table13.entity_id and table13.attribute_id = 12 LEFT JOIN `customer_entity_varchar` AS `table14` ON main_table.customer_id = table14.entity_id and table14.attribute_id = 4 LEFT JOIN `customer_entity_varchar` AS `table16` ON main_table.customer_id = table16.entity_id and table16.attribute_id = 8 LEFT JOIN `customer_entity_varchar` AS `table17` ON main_table.customer_id = table17.entity_id and table17.attribute_id = 15 LEFT JOIN `customer_entity` ON main_table.customer_id = customer_entity.entity_id

...which produces the following result:

wishlist_id 	customer_id 	shared 	sharing_code 	                    updated_at 	            confirmation 	created_in 	        default_billing 	default_shipping 	dob 	firstname 	gender 	lastname 	middlename 	password_hash 	                        prefix 	suffix 	taxvat 	entity_id 	entity_type_id 	attribute_set_id 	website_id 	email 	            group_id 	increment_id 	store_id 	created_at 	            updated_at 	            is_active
1 	            2 	            0  	    061d54b5caa53871e1cd3d1d1dc3c028 	2011-03-16 02:10:27 	NULL 	        Default Store View 	NULL 	            NULL 	            NULL 	Ben 	    NULL 	Robie 	    NULL 	    f7d9c4687f98a8c166c15c73574da95a:kF 	NULL 	NULL 	NULL 	2 	        1 	            0 	                1 	        brobie@gmail.com 	1 	  	                    1           2011-03-16 02:10:25 	2011-03-16 02:10:26 	1

Hope this helps! Join on!

15Mar/110

Debugging Translations

Posted by Ben Robie

When your store is dealing with multiple translations, sometimes you just want to see a list of which translations are coming from what modules. Magento doesn't offer a way to get this information without editing the core, so what I am about to do should only be done for debugging and then removed.

First, located:

  • File: app/code/core/Mage/Core/Model/Translate.php
  • Class: Mage_Core_Model_Translate
  • Function: translate()

Then, right before the line "return $result;", put the following snippet of code:

$logModule = str_pad($module, 25, "-", STR_PAD_RIGHT);
$logInString = str_pad($text, 150, "-", STR_PAD_RIGHT);
$logOutString = str_pad($result, 200, " ", STR_PAD_RIGHT);
Mage::log($logModule . '>' . $logInString . '>' . $logOutString, null, 'translate.log');

This will, for every request, write a bunch of lines to the translate.log file under /var/log.

The file will looks something like:


2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->Qty--------------------------------------------------------------------------------------------------------------------------------------------------->Qty
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->OR---------------------------------------------------------------------------------------------------------------------------------------------------->OR
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->Add to Wishlist--------------------------------------------------------------------------------------------------------------------------------------->Add to Wishlist
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->Add to Compare---------------------------------------------------------------------------------------------------------------------------------------->Add to Compare
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->Quick Overview---------------------------------------------------------------------------------------------------------------------------------------->Quick Overview
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->Details----------------------------------------------------------------------------------------------------------------------------------------------->Details
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Tag----------------->Product Tags------------------------------------------------------------------------------------------------------------------------------------------>Product Tags
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Tag----------------->Add Your Tags:---------------------------------------------------------------------------------------------------------------------------------------->Add Your Tags:
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Tag----------------->Add Tags---------------------------------------------------------------------------------------------------------------------------------------------->Add Tags
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Tag----------------->Add Tags---------------------------------------------------------------------------------------------------------------------------------------------->Add Tags
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Tag----------------->Use spaces to separate tags. Use single quotes (') for phrases.--------------------------------------------------------------------------------------->Use spaces to separate tags. Use single quotes (') for phrases.
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Checkout------------>My Cart----------------------------------------------------------------------------------------------------------------------------------------------->My Cart
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Checkout------------>You have no items in your shopping cart.-------------------------------------------------------------------------------------------------------------->You have no items in your shopping cart.
2011-03-14T18:55:25+00:00 DEBUG (7): Mage_Catalog------------->Compare Products-------------------------------------------------------------------------------------------------------------------------------------->Compare Products            

After you figure out which module is displaying the translation, remove the lines of code from Mage_Core_Model_Translate and be on your merry way!

14Mar/114

Debugging “HEADERS ALREADY SENT” error

Posted by Trent Ohannessian

Sometimes Magento throws the "HEADERS ALREADY SENT" error to the system.log and it can be quite frustrating determining where headers were already sent.  Especially when working on a shared codebase and you're pretty sure that you and your team haven't done anything to cause this new error.  All you normally get in the system.log file is the error message and a stacktrace to where that exception was thrown from.  Nothing about where the headers were previously sent.  It would be nice if Magento would tell us the exact file and line that is causing the problem.  Turns out that Magento does know this information and we can do a little tweak to send it to the log.

That error is thrown from Mage_Core_Controller_Response_Http -> sendHeaders().  This function calls the super class function that actually does the check to see whether or not headers have already been sent, Zend_Controller_Response_Abstract -> canSendHeaders().

The Zend_Controller_Response_Abstract class handles, among other things, sending response headers and tracking the last time the headers were sent (and from what file and line).  Here is what that function looks like, and where we'll make a change:

public function canSendHeaders($throw = false) {
    $ok = headers_sent($file, $line);
    if ($ok && $throw && $this->headersSentThrowsException) {
        #require_once 'Zend/Controller/Response/Exception.php';
        throw new Zend_Controller_Response_Exception('Cannot send headers; headers already sent in ' . $file . ', line ' . $line);
    }
    return !$ok;
}

You'll notice the code inside the if statement attempts to include the file and line that previously sent headers in the exception message.  More often than not the $throw param isn't passed in so the exception isn't thrown and that useful message is never seen.

Since we really need to get at that information, add the following code immediately before the return:

if ($ok) {
    Mage::log('Cannot send headers; headers already sent in ' . $file . ', line ' . $line);
}

The $ok variable is a little confusing since it really means "have headers already been sent."  The added code will log the offending file and line in system.log immediately before the exception shows up.  Now, navigate to that file and see if there is an inadvertent echo or var_dump or something else that's causing the problem.

Once you have the problem fixed make sure you revert these changes since we don't like changing Core code.

12Mar/114

Rewriting Or Rerouting A Controller

Posted by Ben Robie

Just as with models/blocks/helpers, there are times when we want to change the way a core Magento controller works. Since we DON'T EVER WANT TO CHANGE THE CORE, we need a way to tell a request to use OUR controller instead of Magento's controller. We do this by changing the configurations that hold the routing instructions.

Many MVC frameworks use the concept of a route to tell the application how to map the URL to the correct controller/action. Magento has part of that mapping in the config.xml. Below is an example of a common route configuration for the frontend.

<frontend>
   <routers>
      <awesome>
           <use>standard</use>
           <args>
              <module>Super_Awesome</module>
              <frontName>awesome</frontName>
           </args>
      </awesome>
   </routers>
</frontend>

With that frontend route, a url that looks like this: http://example.com/awesome/noun/verb will be routed to the NounController's verbAction() inside the Super_Awesome module.

In our example, I want to dispatch an event at the beginning of the indexAction(), but I don't want to make ANY other changes. To do this, I will have to create my controller that extends the Mage_Checkout_OnepageController and override the indexAction().

But how do I tell Magento to use my controller instead of Mage_Checkout_OnepageController? Well, that is the point of this post.

First, we need to create our controller:

<?php

require_once 'Mage/Checkout/controllers/OnepageController.php';

class Super_Awesome_OnepageController extends Mage_Checkout_OnepageController
{
    public function indexAction()
    {
    	Mage::dispatchEvent('awesome_event', array('piece of data'));
    	parent::indexAction();

    }
}

The require_once is important. If you don't do this, your code will blow up. How we need to tell Magento to change it's route mappings.

Here is the piece of the config.xml where we do just that -

	<frontend>
		<routers>
	    	<checkout>
	        	<use>standard</use>
		        <args>
	               <modules>
	                  <Super_Awesome before="Mage_Checkout">Super_Awesome</Super_Awesome>
	               </modules>
	            </args>
		    </checkout>
		</routers>
	</frontend>

If we were to write this in English, it would simply say, "When a request comes in with "checkout" as the module, first look for whatever controller it is requesting in Super_Awesome before looking in Mage_Checkout. If you don't find it in Super_Awesome, fall back on Mage_Checkout".

For this to work, the Controller has to be named the same.

If you are wanting to reroute an adminhtml controller, you would use the same concept. If I have a module called Super_Awesome, I tend to create a Adminhtml directory under controllers to house any of my Admin controllers. The directory structure would be: Super/Awesome/controllers/Adminhtml/NounController.php.

So in the config.xml, I would have something equivalent to:

	<admin>
		<routers>
			<adminhtml>
				<args>
					<modules>
						<awesome before="Mage_Adminhtml">Super_Awesome_Adminhtml</awesome>
					</modules>
				</args>
			</adminhtml>
		</routers>
	</admin>

...and now you know how to rewrite/reroute requests to different controllers.

12Mar/110

Rewriting a Helper Class

Posted by Ben Robie

The ability to rewrite classes are a super valuable tool when you want to extend the business logic of Magento, or make tweaks to the core. And since we have already talked about rewriting models and blocks, lets talk about helpers!

For a third time, I will state that you SHOULD NEVER CHANGE THE CORE, and if you can help it, you shouldn't just copy whole classes down into the local pool. If you want a more extensive lesson on rewriting, read the Rewriting a Model Class post. Below we will show you what you need to do to rewrite a helper.

Just as with models, helpers have a specific way to instantiate them. To use a model, we simple say: Mage::helper('awesome'). The thing that is unique about helpers is that you don't NEED to put the class name in the alias. If you don't, it will automatically look for Mage::helper('awesome/data').

Rewriting helpers is done the SAME way that models and blocks are done, but within the "helpers" group element. In our example, we will pretend that we NEVER EVER EVER want ANYONE to check out as a guest,. Because this is the case, we will need to change the Mage_Checkout_Helper_Data class's isAllowedGuestCheckout() function. Here is how we rewrite that class to do what we want:

Contents of the config.xml for the rewrite:

<helpers>
	<awesome>
		<class>Super_Awesome_Helper</class>
	</awesome>
	<checkout>
		<rewrite>
			<data>Super_Awesome_Helper_Data</data>
		</rewrite>
	</checkout>
</helpers>

And here is our helper -

<?php

class Super_Awesome_Helper_Data extends Mage_Checkout_Helper_Data
{
    public function isAllowedGuestCheckout(Mage_Sales_Model_Quote $quote, $store = null)
    {
        return false;
    }
}

Now, whenever Mage::helper('checkout') or Mage::helper('checkout/data') is called, it will create an instance of Super_Awesome_Helper_Data instead of Mage_Checkout_Helper_Data.

Happy helping!

11Mar/1123

Creating Custom Magento Reports

Posted by Ben Robie

This post is going to be LONG. And not fun. To be honest, I'm not sure how some of this stuff works, but I do know that I have successfully created two different kinds of reports.

Some things to note:

  • You should never create reports off of a transactional table. Running the report could potentially create locks that would affect your customer facing website.
  • This blog post will not contain examples of "aggregation observers and tables", but you should use them when creating reports. You run them through cron and an example observer can be found here: Mage_Sales_Model_Observer.

Let's get started shall we? I will give examples of two different kinds of reports in this post. They will be called:

  • Simple - A report that has the thin/horizontal form for selecting "to", "from" and "period". (See Reports > Customers > New Accounts)
  • Complex - A report that has a seperatly defined filter block for filtering the report data. ( See Reports > Products > Best Sellers)

Simple Report

Table structure (setup script):

<<?php

$installer = $this;

$installer->startSetup();

$installer->run("

-- DROP TABLE IF EXISTS {$this->getTable('super_awesome_example_simple')};
CREATE TABLE {$this->getTable('super_awesome_example_simple')} (
`id` INT NOT NULL AUTO_INCREMENT ,
`description` VARCHAR( 100 ) NOT NULL ,
`value` DECIMAL(12,2) NOT NULL ,
`period` DATE NOT NULL ,
PRIMARY KEY ( `id` )
) ENGINE = MYISAM ;

INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example One Description', 10.00, '2011-02-01');
INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example Two Description', 12.50, '2011-02-15');
INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example Three Description', 5.35, '2011-03-01');
INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example Four Description', 7.67, '2011-03-04');
INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example Dupe', 1.23, '2011-03-01');
INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example Dupe', 2.34, '2011-03-02');
INSERT INTO {$this->getTable('super_awesome_example_simple')} (`description`, `value`, `period`) values ('Example Dupe', 3.45, '2011-03-03');

");

$installer->endSetup();

The types of classes needed:

  • Report Controller
  • Report Block
  • Report Grid
  • Configs (Menu and Table Definitions)
  • Model, Resource Model, Collection Model

The Folder Structure (can be different if you want it to be):

Super
  |_ Awesome
      |_Block
      |   |_Adminhtml
      |       |_Report
      |          |_Simple
      |          |   |_Grid.php
      |          |_Simple.php
      |_controllers
      |    |_Adminhtml
      |       |_Report
      |          |_ExampleController.php
      |_etc
      |   |_adminhtml.xml
      |   |_config.xml
      |_Helper
      |   |_Data.php
      |_Model
            |_Mysql4
            |    |_Report
            |    |   |_Simple
            |    |      |_Collection.php
            |    |_Simple.php
            |_Simple.php

The first thing we need is to create a menu item to get to the report. Most likely, you will put your reports under the "Report" section, but I choose to put it in my own section:

Contents of adminhtml.xml -

<?xml version="1.0"?>
<config>
    <menu>
        <awesome translate="title" module="awesome">
            <title>Awesome</title>
            <sort_order>15</sort_order>
            <children>
                <simple translate="title" module="awesome">
                    <title>Simple Report</title>
                    <sort_order>1</sort_order>
                    <action>adminhtml/report_example/simple</action>
                </simple>
            </children>
        </awesome>
    </menu>
</config>

Now that you have an "action" element pointing to "adminhtml/report_example/simple", we need to make sure that route works. To do that, we need to configure the route in the config.xml and then create the controller. While we are in the config.xml, I will put my table definitions in.

Partial of config.xml

	<admin>
		<!--
			Here we are telling the Magento router to look for the controllers in the Super_Awesome_controllers_Adminhtml before we look in the
			Mage_Adminhtml module for all urls that begin with /admin/controller_name
		 -->
		<routers>
			<adminhtml>
				<args>
					<modules>
						<awesome before="Mage_Adminhtml">Super_Awesome_Adminhtml</awesome>
					</modules>
				</args>
			</adminhtml>
		</routers>
	</admin>
	<models>
            <awesome>
                <class>Super_Awesome_Model</class>
                <resourceModel>awesome_mysql4</resourceModel>
            </awesome>
             <awesome_mysql4>
                <class>Super_Awesome_Model_Mysql4</class>
                <entities>
                    <simple>
                        <table>super_awesome_example_simple</table>
                    </simple>
                </entities>
            </awesome_mysql4>
        </models>

        <resources>
            <awesome_setup>
                <setup>
                    <module>Super_Awesome</module>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </awesome_setup>
            <awesome_write>
                <connection>
                    <use>core_write</use>
                </connection>
            </awesome_write>
            <awesome_read>
                <connection>
                    <use>core_read</use>
                </connection>
            </awesome_read>
        </resources>

The contents of Super_Awesome_Adminhtml_Report_ExampleController:

<?php

class Super_Awesome_Adminhtml_Report_ExampleController extends Mage_Adminhtml_Controller_Action
{

	public function _initAction()
	{

		$this->loadLayout()
		->_addBreadcrumb(Mage::helper('awesome')->__('Awesome'), Mage::helper('awesome')->__('Awesome'));
		return $this;
	}

	public function simpleAction()
	{
		$this->_title($this->__('Awesome'))->_title($this->__('Reports'))->_title($this->__('Simple Report'));

		$this->_initAction()
		->_setActiveMenu('awesome/report')
		->_addBreadcrumb(Mage::helper('awesome')->__('Simple Example Report'), Mage::helper('awesome')->__('Simple Example Report'))
		->_addContent($this->getLayout()->createBlock('awesome/adminhtml_report_simple'))
		->renderLayout();

	}

	public function exportSimpleCsvAction()
	{
		$fileName   = 'simple.csv';
		$content    = $this->getLayout()->createBlock('awesome/adminhtml_report_simple_grid')
		->getCsv();

		$this->_prepareDownloadResponse($fileName, $content);
	}

}

The line that says: "->_addContent($this->getLayout()->createBlock('awesome/adminhtml_report_simple'))" really is the important one here. This is telling Magento which block to use to drive the WHOLE report. Oh, and look at the exportSimpleCsvAction() - that all you need to export a grid into CSV.

So let's take a look at the block Super_Awesome_Block_Adminhtml_Report_Simple -

<?php

class Super_Awesome_Block_Adminhtml_Report_Simple extends Mage_Adminhtml_Block_Widget_Grid_Container
{
 	public function __construct()
    {
    	$this->_blockGroup = 'awesome';
        $this->_controller = 'adminhtml_report_simple';
        $this->_headerText = Mage::helper('awesome')->__('Simple Report');
        parent::__construct();
        $this->_removeButton('add');
    }
}

There isn't much to it. It will use the parent class to auto generate the name of the "grid" block (aka the block with all of the report data):

Here is the content of that block (Super_Awesome_Block_Adminhtml_Report_Simple_Grid) -

<?php

class Super_Awesome_Block_Adminhtml_Report_Simple_Grid extends Mage_Adminhtml_Block_Report_Grid
{

	public function __construct()
	{
		parent::__construct();
		$this->setId('gridSimple');
	}

	protected function _prepareCollection()
	{
		parent::_prepareCollection();
		$this->getCollection()->initReport('awesome/report_simple_collection');

	}

	protected function _prepareColumns()
	{
		$this->addColumn('description', array(
            'header'    =>Mage::helper('reports')->__('Description'),
            'index'     =>'description',
            'sortable'  => false
		));

		$currencyCode = $this->getCurrentCurrencyCode();

		$this->addColumn('value', array(
            'header'    =>Mage::helper('reports')->__('Value'),
            'index'     =>'value',
	       	'currency_code' => $currencyCode,
            'total'     =>'sum',
            'type'      =>'currency'
            ));

            $this->addExportType('*/*/exportSimpleCsv', Mage::helper('reports')->__('CSV'));

            return parent::_prepareColumns();
	}

}

The important parts:

  1. The fact that it extends Mage_Adminhtml_Block_Report_Grid - This will require you to do things specific ways and will also automatically put the filter piece at the top of your report so you can query by date and period.
  2. The collection that it is using - By default it uses the Mage::getResourceModel('reports/report_collection') collection, but you need to call the initReport() function on that collection with the collection class that will handle YOUR data. Ours is: initReport('awesome/report_simple_collection')
  3. The columns - Like I said earlier, you really should be running these reports on "already aggregated" data, but you still might want to sum some things up for totals. Any column that has a 'total' => 'sum" attribute will be included in the total section correctly.
  4. The export types - One thing that I discovered while doing this is that every grid has the ability to export itself as CSV or XML file. Cool huh?

We mentioned the collection in #2, so let's take a look at that. The contents of the collection -

<?php

class Super_Awesome_Model_Mysql4_Report_Simple_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{

	protected function _construct()
	{
		$this->_init('awesome/simple');
	}

	protected function _joinFields($from = '', $to = '')
	{
		$this->addFieldToFilter('period' , array("from" => $from, "to" => $to, "datetime" => true));
		$this->getSelect()->group('description');
		$this->getSelect()->columns(array('value' => 'SUM(value)'));

		return $this;
	}

	public function setDateRange($from, $to)
	{
		$this->_reset()
		->_joinFields($from, $to);
		return $this;
	}

	public function load($printQuery = false, $logQuery = false)
	{
		if ($this->isLoaded()) {
			return $this;
		}
		parent::load($printQuery, $logQuery);
		return $this;
	}

	public function setStoreIds($storeIds)
	{
		return $this;
	}
}

There really isn't anything special about this collection. It is a normal collection that inits the resource model (so you will need a normal one of those). The only hitch here is that the Mage_Adminhtml_Block_Report_Grid is expecting specific methods to be in this collection. Those methods are:

  • public function setStoreIds($storeIds)
  • public function setDateRange($from, $to)

I am not running the query by store, so I don't care about that. There are examples in other classes if you need to see how to do it, but basically it is adding the store_id to the where clause.

I do however want to add some date range stuff, so I simply call a protected method in my collection to set up the SQL.

If you are having trouble with your SQL, you can debug it easily by changing your load to: parent::load(true, true). This will print out the SQL to the screen and the system.log. Also something that stumped me for a while was that my data was not necessarily showing up on the correct date in my report. This has to do with some code in Mage_Reports_Model_Mysql4_Report_Collection class.

 public function getReportFull($from, $to)
    {
        return $this->_model->getReportFull($this->timeShift($from), $this->timeShift($to));
    }

    public function timeShift($datetime)
    {
        return Mage::app()->getLocale()->utcDate(null, $datetime, true, Varien_Date::DATETIME_INTERNAL_FORMAT)->toString(Varien_Date::DATETIME_INTERNAL_FORMAT);
    }

It does some "time shifting" based on locale, so BE AWARE.

The only other things I haven't talked about are the (empty) Data.php (helper) -

<?php

class Super_Awesome_Helper_Data extends Mage_Core_Helper_Abstract
{

}

...and the normal model class that you should already know how to create. All that has is the init of the resource model in the construct -

<?php

class Super_Awesome_Model_Simple extends Mage_Core_Model_Abstract
{
	protected function _construct()
	{
		parent::_construct();
		$this->_init('awesome/simple');
	}
}

Complex Report

With this, you will see a lot of duplication, but I want to be thorough.

The table structure (setup script):

<?php

$installer = $this;

$installer->startSetup();

$installer->run("

-- DROP TABLE IF EXISTS {$this->getTable('super_awesome_example_complex')};
CREATE TABLE {$this->getTable('super_awesome_example_complex')} (
`id` INT NOT NULL AUTO_INCREMENT ,
`description` VARCHAR( 100 ) NOT NULL ,
`value` DECIMAL(12,2) NOT NULL ,
`period` DATE NOT NULL ,
PRIMARY KEY ( `id` )
) ENGINE = MYISAM ;

INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example One Description', 10.00, '2011-02-01');
INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example Two Description', 12.50, '2011-02-15');
INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example Three Description', 5.35, '2011-03-01');
INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example Four Description', 7.67, '2011-03-04');
INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example Dupe', 1.23, '2011-03-01');
INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example Dupe', 2.34, '2011-03-02');
INSERT INTO {$this->getTable('super_awesome_example_complex')} (`description`, `value`, `period`) values ('Example Dupe', 3.45, '2011-03-03');

");

$installer->endSetup();

The types of classes needed:

  • Report Controller
  • Report Block
  • Report Grid
  • Configs (Menu and Table Definitions)
  • Model, Resource Model, Collection Model
  • The layout.

The Folder Structure (can be different if you want it to be):

Super
  |_ Awesome
      |_Block
      |   |_Adminhtml
      |       |_Report
      |          |_Complex
      |          |   |_Grid.php
      |          |_Complex.php
      |_controllers
      |    |_Adminhtml
      |       |_Report
      |          |_ExampleController.php
      |_etc
      |   |_adminhtml.xml
      |   |_config.xml
      |_Helper
      |   |_Data.php
      |_Model
            |_Mysql4
            |    |_Report
            |    |   |_Complex
            |    |      |_Collection.php
            |    |_Complex.php
            |_Complex.php

And we also have the awesome.xml in design/adminhtml/default/default/layout.

First we create the menu item -
Contents of adminhtml.xml -

<?xml version="1.0"?>
<config>
    <menu>
        <awesome translate="title" module="awesome">
            <title>Awesome</title>
            <sort_order>15</sort_order>
            <children>
                <complex translate="title" module="awesome">
                    <title>Complex Report</title>
                    <sort_order>1</sort_order>
                    <action>adminhtml/report_example/complex</action>
                </complex>
            </children>
        </awesome>
    </menu>
</config>

Then we create the necessary config.xml settings:

<config>
    <modules>
        <Super_Awesome>
            <version>0.1.2</version>
        </Super_Awesome>
    </modules>
    <adminhtml>
        <!-- The <layout> updates allow us to define our block layouts in a seperate file so are aren't messin' with the magento layout files.  -->
    	<layout>
			<updates>
				<awesome>
					<file>awesome.xml</file>
				</awesome>
			</updates>
		</layout>
	</adminhtml>
	<admin>
		<!--
			Here we are telling the Magento router to look for the controllers in the Super_Awesome_controllers_Adminhtml before we look in the
			Mage_Adminhtml module for all urls that begin with /admin/controller_name
		 -->
		<routers>
			<adminhtml>
				<args>
					<modules>
						<awesome before="Mage_Adminhtml">Super_Awesome_Adminhtml</awesome>
					</modules>
				</args>
			</adminhtml>
		</routers>
	</admin>

    <global>
    	<models>
            <awesome>
                <class>Super_Awesome_Model</class>
                <resourceModel>awesome_mysql4</resourceModel>
            </awesome>
            <awesome_mysql4>
                <class>Super_Awesome_Model_Mysql4</class>
                <entities>
                    <complex>
                        <table>super_awesome_example_complex</table>
                    </complex>
                </entities>
            </awesome_mysql4>
        </models>

        <resources>
            <awesome_setup>
                <setup>
                    <module>Super_Awesome</module>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </awesome_setup>
            <awesome_write>
                <connection>
                    <use>core_write</use>
                </connection>
            </awesome_write>
            <awesome_read>
                <connection>
                    <use>core_read</use>
                </connection>
            </awesome_read>
        </resources>

   	 	<blocks>
            <awesome>
                <class>Super_Awesome_Block</class>
            </awesome>
        </blocks>
        <helpers>
            <awesome>
                <class>Super_Awesome_Helper</class>
            </awesome>
        </helpers>
    </global>
</config>

Now we create our controller -

<?php

class Super_Awesome_Adminhtml_Report_ExampleController extends Mage_Adminhtml_Controller_Action
{

	public function _initAction()
	{

		$this->loadLayout()
		->_addBreadcrumb(Mage::helper('awesome')->__('Awesome'), Mage::helper('awesome')->__('Awesome'));
		return $this;
	}

	public function _initReportAction($blocks)
	{
		if (!is_array($blocks)) {
			$blocks = array($blocks);
		}

		$requestData = Mage::helper('adminhtml')->prepareFilterString($this->getRequest()->getParam('filter'));
		$requestData = $this->_filterDates($requestData, array('from', 'to'));
		$params = new Varien_Object();

		foreach ($requestData as $key => $value) {
			if (!empty($value)) {
				$params->setData($key, $value);
			}
		}

		foreach ($blocks as $block) {
			if ($block) {
				$block->setPeriodType($params->getData('period_type'));
				$block->setFilterData($params);
			}
		}
		return $this;
	}

	public function complexAction()
	{
		$this->_title($this->__('Awesome'))->_title($this->__('Reports'))->_title($this->__('Complex Report'));

		$this->_initAction()
		->_setActiveMenu('awesome/report')
		->_addBreadcrumb(Mage::helper('awesome')->__('Complex Example Report'), Mage::helper('awesome')->__('Complex Example Report'));

		$gridBlock = $this->getLayout()->getBlock('adminhtml_report_complex.grid');
		$filterFormBlock = $this->getLayout()->getBlock('grid.filter.form');

		$this->_initReportAction(array(
		$gridBlock,
		$filterFormBlock
		));

		$this->renderLayout();

	}

	public function exportComplexCsvAction()
	{
		$fileName   = 'complex.csv';
	 	$grid       = $this->getLayout()->createBlock('awesome/adminhtml_report_complex_grid');
        $this->_initReportAction($grid);
        $this->_prepareDownloadResponse($fileName, $grid->getCsvFile($fileName));
	}

}

So this is where the "complex" report is a little more complex. The layout that is loaded at first is from the awesome.xml file:


        
        
            
                
                    
                        report_type
                        0
                    
                 
            
        
    

So with the layout and the controller combined we know that we have to worry about 3 different blocks:

  1. awesome.xml: awesome/adminhtml_report_complex (Super_Awesome_Block_Adminhtml_Report_Complex)
  2. controller: adminhtml_report_complex.grid (Super_Awesome_Block_Adminhtml_Report_Complex_Grid)
  3. awesome.xml and controller: grid.filter.form (Mage_Adminhtml_Block_Report_Filter_Form) - This is a Mage class and we don't need to touch this.

The first block is the Grid container. Here is his contents -

<?php

class Super_Awesome_Block_Adminhtml_Report_Complex extends Mage_Adminhtml_Block_Widget_Grid_Container
{
 	public function __construct()
    {
    	$this->_blockGroup = 'awesome';
        $this->_controller = 'adminhtml_report_complex';
        $this->_headerText = Mage::helper('awesome')->__('Complex Report');
        $this->setTemplate('report/grid/container.phtml');
        parent::__construct();
        $this->_removeButton('add');
        $this->addButton('filter_form_submit', array(
            'label'     => Mage::helper('awesome')->__('Show Report'),
            'onclick'   => 'filterFormSubmit()'
        ));
    }
	public function getFilterUrl()
    {
        $this->getRequest()->setParam('filter', null);
        return $this->getUrl('*/*/complex', array('_current' => true));
    }
}

Every line is important, so pay close attention to this file. In the parent of this class, it has the function ...

protected function _prepareLayout()
    {
        $this->setChild( 'grid',
            $this->getLayout()->createBlock( $this->_blockGroup.'/' . $this->_controller . '_grid',
            $this->_controller . '.grid')->setSaveParametersInSession(true) );
        return parent::_prepareLayout();
    }

... which auto generates the block "awesome/adminhtml_report_complex_grid" with the name adminhtml_report_complex.grid. This is #2 in the "blocks we care about" list.

Here is the contents of Super_Awesome_Block_Adminhtml_Report_Complex_Grid -

<?php

class Super_Awesome_Block_Adminhtml_Report_Complex_Grid extends Mage_Adminhtml_Block_Report_Grid_Abstract
{

	protected $_columnGroupBy = 'period';

	public function __construct()
	{
		parent::__construct();
		$this->setCountTotals(true);
	}

	public function getResourceCollectionName()
    {
        return 'awesome/report_complex_collection';
    }

	protected function _prepareColumns()
	{

           $this->addColumn('period', array(
            'header'        => Mage::helper('awesome')->__('Period'),
            'index'         => 'period',
            'width'         => 100,
            'sortable'      => false,
            'period_type'   => $this->getPeriodType(),
            'renderer'      => 'adminhtml/report_sales_grid_column_renderer_date',
            'totals_label'  => Mage::helper('adminhtml')->__('Total'),
            'html_decorators' => array('nobr'),
		));

        $this->addColumn('description', array(
            'header'    =>Mage::helper('awesome')->__('Description'),
            'index'     =>'description',
            'sortable'  => false
        ));

        $currencyCode = $this->getCurrentCurrencyCode();

        $this->addColumn('value', array(
            'header'    => Mage::helper('awesome')->__('Value'),
        	'currency_code' => $currencyCode,
                'index'     =>'value',
        	'type'      => 'currency',
        	'total'     => 'sum',
                'sortable'  => false
        ));

        $this->addExportType('*/*/exportComplexCsv', Mage::helper('awesome')->__('CSV'));

        return parent::_prepareColumns();
	}

Notice that we are extending Mage_Adminhtml_Block_Report_Grid_Abstract (this is different than the "simple" report example).

In here, the part that we don't want to screw up is the getResourceCollectionName() function. It contains the Collection model that is the bane of my existance. Ready to see it?

BAAAAAAAAAAAAAAAAAM -

<?php

class Super_Awesome_Model_Mysql4_Report_Complex_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract
{

	protected $_periodFormat;
	protected $_selectedColumns 	= array();
	protected $_from                = null;
	protected $_to                  = null;
	protected $_orderStatus         = null;
	protected $_period              = null;
	protected $_storesIds           = 0;
	protected $_applyFilters        = true;
	protected $_isTotals            = false;
	protected $_isSubTotals         = false;
	protected $_aggregatedColumns   = array();

	/**
	 * Initialize custom resource model
	 *
	 * @param array $parameters
	 */
	public function __construct()
	{
		$this->setModel('adminhtml/report_item');
		$this->_resource = Mage::getResourceModel('awesome/complex')->init('awesome/complex');
		$this->setConnection($this->getResource()->getReadConnection());
		$this->_applyFilters = false;
	}

	protected function _getSelectedColumns()
	{
		if (!$this->_selectedColumns) {
			if ($this->isTotals()) {
				$this->_selectedColumns = $this->getAggregatedColumns();
			} else {
				$this->_selectedColumns = array(
                    'period'         => 'period',
                    'value'    		 => 'value',
                    'description'    => 'description',
				);
				if ('year' == $this->_period) {
					$this->_selectedColumns['period'] = 'YEAR(period)';
				} else if ('month' == $this->_period) {
					$this->_selectedColumns['period'] = "DATE_FORMAT(period, '%Y-%m')";
				}
			}
		}
		return $this->_selectedColumns;

	}

	protected  function _initSelect()
	{

		if (!$this->_period) {
			$cols = $this->_getSelectedColumns();
			$cols['value'] = 'SUM(value)';
			$this->getSelect()->from($this->getTable('awesome/complex'), $cols);
			$this->_applyDateRangeFilter();
			$this->getSelect()
			->group('description')
			->order('value DESC');
			return $this;
		}
		$this->getSelect()->from($this->getTable('awesome/complex'), $this->_getSelectedColumns());

		if (!$this->isTotals()) {
			$this->getSelect()->group(array('period', 'description'));
		}

		//
		$selectUnions = array();

		// apply date boundaries (before calling $this->_applyDateRangeFilter())
		$dtFormat   = Varien_Date::DATE_INTERNAL_FORMAT;
		$periodFrom = (!is_null($this->_from) ? new Zend_Date($this->_from, $dtFormat) : null);
		$periodTo   = (!is_null($this->_to)   ? new Zend_Date($this->_to,   $dtFormat) : null);
		if ('year' == $this->_period) {

			if ($periodFrom) {
				if ($periodFrom->toValue(Zend_Date::MONTH) != 1 || $periodFrom->toValue(Zend_Date::DAY) != 1) {  // not the first day of the year
					$dtFrom = $periodFrom->getDate();
					$dtTo = $periodFrom->getDate()->setMonth(12)->setDay(31);  // last day of the year
					if (!$periodTo || $dtTo->isEarlier($periodTo)) {
						$selectUnions[] = $this->_makeBoundarySelect($dtFrom->toString($dtFormat), $dtTo->toString($dtFormat));

						$this->_from = $periodFrom->getDate()->addYear(1)->setMonth(1)->setDay(1)->toString($dtFormat);  // first day of the next year
					}
				}
			}

			if ($periodTo) {
				if ($periodTo->toValue(Zend_Date::MONTH) != 12 || $periodTo->toValue(Zend_Date::DAY) != 31) {  // not the last day of the year
					$dtFrom = $periodTo->getDate()->setMonth(1)->setDay(1);  // first day of the year
					$dtTo = $periodTo->getDate();
					if (!$periodFrom || $dtFrom->isLater($periodFrom)) {
						$selectUnions[] = $this->_makeBoundarySelect($dtFrom->toString($dtFormat), $dtTo->toString($dtFormat));

						$this->_to = $periodTo->getDate()->subYear(1)->setMonth(12)->setDay(31)->toString($dtFormat);  // last day of the previous year
					}
				}
			}

			if ($periodFrom && $periodTo) {
				if ($periodFrom->toValue(Zend_Date::YEAR) == $periodTo->toValue(Zend_Date::YEAR)) {  // the same year
					$dtFrom = $periodFrom->getDate();
					$dtTo = $periodTo->getDate();
					$selectUnions[] = $this->_makeBoundarySelect($dtFrom->toString($dtFormat), $dtTo->toString($dtFormat));

					$this->getSelect()->where('1<>1');
				}
			}

		}
		else if ('month' == $this->_period) {
			// Start of custom hackish...
			if (!$this->isTotals()) {
				$columns = $this->getSelect()->getPart('columns');
				foreach($columns as $index => $column){
					if ($column[1] == 'value'){
						$column[1] = new Zend_Db_Expr('sum(value)');
						$columns[$index] = $column;
					}
				}
				$this->getSelect()->setPart('columns', $columns);

			}

			$this->getSelect()->reset('group');
			$this->getSelect()->group(array(new Zend_Db_Expr("DATE_FORMAT(period, '%Y-%m')"), 'description'));
			// End of custom hackish...

			if ($periodFrom) {
				if ($periodFrom->toValue(Zend_Date::DAY) != 1) {  // not the first day of the month
					$dtFrom = $periodFrom->getDate();
					$dtTo = $periodFrom->getDate()->addMonth(1)->setDay(1)->subDay(1);  // last day of the month
					if (!$periodTo || $dtTo->isEarlier($periodTo)) {
						$selectUnions[] = $this->_makeBoundarySelect($dtFrom->toString($dtFormat), $dtTo->toString($dtFormat));

						$this->_from = $periodFrom->getDate()->addMonth(1)->setDay(1)->toString($dtFormat);  // first day of the next month
					}
				}
			}

			if ($periodTo) {
				if ($periodTo->toValue(Zend_Date::DAY) != $periodTo->toValue(Zend_Date::MONTH_DAYS)) {  // not the last day of the month
					$dtFrom = $periodTo->getDate()->setDay(1);  // first day of the month
					$dtTo = $periodTo->getDate();
					if (!$periodFrom || $dtFrom->isLater($periodFrom)) {
						$selectUnions[] = $this->_makeBoundarySelect($dtFrom->toString($dtFormat), $dtTo->toString($dtFormat));

						$this->_to = $periodTo->getDate()->setDay(1)->subDay(1)->toString($dtFormat);  // last day of the previous month
					}
				}
			}

			if ($periodFrom && $periodTo) {
				if ($periodFrom->toValue(Zend_Date::YEAR) == $periodTo->toValue(Zend_Date::YEAR)
				&& $periodFrom->toValue(Zend_Date::MONTH) == $periodTo->toValue(Zend_Date::MONTH)) {  // the same month
					$dtFrom = $periodFrom->getDate();
					$dtTo = $periodTo->getDate();
					$selectUnions[] = $this->_makeBoundarySelect($dtFrom->toString($dtFormat), $dtTo->toString($dtFormat));

					$this->getSelect()->where('1<>1');
				}
			}

		}

		$this->_applyDateRangeFilter();

		// add unions to select
		if ($selectUnions) {
			$unionParts = array();
			$cloneSelect = clone $this->getSelect();
			$unionParts[] = '(' . $cloneSelect . ')';
			foreach ($selectUnions as $union) {
				$unionParts[] = '(' . $union . ')';
			}
			$this->getSelect()->reset()->union($unionParts, Zend_Db_Select::SQL_UNION_ALL);
		}

		if ($this->isTotals()) {
			// calculate total
			$cloneSelect = clone $this->getSelect();
			$this->getSelect()->reset()->from($cloneSelect, $this->getAggregatedColumns());
		} else {
			// add sorting
			$this->getSelect()->order(array('period ASC', 'value DESC'));
		}

		return $this;
	}

	protected function _makeBoundarySelect($from, $to)
	{
		$cols = $this->_getSelectedColumns();
		$cols['value'] = 'SUM(value)';
		$sel = $this->getConnection()->select()
		->from($this->getResource()->getMainTable(), $cols)
		->where('period >= ?', $from)
		->where('period <= ?', $to)
		->group('description')
		->order('value DESC');
		return $sel;
	}

	public function addStoreFilter($storeIds)
	{
		$this->_storesIds = $storeIds;
		return $this;
	}

	public function addOrderStatusFilter($orderStatus)
	{
		$this->_orderStatus = $orderStatus;
		return $this;
	}

	protected function _applyStoresFilterToSelect(Zend_Db_Select $select)
	{
		return $this;
	}

	public function setAggregatedColumns(array $columns)
	{
		$this->_aggregatedColumns = $columns;
		return $this;
	}

	public function getAggregatedColumns()
	{
		return $this->_aggregatedColumns;
	}

	public function setDateRange($from = null, $to = null)
	{
		$this->_from = $from;
		$this->_to = $to;
		return $this;
	}

	public function setPeriod($period)
	{
		$this->_period = $period;
		return $this;
	}

	protected function _applyDateRangeFilter()
	{
		if (!is_null($this->_from)) {
			$this->getSelect()->where('period >= ?', $this->_from);
		}
		if (!is_null($this->_to)) {
			$this->getSelect()->where('period <= ?', $this->_to);
		}
		return $this;
	}

	public function setApplyFilters($flag)
	{
		$this->_applyFilters = $flag;
		return $this;
	}

	public function isTotals($flag = null)
	{
		if (is_null($flag)) {
			return $this->_isTotals;
		}
		$this->_isTotals = $flag;
		return $this;
	}

	public function isSubTotals($flag = null)
	{
		if (is_null($flag)) {
			return $this->_isSubTotals;
		}
		$this->_isSubTotals = $flag;
		return $this;
	}

	public function load($printQuery = false, $logQuery = false)
	{
		if ($this->isLoaded()) {
			return $this;
		}
		$this->_initSelect();
		if ($this->_applyFilters) {
			$this->_applyDateRangeFilter();
		}
		return parent::load($printQuery, $logQuery);
	}

}

So remember the part about "I'm not sure how it all works"? Yeah. So I will talk about snippets that I found are important, but might not have a lot of info on why:
1. When I didn't have this, it complained about "period" in the where clause:

$this->_applyFilters = false;

2. Because I am not already aggregating the sums in my table (BAD ME!), I had to introduce the following code:

			// Start of custom hackish...
			if (!$this->isTotals()) {
				$columns = $this->getSelect()->getPart('columns');
				foreach($columns as $index => $column){
					if ($column[1] == 'value'){
						$column[1] = new Zend_Db_Expr('sum(value)');
						$columns[$index] = $column;
					}
				}
				$this->getSelect()->setPart('columns', $columns);

			}

			$this->getSelect()->reset('group');
			$this->getSelect()->group(array(new Zend_Db_Expr("DATE_FORMAT(period, '%Y-%m')"), 'description'));
			// End of custom hackish...

I found that if I wanted to group my period by a date that was formated, I had to include the SAME formatted date in the group clause.
3. I wanted the SUM of the value column, so you will see this in lots of places:

$cols['value'] = 'SUM(value)';

Just like in the simple report, this report also needs the "resource model", the "model", and the empty helper.

I hope this gives you SOME hints and/or direction in creating reports in magento.

6Mar/1112

Magento Examples, Blogs, and Tutorials

Posted by Ben Robie

Magento tutorials are lacking, as are Magento examples and Magento blogs. This being the case, we started this site to help any/all Magento developers to be more proficient in Magento coding, we wanted to put out a brief list of sites that offer the same type of help.

Quite obviously, the best is CodeMagento.com, but there are other sites that are quite helpful and should be subscribed to via their RSS feeds. In no particular order:

  • Inchoo - This shop is looking to get your business by showing that they are wizards in the land of Magento. By the looks of their blog (including tons of examples) they definitely know what they are doing. They update their blog daily and the blog entries are quick reads with fresh ideas. Subscribe to their feed.
  • Alan Storm - This guy is a consultant, but an active member of the Magento community. His commercial contribution to Magento, Commerce Bug, would be a great asset to ANY Magento developer. His blog posts are also full of code and useful tidbits of information that anyone can benefit from. Subscribe to his feed.
  • Active Codeline - This is kind of cheating since Branko Ajzele also works for Inchoo, but he provides such good content, we had to list Active Codeline too. Branko Ajzele is also an active voice in the Magento community (at least in Twitter land), and provides fresh ways of looking at the things we take for granted in Magento. Follow his feed.
  • Classy Llama Studios Classy Llama is another consulting/design firm that works with Magento. The blog posts are "kinda" useful and worth subscribing to. I couldn't find an RSS feed, so you will just have to remember to check out the site every now and then.
  • StackOverflow - This is kind of a weird one to add, but there have been a few times that I have found some helpful nuggets on this site. Keep an eye out for it on your Google results.
  • Magento Forums / Wiki - Although there is a lot of ruckus around the lack of documentation for Magento, the community tends to pick up a lot of that load. I find it helpful to subscribe to the Programming Questions forum and to try to answer the questions there. It sharpens my skills and helps other out. I would highly recommend that you do the same. If you are a noob, then use that resource to gain some knowledge from people that have "been where you are".

If you want to be added to our brief list, and have an active blog, use our super convenient Contact Us form to let us know who you are.