PHP 5 CMS Framework Development / 2nd Edition
上QQ阅读APP看书,第一时间看更新

Framework solution

By now, I hope that you are persuaded by architectural security and practical coding considerations that development is best done by the creation of as many classes as are needed to solve the problem, with each usually in its own file. Fortunately, PHP 5 is clearly designed to support this scenario. How does it do it?

Autoloading

In version 5.1.2, PHP provides an improved version of what we need: the spl_autoload_register function. We are going to build our class management logic into a class called smartClassMapper. It will have a subclass called smartAdminClassMapper, which also knows about the classes used exclusively on the administrator side of our CMS, but is not described in any detail here. Our call to set up autoloading for the CMS is made very early in the processing of each request and consists of:

spl_autoload_register(array('smartClassMapper', 'autoloadClass'));

The PHP function expects a callback as the parameter, and we supply an array to indicate that a class and a static method are being provided. We will see the reason for it being a static method when we look at the code of the class.

The smart class mapper

To complete the autoloading mechanism the full mapper class has to be constructed. First, consider its structure by looking at the code with most data removed, several minor methods left out, and the longer methods' code removed:

class smartClassMapper extends cachedSingleton
{
protected static $instance = CLASS ;
protected $dynamap = array(); protected $debug_log = array(); protected $populating = false;
protected $classlist = array();
protected $subclasses = array();
protected $classmap = array ('aliroAbstractDatabase' =>
'aliroDatabase','aliroDatabaseHandler' => 'aliroDatabase',);
protected $extmap = array('PclZip' => 'pclzip.lib',
'Archive_Tar' => 'Tar');
protected function construct ()
{
// Enforce singleton by being protected
$this->classlist = $this->listDir(_ALIRO_CLASS_BASE.'/classes');
}
public static function getInstance () {
if (!is_object(self::$instance)) {
self::$instance = parent::getCachedSingleton(self::$instance);
self::$instance->reset();
}
self::$instance->checkDynamic();
return self::$instance;
}
public function print ()
{
return sprintf(T_('SmartClassMapper, %s dynamic items, %
logs'), count($this->dynamap), count($this->debug_log));
}
protected function populateMap ()
{
}
public function getClassPath ($classname)
{
}
protected function saveMap ($path, $map)
{
}
public function requireClass ($classname) {
$path = $this->getClassPath($classname);
if ($path AND file_exists($path)) {
require_once($path);
}
else {
$message = sprintf('Class %s not found, trying with path = %s', $classname, $path);
trigger_error($message);
}
}
public static function autoloadClass ($classname) {
$mapper = _ALIRO_IS_ADMIN ? call_user_func(array('smartAdminClassMapper', 'getInstance')) : call_user_func(array('smartClassMapper', 'getInstance'));
$mapper->requireClass($classname);
}
}

Note that the class mapper inherits from the cachedSingleton, which is an abstract class described in detail in the chapter on caches. The inheritance makes it easy to build a singleton class that will only be constructed infrequently so as to reduce the overhead of organizing data, and cut down on database access. Most times, the single instance of the class is retrieved from cache by methods in cachedSingleton and related classes.

Any number of variations is possible, but the scheme currently adopted for Aliro is that commonly used framework classes for the user (as opposed to administration) side of Aliro are stored in files that have the same name as the class apart from the addition of the extension php. They are all held in a classes directory. There is no stored data in the mapper for these classes, but they are found using the listdir method. Classes belonging to extensions that have been added to the CMS are described in a database table, which is updated by the installer. Other class information comes from the arrays that are properties of the mapper and filled with data in the code of the class:

  • dynamap: Is filled with static information about the classes that make up extensions. The universal installer extracts information about classes from the source files of an extension during the installation process, and stores it in the classmap table of the database. The dynamap table is filled up from the database by the populateMap and saveMap methods discussed in detail later.
  • classmap: Is set with information as part of the development of Aliro, and refers to the classes stored in the classes directory mentioned previously. Where a file contains more than one class, any class that does not share its name with the containing file must have an entry in classmap. The array key is the name of the class, and the array value is the name of the file without the php extension.
  • extmap: Refers to classes held in the extclasses directory of an Aliro system. This directory is populated with classes taken from other open source projects that have yielded valuable capabilities for Aliro. To keep the situation clear, the mapping of these external classes is kept separate from Aliro's own classes. Array keys in extmap are again class names, and the values are filenames without the php extension. Where necessary, the file name can be preceded by one or more directory names, relative to the extclasses directory.

Some PHP code goes to great pains to handle paths with backslashes for Windows, and slashes for Linux. This is unnecessary. It is tidier to be consistent, and far easier to handle ordinary (forward) slashes. PHP sorts out the operating system dependencies.

Other properties of the Smart Class Mapper are the static instance variable that is used to hold the one and only object instance of the singleton class, and the debug_log array that contains an entry for every class that is loaded. As its name implies it is available for debug, or performance tuning.

The short methods that are shown in full in the last code are as follows:

  • construct: Is invoked automatically when the singleton instance is created. It does nothing but declaring it as protected prevents the class from being created using new rather than through the static getInstance method.
  • getInstance: Is the only way to obtain a smartClassMapper instance. It checks whether an instance already exists in the class variable of that name. Otherwise, it enlists the aid of its parent class, the cachedSingleton, to try to obtain data from cache, or to create a brand new instance.
  • print: Is provided to aid diagnostics, so that in any situation where variables are being printed out, the class mapper object is capable of producing meaningful output.
  • requireClass: Is passed a class name and either loads the requested class, or issues an error message. This method is used within the smartClassMapper and is also available for use elsewhere.
  • autoloadClass: Is the static method that was passed to spl_autoload_register to be triggered when a previously unknown class is encountered.

With the code in front of us, we can now see how the autoload method works. It is static because until we know whether we are working with the user side or the administrator side, we cannot tell which class to use. The symbol _ALIRO_IS_ADMIN has been set at the very start of request processing to be true or false as appropriate. Now, it determines which class will be used for handling the loading of classes.

What happens as a result of the autoload mechanism is that whenever PHP encounters a reference to a class that is unknown, it calls the autoloadClass method. (On the few occasions this needs to be bypassed, a possible solution is to use the PHP5 function class_exists with the optional second parameter set to false to prevent autoloading; another way is to call this class's classExists method.)

One parameter is passed to autoloadClass, which is the name of the class. The real work is done by the requireClass method, which figures out where to find the requested class and then loads it using the PHP require function. The way it finds out the whereabouts of the class is explained in the next section as other methods are introduced. Assuming the Smart Class Mapper has done its work well, PHP will now know all about the class whose name was passed in as a parameter to the autoloadClass method. If the path for the class is not found, or turns out not to exist, then an error is triggered.

Finding a path to the class

Now we can return to the mechanisms of the Smart Class Mapper. The real hard work is done in the getClassPath method. The Aliro code is:

protected function getClassPath ($classname) {
aliroDebug::getInstance()->setDebugData (sprintf('About to load %s, current used memory %s', $classname, (is_callable('memory_get_usage') ? memory_get_usage() : $this->T_('not known')).$this->timeSoFar()));
$base = _ALIRO_CLASS_BASE.'/';
if (isset($this->dynamap[$classname])) return $base.$this->dynamap[$classname].'.php';
if (isset($this->classmap[$classname])) return $base.'classes/'.$this->classmap[$classname].'.php';
if (isset($this->extmap[$classname])) return $base.'extclasses/'.$this->extmap[$classname].'.php';
//if (file_exists($base.'classes/'.$classname.'.php')) return $base.'classes/'.$classname.'.php';
if (in_array($classname.'.php', $this->classlist)) return $base.'classes/'.$classname.'.php';
if (in_array($classname.'.php', $this->oemlist)) return $base.'oemclasses/'.$classname.'.php';
return '';
}

The first line simply records the request to find the location of a class for diagnostic or tuning purposes. The base path is given by a symbol set at the start of each request, although here we need it to be followed by a slash. Next, we try to see if we can go directly to the answer by looking in the array properties that were described previously. If possible, a value is returned immediately. If all of these fail, we return a null string for the path.

Populating the dynamic class map

During the setup of the class, the dynamic class mapping array needs to be populated. You will recall that it contains information about the classes that belong to extensions added to the core system. The work to be done is a simple database read, after which each entry is processed according to its type, as shown in the following code snippet:

protected function populateMap () {
$maps = aliroCoreDatabase::getInstance()->doSQLget($this->classSQL);
foreach ($maps as $map) {
if ($map->extends) $this->subclasses[$map->extends][] = trim($map->classname);
switch ($map->type) {
case 'component':
case 'application':
$path = 'components/'.$map->formalname.'/';
break;
case 'module':
$path = 'modules/'.$map->formalname.'/';
break;
case 'mambot':
$path = 'mambots/'.$map->formalname.'/';
break;
case 'template':
$path = 'templates/'.$map->formalname.'/';
break;
default: continue;
}
$this->saveMap(('admin' == $map->side ? $this->admindir.$path : $path), $map);
}
unset($maps);
}

More details are given about database operations in a later chapter. The SQL query is obtained from the classSQL property, which is set differently depending on whether we are dealing with a request from the administration login, or from the regular user interface. The query retrieves all mappings that should be known in the circumstances. The extends field from the database table, if set, tells us that a class is a subclass of the class named by the extends field. This information is stored so that the class can answer questions about what subclasses exist for a particular class.

As the different kinds of extensions have their files stored in different directories, the paths are different. Once the alternatives have been handled, the common processing for saving an entry is done in the saveMap method. Within this call, the directory may be modified if a class is exclusive to administrators, using the admindir property, which has a non-null value only if we are handling an administrator request.

Note that populateMap should be called relatively infrequently as the Smart Class Mapper is a cached singleton. This means that for most requests the singleton object is read from the cache, and is likely to have the dynamic mappings already populated as a result of some earlier request. Occasionally, the cache will expire or be cleared, and only in those cases is the singleton object created afresh.

Saving map elements

Each element of the dynamic class map is put into the class mapper's dynamap array by this fairly simple method:

protected function saveMap ($path, $map) {
$path .= $map->filename;
$map->classname = trim($map->classname);
if (false !== strpos($map->classname, '..')) {
var_dump($map);
die($this->T_('Class mapping includes illegal “..”.'));
}
if (!isset($this->dynamap[$map->classname])) $this->dynamap[$map->classname] = $path;
else trigger_error (sprintf('Class %s defined in %s but already defined in %s',$map->classname, $path, $this->dynamap[$map->classname]));
}

The path parameter provides the directory in which the class file is to be found, and the name of the file has to be added to it. If the map array does not already have an entry for this class, one is added. Otherwise an error is triggered, since having multiple classes with the same name is a very unhealthy situation.

Obtaining class information

Although not part of the smartClassLoader, it is worth mentioning how the class information that relates to extensions is obtained. Within the aliroExtensionInstaller class that is at the heart of installing either a new extension or an upgrade to an existing one is this method:

protected function handleClassFile ($extension, $side, $filename, $path) {
$tokens = token_get_all(file_get_contents($path.'/'.$filename));
if (!empty($tokens)) foreach ($tokens as $key=>$token) {
if (T_CLASS == $token[0]) {
$classname = isset($tokens[$key+2][1]) ? $tokens[$key+2][1] : '';
$extends = (isset($tokens[$key+4][0]) AND T_EXTENDS == $tokens[$key+4][0] AND isset($tokens[$key+6][1])) ? $tokens[$key+6][1] : '';
$classes[] = array('classname' => $classname, 'extends' => $extends);
}
}
if (!empty($classes)) {
$filedir = dirname($filename);
$filemap = ('.' == $filedir ? '' : $filedir.'/').basename($filename, '.php');
foreach ($classes as $class) {
smartClassMapper::insertClass($extension->type, $extension->formalname, $side, $filemap, $class['classname'], $class['extends']);
}
}
unset($tokens);
}

It is passed each file that is expected to contain PHP code, as well as an object that represents the extension and an indicator of whether the code is for the exclusive use of administrators or not.

The method relies on the powerful PHP function token_get_all to parse the PHP code and turn it into an array of tokens. The array is searched for instances of the class keyword, with the name of the class being the next but one token. If the class is a subclass, then the keyword extends will follow two items later, and the name of the parent class another two items later (the intervening tokens will be white space).

Once all the tokens have been analyzed, the class information is processed. Each class is used to insert a record in the class mapping database table, using a method provided by the smartClassMapper class. Clearly the database operation could have been coded directly, but it is preferable to put it into the smartClassMapper so that all operations on the table are within the one class. That way, any changes in the implementation are kept within the one class.