Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Upgrading projects to PHP namespaces: An easy solution

5.00/5 (1 vote)
26 Aug 2013CPOL3 min read 19.2K  
How to add optional PHP namespaces to your project

Introduction

In this article, we'll explain why and how we implement PHP namespaces in our web application projects. As nearly all of them are based on it, the implementation is done in the Scavix Web Development Framework, so you will find the complete code over there. If you are not familiar with it you may want to read our other articles first:

Audience

Experienced software developers with solid PHP knowledge.

The problem

You probably experienced it yourself: You're including a third party library into a project and it uses some classnames you're already using somewhere in your project (common names like 'Logger' or 'Model').
This is bad if you are not able to change your classnames because of...

  • ... backwards compatibility.
  • ... external class references in a database.
  • ...

It turns even worse if you can't change the third party library because it's i.e. closed source (or for whatever reason). In such a case you wouldn't be able to use this library then.

The solution

The short version: Use namespaces in your code. In more detail: If you encapsulate your code into namespaces you may use any other library that doesn't use the same namespace. Of course such a conflict is much more unlikely and if you choose your namespace wisely (like CompanyName\ProductName) it will never occur.

The problems with the solution

As always, there are some obstacles to take before you have clean namespacing. There are none if you start from zero with a new project, but remember: if you had a conflict with a third party library so you're working on an existing project. If you add namespaces to your files, you need to do it for the whole project. It will crash until you are done with it. That is of course not nice and you will probably forget some places so the namespacing redesign will take you quite a while. But it's worth it, really.

Solving problems with the solution

In short: Be backwards compatible. Of course it's not that simple. Oh wait: In fact in this case it is! Oh wait again: it's only if you are using autoloading, but as we're sure you do we will just continue :)

The basic procedure is to create a class alias if the requested class was plain but the found class has a qualified name. Sounds cool, eh? Let's see in this example:

PHP
//file: myclass.php
namespace Scavix\SomeCoolProject;
class MyClass { /* some magic happens here */ }
This is the standard autoloader mechanism for classnames:
PHP
// file: autoloader.php
function system_spl_autoload($class_name)
{
    // use some function to find the file that declares the class requested
    $file = __search_file_for_class($class_name);
    require_once($file);
}
spl_autoload_register("system_spl_autoload",true,true);

References: __search_file_for_class, spl_autoload_register.

PHP
// file: index.php
require_once("autoloader.php");
$mc = new MyClass(); // Throws an exception: Class not found
$mc = new Scavix\SomeCoolProject\MyClass(); // OK

Of course the 'class not found' exception is expected in this example. So to make that code work we will now extend the autoloader to implement that rule from above automatically: Create a class alias if the requested class was plain but the found class has a qualified name.

PHP
// file: autoloader_ex.php
function system_spl_autoload($class_name)
{
    if( strpos($class_name, '\\') !== false )
    {
        $orig = $class_name;
        $class_name = array_pop(explode('\\',$class_name));
    }
    // use some function to find the file that declares the class requested
    $file = __search_file_for_class($class_name);
    
    // remember the defined classes, include the $file and detect newly declared classes
    $pre = get_declared_classes();
    require_once($file);
    $post = array_unique(array_diff(get_declared_classes(), $pre));
    
    // loop through the new class definitions and create weak aliases if they are given with qualified names
    foreach( $post as $cd )
    {
        $d = explode('\\',$cd);
        if( count($d) > 1 )
        {
            // Aliasing full qualified classnames to their simple ones. Note: weak alias!
            create_class_alias($cd,array_pop($d));
        }
    }
    
    // get the class definition. note: we assume that there's only one class/interface in each file!
    $def = array_pop($post);
    if( !isset($orig) && !$def )
    // plain class requested AND file was already included, so search up the declared classes and alias
    {
        foreach( array_reverse($pre) as $c )
        {
            if( !ends_with($c,$class_name) )
                continue;
            // Aliasing previously included class
            create_class_alias($c,$class_name,true);
            break;
        }
    }
    else
    {
        $class_name = isset($orig)?$orig:$class_name;
        if( strtolower($def) != strtolower($class_name) && ends_iwith($def,$class_name) )
        // no qualified classname requested but class was defined with namespace
        {
            // Aliasing class
            create_class_alias($def,$class_name,true);
        }
    }
}
spl_autoload_register("system_spl_autoload",true,true);

function create_class_alias($original,$alias,$strong=false)
{
    // if strong create a real alias known to PHP
    if( $strong )
        class_alias($original,$alias);
    
    // In any case store the alias in a global variable
    $alias = strtolower($alias);
    if( isset($GLOBALS['system_class_alias'][$alias]) )
    {
        if( $GLOBALS['system_class_alias'][$alias] == $original )
            return;
        
        if( !is_array($GLOBALS['system_class_alias'][$alias]) )
            $GLOBALS['system_class_alias'][$alias] = array($GLOBALS['system_class_alias'][$alias]);
        $GLOBALS['system_class_alias'][$alias][] = $original;
    }
    else
        $GLOBALS['system_class_alias'][$alias] = $original;
}

References: __search_file_for_class, spl_autoload_register, get_declared_classes, ends_with, ends_iwith, class_alias.

Okay: Some more lines than the original one, but not too complicated, right? ;). Now this will work just fine:

PHP
// file: index.php
require_once("autoloader_ex.php");
$mc = new MyClass(); // OK
$mc = new Scavix\SomeCoolProject\MyClass(); // OK

So now you may migrate your project part by part to use namespacing without the fear of crashes due to missing namespace declarations. Sorry for dropping this last excuse to skip namespacing :)

Some words on namespacing

As you may have noticed: We like namespacing! But you will have to do it the right way and think about where it's useful. So we follow these rules in our projects:

  • Use a unique namespace root
  • Only use ONE namespace per file
  • Ensure that each class/interface is in a namespace
  • Put each class/interface in it's own file and don't put anything else in there
  • don't add functions to namespaces

Change-log

  • 2013/08/26: Initial publishing

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)