Grokking Magento Book01 Basics and Req Flow
Grokking Magento Book01 Basics and Req Flow
Vinai Kopp
2
Contents
1 Introduction 7
About this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
The Magento Developer Certification Preparation Study Group Moderator Kit 9
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
What this book offers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
How to use this material . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2 Development Environment 17
PHP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Testing Framework Integrations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3
4 CONTENTS
Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
The Route Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
The Action Controller Class . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
10 Addendum 165
Varien_Object Magic Setters and Getters . . . . . . . . . . . . . . . . . . . . . . 165
Class Name Resolution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
Model Instantiation Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
Resource Model Instantiation Steps . . . . . . . . . . . . . . . . . . . . . . . 169
Chapter 1
Introduction
This book series has been a long time dream of mine. I am happy to be able to finally start
this project, together with my friend and colleague Ben Marks.
The target audience for this book are experienced PHP developers wanting to learn about
the Magento platform.
The book series has three main goals:
The book content follows the Magento Certification Study Group Moderator Kit1 published
by Magento.
1 http://www.magentocommerce.com/certification/moderators-kit
7
8 CHAPTER 1. INTRODUCTION
It takes the bare bone exercises and solutions provided by the Study Group Kit and provides
insight into the core architecture by researching how a solution may be approached.
Then it discusses the example exercise solutions provided with this book.
Along the way, many aspects of the Magento architecture are uncovered, thus providing
the background knowledge and mindset required to pass the MCD exam.
Almost as a side effect readers will find they become more efficient and effective as Magento
developers.
This first book in the series focuses on the exercises from the Basics and Request Flow
section of the Study Group Kit.
It covers the following topics:
One of the main tools Magento offers to prepare for the Magento Certified Developer
exam (in short: MCD) is the Magento Developer Certification Preparation Study Group
Moderator Kit.
Phew, what a name!
We’ll just call it the “Study Group Kit”.
The study group kit is good but rather bare-bones. Besides instructions on how to organize
and structure meetings, it comes with a bunch of exercises for each of the content sections
and some example solutions.
However, for some of the exercises no solutions are provided, and the solutions that do
exist only have a few comments in the source code to explain what is happening.
The Study Group Kit really is relying on you, or your study group, to dig deep into the
Magento source code to figure out how things work exactly.
On one hand this makes sense, since it facilitates learning the ins and outs of the Magento
framework.
On the other hand, it may not be the fastest way to learn for everybody, and certainly not
the easiest. Also it is easy to miss things.
This book and the others in the Grokking Magento series aim to be a guide through the
exercises and solutions, providing additional insight while still helping the reader to become
accustomed to the Magento core code.
Prerequisites
The more context you already have, the more you will benefit from this book.
If you are reading this book as a novice Magento developer, it makes sense to return to
this book again later with more knowledge.
You can be sure to gain additional knowledge and better understanding the second time
around.
Firstly, this book contains better solutions to the exercises of one section of the MCD study
group kit.
What’s more, it not only comes with the solution code for the exercises, but also provides
extensive documentation for the code in the form of this book.
This book can be used as a reference whenever you want to dig into a specific area of
Magento development, independently from being in the process of preparing for the MCD
exam.
One of the best skills to develop in order to become a better Magento developer is the
ability to read core code.
As everything in life, practice is key. The series also hopes to facilitate building that skill
by taking apart core methods, commenting on what code sections do, and putting them
into context.
Some ideas might be a little far-fetched, but I hope to come up with good scenarios that
make sense for most cases.
I personally find it a lot easier to think about a problem if I know why I need to solve it.
Putting an exercise into context also helps remembering the solution, so the knowledge
can be called upon in future projects.
The next part of each chapter is some background research.
This part of the chapter is a dive into the Magento core code in order to find out what
needs be known to be able to implement a good solution.
Along the way, the discussions might take some detours on related subjects once in a while.
This may happen out of two reasons:
1) The content could be part of the certification exam, and there was no better place to
talk about it.
2) The content serves to get a bigger picture of how Magento works.
Simply follow along, as the text will be back on the main track towards the exercise solution
again in a little while.
Then, after gathering the required architectural information, the actual solution code is
dissected step by step.
Because the example source code supplied with the study group kit is not free to be shared,
all solutions have been developed specifically for this book.
However, it’s in the nature of Magento that two solutions for the same problem might be
similar to each other.
For example, building an adminhtml grid based on the adminhtml widget blocks simply
has to be done in a specific way to get the desired outcome.
We recommend you take the time to compare the solutions provided by this book and the
study group kit to gain additional insights, if possible.
However, of course this book may also be used on its own, without a copy of the study
group kit.
Since the current version of the MCD exams is based on Magento CE 1.7.0.2, that is also
the version used for this companion material.
At the time of writing, the current version of Magento 1 CE is 1.8.1.0.
If the exam is updated, we will be updating this book, too.
Almost everything in this book also applies to newer versions of Magento 1, too.
12 CHAPTER 1. INTRODUCTION
To summarize, this series hopes to supply the “meat” to the “bare-bones” study group kit,
in addition to serve as a reference, by providing
Obviously, this work is meant to be read. Its purpose is to help get you ready to pass the
Magento certification, or to solve problems during Magento development.
Reading this book is only one step. While reading the pages, you should do it in a way
that helps you remember all the information.
Here are a few tips that help me tremendously when learning new things.
• After reading a section, put the book aside and try to implement the solution on
your own. If you get stuck, read the chapter again up where you got stuck, and then
continue to work on your own.
• While the text is discussing parts of the core, follow along through the source code
in your IDE.
Because the core code sometimes is quite verbose, only relevant parts will be included
in the chapters. Going through the real code will give you context, and help you
build your core code reading skill.
• Don’t rush. If you can, approach reading this book in the spirit of relaxed exploration.
• Take breaks. Your brain will probably be able to absorb much more information if
you take a 20 minute break after each exercise. Do not plan to cover more than two
or three exercises per day.
• A day or so after completing an exercise using this book, compare this solution with
the one supplied by the study group kit. Reading more code that does the same
thing will help anchor the information in your long term memory.
Some solutions might differ, and you may gain additional insights by looking at “a
second opinion”.
ABOUT THIS BOOK 13
• If you find yourself having difficulties understanding a concept or some code, don’t
just push through.
Instead, make a list of things you need to know, and then fill those gaps, before
returning to the chapter and reading on.
Filling the gaps might mean re-reading a previous chapter, visiting the PHP manual3 ,
or looking at / asking questions on the Magento StackExchange site4 .
Often, parts of the Magento core code will be discussed in the chapters.
The samples in the book might be abbreviated in order to make them easier to read.
But also larger sections might be taken away to emphasise the main concept the book
content is trying to transport.
Please don’t be surprised when you find things to be somewhat different when comparing
code in the book to the actual sources.
Any code in the book will contain all relevant information you need in order to follow
along.
The exercise solution code referred to in the chapters can be downloaded from
shop.vinaikopp.com/grokking-magento/pk342e/5 If you purchased this book as an ePub
from shop.vinaikopp.com6 the code is also available as an additional download with your
order.
Please respect the work that has gone into creating the book and the code and instead of
distributing the it, encourage others to purchase their own copy.
3 http://php.net/en/manual
4 http://magento.stackexchange.com/
5 https://shop.vinaikopp.com/grokking-magento/pk342e/
6 https://shop.vinaikopp.com/
14 CHAPTER 1. INTRODUCTION
Magento Trivia
There are aspects of Magento that knowing about is absolutely useless for real work projects.
They also are not required to pass the MCD exam.
There is a certain type of developer personality who enjoys that kind of thing nevertheless
(me included).
For that reason, the book will sometimes contain such information.
In that case it will be marked as Magento Trivia, so you can choose to skip it if you
want to.
Acknowledgements
First and foremost, a big Thank You to the Magento developer community.
There are so many excellent developers from whom I have learned so much.
Regardless what anyone might think about the product itself, the Magento developer
community is first class!
Second, thank you Magento for creating such a powerful and flexible platform, and for
releasing it as open source! Without you, there would be no Magento eco system - I can’t
even imagine a world like that.
Third, thank you to my mentors and friends from without and within Magento.
All of you have played a vital role at one point during my continued journey of learning
Magento. I especially want to include a shout out to the following
• Vitaliy Golomoziy - you inspired me by showing how deep broad understanding can
be.
• Lee Saferite - you taught me that learning to read and understand core code is not
only possible but is an essential skill.
• Susie Sedlacek - you taught me about the business game, and did so in a good way.
There are many other people within Magento that I have come to think of as friends more
then colleagues. Thank you for being you.
A special thank you to all Ukrainian members of the Magento offices and everybody of the
Ukrainian Magento community I have the privilege to know.
You inspire me to grow and expand continuously, not only as a developer. I value your
passion and honesty.
ABOUT THIS BOOK 15
Also, I thank all of the students from all the trainings I’ve lead over the years. Your
questions challenge me and your curiosity and participation keeps my love for my work
alive. What’s more, you make every class a fun experience. Thank you!
Thanks to all MagentoRunning buddies, in particular Brent Peterson and Thomas Fleck.
Looking forward to our next sweaty adventures!
A big thank you to Sandro Kopp for designing and drawing the lovely cover art (it is a
silver fern frond if you where wondering).
And finally: thank you to my family - you are my sunshine.
16 CHAPTER 1. INTRODUCTION
Chapter 2
Development Environment
How to set up a proper Magento Development environment isn’t included in the Study
Group Kit.
However, a good moderator should point out what will be needed to set up a decent
development environment during the first meeting.
PHP
Magento 1 officially still supports the ancient version 5.2 of PHP, but consider running at
least PHP 5.3.6 since it contains many useful features.
Magento has provided download patches to support PHP 5.4 for older versions of Magento,
too, so there is no real reason to still use PHP 5.2.
Many community developers are using PHP 5.5 already with Magento in order to take
advantage of the many new features without experiencing any real issues.
Using an OpCode Cache like the Zend Optimizer or APC is also highly recommended.
Magento is a heavy weight application, and every little improvement helps.
Installing and learning to use xdebug1 (or the Zend debugger if you are using Zend Server)
is one of the most valuable things a PHP developer can do.
So in case you aren’t familiar with it, start now.
1 http://xdebug.org/
17
18 CHAPTER 2. DEVELOPMENT ENVIRONMENT
IDE
Currently the IDE most Magento developers favor is PhpStorm2 in combination with the
Magicento3 plugin.
This makes for a highly efficient Magento development setup.
A single developer licence for PhpStorm is very cheap when compared to other commercial
IDE products.
There are free alternatives of course: Eclipse4 and Eclipse Magento Plugin5 , or Netbeans6
most prominently.
Both IDEs are open source, free, and excellent products. However, working with them isn’t
as smooth and effortless as with PhpStorm, and they tend to be slower.
Sublime Text 27 with SublimePHPIntel8 is a combination many developers love, however
it can’t match the amount of support a developer gets from a full IDE (at least currently).
There are other IDEs and editors (Zend Studio, Aptana, Komodo etc), however they are
far less popular and seem to be slowly losing ground in the marketplace.
That said, it certainly is possible to develop Magento extensions using nothing but a text
editor.
However, only a person with superhuman powers could claim to be as efficient that way as
with an IDE.
Since the first release of PHPUnit9 a lot has happened in the world of PHP testing.
It is a complex topic, however everybody who gets into the habit of writing tests doesn’t
want to go back.
It is very possible to write tests for custom Magento modules with plain PHPUnit.
2 http://www.jetbrains.com/phpstorm/
3 http://magicento.com/
4 http://projects.eclipse.org/projects/tools.pdt
5 http://eclipse.snowdog.pl/
6 https://netbeans.org/features/php/
7 http://www.sublimetext.com/2
8 https://github.com/jotson/SublimePHPIntel
9 http://phpunit.de/manual/current/en/index.html
TESTING FRAMEWORK INTEGRATIONS 19
However, because Magento uses many static method calls and practically violates the Law
of Demeter10 in almost every method, it is sometimes not a straightforward process.
Either large amounts of objects need to be mocked, or complex fixtures need to be created.
Several integrations have been developed to help writing tests for Magento.
The most well-known one is EcomDev_PHPUnit11 . Its outstanding feature is the excellent
support for creating fixtures in yaml files.
Besides that it also offers many assertions and workarounds to make the Magento framework
more test-friendly.
Check out the development branch if you choose to try it out.
The Mage_Test12 integration is a much more lightweight PHPUnit integration. It provides
basic support and help to make Magento testable.
It is under active, albeit slow, development. Check out the development branch to see
what is going on. However, if you already are familiar with PHPUnit you might feel quite
at ease with it.
The project magetest.org13 aims to provide a set of unit-tests for the Magento core. It might
be useful as a smoke-test. For the Magento testing integration it uses EcomDev_PHPUnit.
The BDD framework Behat14 and its Magento integration BehatMage15 are different.
It doesn’t aim to provide unit test features, but instead enables users to test business
specifications written in a human readable language known as Gherkin. Behat itself is
getting quite mature, but the Magento integration is still lacking some features.
For example testing multi-site instances with separate domains isn’t possible out of the
box.
The PHPSpec216 functional testing framework is another framework not based on PHPUnit.
It features the semi-automatic generation of tests. However, the standard mocking library
Prophecy doesn’t play well with Magento’s magic setter and getter methods, so it’s always
necessary to fall back to Mockery (or even PHPUnit) for stubs and mocks.
There is a project to integrate Magento with PHPSpec2 called MageSpec17 , but once again
development isn’t going very quickly.
10 http://en.wikipedia.org/wiki/Law_of_Demeter
11 https://github.com/EcomDev/EcomDev_PHPUnit
12 https://github.com/MageTest/Mage-Test
13 http://www.magetest.org/
14 http://behat.org/
15 https://github.com/MageTest/BehatMage
16 http://www.phpspec.net/
17 https://github.com/MageTest/MageSpec
20 CHAPTER 2. DEVELOPMENT ENVIRONMENT
18 http://codeception.com/
Chapter 3
Rewrite the sales/order model to add the customer group model as an email
template variable in the sendNewOrderEmail() method, so the group code can
be added to the email using {{var customer_group.getCode()}}.
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
21
22 CHAPTER 3. REWRITE THE ORDER MODEL
Scenario
Imagine that for some reason the store owner wants to include the customer group name
in the new order email.
Maybe they would like to add conditional blocks in to the template, so they are able to
display different promotions to different groups.
Research
Let’s start by having a look at the method in question:
Mage_Sales_Model_Order::sendNewOrderEmail().
The method is quite long. A pity it doesn’t follow the good old rule of thumb that each
method should completely fit on the screen.
For reference purposes it is a good idea if you open the method in your IDE, while we
break it up and examine it section by section here.
Along the way we will examine different areas of the Magento core code that make the
process of sending emails work, but also are useful in other contexts.
The first interesting part is at the beginning, where the store is pseudo-switched to the
store view of the order.
The essential section of code in regards to this is about 10 lines into the method:
return $initialEnvironmentInfo;
}
The method call takes care that the following aspects of Magento are set as if the request
had been for the specified store:
• The area
• The theme configuration
• the locale
• the current store
24 CHAPTER 3. REWRITE THE ORDER MODEL
To summarize, using core/app_emulation is more complete than just calling the common
Mage::app()->setCurrentStore($storeId).
Going back to the sendNewOrderEmail() method, what happens while the order’s store is
being emulated?
$paymentBlock = Mage::helper('payment')->getInfoBlock($this->getPayment())
->setIsSecureMode(true);
$paymentBlock->getMethod()->setStore($storeId);
$paymentBlockHtml = $paymentBlock->toHtml();
The payment info block is rendered into a variable $paymentBlockHtml. After that, the
environment emulation is ended.
Ask yourself, “why is the environment emulation needed?”
Usually the store ID set on an order matches the current store while the order is placed.
But if the order is being placed using the Admin panel, the current store is the admin
store view. The adminhtml theme is being used, together with the admin user’s locale
configuration.
But of course the payment information in an order email should match the customer’s
settings, for whom the order is being created.
For that reason the settings are switched accordingly during the payment info block
rendering using the app emulation.
$mailer->setTemplateParams(array(
'order' => $this,
'billing' => $this->getBillingAddress(),
'payment_html' => $paymentBlockHtml
)
);
$mailer->send();
RESEARCH 25
The array values will be accessible in the email templates by using {{var ...}} placeholders
referencing the array keys passed to the setTemplateParams() method. One example for
such a placeholder directive could be {{var payment_html}}, which would include the
rendered payment information block into the email.
Note that the template parameters are specified directly before the email is sent.
Please note that we are currently not discussing Magento’s .phtml template files!
This section of the book is about the template system used for transactional emails and
CMS content.
Only the name template is the same, but otherwise these two are quite distinct!
More information on theme template files can be found in the next book in this series,
“Rendering & Widgets”.
According to best practices, it would be ideal if we could add the new customer group
template variable using an event observer.
But looking at the methods
Mage_Core_Model_Email_Template_Mailer::setTemplateParams() and send() doesn’t
reveal any useful events.
There don’t seem to be any earlier ones, either, as searching for the string “dispatchEvent”
within the class turns up nothing.
We will have to look further for the best way to implement our customization.
Lets continue to analyze how the template variables are processed further.
The core/email_template_mailer class delegates to core/email_template, more specif-
ically, to its sendTransactional() method.
This in turn calls its send() method, and there, finally, the template variables are processed.
The value passed to the method is the content part of a template {{var ...}} placeholder
directive.
For example, for a placeholder {{var foo}}, the argument variable $value would be set
to foo.
This value is the tokenized, that is split, using the dot character as a separator. This
character is hardcoded in the Varien_Filter_Template_Tokenizer_Variable class.
Then each of the tokens are analyzed in a for loop.
This is required because tokens can be chained, as we will see further below.
Each token that is resolved will update the token stack to the latest value.
This continues until the full stack of tokens is resolved, and the final replacement value has
been determined.
$result = $default;
$last = 0;
for($i = 0; $i < count($stackVars); $i ++) {
if ($i == 0 && isset($this->_templateVars[$stackVars[$i]['name']])) {
// Getting of template value
$stackVars[$i]['variable'] =& $this->_templateVars[$stackVars[$i]['name']];
} else if (isset($stackVars[$i-1]['variable'])
&& $stackVars[$i-1]['variable'] instanceof Varien_Object) {
// If object calling methods or getting properties
if($stackVars[$i]['type'] == 'property') {
$caller = "get" . uc_words($stackVars[$i]['name'], '');
if(is_callable(array($stackVars[$i-1]['variable'], $caller))) {
// If specified getter for this property
$stackVars[$i]['variable'] = $stackVars[$i-1]['variable']->$caller();
} else {
$stackVars[$i]['variable'] = $stackVars[$i-1]['variable']
RESEARCH 27
->getData($stackVars[$i]['name']);
}
} else if ($stackVars[$i]['type'] == 'method') {
// Calling of object method
if (is_callable(
array($stackVars[$i-1]['variable'], $stackVars[$i]['name'])
) || substr($stackVars[$i]['name'],0,3) == 'get'
) {
$stackVars[$i]['variable'] =
call_user_func_array(array($stackVars[$i-1]['variable'],
$stackVars[$i]['name']),
$stackVars[$i]['args']);
}
}
$last = $i;
}
}
if(isset($stackVars[$last]['variable'])) {
// If value for construction exists set it
$result = $stackVars[$last]['variable'];
}
return $result;
}
• Mage_Core_Model_Template_Filter::varDirective()
• Varien_Filter_Template::varDirective()
• Varien_Filter_Template::dependDirective()
• Varien_Filter_Template::ifDirective()
28 CHAPTER 3. REWRITE THE ORDER MODEL
Each of the methods whose name ends in Directive processes a matching directive that
can be used in the template text.
For example, {{if ...}} will be processed by the ifDirective() method.
The $value passed to the method above is the placeholder directive’s content string.
Since assigned variables can be used in a variety of contexts, it is quite useful to know how
to access variable contents in email and CMS templates.
Here are some examples of valid variable template declarations so we know what we are
looking for while reading the _getVariable() method.
Variables with plain text or HTML content:
• {{var payment_html}}
• {{htmlescape var=$payment_html}}
These would be the predifined names checked first in the for loop.
Properties of objects contained in variables can be accessed using a dot as a separator in a
variety of ways.
Consider the following two variables from the current exercise:
• {{var order.customer_name}}
• {{var order.getCustomerName()}}
If such a token value “returns” a Varien_Object instance, further calls may be chained:
• {{var order.billing_address.getCity()}}
• {{var order.getBillingAddress().city}}
• {{htmlescape var=$order.billing_address().city}}
This kind of method or property chaining to access object properties can be used everywhere
_getVariable() is used.
Please read over the _getVariable() method one more time for each of the given examples,
and step through the code in your mind, resolving each of the tokens one at a time.
I’m sure after this brain exercise the method will be quite clear.
RESEARCH 29
Let’s go a bit deeper into the subject of email and CMS page directives, even if it’s not
directly relevant for the current exercise.
So far this chapter only talked about the {{var ...}} and {{htmlescape var=...}}
directives.
It is quite easy to implement custom directives, for example {{customergroup}}, to output
the current customer group name.
The base functionality for this is inherited from the class
Varien_Filter_Template::filter().
Whenever a placeholder is found, the class checks if there is a method matching the
placeholder name, followed by the word Directive.
The {{custmrgrp}} placeholder for example would cause the filter class to check for a
method custmrgrpDirective().
Note that no underscores are allowed in the placeholder names.
The regular expression that is used to match the placeholders is
/{{([a-z]{0,10})(.*?)}}/si.
The method implementing a directive receives the captured parts from the regular expression
as an argument.
Using $params = $this->_getIncludeParameters($matches[2]) any arguments to the
directive can be easily parsed.
Any parameter values starting with a $ will automatically be evaluated using the
_getVariable() method discussed above.
The placeholder will then be replaced by the string value the directive returns.
For example, a directive for the placeholder
{{custmrgrp id=$order.getCustomerGroupId()}} could look like this:
$group = Mage::getModel('customer/group')->load($groupId);
return $group->getCode();
}
The method will render the code of the customer group whose ID was specified as an
argument using the id parameter.
If no group ID was specified, the current customer group is used.
The directive is called custmrgrp instead of customergroup because the regular expression
matching the placeholders allows for a maximum of 10 characters as the placeholder name.
There are several options available to let Magento know you want a custom filter class to
be used.
For CMS pages and blocks you can either configure a class rewrite for the
cms/template_filter model, or you can specify the class using the config XPath
global/cms/page/tempate_filter and global/cms/page/tempate_filter.1
To make your custom directive available in email templates you need to rewrite the model
core/email_template_filter.
Enough of the template variable detour! Time to get back on track and figure out how to
specify the customer group as a template variable.
Since we did not find any matching events, we need to fall back to a class rewrite of the
sales/order class.
That is the only way we will be able to add our custom logic to the sendNewOrderEmail()
method.
Solution
1 The typo tempate instead of template in the config path is Magento’s, not mine.
SOLUTION 31
All Magento developers know how to specify a model rewrite in configuration XML, but
few know why that works, and that is what is needed during the MCD exam.
Magento uses factory methods. These factory methods take a string as their first argument.
This string is then mapped to a real PHP class name by a series of steps consisting mostly
of looking up values from config XML. This process is known as class name resolution.
Let’s go through a concrete example by having a look at the configuration XML which is
necessary to declare a rewrite for the sales/order model, and then see how it is processed
by Magento.
<config>
<global>
<models>
<sales>
<rewrite>
<order>Meeting02_RewriteOrder_Model_Sales_Order</order>
</rewrite>
</sales>
</models>
</global>
</config>
$order = Mage::getModel('sales/order');
Note: there is an optional second argument which will be passed to the constructor if
present. This feature is shared by most of Magento’s factory methods.
The call is delegated to Mage_Core_Model_Config::getModelInstance().
32 CHAPTER 3. REWRITE THE ORDER MODEL
The condition in this method has the consequence that if you pass a regular PHP class
name to Mage::getModel(), it will abort the lookup process and simply use the name
as-is.
This means that for any rewrites to take effect, the class alias (with the /) needs to be
used.
For example:
if (empty($groupRootNode)) {
$groupRootNode = 'global/'.$groupType.'s';
}
The first argument is the object type, which is one of model, block, or helper.
The second argument is the string that was specified as an argument to the factory method,
for example sales/order.
We know it contains a / since that is confirmed in the preceding methods.
The third argument is not used in the core. It could be used to tell the method to do the
class name resolution by looking at configuration in a custom branch of XML.
In the code section displayed above the variable $groupRootNode is set to point to the
XML configuration branch for the specified object type.
Models will be resolved using configuration in global/models, block configuration will be
in global/blocks and helpers will be found under global/helpers.
Note the ’s’ that is appended to the group type. It is a common mistake to forget the
plural form there.
Let’s move on.
if (isset($this->_classNameCache[$groupRootNode][$group][$class])) {
return $this->_classNameCache[$groupRootNode][$group][$class];
}
This is one of the most important steps: the class alias is exploded on the /.
The string sales/order is split into sales and order. Let’s clarify some naming conven-
tions before we continue.
I will stick to the identifiers under the Name column, but at Magento meetups or confer-
ences you might also hear the parts called by other names.
As you can see, the core/config class which is doing the resolution utilizes a lookup cache.
Once a class alias is resolved, following requests for the same alias no longer need to go
through the whole process again.
$config = $this->_xml->global->{$groupType.'s'}->{$group};
The actual configuration for the specified class group is stored in the variable $config.
In our example case, sales/order, that would be the entire configuration contained within
the <sales> node.
<config>
<global>
<models>
<sales>
...
</sales>
</models>
</global>
</config>
<config>
<global>
<[Object Type]s>
<[Class Group]>
<rewrite>
<[Class Suffix]>The_Php_Class_Name_That_Is_Used</[Class Suffix]>
</rewrite>
</[Class Group]>
</[Object Type]s>
</global>
</config>
Or, in our concrete example case, a class rewrite for sales/order would need to be specified
as follows.
<config>
<global>
<models>
<sales>
<rewrite>
<order>Meeting02_RewriteOrder_Model_Sales_Order</order>
</rewrite>
</sales>
</models>
</global>
</config>
Class name resolution is not applied recursively, so it is not possible to rewrite a rewritten
class and have both rewrites be in effect.
But what happens if no rewrite exists for the class alias in question?
} else {
/**
* Backwards compatibility for pre-MMDB extensions.
36 CHAPTER 3. REWRITE THE ORDER MODEL
Luckily we get a nice comment. To understand it, we need to remember that this method
is not only used for models, blocks and helpers, but also for resource model class name
resolution.
Magento 1.6 introduced a full database abstraction layer called MMDB, I guess which
stands for Magento Multi DataBase.
(They also published a nice PDF on MMDB2 at the time.)
While doing so, they unified the resource model structure within all modules, and renamed
all resource class groups. 3
As you can see, since Magento 1.6 all resource class group names follow the same convention.
2 http://www.magentocommerce.com/images/uploads/RDBMS_Guide.pdf
3 Resource class group is a new term. It would lead us too far astray from the actual exercise to go
into more depth here. For now please refer to the addendum Class Name Resolution Steps for further
information.
SOLUTION 37
Assume a rewrite would have been specified for the customer/address resource model in
a Magento version before 1.6.
The configuration path would have had to be:
global/models/customer_entity/rewrite/address
global/models/customer_resource/rewrite/address
Any modules using the old path would break. For that reason, a new node was introduced:
<deprecatedNode>.
It contains the old, pre-MMDB, resource model class group of each module.
If no regular rewrite for a class is found, the configuration model checks if a rewrite for the
old name is present (see the code block above).
We examined the code responsible for class rewrites, but there still is a little remainder of
the getGroupedClassName() method left.
Instead of finishing here, let’s continue the exploration, even if it doesn’t directly apply to
the current exercise.
It is definitely useful to know, MCD exam-relevant, and not very long.
Back to the previous question, what happens if no rewrite exists for the class alias being
resolved?
// Second - if entity is not rewritten then use class prefix to form class name
if (empty($className)) {
if (!empty($config)) {
$className = $config->getClassName();
}
if (empty($className)) {
$className = 'mage_'.$group.'_'.$groupType;
}
if (!empty($class)) {
$className .= '_'.$class;
38 CHAPTER 3. REWRITE THE ORDER MODEL
}
$className = uc_words($className);
}
In the code block the configuration model tries to establish a Class Prefix, onto which it
appends the class suffix, to complete the class name resolution.
First it checks for the value returned by the method
Mage_Core_Model_Config_Element::getClassName(). In effect it returns the value of a
child node <class> or <model> of the class group configuration.
For the class group sales the configuration of the class prefix will be at
global/models/sales/class or global/model/sales/model.4
It might be confusing that the node name is <class>, however, the node value is never a
PHP class - it is the class prefix only!
Bad naming choices like this one is part of the reason for Magento’s reputation of being
difficult to learn.
<global>
<models>
<sales>
<class>Mage_Sales_Model</class>
</sales>
</model>
</global>
Note: The class prefix is prepended directly to the $class variable without removing
whitespace. Hence the following creates an invalid class name containing whitespace:
<class>
Mage_Sales_Model
</class>
$className = 'mage_'.$group.'_'.$groupType;
4 In practice <class> is always used by Magento developers to specify a class prefix. Better stick to that
The consequence is that core developers can get away without specifying a class prefix for
their modules.
We on the other hand, as part of the developer community working outside the Mage
namespace, have to always specify a class group and prefix in our modules.
Finishing up, let’s examine the last few lines of getGroupedClassName().
$this->_classNameCache[$groupRootNode][$group][$class] = $className;
return $className;
}
Not much to say about these two lines, except that the result of the class name resolution
is stored in the $_classNameCache property to speed up future lookups during the current
request. The the result is returned and the method is complete.
To complete this sub-section, here is the XML that specifies the class rewrite for the
exercise at hand.
<config>
<global>
<models>
<sales>
<rewrite>
<order>Meeting02_RewriteOrder_Model_Sales_Order</order>
</rewrite>
</sales>
</models>
</global>
</config>
With this bit of configuration in place, the sales/order class alias resolves to
Meeting02_RewriteOrder_Model_Sales_Order instead of the original
Mage_Sales_Model_Order.
Using the module name in the object type directory (that is, ...Model_Sales_Order
instead of simply ...Model_Order helps organize the folder structure in our module.
Following this convention makes it possible to see at a glance which classes in our Model
directory are originals and which are rewrites.5
5 Very handy for example when a module rewrites more than one Data helper and maybe also contains
According to best practices we should not copy a full method from a parent class when
doing a rewrite, since that compromises the ability to upgrade. If the method changes in a
future version, those changes would be masked by our copy in the rewrite target class.
Instead, we should inject our logic before or after calling the parent method.
The next example would be following best practices.
Unfortunately, we can’t do that in this case. The piece of code we want to change is buried
deep in the middle of the parent method.
In cases such as this there is no other way than copying the full method to our rewritten
class and adding our logic in there directly.
Here is the piece of code including our customization:
$mailer->setTemplateParams(array(
'order' => $this,
'billing' => $this->getBillingAddress(),
'payment_html' => $paymentBlockHtml,
'customer_group' => $this->_getCustomerGroup()
)
);
SOLUTION 41
The method to fetch the customer group model is not part of the native order model; we
need to implement that ourselves.
/**
* @return Mage_Customer_Model_Group
*/
protected function _getCustomerGroup()
{
/** @var Mage_Customer_Model_Resource_Group_Collection $groups */
$groups = Mage::helper('customer')->getGroups();
$groupId = $this->getCustomerGroupId();
$group = $groups->getItemById($groupId);
if (! $group) {
$group = Mage::getModel('customer/group')->load($groupId);
}
return $group;
}
Create an observer that redirects the visitor to the base URL if the CMS home
page URL key is accessed directly (i.e. /home -> /).
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
43
44 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
Scenario
Having the exact same content accessible on two distinct URLs is rated as duplicate content
by Google. For that reason it makes sense to restrict the home page to only be available
through a request to the Magento base URL.
Research
In order to complete the task we need to research the following high-level items:
Additional details such as accessing the current request path will be handled along the way.
The first step requires a look at the cms/page entity.
The second step requires working out how Magento processes requests, a process called
request flow.
The third step requires looking for a suitable event - or a rewrite candidate - to inject our
logic.
Let’s start with the easiest task. CMS pages are flat table entities, meaning they are stored
in a single table - unlike EAV entities. The table name is cms_page.
An additional table called cms_page_store is used to keep the information for the store
views in which a page is accessible - this second table can be ignored for this task though.
RESEARCH 45
Looking at the cms_page table, two columns are the most interesting in regards to the
request flow:
A CMS page can be accessed on the frontend if the request path matches the page’s
identifier. Before the identifier, the request path only contains the store base URL.
For example, if the configured Magento base URL is http://magento.dev/shop/, and the
page’s identifier is enable-cookies, then the full URL to view the CMS page would be
http://magento.dev/shop/enable-cookies.
Let’s dive in and have a look at how Magento figures that out internally.
Request Flow
Almost all of the request flow process we analyze in this section applies to all page requests,
not just the ones related to CMS pages.
Each request starts with the index.php file in the Magento base directory.
In there, right at the end of the file, Mage::run($mageRunCode, $mageRunType); is called.
We will look into how $mageRunCode and $mageRunType are used to to specify the store
view in the chapter Store View Selection.
For now we will take a more generic view of the request flow process.
The call to Mage::run() does two things: first it causes the basic run time environment to
be set up, and second it causes the request to be processed. That process is started by
delegating to Mage_Core_Model_App::run().
The following code block is an abbreviated version of that method and contains only the
code most relevant for us:
}
return $this;
}
Unless the cache processor is able to process the request, the Front Controller is initialized
and dispatched.
The cache processor is only relevant for the Enterprise Edition Full Page Cache module,
so we will not go into more detail about it here. Instead, let’s have a look at the
Front Controller, which comes into play at the end of the code sample above when
$this->getFrontController() is called.
return $this->_frontController;
}
requests to the Magento frontend - because of its name. This is not true. The Front Controller is called
Front Controller because it stands at the front of the routing process, before the routers and action
controllers take over. It actually is used for all requests.
RESEARCH 47
/**
* Init Front Controller
*
* @return Mage_Core_Controller_Varien_Front
*/
public function init()
{
Mage::dispatchEvent('controller_front_init_before', array('front'=>$this));
$routersInfo = Mage::app()->getStore()->getConfig(
self::XML_STORE_ROUTERS_PATH
);
Mage::dispatchEvent('controller_front_init_routers', array('front'=>$this));
return $this;
}
What happens here is actually a very important part of the Magento routing logic.
All the Magento routers are initialized and added to the $this->_routers property of the
48 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
Front Controller.
This process is called gathering all routers (or gathering all routes, which is almost - but
not quite - the same, as we will see below).
The question is, what is a router?
The purpose of a router is to analyze the browser request and check if it knows how to
process it.
The actual processing of the request may then be handed off to an action controller
(sometimes also called page controller).
To summarize what we have covered so far:
The first routers are created and added to the $_routers array in the foreach loop in the
previous code block.
The configuration section that the foreach iterates over can be found in the file
Mage/Core/etc/config.xml 2 :
<default>
<web>
<routers>
<admin>
<area>admin</area>
<class>Mage_Core_Controller_Varien_Router_Admin</class>
</admin>
<standard>
<area>frontend</area>
<class>Mage_Core_Controller_Varien_Router_Standard</class>
</standard>
2 Magento Trivia: the configuration section actually exists twice in that file: once under
stores/config/default and once under config/default.
The former is only used if the current frontend store has the code default. If a different store is set as the
current frontend store for the request, the latter config branch is used.
Technically it would have been enough to only specify the config/default value. When adding a custom
router by adding to the configuration XML, it also needs to be specified in both sections.
Probably we will never know why both XML branches exist in the file.
RESEARCH 49
</routers>
</web>
</default>
Each of those two routers are created in the foreach loop in the init() method.
After instantiation, collectRoutes($routerInfo['area'], $routerCode) is called on
each one, which causes it to create an array with the routes that are configured within the
config XML branch specified by the <area> node.
More details on the route definition will be covered in the section The Route Configuration
of the next chapter.
This is the gather all routes part mentioned earlier, as compared to gathering all routers.
Put simply, routers contain a list of routes.
We will go into more detail later, but I think a bit of context regarding routes is required
now. So here is a brief definition of a route.
A configured route is a mapping between a frontName and a module.
For example, catalog is the frontName provided by the Mage_Catalog module.
While building the list of mappings, each router doesn’t only look in the specified config
area, but also only will gather routes with a matching <use> node.
The standard router builds a list of all routes within the <frontend> config area containing
a <use>standard</use> node.
The admin router builds a list of all routes within the <admin> area that contain a
<use>admin</use> node.
We will look at the route configuration within the Create a Controller chapter in more
detail.
Let’s get back to the Front Controller (which still is building its list of routers where we
left off to look at routes).
After the routers configured in the XML section are instantiated and added to the Front
Controller’s $this->_routers array, the event controller_front_init_routers is dis-
patched.
Mage::dispatchEvent('controller_front_init_routers', array('front'=>$this));
Looking for matching event observers, there will be one result within the Magento core, in
Mage/Cms/etc/config.xml:3
3 More on observer configuration later during the Event Observer section below.
50 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
<global>
<events>
<controller_front_init_routers>
<observers>
<cms>
<class>Mage_Cms_Controller_Router</class>
<method>initControllerRouters</method>
</cms>
</observers>
</controller_front_init_routers>
</events>
</global>
No observer model factory name is specified in the <class> node; instead, a regular PHP
class name is used.
The observer method that is called on the class -
Mage_Cms_Controller_Router::initControllerRouters() - is very simple:
$front->addRouter('cms', $this);
}
The Mage_Cms module’s event observer adds itself to the list of routers of the Front
Controller.
The question presents itself: why isn’t the CMS router configured in the config XML, just
like the standard and admin routers?
The only difference in the result is the order of the routers in the Front Controller’s
$this->_routers array.
The routers configured in the config XML will be listed first, then the routers added via
the event will be appended after them.
The following paragraph could almost be classified as Magento Trivia, but it actually is
valuable to think it through. It serves to deepen the understanding of the config XML
merge process, module dependencies, and how the Front Controller builds the routers array.
RESEARCH 51
Because of module dependencies, the Mage_Core module will always be loaded before any
other module. If the CMS router would have been configured using the XML method, the
order of the routers would still be the same.
This is because the CMS modules section would be merged in to the config XML DOM
structure after the Mage_Core module XML.
So there is no technical reason why the CMS router is added via an event observer.
How about a practical question: if we want to add a custom router to the Front Controller’s
array, is it better to use configuration XML or the controller_front_init_routers
event?
The answer is (as usual): it depends.
If we want a custom router to be added before the CMS one, we should declare the router
in the config XML.4
If we want the custom router to be added after the CMS router, the observer method has
to be used.
Once we proceed to analyzing the Front Controller’s dispatch() method, it will become
clear why the sort order of the routers is important enough to spend so much time on it.
But first, let’s finish dissecting the Front Controller’s init() method - we are almost
finished with it anyway.
The remaining code of the method adds one final router:
Without resorting to hacks using PHP reflection, there is no way to add a custom router
after it.
This Default router is used to display the configured 404 page if no other router knows
how to handle a request5
To summarize, a Magento installation without any customizations uses 4 routers in the
following order:
4 We could also use the event observer approach, but in that case would also have to configure the
Mage_Cms to depend on our extension, so our module’s observer would be processed before the CMS one.
Probably it is better to simply use the configuration XML instead.
5 A name like NoRoute instead of Default would have been much more suitable for the final router.
52 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
Time to move on to the Front Controller’s dispatch() method, where each router in turn
is checked if it can match the current request.
$request->setPathInfo()->setDispatched(false);
if (!$request->isStraight()) {
Mage::getModel('core/url_rewrite')->rewrite();
}
$this->rewrite();
$i = 0;
while (!$request->isDispatched() && $i++<100) {
foreach ($this->_routers as $router) {
if ($router->match($this->getRequest())) {
break;
}
}
}
if ($i>100) {
Mage::throwException(
'Front controller reached 100 router match iterations'
);
}
// This event gives possibility to launch something before sending
RESEARCH 53
The matching of the routers happens in the foreach loop within in the while near the
center of the method.
This nested set of loops requires some explanation.
Before we dive in further: the counter variable $i is just a sentry flag to avoid Magento
getting stuck in an infinite loop. Unless you encounter a bug, it will never hit a count
higher than 2.
The main indicator for the outer loop to exit is the isDispatched flag on the request
object.
As long as the isDispatched flag is false, it will continue.
The inner foreach loop iterates over all routers added during init().
The match() method is called on each of the routers in turn.
The purpose of the match() method is to check if a router knows how to process the request.
Processing the request means generating the response that will be sent back to the browser.
The response may consist of a full response body, or may only contain HTTP headers (for
example in case of a redirect). Either case qualifies as the request being processed.
If a response was generated, the router’s match() method will set the isDispatched flag
on the request to true, and the outer loop will exit.
Besides processing the request and generating output, a router also has another option: it
can modify the $request object without setting the isDispatched flag to true.
In that case the outer loop will continue, and a different router may now process the
request, since it has been modified.
This is how one router delegates to a another.
The following table shows the different cases that might occur while the nested loop is being
processed, depending on the value returned by the match() method and the isDispatched
flag.
54 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
To fully understand this process, it is important to know that the final router - the Default
404 page router - always matches.
It takes advantage of the option to delegate to a different router.
The Default router always modifies the request to point to the configured 404 page and
returns true as the result of the match() method, while keeping isDispatched set to
false.
The result is that the process breaks out of the inner loop, but the outer loop continues,
and so the inner iteration over the routers begins again.
However, this time the Standard router is able to match the modified request, so it will
generate the response (the 404 page content) and set the isDispatched flag to true.
The following list of steps taken might help to illustrate when a non-existent route is
accessed:
The following table describes the requests each router matches and how the match is
handled.
In terms of this chapter’s exercise, this information is relevant: the match type of the CMS
router.
According to the table, the CMS router Mage_Cms_Controller_Router also delegates to
the Standard router to display the page content (just like the Default router).
This means that we can focus on the functioning of the Standard router in order to
understand how the CMS page output is generated.
Also, the Admin router extends the Standard router class, thereby inheriting the
Mage_Core_Controller_Varien_Router_Standard::match() method.
To summarize: in a native Magento installation, every page request always ends up being
processed by the Standard router’s match() method.
This happens either through a direct match, through class inheritance, or through delegation
from anther router.
Before we examine how the Standard router’s match() method works, let’s see how exactly
56 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
$page = Mage::getModel('cms/page');
$pageId = $page->checkIdentifier(
$identifier, Mage::app()->getStore()->getId()
);
if (!$pageId) {
return false;
}
$request->setModuleName('cms')
->setControllerName('page')
->setActionName('view')
->setParam('page_id', $pageId);
return true;
In short, if a CMS page with an identifier matching the current request path info is found,
the request is modified to delegate to the Standard router.
This is what the call to setModuleName(), setControllerName() and setActionName()
is all about.
The delegation from Default to the Standard router in order to display the configured
404 page is done in exactly the same way.
$front = $this->getFront();
$path = trim($request->getPathInfo(), '/');
if ($path) {
$p = explode('/', $path);
} else {
$p = explode('/', $this->_getDefaultPath());
}
The variable $p is set to contain the request path split at the / character.
If the request path is empty, the return value of _getDefaultPath() is used. This value is
taken from the system configuration option found at Web > Default Pages > Default Web
URL, which defaults to simply ’cms’.
Because of this, for a request to the base URL of a Magento instance, $p will look as
follows:
Array
(
[0] => cms
)
Array
(
[0] => customer
[1] => account
[2] => login
)
Of course there might be more parts to the request path, which would lead to more records
in the array.
The next part of the match() method processes the first part of the array.
} else {
if (!empty($p[0])) {
$module = $p[0];
} else {
$module = $this->getFront()->getDefault('module');
}
}
$modules = $this->getModuleByFrontName($module);
However, if at least one possible module is mapped to the frontName, the matching process
continues as follows.
/**
* Going through modules to find appropriate controller
*/
$found = false;
foreach ($modules as $realModule) {
if ($request->getControllerName()) {
$controller = $request->getControllerName();
} else {
if (!empty($p[1])) {
$controller = $p[1];
} else {
$controller = $front->getDefault('controller');
}
}
The router iterates over the possible modules, and looks for a controller class matching the
second part of the request ($p[1]).
Each iteration tries to match the request to a controller in a different module. The module
being checked is stored in the variable $realModule by the foreach statement.
For example, let’s assume the front name catalog is mapped to the modules Mage_Catalog
and Example_Module_Catalog using config XML.
For a request to http://magento.dev/catalog, during the iteration those two values would
be assigned in turn to $realModule.
To clarify:
Luckily the value of $realModule is not used a lot during module development.
It usually is only used during the Standard router matching process.
Inside the foreach loop, the $controller variable is set to the second part of the request
pat.
Similar to the setting of $module, if $request->getControllerName() returns a string,
it takes priority over $p[1].
Again, this will be the case if a previous router set the value to delegate to the Standard
router.
In case neither getControllerName() returns a value nor $p[1] is set, the default controller
name is fetched from the Front Controller.
This will set $controller to ’index’.
Please refer to the code block above to see the code discussed so far.
Now that $realModule and $controller are set, let’s move on.
The third part of the request path is assigned to the variable $action, unless
$request->getActionName() was set by a previous router’s match() method.
The default value for the action name is index, just like for $controller.8
$controllerClassName = $this
->_validateControllerClassName($realModule, $controller);
if (!$controllerClassName) {
continue;
}
Now that $realModule, $controller and $action are set, the router finally checks if a
matching controller file really exists.
8 Magento Trivia: In case you wondered why the determination of the values for $controller and
$action happens inside the loop, even though they will always be the same on each iteration: yes indeed,
that once again isn’t the best implementation. Luckily, the list of modules for a frontName never is very
long.
RESEARCH 61
In order to map the second part of the request $controller to a file, the standard router
follows this process:
Since a bit of code might be easier to understand, here is the method in question:
Note that the controllers directory is added after the first 2 parts of the controller class
name!
Here are some examples:
Mage_Catalog Mage/Catalog/controllers
Meeting02_Example Meeting02/Example/controllers
Mage_Widget_Adminhtml Mage/Widget/controllers/Adminhtml
Example_Module_Catalog Example/Module/controllers/Catalog
If the resulting controller file does not exist, the Standard router will continue to look for
the file in the next module mapped to by the frontName.
However, if the controller file exists, it will be included, and the controller class is instantiated
as can be seen in the following code block
(just as a quick reminder: we are still discussing the match() method of the Standard
router).
if (!$controllerInstance->hasAction($action)) {
continue;
}
$found = true;
break;
}
if (!$found) {
return false;
}
RESEARCH 63
Finally, the router checks if a matching action method exists on the controller.
The name of the PHP method to call for a given action is determined by adding the suffix
Action to the variable $action.
For example, the action index is handled by a method indexAction() of the controller.
In case no matching action is found, the Standard router will continue looking in the next
module mapped to by the frontName.
If all $modules were checked without finding a matching controller with a matching action,
false is returned.
In this case the Standard router was not able to match the current request, and the Front
Controller will continue with the next router.
On the other hand, if a match was found, there are a number of final steps before the
controller’s action method is called.
// dispatch action
$request->setDispatched(true);
$controllerInstance->dispatch($action);
return true;
}
The frontName, controller, action and the $realModule are set on the request object.
This can be very handy if, for example, we need to find out what page is currently being
requested inside an event observer.
Then the isDispatched flag is set to true (finally!), and the router delegates to the
controller by calling dispatch().
Finally we have completed reading through the Standard routers match() method!
Before we continue analyzing the request processing inside the action controller, let’s
summarize the routing process from the Standard router context.
64 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
For some developers it is confusing that the frontName is mapped to a module directory
using configuration XML.
The request frontName can be completely different than the module’s directory name, as
will become apparent in the following two chapters.
However, when mapping the controller to a controller class there is a direct correlation
between the controller name and the file system.
As stated once before, the action method name is found by appending an Action suffix to
the third part of the request path.
If no controller or action were specified in the request, the default controller and action
values are used, which both are index (mapping to an IndexController.php file or an
indexAction() method respectively).
If a matching controller is found, the response content is generated by dispatching the
action controller. Note that all this happens while the router’s match() is called.
Maybe a better name for the method would have been matchAndProcess().
Because knowing the workings of the Front Controller is useful beyond passing the MCD
exam, there is more to be said about it.
So far we only have discussed the aspects of the Front Controller directly related to the
routing process, that is, the gathering of all routes and finding a matching router.
There are 3 further responsibilities:
All of these are handled inside the Front Controller’s dispatch() method. In addition
to these three, there are a few additional aspects - like additional events - that are quite
useful to know about.
Let’s look at these in order to complete our understanding of the Front Controller.
if (!$request->isStraight()) {
Mage::getModel('core/url_rewrite')->rewrite();
}
$this->rewrite();
Not a lot of code. It has changed a little in newer Magento versions since 1.7.0.2 on which,
as a reminder, the current version of the certification is currently based.
9 The isStraight flag is only set on the request object by the Enterprise Edition Full Page Cache
module under certain conditions, so we will not explore that any further here.
66 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
<global>
<rewrite>
<example_rewrite>
<from>#^/customer/account/loginPost(.*)$#i</from>
<to>/example/login/post$1</to>
<!--<complete>1</complete>-->
</example_rewrite>
</rewrite>
</global>
The $_rewritedPathInfo property on the request object will be set to the original path
info that was requested, unless the <complete> node is set in the configuration.
If $_rewritedPathInfo is set (because <complete> is missing), URLs using the wildcard
* character to refer to the current module, current controller, or current action will be
constructed using the original request path.
Kind of confusing, and the naming choice of the node doesn’t help.
What it boils down to is that that the original path info will be used to construct new
URLs if <complete> is set. The name <use_orig_pathinfo> would have been much more
descriptive.
An example for a URL being constructed using wildcards would be
Mage::getUrl('*/*/list') (current frontName, current controller, list action).
Generally it is better to omit the <complete> tag, since complete regex based URL rewrites
can lead to having to rewrite further controllers or actions than wanted because of that
behavior.
Assuming the XML rewrite example above from /customer/account/loginPost to /exam-
ple/login/post is in effect, Mage::getUrl('*/*/test') would return the following request
path:
RESEARCH 67
The first version probably gives the desired result, unless you really are planning to rewrite
further requests to /customer/account/ to the same custom controller.
The fifth and final Front Controller responsibility - displayed in the code block above - is
also part of the dispatch() method.
After the routing has taken place and the response has been generated, it is the final
responsibility of the Front Controller to flush it to the browser.
This is the reason why no output should be echoed out directly from a controller action
(or anywhere).
If some content is printed directly, bypassing the Front Controller, it would no longer be
possible to add HTTP headers to the response after the routing completes.
Also, it would be impossible to capture and process the content that already was sent using
the handy controller_front_send_response_before event you can see being dispatched
in the code snippet above.
68 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
Instead, any response data should always be added to the response object using
$response->setBody(), $response->appendBody() or $response->setHeader().
That way the response content may be modified or new headers may be added via event
observers right up to the the moment the Front Controller calls
$this->getResponse()->sendResponse().
Many caching modules for Magento rely on content being set on the response object,
including the Enterprise Edition Full Page Cache.
There is one additional task the Front Controller takes care of at the beginning of the
dispatch() method, and that is to check that the requested URL matches the configured
base URL for the current store.
The line of code was omitted from the larger code sections of the dispatch() method
further above in order to focus on the routing logic.
The purpose of the method _checkBaseUrl() is to redirect a visitor to the configured base
URL if the current request doesn’t match it.
For example, let’s assume that the webserver is configured to serve Magento on both the
www.example.com and example.com domains.
Without the base URL check, that would mean every page is available on two different
domains. Google would rate that as duplicate content, and reduce both sites’ rank.
For that reason, if the base URL configured in the system configuration is set to, let’s
say, http://www.example.com/, then requests to http://example.com/ are automatically
redirected to the version of the domain with the www prefix.
That way there is only a single resource on the web with the content, and thus Google no
longer sees it as duplicate content.
Please note that it is a smarter approach to configure redirects to a single base URL on
the webserver instead of in Magento, since that causes much less overhead.
For the Apache webserver mod_rewrite offers the option to add rewrite rules. Other
webservers have similar capabilities that can be used to the same effect.
RESEARCH 69
The base URL check in Magento can be disabled in the system configuration with the
option web/url/redirect_to_base.
Checking the base URL is not considered one of the prime responsibilities of the Front
Controller, but it still is useful to know about it.
The purpose of a router is to provide a way to match requests to specific business logic.
An example from a real project for a custom router was a site where requests where mapped
to articles by date, author or keywords, or combinations thereof.
The standard router matching doesn’t fit that scheme at all, so a custom router was an
elegant solution.
However, usually the actual request processing and the generation of the response content
is not done by the router class itself.
That task is delegated to an action controller.
For that reason, most core routers simply modify the response object10 without setting the
isDispatched flag, so the Standard router will match the request on the next iteration.
Once the Standard router has matched a request and found a matching action controller,
it will call dispatch() on it, passing along the requested action name.
10 More specific, they set the module, controller and action name on the request object to delegate to the
Standard router.
70 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
// dispatch action
$request->setDispatched(true);
$controllerInstance->dispatch($action);
All controllers inherit the dispatch() method from the abstract class
Mage_Core_Controller_Varien_Action.
The following code block is very abbreviated. The purpose is to illustrate the structure of
the dispatch() method:
if ($this->getRequest()->isDispatched()) {
if (!$this->getFlag('', self::FLAG_NO_DISPATCH)) {
$this->$actionMethodName();
$this->postDispatch();
}
}
}
Remember, when this method is called by the Front Controller, the request isDispatched
flag has already been set to true.
The structure of the dispatch() method offers two options that can be taken advantage
of in the preDispatch() method:
• If the isDispatched flag is reverted to false, the controller action will not be
processed after all.
This opens the possibility to delegate to a different router from an action controller,
since the outer router matching loop of the Front Controller will continue after the
match.
• If the Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH flag is set on
the action controller, the action method will not be called.
The difference to the previous option is that in this case no further router matching
will take place, since isDispatched still is true.
This allows the complete response to be generated during the preDispatch() method.
Let’s inspect the (abbreviated) preDispatch() method code to see how this might be
utilized.
72 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
if (!$this->getFlag('', self::FLAG_NO_START_SESSION)) {
$session = Mage::getSingleton(
'core/session', array('name' => $this->_sessionNamespace)
)->start();
}
Mage::app()->loadArea($this->getLayout()->getArea());
Mage::dispatchEvent(
'controller_action_predispatch',
array('controller_action' => $this)
);
Mage::dispatchEvent(
'controller_action_predispatch_' . $this->getRequest()->getRouteName(),
array('controller_action' => $this)
);
Mage::dispatchEvent(
'controller_action_predispatch_' . $this->getFullActionName(),
array('controller_action' => $this)
);
}
First, right at the top of the method, another round of rewrites is applied. We will delve
into this _rewrite() method later on.
Second, the session is started, unless the FLAG_NO_START_SESSION flag is set on the action
controller.
This flag might come in handy when writing integration tests and you don’t feel like
mocking the session model.
However, the most important thing to remember is that the session is started by the action
controller’s preDispatch() method.
Then the current request area is loaded on the core/app model.
More on area loading later while discussing event observer configuration.
Third, 3 events are dispatched. They range from very generic to very specific.
RESEARCH 73
Usually these three parts are concatenated using an underscore. This results in an event
name specific to every page in Magento.
For example, a request to the URL path /catalog/product/view would cause the following
three events to be dispatched from the action controller’s preDispatch() method:
1. controller_action_predispatch
2. controller_action_predispatch_catalog
3. controller_action_predispatch_catalog_product_view
$action = Mage::app()->getFrontController()->getAction();
$action->setFlag(
'', Mage_Core_Controller_Varien_Action::FLAG_NO_START_SESSION, 1
);
The following code block is an example to set and get custom values on an action controller
Close to the beginning of the preDispatch() method, another set of configuration based
rewrites are applied to the request.
The method that is called to do this is
Mage_Core_Controller_Varien_Action::_rewrite().
However, this type of configuration based rewrite has also been declared deprecated, just
like the Front Controller configuration based rewrites.
The reason for that is that the whole overhead of routing that has happened up to this
point is wasted if a rewrite is configured using this method.
Similar to the regular expression config based request rewrites applied by the Front
Controller, this rewrite facility still exists solely for backward compatibility.
You might encounter it in third party extensions, so it is a good thing to know about it.
The config based rewrite configuration that is applied by the action controller comes in
two flavors:
To only rewrite a specific action, the <override_actions> node is omitted and the action
is added as a child node.
For example, to only rewrite the add action of the checkout/cart controller, the following
code would be used:
Regardless whether the route which is being rewritten is configured in the frontend or
admin area, the rewrite always is specified in the <global> branch.
We started this section defining three things we need to know in order to implement the
exercise solution:
This has a straightforward answer: the CMS page URL key is specified using the cms/page
attribute identifier.
Before the actual action method is called, the controller’s preDispatch() method dispatches
the page specific event controller_action_predispatch_cms_page_view.
Our redirect can be implemented in an event observer for this event.
78 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
The redirect itself is set on the response object instead of calling the PHP function header()
directly, because the sending of the response is handled by the Front Controller.
To prohibit further processing of the request, the
Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH controller flag is set on the
action controller instance.
Solution
The code can be found in the extension Meeting02_RedirectToRoot.
After a long research section, the solution code is very short.
An event observer for the event
controller_action_predispatch_cms_page_view is declared in the configuration XML.
<frontend>
<events>
<controller_action_predispatch_cms_page_view>
<observers>
<meeting02_redirectToRoot>
<type>model</type>
<class>meeting02_redirectToRoot/observer</class>
<method>controllerActionPredispatchCmsPageView</method>
</meeting02_redirectToRoot>
</observers>
</controller_action_predispatch_cms_page_view>
</events>
</frontend>
Event observers are one of the most powerful tools of a Magento developer.
Even though the implementation in Magento isn’t perfect, it is still possible to use them to
great effect.
However, once again it is useful to have a good understanding of how they work.
Let’s analyze the event system code, starting with the method call which triggers an event:
Mage::dispatchEvent($eventCode, $eventArguments);
SOLUTION 79
The event code is a string. If the optional $eventArguments argument is present it must
be an array of key-value pairs which will be passed to the observer method as arguments,
as we will see shortly.
As often is the case, the Mage “god” class uses a delegate to do the real work.
In this case Mage delegates to the application model Mage_Core_Model_App, where the
main event dispatching process takes place.
Let’s dissect the Mage_Core_Model_App::dispatchEvent() method bit by bit.
The first thing that happens is that Magento iterates over an array containing the registered
event areas.
So what are these?
Event Areas
When the core/app model is instantiated, the $_events property is an empty array.
Then, when the Magento application initialization is triggered by calling Mage::run() or
Mage::app(), the global events area is registered.
This happens by calling
Mage::app()->loadAreaPart(
Mage_Core_Model_App_Area::AREA_GLOBAL,
Mage_Core_Model_App_Area::PART_EVENTS
);
80 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
Once an event area is “loaded” in this way, the foreach loop in core/app::dispatchEvent()
will process the configuration within that config XML branch matching that area (please
refer to the loop in the code block a little further above).
The loadAreaPart() only registers the area in the array. The actual configuration is
lazy-loaded whenever an event is dispatched.
Instead of using loadAreaPart(), the method Mage::app()->addEventArea($area); can
be called alternatively.
The end result is the same.
This alternative addEventArea() method is used in the cron.php script to register the
crontab event area.
In addition to the global event area, the frontend or adminhtml event areas are loaded
during the action controller’s preDispatch() method, depending on whether it is a frontend
or admin controller.
The following table gives an overview over where the different event areas are loaded.
global Mage::app()
Mage::run()
frontend Mage_Core_Controller_Front_Action::preDispatch()
adminhtml Mage_Adminhtml_Controller_Action::preDispatch()
Mage_Api_Model_Server_Handler_Abstract::_construct()
crontab cron.php file
Before the action controller is dispatched, only events configured under the <global> config
XML area will be processed.
This is good to know, especially when registering an event observer for the early events
dispatched by the Front Controller.
Putting them in <frontend> or <adminhtml> just won’t work.
When writing custom command line scripts which initialize the Magento environment, keep
in mind that no area besides global is registered when calling Mage::app().
Even setting a store view by calling Mage::app()->setCurrentStore($storeCode)
doesn’t magically load the events for that store’s area.
If you want the adminhtml or frontend events to be processed, the event area needs to be
loaded manually as shown in the following example code block.
require_once 'app/Mage.php';
Mage::app('admin');
Mage::app()->addEventArea('adminhtml');
if (!isset($events[$eventName])) {
$eventConfig = $this->getConfig()->getEventConfig($area, $eventName);
if (!$eventConfig) {
$this->_events[$area][$eventName] = false;
continue;
}
$observers = array();
foreach ($eventConfig->observers->children() as $obsName=>$obsConfig) {
$observers[$obsName] = array(
'type' => (string)$obsConfig->type,
'model' => $obsConfig->class ?
82 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
(string)$obsConfig->class :
$obsConfig->getClassName(),
'method'=> (string)$obsConfig->method,
'args' => (array)$obsConfig->args,
);
}
$events[$eventName]['observers'] = $observers;
$this->_events[$area][$eventName]['observers'] = $observers;
}
• <type>
• <class> or <model>11
• <method>
• <args>12
All of these nodes must be wrapped by a unique node, called observer name (assigned to
the variable $obsName by the foreach statement).
The only purpose of the observer name is to avoid conflicts between modules that register
observers for the same events.
That’s why it is important to use a unique node as the observer name.
According to best practice, use namespace - underscore - module name in lower case. This
follows the same convention as the class group for a module.
Here is an example observer configuration:
<config>
<frontend>
<events>
<customer_login>
<observers>
11 As mentioned before, nobody uses <model> (even though it works), so it probably is a good idea to
<example_module>
<type>singleton</type>
<class>example_module/observer</class>
<method>customerLogin</method>
</example_module>
</observers>
</customer_login>
</events>
</frontend>
</config>
We will see how exactly the values listed in the table are used in the following code segments.
if (false===$events[$eventName]) {
continue;
} else {
$event = new Varien_Event($args);
$event->setName($eventName);
$observer = new Varien_Event_Observer();
}
If no observer is configured for the event within the area of the current iteration, the loop
continues with the next registered event area.
Otherwise, the code begins to prepare dispatching the event observer methods.
A Varien_Event object is instantiated as a container object.
The $args array which was passed into the dispatchEvent() method is passed on to the
84 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
constructor.
Since Varien_Event extends from Varien_Object all arguments will be available via magic
getters.
The Varien_Event_Observer object, which is instantiated next, is a second container
instance.
Don’t be confused by the class name - the $observer is not “our” observer object. It is
simply another container object for the event arguments, just like the $event object.
Why a second container? I don’t know, probably for historical reasons. Why the strange
naming? Same reason. Look for the next Magento Trivia section for a possible explanation.
Now finally all configured observers are processed in another foreach loop.
The $event instance is passed to the $observer instance.
The container object $observer now contains the other container object $event which
can be accessed using $observer->getEvent().
Once again: that’s history. :)
switch ($obs['type']) {
case 'disabled':
break;
case 'object':
case 'model':
$method = $obs['method'];
$observer->addData($args);
$object = Mage::getModel($obs['model']);
$this->_callObserverMethod($object, $method, $observer);
break;
default:
$method = $obs['method'];
$observer->addData($args);
$object = Mage::getSingleton($obs['model']);
$this->_callObserverMethod($object, $method, $observer);
break;
}
}
}
SOLUTION 85
return $this;
}
Depending on the <type> node value, the real observer class - the one specified in the
<class> node - is instantiated in a different fashion.
The following table shows the factory method that is used depending on the observer type.
model Mage::getModel() -
object Mage::getModel() Generally not used, use model instead.
singleton Mage::getSingleton() The default if the <type> node is omitted
disabled n.a. The event observer is not instantiated
It doesn’t make a difference which way you choose (except for the number of characters
you type) - the result is always the same.
The one piece of information that is only available via the Varien_Event instance is the
event name.
After the container objects have been prepared and the observer instance is created, finally
the observer method is called:
Most of this code is rather self-explanatory after the extensive research chapter.
One thing which probably isn’t clear on casual reading though is why the configuration
setting is split using a | character.
The system configuration value that is read specifies the CMS page to display as the home
page. It is compared against the current request path after the explode() function call.
The question is, why is the value split at a pipe character?
The options of the system configuration select field are generated by the source model
Mage_Adminhtml_Model_System_Config_Source_Cms_Page.
The cms/page title is used as the select label that is visible in the dropdown.
The cms/page identifier string is used for the option values.
However, cms/page identifiers are not globally unique.
88 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
It is possible to create two CMS pages with the same identifier, as long as they are assigned
to different store views.
In that case, the CMS page selection source model values are qualified by appending the
cms/page entity ID, separated by a |.
For example, if two CMS pages exist with the identifier no-route - one for the “All Stores”
scope, and one for the “English” store view scope - the second one in the list will receive
the cms page ID as the suffix.
Technically, the CMS page selection source model for the system configuration
(adminhtml/system_config_source_cms_page) calls the method
Mage_Cms_Model_Resource_Page_Collection::toOptionIdArray() method to generate
the list of options, which is where the qualification with the page ID takes place.
$data['value'] = $identifier;
$data['label'] = $item->getData('title');
if (in_array($identifier, $existingIdentifiers)) {
$data['value'] .= '|' . $item->getData('page_id');
} else {
$existingIdentifiers[] = $identifier;
}
$res[] = $data;
}
return $res;
}
That is the reason why the CMS home page identifier is split using a | character before it
is compared against the current request path by the observer.
It might seem like a lot of effort to extract the name of the configured CMS home page.
Lots of people simply hardcode “home” as the value to test against.
SOLUTION 89
However, this course breaks if a different cms page were chosen, which is the case for default
EE installation.
If the current request path starts with the configured CMS page identifier, a redirect to
Magento base URL is set on the response object:
$action->getResponse()->setRedirect(Mage::getBaseUrl());
$action->setFlag(
”, Mage_Core_Controller_Varien_Action::FLAG_NO_DISPATCH, true
);
Since the visitor will be redirected using a 302 HTTP response code and location header,
the response body will not be displayed by the browser.
Because of that it would be a waste of server resources to generate some HTML content.
To tell the action controller to skip calling the actual action method, the FLAG_NO_DISPATCH
flag is set on it.
Setting the flag to true will cause the response body to be empty, and no resources will be
spent on loading and rendering the CMS page.
Magento Trivia
Besides the event observer system implemented in Mage_Core_Model_App, the Mage class
also contains an alternative Event Observer implementation.
It is functional but is never used within the Magento framework.
Since it it part of the Varien library, it probably predates Magento.
In it observers are configured using the method
Mage::addObserver($eventName, $callback, $args);
To add all configured event observers for a specific area,
Mage::getConfig()->loadEventObservers($areaCode) can be used.
The configuration XML structure that is read is the same as for the core/app event
observer implementation.
To fetch the Varien_Event_Collection with all observers, the method Mage::getEvents()
is available.
90 CHAPTER 4. REDIRECT TO / FROM CMS HOMEPAGE
Add a new frontend route and create an index controller and an index action
that set the return value of $this->getFullActionName() to the response
body.
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
91
92 CHAPTER 5. CUSTOM FRONTEND CONTROLLER
Scenario
Every time an extension adds a new page (aka “a route”) to a Magento instance, a custom
action controller is required.
Example scenarios which use a custom controller could be
Research
In the previous chapter the whole routing process was discussed in detail.
For every request the final situation of the routing process was that the Standard router
dispatches an action controller method.
However, all that the chapter covered was that the standard router loads a list of configured
routes. It did not discuss how the routes are configured.
Lets have a look at the method
Mage_Core_Controller_Varien_Router_Standard::collectRoutes(), which is respon-
sible for gathering all configured routes within the router area.
Just as a reminder: collectRoutes() is called from the Front Controller’s init() method.
Since the Admin router inherits the collectRoutes() method from the Standard router
through class inheritance, the admin and frontend routes share the configuration structure.
The only difference in the configuration XML structure between Admin and Standard routes
is that the former go into the <admin> area, while the latter belong in the <frontend>
config branch.1
This is the code that fetches the router configuration from the routers area:
1 The area the routers are associated with are configured in the config XML branch default/web/routers.
Please refer to the Front Controller Routing Process section of the previous chapter for details.
RESEARCH 93
The variable $routers now contains an array with all the routes that are configured in
the <frontend> or <admin> area respectively.
In the next step the router iterates over all those configured routes.
Note that the value of the <use> node is compared with the value of $useRouterName,
which was passed in as a method argument.
The value that was passed in as the $useRouterName is taken from the router configuration.
Please refer to the Front Controller Routing Process section of the previous chapter for
details.
The following (abbreviated) code is only executed if the value of the <use> node matches
the router $useRouterName value.
$modules = array((string)$routerConfig->args->module);
if ($routerConfig->args->modules) {
foreach ($routerConfig->args->modules->children() as $customModule) {
if ($customModule) {
$modules[] = (string)$customModule;
}
}
}
$frontName = (string)$routerConfig->args->frontName;
$this->addModule($frontName, $modules, $routerName);
}
The $modules array is initialized with the value of the <args><module>. . . node.
94 CHAPTER 5. CUSTOM FRONTEND CONTROLLER
Additional routes can be configured within the <args><modules>. . . branch. Each addi-
tional module is then also added to the $modules array.
This mostly happens for the adminhtml route. For example, if we try the following debug
code at the end of the collectRoutes() method
it displays a list of all modules which register themselves for the adminhtml route.
Array
(
[0] => Mage_Index_Adminhtml
[1] => Mage_Paygate_Adminhtml
[2] => Mage_Paypal_Adminhtml
[3] => Mage_Widget_Adminhtml
[4] => Mage_Oauth_Adminhtml
[5] => Mage_Authorizenet_Adminhtml
[6] => Mage_Bundle_Adminhtml
[7] => Mage_Centinel_Adminhtml
[8] => Mage_Compiler_Adminhtml
[9] => Mage_Connect_Adminhtml
[10] => Mage_Downloadable_Adminhtml
[11] => Mage_ImportExport_Adminhtml
[12] => Mage_Api2_Adminhtml
[13] => Mage_PageCache_Adminhtml
[14] => Mage_XmlConnect_Adminhtml
[15] => Mage_Adminhtml
[16] => Phoenix_Moneybookers
)
However, in a non-customized Magento instance all the frontend routes only contain a
single module.
Extensions however often add themselves to existing routes in this way.
Finally, almost at the end of the collectRoutes() method, this list of modules is added
to the Standard router’s internal map of frontNames to modules.
Here is a frontend route configuration example from the Mage_Catalog module with some
comments thrown in.
Solution
The example solution code can be found in the extension Meeting02_CustomController.
<frontName>custom</frontName>
</args>
</meeting02_customController>
</routers>
</frontend>
Examining the example solution’s controllers directory, we can see there is an IndexCon-
troller.php file.
As discussed in depth in the previous chapter, all files containing controller classes must
end with the Controller.php suffix.
A request path for /custom/index will cause the Standard router to check in this class for
a matching action (the frontName custom and the controller name index).
Repeating another fact discussed in the last chapter, the class name of the controller almost
follows the same conventions as modules and blocks, except that the controllers directory
is omitted from the class name.
The full controller class from the sample exercise solution only has a couple of lines:
class Meeting02_CustomController_IndexController
extends Mage_Core_Controller_Front_Action
{
public function indexAction()
{
SOLUTION 97
$this->getResponse()->setBody($this->getFullActionName());
}
}
The full action name, which is set as the response object body within the indexAction()
method, has also been discussed in the previous chapter in the context of the events
dispatched in the action controller’s preDispatch() method.
The return value consists of the current route name (meeting02_customController), the
controller name (index) and the action name (also index).
For the sample exercise solution the output this gives us is
meeting02_customController_index_index.2
This completes the chapter.
2 The full action name is also used as the layout action handle during the rendering of the view layer.
The original task description from the study group kit for this exercise is as follows:
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
99
100 CHAPTER 6. ACTION CONTROLLER REWRITE
Scenario
There are several scenarios where redirecting the customer after login seems plausible:
Admittedly, redirecting the customer to a category page doesn’t really seem all that realistic,
but it does serve as an educational example.
Maybe the category could have been prepared as a landing page for a special sale, right?
Research
Each of these could be used to route a request to a custom controller when the login page
is requested, effectively implementing a controller rewrite.
But as you will recall, all methods but the DB based one are deprecated.
The database based URL rewrites are mainly used for mapping SEO URLs to internal
routes.
Using the same functionality to implement a rewrite in a module would not be a clean
solution.
Magento provides another mechanism that hasn’t been covered yet for controller rewrites,
and this method is the recommended way to implement them.
It utilizes the fact that a frontName can be mapped to more than module.
This feature was analyzed in depth in the exercise Custom frontend controller route; setting
the response body.
RESEARCH 101
To briefly reiterate: when the Standard (or Admin) router collects all configured routes, the
mapping of the frontName to a module is initially done using the following configuration
section.
<args>
<!--
map /example requests to controllers in Example/Module/controllers/
-->
<module>Example_Module</module>
<frontName>example</frontName>
</args>
If additional modules are listed under the args/modules node - note the plural <modules>
- then they are added to the list of possible matches, too.
The code sections which parse that configuration were discussed in the previous chapter.
However, the code samples there were abbreviated.
To understand how this feature can be used to implement controller rewrites, having a look
at the full code is necessary.
Here is the full route collection code from
Mage_Core_Controller_Varien_Router_Standard::collectRoutes():
$modules = array((string)$routerConfig->args->module);
if ($routerConfig->args->modules) {
foreach ($routerConfig->args->modules->children() as $customModule) {
if ($customModule) {
if ($before = $customModule->getAttribute('before')) {
$position = array_search($before, $modules);
if ($position === false) {
$position = 0;
}
array_splice($modules, $position, 0, (string)$customModule);
} elseif ($after = $customModule->getAttribute('after')) {
$position = array_search($after, $modules);
if ($position === false) {
$position = count($modules);
}
array_splice($modules, $position+1, 0, (string)$customModule);
} else {
$modules[] = (string)$customModule;
102 CHAPTER 6. ACTION CONTROLLER REWRITE
}
}
}
}
The initial frontName-to-module mapping is created right in the first line of the code block
above.
The rest of the code iterates over all additional modules for the route currently being
processed.
The difference from the previously discussed - abbreviated - version of the method is that
the additional modules are not simply appended to $modules.
Instead, what happens depends on the presence or absence of a before or after attribute
on the XML node.
In case a before node is present, the new module will be added to the list of modules
before a record with the attributes value (see the first if branch).
In case an after node is present, the new module will be added after a record with the
attribute value (see the elseif branch).
In case there is neither a before or an after node, the new module will simply be appended
to the list (see the else branch above).
Let’s go over a few examples to make things clearer.
Let’s assume the following route configuration is present in the merged configuration XML:
<admin>
<routers>
<adminhtml>
<use>admin</use>
<args>
<module>Mage_Adminhtml</module>
<frontName>admin</frontName>
<modules>
<Mage_Paypal
before="Mage_Adminhtml">Mage_Paypal_Adminhtml</Mage_Paypal>
<widget
before="Mage_Adminhtml">Mage_Widget_Adminhtml</widget>
<moneybookers
after="Mage_Adminhtml">Phoenix_Moneybookers</moneybookers>
RESEARCH 103
<example_mod
before="Mage_Widget_Adminhtml">Example_Module_Adminhtml</example_mod>
</modules>
</args>
</adminhtml>
</routers>
</admin>
2. The Mage_Paypal module is added next, before the original record. Now the array
contains two modules:
array(
'Mage_Paypal_Adminhtml',
'Mage_Adminhtml'
)
Note that the value added to the array is the node value, not the node name.
3. The widget module is added before Mage_Adminhtml, in effect putting it between
the first two records.
array(
'Mage_Paypal_Adminhtml',
'Mage_Widget_Adminhtml',
'Mage_Adminhtml'
)
array(
'Mage_Paypal_Adminhtml',
'Example_Module_Adminhtml',
'Mage_Widget_Adminhtml',
'Mage_Adminhtml',
'Phoenix_Moneybookers'
)
For this example, this would be the full list of modules that the router would look inside
for a matching controller.
Remember, each of those records is mapped to a directory in the module.
In this example, the router would look in the following directories until it finds a matching
controller class:1
1. Mage/Paypal/controllers/Adminhtml/
2. Example/Module/controllers/Adminhtml/
3. Mage/Widget/controllers/Adminhtml/
4. Mage/Adminhtml/controllers/
5. Phoenix/Moneybookers/controllers/
The router will only dispatch the controller if it has a matching action method, otherwise
it will continue looking for another match.
In fact, the Magento core code contains a Mage/Adminhtml/controllers/AjaxController.php
file.
Will this be the class file loaded?
Usually yes. But what if a controller file with the same name would have been added to a
module earlier in the list?
In that case the controller from the module further up in the array would be used (if that
controller contains a matching action).
This is the recommended way to implement controller rewrites.
To surmise, this feature can be used in two ways:
Adding new controllers is used mostly in the context of the admin area.
Controller rewrites are used for both the frontend and the admin area.
This type of controller rewrite can be very useful, but suffers the same downside as module,
block, or helper class rewrites: each class can only be rewritten once.
If two modules try to rewrite the same controller, only one of them will be used.
The other one will be silently ignored, which can lead to confusing and maybe hard to
debug behavior (“Why is my controller rewrite not working?!?!”).
When adding a new controller to an existing route, usually it is a good idea to use the
after argument with the original module name, so if a module with the same name is
added to the core in future, your module won’t accidentally be masking it.
However, when rewriting a controller class, before has to be used in the configuration,
otherwise the rewrite won’t work if your route is added to the list after the rewrite target.
When implementing a controller rewrite, the new controller usually extends the original
controller class.
Because controller classes are included by the router and not by the autoloader, simply
defining the inheritance in PHP is not enough.
106 CHAPTER 6. ACTION CONTROLLER REWRITE
For action controllers we have to revert to manually specifying the class file to include,
before it can be used as the parent in the class definition.2
The best way to build the path to the original controller class is using the
Mage::getModuleDir() method.3
<?php
class Example_Module_Customer_AccountController
extends Mage_Customer_AccountController {
Controller class files are the only classes of the Magento framework that are not included
by the autoloader (besides Mage).
This section of the book now completes the whole routing process, starting with the Front
Controller instantiating all routers, router matching and router delegation, the Standard
router mapping the request to a route, instantiation of an action controller and finally the
action controller being dispatched.
of simply writing / before the file name, but since PHP will take care of automatically using the correct
separator independently of what we specify, it doesn’t have any benefit (except making the code harder to
read).
RESEARCH 107
Before we wrap up this section, let’s summarize all options Magento provides to apply
controller rewrites:
For the current exercise option number 4 will be the right choice to rewrite the
Mage_Customer_AccountController controller.
But before we move on to the solution, there is one more little bit of useless Magento trivia
for you if you enjoy that kind of thing.
Magento Trivia
The Standard router contains yet another rewrite implementation, but fortunately that
Mage_Core_Controller_Varien_Router_Standard::rewrite() method is never called.
It has been around since the first release of Magento, but it looks like it has long since
been forgotten.
Solution
The example solution code can be found in the extension Meeting02_ControllerRewrite.
SOLUTION 109
<?xml version="1.0"?>
<config>
<frontend>
<routers>
<customer>
<args>
<modules>
<meeting02_controllerRewrite
before="Mage_Customer">Meeting02_ControllerRewrite_Customer</meeting02_controllerRewrite>
</modules>
</args>
</customer>
</routers>
</frontend>
<default>
<meeting02>
<controller_rewrite>
<target_category_id>22</target_category_id>
</controller_rewrite>
</meeting02>
</default>
</config>
The second part of the configuration within the <default> branch specifies the category
ID to which a customer should be redirected after a successful login.
Since the configuration already has been discussed in detail in the research section of this
chapter, let’s move on to the controller code of the example exercise solution.
class Meeting02_ControllerRewrite_Customer_AccountController
extends Mage_Customer_AccountController
{
The parent class is manually included before the class declaration, since the Magento
autoloader will not include it (as already discussed earlier in this chapter).
The new class then can extend from the original rewritten class without PHP complaining
about the unknown class.
The new class overwrites the parent’s loginAction() method and adds one new method
of its own.
$categoryId = Mage::getStoreConfig(
'meeting02/controller_rewrite/target_category_id'
);
$storeId = Mage::app()->getStore()->getId();
$url = $this->_getSeoCategoryUrl($categoryId, $storeId);
$this->_getSession()->setBeforeAuthUrl($url);
}
Before anything else is done, the parent::loginAction() method is called. This keeps
the code as upgrade-safe as possible, since any future changes will probably be picked up
automatically - no code was copied and pasted from the parent method.
To generate the target URL a custom method _getSeoCategoryUrl() is used - we will
look at that next. The returned URL is then set on the customer/session model using
the magic setter setBeforeAuthUrl().
SOLUTION 111
if ($requestPath) {
$url = Mage::getModel('core/url')->getDirectUrl($requestPath);
} else {
$url = Mage::getModel('core/url')->getUrl(
'catalog/category/view', array('id' => $categoryId)
);
}
return $url;
}
}
before set after set Login okay Config option set Redirect target
There still are two special cases that aren’t included in the table that I’ll just mention for
sake of completeness.
4 The cyclomatic complexity number of the Mage_Customer_AccountController::_loginPostRedirect()
1. When the before_auth_url equals the base URL it is not used. The behavior is
just as if before_auth_url isn’t set at all.
2. If the before_auth_url equals the logout page URL it is changed to the customer
account page.
The before_auth_url property is set in many places within the Magento core.
The after_auth_url property usually isn’t set.
The usual flow is the following:
• If the login was successful, the customer now is able to view the protected page.
The original task description from the study group kit for this exercise is as follows:
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
115
116 CHAPTER 7. DYNAMIC CLASS REWRITES
Scenario
The idea behind this exercise is very useful. Being able to dynamically set configuration
values enables many customizations, mostly using observers, that otherwise would require
a class rewrite or other much more invasive code.
For example, it can be used for:
• Providing backward compatibility for modules by rewriting a class only under specific
circumstances
• Enabling and disabling payment methods and shipping carriers on the fly
• Working around method signature changes between Magento versions (for example,
if a method used to be private and now is protected)1
• Creating config fixtures for unit tests
Any configurable value can be set this way, making it an extremely powerful technique.
Research
Let’s start by having a look at how configuration values are read, because then it will
become obvious how they are set.
In Magento there basically are two ways configuration values are accessed.
• Scoped values (mainly used for system configuration settings) accessed via
Mage_Core_Model_Store::getConfig()
• General configuration values accessed via
Mage_Core_Model_Config::getNode()
1 Magento Trivia: an example for a change in method visibility is
Mage_Catalog_Model_Product_Image::_rgbToString() which used to be private up until Magento 1.4.1,
when it was changed to protected.
RESEARCH 117
A scoped configuration value is a setting that can be defined in one or more scopes.
There are three scopes:
Scope Priority
If a value is set in more than one scope, the more specific scope value will override the less
specific one.
Please refer to the next chapter on the dispatch process for details on how the configuration
is loaded.
To access scoped values usually the static Mage::getStoreConfig() or
Mage::getStoreConfigFlag() methods are used.
They automatically return the value from the most specific scope for the specified path.
The two methods of the Mage class fetch the current (or the specified) store model and
then use $store->getConfig() to access the scoped value.
The Mage_Core_Model_Store class getConfig() method looks like this:
$config = Mage::getConfig();
if (!$data) {
return null;
}
return $this->_processConfigValue($fullPath, $path, $data);
}
We can see that the value is cached on the store model once it is accessed, so subsequent
calls with the same path will return the cached value.
When trying to change values on the fly it is important to considering caching, so let’s
keep that in mind while we explore further.
In case a non-true value was returned by $config->getNode(), the method returns null.
However, if a value evaluating to true was returned, it is passed through
_processConfigValue() before the value is returned (at the end of the code block above).
The method _processConfigValue() contains two little-known features.
Lets have a closer look at it.
Just for reference: the variable $fullPath contains the full XPath to the configuration
setting including the store scope prefix.
The variable $path contains the config path without any scope applied.
The $node variable contains the Mage_Core_Model_Config_Element node returned by
$config->getNode()
Let’s inspect the next part of the _processConfigValue() method.
if ($node->hasChildren()) {
$aValue = array();
foreach ($node->children() as $k => $v) {
$aValue[$k] = $this->_processConfigValue(
$fullPath . '/' . $k, $path . '/' . $k, $v
);
}
$this->_configCache[$path] = $aValue;
return $aValue;
}
RESEARCH 119
The above section of code builds the content for the already mentioned configuration cache
property.
If the requested node has children, the values of all branches below the requested node are
cached as an array.
Not a lot we can do with that.
The next section of the method is more useful:
First the node value is cast to a string. Up until now it still was a
Mage_Core_Model_Config_Element instance.2
And here we discover a little-known feature: any scoped configuration value may have a
backend model.
If a backend_model="..." argument is present, the model is instantiated, the config value
is set on it, then afterLoad() is called, and the returned value is used after that.
This functionality might be used for many things:
That list surely is very incomplete but hopefully is enough to trigger your own ideas on
how to use it.
Getting back to the _processConfigValue() method, some further processing of the string
value happens next:
2 The Mage_Core_Model_Config_Element is just a relatively small wrapper around
Varien_Simplexml_Element, which in turn extends the standard PHP SimpleXmlElement class.
120 CHAPTER 7. DYNAMIC CLASS REWRITES
Configuration values can contain references to the configured secure or unsecure base URL
(see the if or the first elseif branches.).
However, the most interesting one is the last placeholder option: {{base_url}} (in the
second elseif condition branch).
If the string {{base_url}} is present in the config value, the config model’s method
substDistroServerVars() replaces it with the currently requested host name (including
the http or https schema).
If the store code is configured to be part of the request path, it will not be included in the
{{base_url}} replacement value.
The remainder of the _processConfigValue() method simply sets the store’s config cache
property for the current path and returns the final value:
$this->_configCache[$path] = $sValue;
return $sValue;
}
Magento Trivia
The method Mage_Core_Model_Config::substDistroServerVars() actually does a
str_replace() on more than just the {{base_url}}, even though that string has to be
present in the config value to trigger the replacement method call.
The method will also process the following placeholders if present in the config value:
RESEARCH 121
Placeholder Replacement
Since the {{base_url}} placeholder has to be present in the config value to trigger the
automatic replacement, the other values will probably never be useful.
Unless, of course, you call Mage::getConfig()->substDistroServerVars($string) your-
self, passing in a string containing any of the four placeholders.
However, when referring to the other directories in custom code, it is much more common
to call Mage::getBaseDir(), with the appropriate type as an argument.
This method directly returns the value from $store->getConfig(). Unless a specific store
code, ID, or model is passed as an argument, the current store view is used.
The scoped configuration is only a small subsection of the overall Magento configuration.
To access an arbitrary node within the config DOM structure, the method
Mage_Core_Model_Config::getNode() is used.
Since the XML processing in Magento is based on PHP’s SimpleXml3 , the request paths
always are in the context of a node.
Usually this is the root node, <config>, which means this node is omitted when specifying
a config path.
For example, given the following XML tree:
<config>
<modules>
<Mage_Log>
<active>true</active>
<codePool>local</codePool>
</Mage_Log>
</modules>
</config>
to read the value of the <codePool> node, the method call would need to be :
If the requested path doesn’t exist, the return value of getNode() is false.
It is very important to be attentive to the casing of the XML path expression.
The nodes codepool and codePool would be completely different, even if they share the
same parent node.
Since scoped values are accessed via the store model, they should also be set that way.
The Mage_Core_Model_Store class offers the method setConfig() for this purpose.
return $this;
}
The reason it is important to use this method instead of simply setting the value on the
core/config singleton is that it takes care of updating the configuration cache property
in the store model.
In case the config value was previously accessed through the core/store model, simply
changing the node value in the core/config singleton would not be enough, since the
$_configCache store property would mask the changes.
The store model’s setConfig() method already shows us how generic configuration values
are set.
124 CHAPTER 7. DYNAMIC CLASS REWRITES
Mage::getConfig()->setNode($path, $value);
Note the value will not be saved to the database or even the Magento cache backend.
It is only valid for the duration of the current request.
This is exactly what we need for the this chapter’s exercise, which is to dynamically
implement a class rewrite.
Before we move on to the solution, let’s have a look at saving configuration values.
Even though it is not required for the current exercise, it is useful to know how to persist
configuration values.
// In any context
Mage::getConfig()->saveConfig('general/locale/code', 'uk_UA', 'stores', 3);
The second way to store scoped configuration values is to use the core/config_data
model:
RESEARCH 125
// In any context
Mage::getModel('core/config_data')
->setScope('stores')
->setScopeId(3)
->setPath('general/locale/code')
->setValue('uk_UA')
->save();
Any existing value for the specified path and scope would automatically be replaced, even
if the model wasn’t previously loaded.
The third and final way to save scoped config values is mostly used within setup scripts:
// In setup scripts
$installer->setConfigData('general/locale/code', 'uk_UA', 'stores', 3);
The Magento core doesn’t provide a mechanism to save unscoped configuration values.
The only way to accomplish that would be to write them to an XML file, which is then
merged into the config DOM on subsequent requests.
If you ever find yourself in the need for doing that, chances are there is a better approach
then saving arbitrary config XML.
However, let’s explore how it might be accomplished nevertheless.
Since the installation requires the app/etc/ directory to be writable for creating the
app/etc/local.xml file, writing a file there would probably work on many installations.
The file would also automatically be parsed and included in the configuration DOM during
the loading of the base configuration.
However, it probably isn’t a good idea, since any security-aware webmaster should restrict
write access to that directory again after the Magento installation is complete.
126 CHAPTER 7. DYNAMIC CLASS REWRITES
So how about writing the XML to a file in a subdirectory of media/ or var/, and manually
merge it into the loaded configuration?
That would certainly be possible. Making it work with more then a single webserver would
be more challenging though.
Probably it would be better to revise your module’s architecture to use regular entity
storage tables instead.
Solution
The example solution code can be found in the extension Meeting02_DynamicRewrite.
The entry point for the module logic is an event observer for the
controller_front_init_before event.
As discussed in the observer section of the “redirect to base URL chapter”, that event is
triggered very early during every dispatch.
It is a good point to do dynamic config changes, since chances are that the evaluation of
the configuration values in question hasn’t taken place yet.
When choosing this event to do dynamic class rewrites, one thing to watch out for is that
the rewrite will only be applied if Magento is processing a request.
If the Magento runtime environment is initialized from a cron script or a custom command
line script, the Front Controller will not be dispatched, and thus the event will not be fired.
The observer method in exercise solution’s class
Meeting02_DynamicRewrite_Model_Observer looks as follows:
Adhering to best practices, the observer delegates the main work to a model, or in this
case, a helper. The method is
Meeting02_DynamicRewrite_Helper_Data::rewritePaymentHelperIfAncient():
SOLUTION 127
According to the method, an ancient version is anything older than Magento version 1.4.
In that case, the config value Meeting02_DynamicRewrite_Helper_Payment_Data is set
for the config path global/helpers/payment/rewrite/data using setNode() on the
core/config model.4
In the replacement class for the payment helper, the filtering of available payment providers
can take place for Magento versions older than 1.4.
The reason the rewrite only needs to be done in Magento versions older then 1.4 is
because since then payment provider filtering can be done using an observer for the event
payment_method_is_active, which is dispatched in
Mage_Payment_Model_Method_Abstract::isAvailable().
This completes the exercise solution code discussion.
Before finishing this chapter, please note that payment method availability could also be
altered by setting the configuration node values of the payment method’s <active> setting
to false.
This is an alternative way to achieve the same result - filtering available payment methods
- which would not require a class rewrite in any Magento version.
4 The helper class Meeting02_DynamicRewrite_Helper_Data uses a getter method instead of calling
Mage::getConfig() directly because it implements optional constructor injection in order to increase the
testability.
128 CHAPTER 7. DYNAMIC CLASS REWRITES
On the other hand, it might be more difficult to find a good event candidate where all
required information is available that is needed decide if a payment method should be
active or not.
Here is an example of how a payment method could be deactivated using this approach:
$store = Mage::app()->getStore();
if (version_compare(Mage::getVersion(), '1.4', '<')) {
$store->setConfig('payment/ccsave/active', false);
}
Chapter 8
The original task description from the study group kit for this exercise is as follows:
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
Scenario
Since this chapters exercise doesn’t require writing any code, there really is no scenario for
it.
129
130 CHAPTER 8. DISPATCH PROCESS DIAGRAM
Having a solid understanding of the Magento dispatch process of course is very valuable
while debugging and when customizing Magento.
Research
This exercise gives us the opportunity to have a look at one very important aspect of
Magento which we haven’t covered so far: the configuration load process.
But before the configuration is loaded, a few even more basic elements of the Magento
framework have to be initialized.
The very first step is including the file app/Mage.php, which sets up the autoloader and
the include path.
But that alone isn’t enough to initialize Magento. The next step is to prepare the Magento
runtime environment.
There are two ways the Magento runtime environment is initialized after the file
app/Mage.php is included:
The difference between the static Mage class and the instance returned by Mage::app() is
a little confusing to some people.
The Mage class could be called a god class.
It is used all over the Magento codebase mainly for accessing the two most important
objects of the framework: Mage::getConfig() and Mage::app().
It also provides most of the factory methods, for example Mage::getModel() and
Mage::helper().
The special method Mage::run() starts the Magento dispatch process after initializing the
runtime environment.
Also the Mage class is the home of the Magento registry.1
On the other hand, the Mage_Core_Model_App instance generally is used to access most of
the request specific object instances within the Magento framework.
These include but are not limited to:
Consider core/app more of a stateful object then the static Mage class, even though that
distinction isn’t 100% accurate.
After Mage_Core_Model_App is instantiated, the first thing that it does is register the
Magento error handler and set the default time zone.
This happens within the method Mage_Core_Model_App::_initEnvironment().
The Magento error handler mageCoreErrorHandler() can be found in the file
app/code/core/Mage/Core/functions.php, which is included directly by app/Mage.php.
At runtime the XML files are loaded into Magento into a DOM-tree2 like object structure.
Let’s inspect how the config DOM is created, what aspects to keep in mind when working
with it, and how to use it to our advantage.
Lets get back to the configuration load process.
The configuration model is assigned using the method
self::_setConfigModel($options);.
The $options argument seen in the next code block is passed to Mage::run() or
Mage::app() as an optional third argument.
if (
!is_null($alternativeConfigModel) &&
($alternativeConfigModel instanceof Mage_Core_Model_Config)
) {
self::$_config = $alternativeConfigModel;
} else {
self::$_config = new Mage_Core_Model_Config($options);
}
}
The code block reveals that an alternative configuration model can be specified in the
$options array.
This can be used to customize Magento to a very high degree, for example to utilize a
different directory structure than the normal Magento folder hierarchy.
2 DOM is an acronym for Document Object Model.
RESEARCH 133
Using an alternative config model also is a technique used by test framework integrations
to provide a way to mock models and resource models.
The norm however is that no custom config model is specified, in which case the default
config model Mage_Core_Model_Config will used.
After the configuration model is assigned, the Mage class delegates to
Mage_Core_Model_App.
The most important next steps are the same, regardless if Mage::run() or Mage::app()
was used to fire up Magento.
First, a custom error handler is registered, before a reference to the config model is set on
the core/app instance.
From this point onward calling Mage::getConfig() or Mage::app()->getConfig() will
return the same instance.
Note that currently the configuration object still is “empty”. No XML has been loaded yet.
The method loads all XML files in the app/etc/ directory in alphabetical order.
All the files are listed in the following table (unless of course custom XML files where
added there).
Additional custom XML files can be added, but be aware that they are not cached and
will be parsed and merged on each request.
The most interesting part of the loadBase() method above are the lines inside the while
loop.
Each file is loaded using a Mage_Core_Model_Config_Base instance cloned from
$this->_prototype.
The core/config_base class uses the PHP function simplexml_load_string() to parse
the file contents, converting each node into a Mage_Core_Model_Config_Element instance
along the way.
The Mage_Core_Model_Config_Element class is a small wrapper for
Varien_Simplexml_Element, which in turn extends from the native PHP class
SimpleXMLElement.
Then the loaded nodes are merged into the main DOM tree using the extend() method.
The extend() method takes an optional second parameter - $overwrite - which defaults
to true.
This refers to existing node values, which will be overwritten by nodes at the same position
during the merge.
This is the basis of the Magento configuration load process.
New DOM node children are added to the existing XML tree structure during the merging.
RESEARCH 135
Existing DOM node values are overwritten in case a file that is loaded later contains the
same node structure with a different value.
For example, let’s assume the first XML file which is loaded contains the following nodes:
<config>
<modules>
<Meeting02_ExampleXml>
<active>true</active>
<codePool>local</codePool>
</Meeting02_ExampleXml>
</modules>
</config>
Now also assume the second XML file to be loaded looks as follows:
<config>
<modules>
<Meeting02_ExampleXml>
<version>0.1.0</version>
</Meeting02_ExampleXml>
</modules>
</config>
Once the two files are merged, the resulting DOM structure would be this:
<config>
<modules>
<Meeting02_ExampleXml>
<active>true</active>
<codePool>local</codePool>
<version>0.1.0</version>
</Meeting02_ExampleXml>
</modules>
</config>
<config>
<modules>
<Meeting02_ExampleXml>
<active>false</active>
</Meeting02_ExampleXml>
</modules>
</config>
<config>
<modules>
<Meeting02_ExampleXml>
<active>false</active>
<codePool>local</codePool>
<version>0.1.0</version>
</Meeting02_ExampleXml>
</modules>
</config>
However, the order of the following next steps regarding the loading of the configuration
are the same:
# Method Comment
Every module that is known to Magento should be registered in an XML file in the
app/etc/modules directory.
The method Mage_Core_Model_Config::loadModules() delegates to
$this->_loadDeclaredModules() in order to process those registry files.
That method does so by
if ($name == 'Mage_All') {
$collectModuleFiles['base'][] = $v;
} else if (substr($name, 0, 5) == 'Mage_') {
$collectModuleFiles['mage'][] = $v;
} else {
$collectModuleFiles['custom'][] = $v;
}
}
The registration files should not contain any other information, even if it seems related
beacuse of a similar XML path.
For example, leave out a module’s <version> - that belongs into the modules etc/config.xml
file.
Here is an example of a module registry file:
<?xml version="1.0"?>
<config>
<modules>
<Meeting02_RewriteOrder>
<active>true</active>
<codePool>local</codePool>
<depends>
<Mage_Core/>
<Mage_Sales/>
</depends>
</Meeting02_RewriteOrder>
</modules>
</config>
When the registry files are loaded, the contents are first merged into a separate DOM
structure.
The module list will be added to the main configuration DOM soon, but first
_sortModuleDepends() is used to create a sorted list based on module dependencies.
If a dependency can’t be satisfied because a depended on module is missing or inactive,
Magento chickens out with an exception.4
After the list of modules is sorted, it is used to append each module to the main XML
DOM structure in the correct order:
$sortedConfig->getNode('modules')->appendChild($node);
4 Yes, circular module dependencies are caught and Magento throws the unsatisfied dependency exception,
too.
140 CHAPTER 8. DISPATCH PROCESS DIAGRAM
The loading of the modules’ config.xml files is often referred to as module initialization.
It means that a module’s configuration is merged into the main config DOM.
Now that Magento knows which modules to initialize, and the order in which to do it, the
core/config method loadModulesConfiguration() is called.
This method can be used to load an arbitrary XML file from every module’s etc/ directory.
It’s not only used for the etc/config.xml files, but also for every other file in that directory,
for example adminhtml.xml or system.xml.
It can also be used to load and merge custom XML files.
For example, the extension Firegento_GridControl5 uses the method
loadModulesConfiguration() to load a custom configuration file called gridcontrol.xml
from every module.
It is commonly believed by many module developers that extensions are loaded in alpha-
betical order. However, this is not true.
Only extension registry files are loaded in alphabetical order. The more important
module config.xml files load order is determined by module dependencies.
The config.xml files of modules depended upon are merged before the depending module is
loaded.
So modules are not really loaded in alphabetical order. If they are, it’s just a coincidence.
Remember, the reason it is important to know the module load order is because through
the merge process a module can overwrite configuration values from any module that was
loaded earlier.
Magento Trivia
The following is a feature that isn’t used by the core, and so far I haven’t encountered it
being used in any third party project either.
Configuration XML nodes can be forced to extend another existing node besides the parent
by specifying an extends="the/x/path" argument.
5 https://github.com/magento-hackathon/GridControl
RESEARCH 141
This has no benefit over using normal nesting in XML, except maybe avoiding code
duplication.
For further information have a look at the method
Varien_Simplexml_Config::applyExtends().
It is called from Mage_Core_Model_Config::loadModules().
After all modules are loaded, the contents of the local.xml file are merged into the configu-
ration DOM a second time.
This happens so that no module is able to overwrite values declared in local.xml.
Another way to say it is that the contents of local.xml have a higher priority than any
module configuration file.
At this point during the Magento initialization all XML files have been merged into the
config DOM structure.
The things missing are the system configuration settings which are stored in the
core_config_data table.
To add these settings to the other configuration, the core/config model’s loadDb()
method delegates to the resource model’s
$this->getResource()->loadToXml($this) method.
This method has 120 lines of code, but fortunately it is easy to read.
Let’s have a look at the table structure to provide a little context for the next section.
The loadToXml() method loads all records from the table and then does some multi-pass
processing to merge the values into the configuration DOM.
The steps it takes are as follows.
First it copies all default scope records to the default configuration branch.
Let’s have a look at an example.
The following record
default 0 contacts/contacts/enabled 1
<config>
<default>
<contacts>
<contacts>
<enabled>1</enabled>
</contacts>
</contacts>
</default>
</config>
All settings from the default node of the main configuration DOM - including the data
from the XML files as well as the default values from the database table - are copied to a
branch under websites.
This happens once for each website in Magento.
To continue the previous example, assuming two websites with the codes base_website
and wholesale_website exist in a shop, the DOM structure is extended in the following
way:
<config>
<websites>
<base_website>
<contacts>
<contacts>
<enabled>1</enabled>
</contacts>
</contacts>
</base_website>
<wholesale_website>
<contacts>
<contacts>
<enabled>1</enabled>
</contacts>
</contacts>
</wholesale_website>
</websites>
</config>
If this is looking like data duplication to you, you are absolutely right.
But this is only the beginning. . .
The next step is that all websites scope records from the core_config_data table are
merged into the config DOM, into each matching website branch.
After this, all the website scope DOM branches contain all settings from the default
scope, which might have been changed or amended by the websites scope values from the
database.
All these values are now copied into each store view scope under the stores branch of the
config DOM, similar to the websites branch.
Every default setting now is present once under <default>, once for each website under
<websites>, and once for each store under <stores>
144 CHAPTER 8. DISPATCH PROCESS DIAGRAM
Finally, all the stores scope values from the core_config_data table are added to each
matching stores branch of the config DOM.
In the end, each store branch contains a full set of configuration values that are valid for
its scope, regardless which scope the setting was specified on.
These are the values that are accessed when getConfig() is called on a store model.
Please refer to the Scoped Configuration Values section of the previous chapter for details.
It’s good to remember that most of the configuration merging is bypassed when the
configuration cache is turned on.
That not only saves Magento from having to read and parse many XML files, but also
Magento will only load the configuration sections that are actually used, which reduces the
memory footprint quite a bit.
The details can always be looked up in the core, but in many situations it is helpful to be
aware of the basic load order of all the parts that make up the configuration XML.
The load order is so important because:
• Parts that are loaded later can overwrite node values from parts that where loaded
earlier
• Observers are processed in module load order
• The processing order of layout XML instructions within one update handle are defined
by module load order
• Module load order can help resolving rewrite conflicts
Remember, the module load order can be influenced through module dependencies.
The load order of the different configuration parts is listed in the following table:
1 app/etc/*.xml No loadBase()
loadModules() delegates to
2 app/etc/modules/Mage_All.xml Yes _loadDeclaredModules()
RESEARCH 145
So far this book covered the following aspects of the dispatch process:
One important part of the dispatch process that hasn’t been covered yet is the view layer
(the V in MVC).
The details of the rendering process will be discussed in detail in the next book of this
series: Rendering and Widgets.
Let’s have a high level look at the main PHP classes used, so the diagram for this exercise
can be completed: the layout and the layout update models.
The Magento view layer usually is initialized when $this->loadLayout() is called from
within the context of an action controller.
// ...
$this->getResponse()->sendResponse();
Now that the most important parts of the Magento dispatch process have been discussed,
it’s time to move on to the solution.
Solution
The diagram lists the most important classes during the dispatch process:
SOLUTION 147
• Mage
• Mage_Core_Model_App
• Mage_Core_Model_Config
• Mage_Core_Model_Resource_Setup
• Varien_Db_Adapter_Pdo_Mysql
• Mage_Core_Model_Website
• Mage_Core_Model_Store
• Mage_Core_Controller_Request_Http
• Mage_Core_Controller_Varien_Front
• Mage_Core_Controller_Varien_Router_Standard
(actually, all the routers)
• Mage_Core_Controller_Varien_Action
(actually, the concrete action controller handling the request)
• Mage_Core_Model_Layout
• Mage_Core_Model_Layout_Update
• Mage_Core_Controller_Response_Http
Of course there are many more classes involved while a request is being processed.
Some of them might arguably belong in the sequence displayed in the diagram (for example
the session initialization).
However, in some cases the place where a class is used for the first time might vary
depending on which page is requested, which modules are installed, or how Magento is
initialized.
A senior Magento developer should know the classes listed above and the role they play
during the dispatch process.
The ability to create a process diagram like this helps to identify possible targets for
customizations and also during debugging.
Chapter 9
The original task description from the study group kit for this exercise is as follows.
Magento can use several ways to specify the current store view for a given
request. List the priority of all the different ways.
Overview
This chapter discusses the following topics in the research section and the examination of
the exercise solution:
Scenario
Whenever you need to set up a Magento instance with localized domains for more than
one website or store view, it is helpful to know how the store view selection process works.
149
150 CHAPTER 9. STORE VIEW SELECTION PRIORITIES
Research
To analyze the store view selection process, we need to start right at the beginning, right
before Mage::run() is called in the index.php file:
Mage::run($mageRunCode, $mageRunType);
self::$_app->run(array(
'scope_code' => $code,
'scope_type' => $type,
'options' => $options,
));
As you will have noticed, the code sample above is very abbreviated and only contains the
code relevant to the current exercise.
The arguments passed to Mage::run() are passed on to Mage_Core_Model_App::run()
as an array.
There, after the configuration is loaded, the arguments are extracted again and passed to
_initCurrentStore() like this, as can be seen in the following code block:
$this->_initModules();
if ($this->_config->isLocalConfigLoaded()) {
$scopeCode = isset($params['scope_code']) ? $params['scope_code'] : '';
$scopeType = isset($params['scope_type']) ? $params['scope_type'] : 'store';
$this->_initCurrentStore($scopeCode, $scopeType);
$this->getFrontController()->dispatch();
}
}
Judging by the method name _initCurrentStore() it looks like we are finally getting to
the code responsible for the store view selection.
The same is done for the default website, only in this case it is a property of the
core/website model that specifies which one is the default website.
if ($website->getIsDefault()) {
$this->_website = $website;
}
After the _initStores() method completes, any store or website can be fetched from the
preloaded lists using Mage::app()->getStore($code) or Mage::app()->getWebsite($code).
At this time the default store and website are set, but the current store - that is, the
context for the current request - has not been determined yet.
Let’s continue with the _initCurrentStore() method after $this->_initStores() was
called:
The gist of it is that the current store is set according to the specified $scopeCode and
$scopeType.
However, in an unmodified Magento installation, the $scopeCode will be set to an empty
string, in which case the scope type and code are set to the default website (see the if
condition).
Because of that the switch condition case 'website': will match and the current store
will be set to the default website’s default store.
At this point, if the scope code was empty, the current store is set to the default value.
If the scope code was set however, the specified store will be set as the current one for the
request.
But the _initCurrentStore() method isn’t complete yet:
if (!empty($this->_currentStore)) {
$this->_checkCookieStore($scopeType);
$this->_checkGetStore($scopeType);
}
}
The two methods _checkCookieStore() and _checkGetStore() take care of setting the
current store to something else then the default, if the current request says so.
First _checkCookieStore() checks for a cookie with the name store.
If it is set to a valid store code, $this->_currentStore is set to that value.
After that _checkGetStore() checks if $_GET['___store'] is set to a valid store code.
If yes, it replaces any previous value in $this->_currentStore.
The ___store query parameter is used by the Magento store switcher to specify the target
store view.
The _checkGetStore() method also contains some additional logic to set the store cookie
that was read earlier in _checkCookieStore():
if ($store->getWebsite()->getDefaultStore()->getId() == $store->getId()) {
$this->getCookie()->delete(Mage_Core_Model_Store::COOKIE_NAME);
} else {
$this->getCookie()->set(
Mage_Core_Model_Store::COOKIE_NAME, $this->_currentStore, true
);
}
154 CHAPTER 9. STORE VIEW SELECTION PRIORITIES
If the current store is the default store of the current website, the store cookie is deleted.1
Otherwise the store cookie is set to the current store’s code.
Most of the time this works as expected.
That is, it works as expected if the default store of the current website is the same that
was specified as the default store for the current request using MAGE_RUN_CODE, or if the
store cookie or query parameter was present.
However, if a different default store view was specified using the MAGE_RUN_CODE environ-
ment variable, then unsetting the store cookie introduces a bug:
To reproduce the issue, a Magento instance with at least two store views is needed.
For example, let’s assume there are two store views with the codes english and french
present.
Further, let’s assume there is only a single website. This website’s default store view is
english.
If Mage::run('', 'store'); is called, and no cookie or query parameter is present, the
english store view will be rendered.
Now, let’s assume the environment variable MAGE_RUN_CODE was set to french.
In this case Magento will be initialized with the values Mage::run('french', 'store');.
The store cookie will be set to the value french.
So far no problem, the french view will be displayed.
But let’s play through what happens if the visitor now switches to the english store view.
The query parameter ___store=english will be appended to the request path by the store
switcher.
The english store will be displayed, despite MAGE_RUN_CODE being set to french, as it
should.
But because the current store now matches the websites default store, the store cookie is
removed.
1 The only reason to unset the store cookie I can think of is that it might make the configuration of a
If the visitor now clicks on any link, the request will no longer contain the
___store=english query parameter, and the value from the MAGE_RUN_CODE envi-
ronment variable will be used again, switching the current store back to french.
One way to fix that behavior is to not compare the current store with the websites’ default
store, but instead compare the current store with the store specified by MAGE_RUN_CODE.
A alternative fix would be to always set the store cookie, regardless if it is the default
store view or not.
The latter fix is easy to implement, for example using an observer for the
controller_front_init_before event. The former fix however requires a change to the
Mage_Core_Model_App core code.
The following store view selection priorities have become apparent from the code discussed
so far in this chapter:
1. If the ___store query parameter is set, it will override any other method of setting
the current store.
2. Otherwise, if present, the store cookie value is used.
3. If neither query parameter nor cookie are present, the value of the MAGE_RUN_CODE
environment variable is used to specify the current store.
4. The default store view of the default website is used.
There is one system configuration option which also influences the current store view
selection.
It can be found under System > Configuration > Web > Url Options > Add Store Code to
Urls.
The XML config path for that setting is web/url/use_store.
If set to Yes, all Links will be rendered with the store code directly after the base URL.
156 CHAPTER 9. STORE VIEW SELECTION PRIORITIES
For example, assuming the current store code is enand the base URL is http://mage.example.com/,
then the login page route would be
http://mage.example.com/en/customer/account/login.
By changing the store code in the request path, the current store views is changed.
Since we haven’t encountered this setting while going through the store view selection
process within the core/app code, the question arises where this setting is applied.
The answer can be found during the initialization of the request object
Mage_Core_Controller_Request_Http.
The request object is initialized in Mage_Core_Model_App::run().
Right after the code we have dissected so far, after the stores array and the current store
are set, $this->_initRequest() is called.
The only thing this method does is to call $this->getRequest()->setPathInfo() on the
request object.
The purpose of the setPathInfo() method is to analyze the current request and extract
the request path, so it is available during the routing process.
In that method we can find the following code:
if ($this->_canBeStoreCodeInUrl()) {
$pathParts = explode('/', ltrim($pathInfo, '/'), 2);
$storeCode = $pathParts[0];
if (!$this->isDirectAccessFrontendName($storeCode)) {
$stores = Mage::app()->getStores(true, true);
if ($storeCode!=='' && isset($stores[$storeCode])) {
Mage::app()->setCurrentStore($storeCode);
$pathInfo = '/'.(isset($pathParts[1]) ? $pathParts[1] : '');
}
elseif ($storeCode !== '') {
$this->setActionName('noRoute');
}
}
}
Mage::run($mageRunCode, $mageRunType);
As the comments in the code tell us, the run code specifies a store or website code, and the
run type has to be set to either website or store, depending on what was used as the run
code.
Since environment variables are used to specify the values, no Magento files need to be
modified - not even the index.php file.
2 The global/request/direct_front_name node lists special front names which are always accessible
The idea is that the variables are set on a webserver level, ideally within the virtual host
configuration.
Since most Magento instances are served on Apache, what follows is an example of how
the environment variables may be set:
Depending on the requested host name, the environment variable is set to a different value.
For further information on the SetEnv and related configuration directives, please refer to
the apache documentation3 .
Still a number of tutorials can be found with instructions to set up a multi-website Magento
instance using subdirectories and symlinks.
However, just setting the environment variables is a much easier way to configure a
multi-domain setup.4
The specified domain names also need to be configured in the system configuration within
the matching store or website scope under System > Configuration > Web > Unsecure >
Base URL and Web > Secure > Base URL.
The base URL config value is used to render the correct domain name for links on the
website.
Of course the MAGE_RUN_CODE and MAGE_RUN_TYPE variables can be set based on different
criteria then the requested domain, too.
A popular choice is to use a geo-ip lookup service to determine the country a visitor is
physically located in, and then display the appropriate website.
Another often-used criterion is to read the browser’s Accept-Language HTTP request
header to find the best matching store view.
But since the environment variables have to be set before Magento is initialized, this
provides us with a little challenge.
3 http://httpd.apache.org/docs/2.2/mod/mod_setenvif.html#SetEnvIfNoCase
4 The option to specify the default store view using environment variables was introduced in Magento
1.4.
RESEARCH 159
If they can be set on the webserver level - like when matching the requested domain name
- it’s no problem.
However, conditions like checking more complex HTTP headers or geo-ip lookups are more
difficult to set up, since they require additional logic.
The quick and dirty solution would be to introduce a core code hack by adding the required
PHP to the index.php file before Mage::run() is called.
Changing core code however always impacts Magento when it comes time to upgrade.
For that reason, a more elegant solution is to either implement the logic as a custom
Apache module, or to use the PHP configuration setting auto_prepend_file to always
include the code before the index.php file is executed.
Magento Trivia
When switching between store views using the built-in Magento store switcher, the ___store
query parameter is used to specify the new store view to display.
If the store-code-in-url feature is enabled, the ___store query argument is not appended,
since the store code already is part of the request path.
Under both circumstances however, Magento also adds the additional query parameter
___from_store with the value of the previous store code.
Since on first glance it doesn’t seem to serve any purpose, it is a very common customization
to remove it from the store switcher target URLs.
So why is it added then, if it doesn’t do anything?
The ___from_store query parameter is actually is used in the method
Mage_Core_Model_Url_Rewrite::rewrite() to check if there is a record matching the
current request path for the previous store.
If it does find a match, the visitor is redirected to the new store view’s SEO friendly URL
for that page.5
5 To enable store view scope url_key attribute values for products, the Attribute Management page in
the Magento backend can be used to change the scope of the attribute.
For categories it is also possible, however the attribute value scope has to be set using a setup script, since
the admin interface doesn’t allow editing category attribute properties.
More details on EAV attribute scopes and how to change them will be covered in the fourth book of this
series: EAV.
160 CHAPTER 9. STORE VIEW SELECTION PRIORITIES
There is one special case in regards to store views, and that is the admin interface.
6 https://github.com/Vinai/VinaiKopp_StoreUrlRewrites
SOLUTION 161
Mage::dispatchEvent('adminhtml_controller_action_predispatch_start', array());
The Mage_Adminhtml module declares an event observer for this event, more specifically
the method bindStore() of the class adminhtml/observer.
The method is very simple. Its only responsibility is to set the current store view to admin.
To summarize: whenever an Adminhtml page is accessed, first the regular store selection
process takes place.
Then the Admin router matches the request and dispatches the action controller.
The action controller then dispatches the
adminhtml_controller_action_predispatch_start event, and the configured observer
overwrites the previous value for the current store with admin.
Solution
The lower the number in the left column, the higher the priority.
SOLUTION 163
Since there is no code to be discussed as part of the solution, this completes the current
chapter.
164 CHAPTER 9. STORE VIEW SELECTION PRIORITIES
Chapter 10
Addendum
165
166 CHAPTER 10. ADDENDUM
$obj->setBackgroundColor('magenta');
$obj->setData('background_color', 'magenta');
$obj->getBackgroundColor();
$obj->getData('background_color);
$obj->setColor(array(
'background' => 'magenta',
'foreground' => 'lightcyan'
));
Lets look at some ways to retrieve the background color again in this case.
The obvious way would be the regular getter methods, returning the array, which then
needs to be dereferenced using the correct key:3
$obj->getColor()['background'];
$obj->getData('color')['background'];
However, the getData() method also allows to specify child array records directly:
3 The example uses direct array dereferencing available since PHP 5.4. In older PHP versions the
$obj->getColor('background');
$obj->getData('color/background');
If the sub array is nested deeper than one level, the method getData() has to be used
directly.
The magic getter only allows access to the first nesting level.
$obj->setColor(array(
'light' => array(
'background' => 'lightcyan',
'foreground' => 'orange',
),
'dark' => array(
'background' => 'cyan',
'foreground' => 'red',
)
));
$obj->getData('color/light/foreground');
Knowing these steps by heart takes some time, however, it is so worth it.
I have found an astonishing jump in efficiency once I had memorized the XPaths that
Magento uses during class resolution.
If you don’t know them like the back of your hand already, to get started, I suggest you
read through the following list of steps.
Then go through two examples. And then, in future, every time an instantiation or rewrite
doesn’t immediately work as expected, take this list and go through each step in your
mind.
Using a piece of old fashioned paper can be a helpful tool while doing so.
I guarantee you will find the reason for the unexpected result. And what is even better,
after a while you will automatically make no more mistakes like that any more.
Reading the code of other developers and the core also gets a lot easier.
And finally, to give you one more reason, knowing these steps in detail will help you through
the certification exam, too.
In the examples below I use a factory name of example/thing.
This is split by Magento to a “class group” “example and a part-after-the-slash “thing”.
Please refer to the class rewrites section of the Order Rewrite chapter for more details.
All of the following class name resolution steps are handled inside of the class
Mage_Core_Model_Config.
The following steps are used whenever a factory name is passed to one of the following
factory methods (that is the class name is specified using the Magento notation with a /):
• Mage::getModel()
• Mage::getSingleton()
• Mage::helper()
• Mage::app()->getLayout()->createBlock()
The most common mistake is that step 6 fails, that is, Magento is unable to resolve the
class group to a class prefix.
The best method to start debugging instantiation within the core/config instance is
Mage_Core_Model_Config::getGroupedClassName().
The following steps are used when instantiating resource models or collections.
That is, any class instantiated using Mage::getResourceModel() or
Mage::getResourceSingleton() will use the following steps during class resolution (if a
factory name containing a / is used to specify the class).
Note that the purpose of the <deprecatedNode> lookup during step 8 provides backward
compatibility for old modules which rewrite resource models using the old, pre-Magento
1.6 resource class group.
The best method to start debugging resource model instantiation within the core/config
instance is Mage_Core_Model_Config::getResourceModelClassName().