Code Organized in Modules
Magento organizes its
code into individual Modules. In a typical PHP Model-View-Controller
(MVC) application, all the
Controllers will be in one folder, all the Models in another, etc. In Magento,
files are grouped together based on functionality, which are called modules in Magento.
Magento's Code
For example, you'll find Controllers, Models,
Helpers, Blocks, etc. related to Magento's checkout functionality in
app/code/core/Mage/Checkout
You'll find Controllers, Models, Helpers,
Blocks, etc. related to Magento's Google Checkout functionality in
app/code/core/Mage/GoogleCheckout
Your Code
When you want to customize or extend Magento,
rather than editing core files directly, or even placing you new Controllers,
Models, Helpers, Blocks, etc. next to Magento code, you'll create your own
Modules in
app/code/local/Package/Modulename
Package (also often referred to as a Namespace) is a unique name that identifies your company or organization.
The intent is that each member of the world-wide Magento community will use
their own Package name when creating modules in order to avoid colliding with
another user's code.
When you create a new Module, you need to tell
Magento about it. This is done by adding an XML file to the folder:
app/etc/modules
There are two kinds of
files in this folder, the first enables an individual Module, and is named in
the form:Packagename_Modulename.xml
The second is a file
that will enable multiple Modules from a Package/Namespace, and is named in the
form:Packagename_All.xml. Note it is only used by the core team with
the file Mage_All.xml.
It is not recommended to activate several modules in a single file,
as this breaks the modularity of your modules.
Configuration-Based MVC
Magento is a configuration-based MVC system. The alternative to this would a convention-based MVC system.
In a convention-based MVC system, if you
wanted to add, say, a new Controller or maybe a new Model, you'd just create
the file/class, and the system would pick it up automatically.
In a
configuration-based system, like Magento, in addition to adding the new
file/class to the codebase, you often need to explicitly tell the system about
the new class, or new group of classes. In Magento, each Module has a file
named config.xml. This file contains all the relevant
configuration for a Magento Module. At runtime, all these files are loaded into
one large configuration tree.
For example, want to use Models in your custom
Module? You'll need to add some code to config.xml that tells Magento you want
to use Models, as well as what the base class name for all your Models should
be.
<models>
<packagename>
<class>Packagename_Modulename_Model</class>
<packagename>
</models>
<packagename>
<class>Packagename_Modulename_Model</class>
<packagename>
</models>
The same goes for Helpers, Blocks, Routes for
your Controllers, Event Handlers, and more. Almost anytime you want to tap into
the power of the Magento system, you'll need to make some change or addition to
your config file.
In any PHP system, the
main PHP entry point remains a PHP file. Magento is no different, and that file
isindex.php.
However, you never CODE in index.php. In an
MVC system, index.php will contains code/calls to code that does the following:
1.
Examines the URL
2.
Based on some set of
rules, turns this URL into a Controller class and an Action method (called
Routing)
3.
Instantiates the
Controller class and calls the Action method (called dispatching)
This means the practical entry point in Magento (or any MVC-based system) is a method in
a Controller file. Consider the following URL:
http://example.com/catalog/category/view/id/25
Each portion of the path after the server name
is parsed as follows.
Front Name - catalog
The first portion of
the URL is called the front name. This, more or less, tells magento which
Module it can find a Controller in. In the above example, the front name is catalog, which corresponds to the Module located at:
app/code/core/Mage/Catalog
Controller Name - category
The second portion of the URL tells Magento
which Controller it should use. Each Module with Controllers has a special
folder named 'controllers' which contains all the Controllers for a module. In
the above example, the URL portion category is translated into the Controller
file
app/code/core/Mage/Catalog/controllers/CategoryController.php
Which looks like
class Mage_Catalog_CategoryController extends Mage_Core_Controller_Front_Action
{
}
{
}
All Controllers in the Magento cart
application extend from Mage_Core_Controller_Front_Action.
Action Name - view
Third in our URL is the action name. In our
example, this is "view". The word "view" is used to create
the Action Method. So, in our example, "view" would be turned into
"viewAction"
class Mage_Catalog_CategoryController extends Mage_Core_Controller_Front_Action
{
public function viewAction()
{
//main entry point
}
}
{
public function viewAction()
{
//main entry point
}
}
People familiar with the Zend Framework will
recognize the naming convention here.
Paramater/Value - id/25
Any path portions after the action name will
be considered key/value GET request variables. So, in our example, the
"id/25" means there will get a GET variable named "id",
with a value of "25".
As previously mentioned, if you want your
Module to use Controllers, you'll need to configure them. Below is the
configuration chunk that enables Controllers for the Catalog Module
<frontend>
<routers>
<catalog>
<use>standard</use>
<args>
<module>Mage_Catalog</module>
<frontName>catalog</frontName>
</args>
</catalog>
</routers>
</frontend>
<routers>
<catalog>
<use>standard</use>
<args>
<module>Mage_Catalog</module>
<frontName>catalog</frontName>
</args>
</catalog>
</routers>
</frontend>
Don't worry too much
about the specifics right now, but notice the <frontName>catalog</frontName>
This is what links a Module with a URL
frontname. Most Magento core Modules choose a frontname that is the same as
their Module name, but this is not required.
Multiple Routers
The routing described
above is for the Magento cart application (often called the frontend). If
Magento doesn't find a valid Controller/Action for a URL, it tries again, this
time using a second set of Routing rules for the Admin application. If Magento
doesn't find a valid Admin Controller/Action, it uses a special
Controller namedMage_Cms_IndexController.
The CMS Controller checks Magento's content
Management system to see if there's any content that should be loaded. If it
finds some, it loads it, otherwise the user will be presented with a 404 page.
For example, the main magento
"index" page is one that uses the CMS Controller, which can often
throw newcomers for a loop.
Now that we're in our Action method entry
point, we'll want to start instantiating classes that do things. Magento offers
a special way to instantiate Models, Helpers and Blocks using static factory
methods on the global Mage class. For example:
Mage::getModel('catalog/product');
Mage::helper('catalog/product');
Mage::helper('catalog/product');
The string 'catalog/product' is called a
Grouped Class Name. It's also often called a URI. The first portion of any
Grouped Class Name (in this case, catalog), is used to lookup which Module the
class resides in. The second portion ('product' above) is used to determine
which class should be loaded.
So, in both of the
examples above, 'catalog' resolves to the Module app/code/core/Mage/Catalog.
Meaning our class name
will start with Mage_Catalog.
Then, product is added to get the final class
name
Mage::getModel('catalog/product');
Mage_Catalog_Model_Product
Mage::helper('catalog/product');
Mage_Catalog_Helper_Product
Mage_Catalog_Model_Product
Mage::helper('catalog/product');
Mage_Catalog_Helper_Product
These rules are bound
by what's been setup in each Module's config file. When you create your own
custom Module, you'll have your own grouped classnames (also calles
classgroups) to work withMage::getModel('myspecialprefix/modelname');.
You don't have to use Groupd Class Names to instantiate your classes. However,
as we'll learn later, there are certain advantages to doing so.
Magento, like most frameworks these days,
offers an Object Relational Mapping (ORM) system. ORMs get you out of the
business of writing SQL and allow you to manipulate a datastore purely through
PHP code. For example:
$model = Mage::getModel('catalog/product')->load(27);
$price = $model->getPrice();
$price += 5;
$model->setPrice($price)->setSku('SK83293432');
$model->save();
$price = $model->getPrice();
$price += 5;
$model->setPrice($price)->setSku('SK83293432');
$model->save();
In the above example
we're calling the methods "getPrice" and "setPrice" on our
Product. However, the Mage_Catalog_Model_Product class has no methods with
these names. That's because Magento's ORM uses PHP's magic __call method to implement getters and setters.
Calling the method $product->getPrice(); will "get" the Model attribute
"price".
Calling $product->setPrice(); will "set" the Model attribute
"price". All of this assumes the Model class doesn't already have
methods named getPrice or setPrice. If it does, the magic methods will be bypassed.
If you're interested in the implementation of this, checkout the Varien_Object
class, which all Models inherit from.
If you wanted to get
all the available data on a Model, call $product->getData(); to get an array of all the attributes.
You'll also notice
it's possible to chain together several calls to the set method:
$model->setPrice($price)->setSku('SK83293432');
$model->setPrice($price)->setSku('SK83293432');
That's because each set method returns and
instance of the Model. This is a pattern you'll see used in much of the Magento
codebase.
Magento's ORM also contains a way to query for
multiple Objects via a Collections interface. The following would get us a
collection of all products that cost $5.00
$products_collection = Mage::getModel('catalog/product')
->getCollection()
->addAttributeToSelect('*')
->addFieldToFilter('price','5.00');
->getCollection()
->addAttributeToSelect('*')
->addFieldToFilter('price','5.00');
Again, you'll notice Magento's implemented a
chaining interface here. Collections use the PHP Standard Library to implement
Objects that have array like properties.
foreach($products_collection as $product)
{
echo $product->getName();
}
{
echo $product->getName();
}
You may be wondering what the
"addAttributeToSelect" method is for. Magento has two broad types of
Model objects. One is a traditional "One Object, One Table" Active
Record style Model. When you instantiate these Models, all attributes are
automatically selected.
The second type of Model is an Entity
Attribute Value (EAV) Model. EAV Models spread data across several different
tables in the database. This gives the Magento system the flexibility to offer
its flexible product attribute system without having to do a schema change each
time you add an attribute. When creating a collection of EAV objects, Magento
is conservative in the number of columns it will query for, so you can use
addAttributeToSelect to get the columns you want, or addAttributeToSelect('*')
to get all columns.
Magento's Helper
classes contain utility methods that will allow you to perform common tasks on
objects and variables. For example:
$helper = Mage::helper('catalog');
$helper = Mage::helper('catalog');
You'll notice we've
left off the second part of the grouped class name. Each Module has a default
Data Helper class. The following is equivalent to the above:
$helper = Mage::helper('catalog/data');
$helper = Mage::helper('catalog/data');
Most Helpers inherit form
Mage_Core_Helper_Abstract, which gives you several useful methods by default.
$translated_output = $helper->__('Magento is Great'); //gettext style translations
if($helper->isModuleOutputEnabled()): //is output for this module on or off?
if($helper->isModuleOutputEnabled()): //is output for this module on or off?
So, we've seen Controllers, Models, and
Helpers. In a typical PHP MVC system, after we've manipulated our Models we
would
1.
Set some variables for
our view
2.
The system would load
a default "outer" HTML layout>
3.
The system would then
load our view inside that outer layout
However, if you look at a typical Magento
Controller action, you don't see any of this:
/**
* View product gallery action
*/
public function galleryAction()
{
if (!$this->_initProduct()) {
if (isset($_GET['store']) && !$this->getResponse()->isRedirect()) {
$this->_redirect('');
} elseif (!$this->getResponse()->isRedirect()) {
$this->_forward('noRoute');
}
return;
}
$this->loadLayout();
$this->renderLayout();
}
* View product gallery action
*/
public function galleryAction()
{
if (!$this->_initProduct()) {
if (isset($_GET['store']) && !$this->getResponse()->isRedirect()) {
$this->_redirect('');
} elseif (!$this->getResponse()->isRedirect()) {
$this->_forward('noRoute');
}
return;
}
$this->loadLayout();
$this->renderLayout();
}
Instead, the Controller action ends with two
calls
$this->loadLayout();
$this->renderLayout();
$this->renderLayout();
So, the "V" in Magento's MVC already
differs from what you're probably used to, in that you need to explicitly kick
off rendering the layout.
The layout itself also differs. A Magento
Layout is an object that contains a nested/tree collection of "Block"
objects. Each Block object will render a specific bit of HTML. Block objects do
this through a combination of PHP code, and including PHP .phtml template
files.
Blocks objects are meant to interact with the
Magento system to retrieve data from Models, while the phtml template files
will produce the HTML needed for a page.
For example, the page
header Block app/code/core/Mage/Page/Block/Html/Head.php uses the head.phtml file page/html/head.phtml.
Another way of thinking about it is the Block
classes are almost like little mini-controllers, and the .phtml files are the
view.
By default, when you call
$this->loadLayout();
$this->renderLayout();
$this->renderLayout();
Magento will load up a
Layout with a skeleton site structure. There will be Structure Blocks to give
you your html,head, and body, as well as HTML to setup single or multiple columns of Layout.
Additionally, there will be a few Content Blocks for the navigation, default
welcome message, etc.
"Structure" and "Content"
are arbitrary designations in the Layout system. A Block doesn't
programmatically know if it's Structure or Content, but it's useful to think of
a Block as one or the other.
To add Content to this Layout you need to tell
the Magento system something like
"Hey,
Magento, add these additional Blocks under the "content" Block of the
skeleton"
or
"Hey,
Magento, add these additional Blocks under the "left column" Block of
the skeleton"
This can be done programmatically in a
Controller action
public function indexAction()
{
$this->loadLayout();
$block = $this->getLayout()->createBlock('adminhtml/system_account_edit')
$this->getLayout()->getBlock('content')->append($block);
$this->renderLayout();
}
{
$this->loadLayout();
$block = $this->getLayout()->createBlock('adminhtml/system_account_edit')
$this->getLayout()->getBlock('content')->append($block);
$this->renderLayout();
}
but more commonly (at least in the frontend
cart application), is use of the XML Layout system.
The Layout XML files in a theme allow you, on
a per Controller basis, to remove Blocks that would normally be rendered, or
add Blocks to that default skeleton areas. For example, consider this Layout
XML file:
<catalog_category_default>
<reference name="left">
<block type="catalog/navigation" name="catalog.leftnav" after="currency" template="catalog/navigation/left.phtml"/>
</reference>
</catalog_category_default>
<reference name="left">
<block type="catalog/navigation" name="catalog.leftnav" after="currency" template="catalog/navigation/left.phtml"/>
</reference>
</catalog_category_default>
It's saying in the catalog Module, in the
category Controller, and the default Action, insert the the catalog/navigation
Block into the "left" structure Block, using the
catalog/navigation/left.phtml template.
One last important
thing about Blocks. You'll often see code in templates that looks like this:
$this->getChildHtml('order_items')
$this->getChildHtml('order_items')
This is how a Block
renders a nested Block. However, a Block can only render a child Block if the
child Block is included as a
nested Block in the Layout XML file. In the example above our catalog/navigation Block has no
nested Blocks. This means any call to $this->getChildHtml() in left.phtml
will render as blank.
If, however, we had something like:
<catalog_category_default>
<reference name="left">
<block type="catalog/navigation" name="catalog.leftnav" after="currency" template="catalog/navigation/left.phtml">
<block type="core/template" name="foobar" template="foo/baz/bar.phtml"/>
</block>
</reference>
</catalog_category_default>
<reference name="left">
<block type="catalog/navigation" name="catalog.leftnav" after="currency" template="catalog/navigation/left.phtml">
<block type="core/template" name="foobar" template="foo/baz/bar.phtml"/>
</block>
</reference>
</catalog_category_default>
Observers
Like any good object-oriented system, Magento
implements an Event/Observer pattern for end users to hook into. As certain
actions happen during a Page request (a Model is saved, a user logs in, etc.),
Magento will issue an event signal.
When creating your own Modules, you can
"listen" for these events. Say you wanted to get an email every time
a certain customer logged into the store. You could listen for the
"customer_login" event (setup in config.xml)
<events>
<customer_login>
<observers>
<unique_name>
<type>singleton</type>
<class>mymodule/observer</class>
<method>iSpyWithMyLittleEye</method>
</unique_name>
</observers>
</customer_login>
</events>
<customer_login>
<observers>
<unique_name>
<type>singleton</type>
<class>mymodule/observer</class>
<method>iSpyWithMyLittleEye</method>
</unique_name>
</observers>
</customer_login>
</events>
and then write some code that would run
whenever a user logged in:
class Packagename_Mymodule_Model_Observer
{
public function iSpyWithMyLittleEye($observer)
{
$data = $observer->getData();
//code to check observer data for out user,
//and take some action goes here
}
}
{
public function iSpyWithMyLittleEye($observer)
{
$data = $observer->getData();
//code to check observer data for out user,
//and take some action goes here
}
}
Finally, the Magento System offers you the
ability to replace Model, Helper and Block classes from the core modules with
your own. This is a feature that's similar to "Duck Typing" or "Monkey
Patching" in a language like Ruby or Python.
Here's an example to
help you understand. The Model class for a product is Mage_Catalog_Model_Product.
Whenever the following code is called, a
Mage_Catalog_Model_Product object is created
$product = Mage::getModel('catalog/product');
This is a factory pattern.
What Magento's class override system does is
allow you to tell the system
"Hey,
whenever anyone asks for a catalog/product, instead of giving them a
Mage_Catalog_Model_Product,
give
them a Packagename_Modulename_Model_Foobazproduct instead".
Then, if you want, your
Packagename_Modulename_Model_Foobazproduct class can extend the original
product class
class Packagename_Modulename_Model_Foobazproduct extends Mage_Catalog_Model_Product
{
}
{
}
Which will allow you to change the behavior of
any method on the class, but keep the functionality of the existing methods.
class Packagename_Modulename_Model_Foobazproduct extends Mage_Catalog_Model_Product
{
public function validate()
{
//add custom validation functionality here
return $this;
}
}
{
public function validate()
{
//add custom validation functionality here
return $this;
}
}
As you might expect, this overriding (or
rewriting) is done in the config.xml file.
<models>
<!-- does the override for catalog/product-->
<catalog>
<rewrite>
<product>Packagename_Modulename_Model_Foobazproduct</product>
</rewrite>
</catalog>
</models>
<!-- does the override for catalog/product-->
<catalog>
<rewrite>
<product>Packagename_Modulename_Model_Foobazproduct</product>
</rewrite>
</catalog>
</models>
One thing that's
important to note here. Individual classes in your Module are overriding
individual classes in otherModules. You are not, however, overriding the
entire Module. This allows you to change specific method behavior without
having to worry what the rest of the Module is doing.
No comments:
Post a Comment