Magento’s Global Variable Design Patterns
One of the earliest things you’ll learn in any programming class is that
Global Variables Are Evil
When a good teacher tells you this, what they’re really saying is
You’re too inexperienced to understand the tradeoffs involved in using global variables, and it’s too tempting for you, as a beginner, to take the easy way out and introduce some global state to solve your problem, which mean you’ll never get experience building encapsulated systems, which is what will give you the ability to determine what is, and isn’t an acceptable tradeoff. So why not stay away from globals for now
When a bad teacher tells your this, what they’re really saying is
My teacher told me so, so I’m telling you so
Once you gain a little experience building applications and systems, you can start to see why globals developed their reputation for evil. Consider the following
1
2
|
global $foo $value = $foo ->getValue();??? |
We get a reference to the global $foo
, and then immediately call a method on it. We’re trusting that other code, code we have no control over, has set foo up to be an object, and an object which has a getValue
method. We have no way of knowing if this is true, and worse there’s no way to infer this is true from the code, short of tracing the entire execution path from start to finish, or setting up a test framework to run our program with multiple different inputs.
In turn, we can now mess with $foo
, either changing its state in an unexpected way
1
|
$foo ->setValue( 'some other value that other code is not expecting ' ); |
or in languages that play fast and loose with types, changing its type
1
|
$foo = 'not an object' ; |
This sort of system design is a recipe for bugs, especially if more than one developer is working on and maintaining code. The big problem with globals is they hide what’s going on with a variable from the programmer, and there are no cues that help that programmer understand what to expect from the global variable. This is bad enough in simple linear programming, but can get really nasty in threaded languages, where even if you run multiple checks on some global variable before operating on it
1
2
3
4
5
|
//fake pseudo code from an alternate universe where PHP has threads if ( is_object ( $foo ) && is_callable ( array ( $foo , 'getValue' ))) { ???? echo $foo ->getValue(); } |
there’s the possibility that another thread is manipulating the global $foo
in unexpected ways.
Global Patterns
The flip side of the global trade off is that sometimes it’s very convenient and very useful to have access to some global state. It’s inevitable that some of the design patterns gang would come up with something that was global variables dressed up in fancy OOP clothes. Two design patterns that Magento uses to manage global-ish variables are the singleton pattern and a simple registry pattern.
Coding forums are filled with arguments as to whether theses are “good” or “bad” (or “evil”) patterns, with most arguments coming back around to “global are evil”. Regardless of your opinions in these coding holy wars, youwill see these patterns out in the wild, so it’s better that you understand how they work so you can decide what level of engagement you want with the pattern, and can understand any peers who have decided to embrace them whole hog.
Magento’s Registry Pattern
With the CS-101 and boilerplate Magento apologies out of the way, let’s take a look at Magento’s registry pattern.
There are three static methods on the Mage
class which implement Magento’s registry. The Mage
class can be thought of as the main Magento application class. In addition to the registry methods, this class also contains many other static helper methods, and also contains the run
method, which is what starts the Magento bootstrapping and controller dispatch process (which is, of course, preceded by a PHP bootstrap in index.php
)
The three registry methods are
1
2
3
|
Mage::register Mage::unregister??? Mage::registry??? |
The register
method is how you set a global-like variable.
1
|
Mage::register( 'some_name' , $var ); |
Then, later in the request execution, (from any method), you can fetch your variable back out
1
|
$my_var = Mage::registry( 'some_name' ); |
Finally, if you want to make you variable unavailable, you can use the unregister
method to remove it from the registry.
1
|
Mage::unregister( 'some_name' ); |
Let’s take a look at each method definition to ‘suss out any special behavior exposed by Magento’s registry that would make this system more than a reimplementation of PHP’s $GLOBALS
registry. First, we’ll take a look at theregister
method
1
2
3
4
5
6
7
8
9
10
11
|
#File: app/Mage.php public static function register( $key , $value , $graceful = false) { ???? if (isset(self:: $_registry [ $key ])) { ???????? if ( $graceful ) { ???????????? return ; ???????? } ???????? self::throwException( 'Mage registry key "' . $key . '" already exists' ); ???? } ???? self:: $_registry [ $key ] = $value ; } |
We can see that any object or value we’re storing in the registry is ultimately being stored in the static$_registry
class variable. We can also see that before storing the value, Magento checks if it’s already set. If so, Magento will either throw an Exception
(the default behavior) or gracefully return null
. So, here Magento’s already provided one small benefit over global variables: You can’t accidentally overwrite someone else’s global without explicitly unregistering it.
Consider the following
1
2
|
global $my_var ; $my_var = 'foo' ; |
Here, if another piece of previously executed code has also defined a $my_var
global, this code would silently overwrite that value, and mostly likely cause problems down the line. With Magento’s registry, that chances of this happening accidentally are removed.
Next up is the unregister
method
1
2
3
4
5
6
7
8
9
10
|
#File: app/Mage.php public static function unregister( $key ) { ???? if (isset(self:: $_registry [ $key ])) { ???????? if ( is_object (self:: $_registry [ $key ]) && (method_exists(self:: $_registry [ $key ], '__destruct' ))) { ???????????? self:: $_registry [ $key ]->__destruct(); ???????? } ???????? unset(self:: $_registry [ $key ]); ???? } } |
This method uses the passed in key to unset the variable stored in the static $_registry
class variable. However, there’s more special functionality implemented here. In addition to unsetting the variable, if that variable is an object and if it has a __destruct
method implemented, the registry will immediately call the__destruct
method, immediately freeing the memory used by the object rather than waiting for the PHP garbage collector to do its job.
This feature is a mixed bag. Sometimes manually freeing memory is useful, but other times it might have unintended consequences. The whole idea of a garbage collected language is to free the programmer from having to deal with these issues. For the most part, you can ignore this feature as it won’t have major impact on your Magento program, but it is good to know it’s happening.
Finally, we have the registry
method, which allows us to fetch a set variable
1
2
3
4
5
6
7
8
|
#File: app/Mage.php public static function registry( $key ) { ???? if (isset(self:: $_registry [ $key ])) { ???????? return self:: $_registry [ $key ]; ???? } ???? return null; } |
Nothing fancy here, just a simple key lookup that returns the variable if it’s set, or null if it’s not. If you wanted to make an intellectual stretch, you could claim this is better than a normal global implementation, as it explicitly defines null
if you attempt to fetch an undefined global, whereas a generic programming language may initialize new globals in an undefined way. However, PHP always defines new globals as null
, so you can see why that’s a stretch.
Core Magento Registry Use
Core Magento code makes heavy use of the registry for controller/block communication. Because Magento eschewed the simple dumb-views found in most PHP MVC systems, that also means it eschewed the simple “set a variable on the view” pattern. While it’s possible to pass information down to the blocks with something like
1
2
3
|
$this ->loadLayout(); $this ->getLayout()->getBlock( 'content' )->setSomeVar( $var ); $this ->renderLayout(); |
knowing which blocks would need which object isn’t always possible, and leads to lots of tedious, and redundant code in controller actions.
Instead, Magento controller actions will contain code that looks something like this
1
|
Mage::register( 'product' , $product ); |
and then a block class will contain something like
1
2
3
4
|
public function getProduct() { ???? return Mage::registry( 'product' ); } |
which allows someone to call something like
1
|
echo $this ->getProduct()->getName(); |
in the block’s template. You’ll also see parent blocks setting registry variables for child blocks to use.
While this works as a method to send information down to the views, it has a few negative side effects. First, it creates some confusion in the MVC structure: Should blocks only read form registry entries, or is it “more correct” to instantiate models and read directly from the system? More troubling, by using registered variables, reusing block code becomes more difficult, as you need to be be aware of what registered variables a block uses, and then set these before instantiating and/or rendering a tree of blocks.
Magento’s Singleton Pattern
Next, lets take a look at the singleton pattern. If you’re not familiar with it already, a singleton is a design pattern that ensures you only ever get one instance of a particular object type. Normally, if you do something like this
1
2
|
$foo = new Some_Object_Type(); $bar = new Some_Object_Type(); |
the variables $foo
and $bar
will be independent of each other. Each will be a separate instance object. However, if Some_Object_Type
has implemented a singleton pattern in its constructor, or has implemented a singleton getInstance
method
1
2
3
4
5
|
$foo = new Some_Object_Type(); $bar = new Some_Object_Type(); $foo = Some_Object_Type::getInstance(); $bar = Some_Object_Type::getInstance(); |
then $foo
and $bar
are actually pointing at the same object.
So far so good. Magento’s interpretation of the singleton pattern is slightly different. In Magento, you can instantiate a model class with the following
1
2
|
$foo = Mage::getModel( 'group/class' ); $bar = Mage::getModel( 'group/class' ); |
Again, as with traditional instantiation, $foo
and $bar
are now different, independent objects. However, if you use the getSingleton
method to instantiate the model
1
2
|
$foo = Mage::getSingleton( 'group/class' ); $bar = Mage::getSingleton( 'group/class' ); |
then $foo
and $bar
are the same object, and $bar
has any state that was set previously set on $foo
. In other words, a singleton. Magento has made the “singleton-ness” off an object independent of it’s constructor implementation. Any model class may be instantiated and used as a singleton, so long as you always instantiate it with the getSingleton
method.
If we take a look at the implementation of getSingleton
1
2
3
4
5
6
7
8
9
|
#File: app/Mage.php public static function getSingleton( $modelClass = '' , array $arguments = array ()) { ???? $registryKey = '_singleton/' . $modelClass ; ???? if (!self::registry( $registryKey )) { ???????? self::register( $registryKey , self::getModel( $modelClass , $arguments )); ???? } ???? return self::registry( $registryKey ); } |
we can see that a key is created out of the class alias
1
|
$registryKey = '_singleton/' . $modelClass ; |
Then, Magento uses the registry to see if there’s a value there for that key (self
refers to the Mage
class).
1
2
3
|
if (!self::registry( $registryKey )) { ???? ... } |
If not, then, an object is instantiated via the normal getModel
method, and stored in the registry.
1
|
self::register( $registryKey , self::getModel( $modelClass , $arguments )); |
Finally, Magento returns the value from the registry, ensuring that future calls to getSingleton will return the already instantiated object
1
|
return self::registry( $registryKey ); |
It’s interesting that Magento chose to use their one global design pattern (registry), to implement another (the singleton). In theory, this means that end-user-programmers could potentially muck about with singletons by diving into the registry with something like
1
|
Mage::unregister( '_singleton/core/layout' ) |
However, while there’s nothing in the system enforcing protection for the singleton, the chances of an end-user-programmer do this unintentionally are slim to none, so the tradeoff is worth it. (or so thought whomever implemented this!)
Wrap Up
So that’s the registry and singleton patterns. Like any global variable patterns, they each have a set of tradeoffs, some more questionable than others. Whether you decide to use them in your own code and extensions, you’ll need to be aware of these patterns as the Magento core make heavy use of them to implement core functionality. Like so much of Magento (and software engineering/development in general), it’s better to concentrate on understanding why something was built the way it was rather than bitching about how it was built. At the end of the day it’s about the product you build or service you provide, and code is just a tool for getting there.
原文链接?Magento’s Global Variable Design Patterns
转载请注明:(●--●) Hello.My Weicot » Magento’s Global Variable Design Patterns