RSS
 

PHP 5.3 Dynamic Namespace Resolution

10 Apr

As PHP continues evolving, it just keeps getting better and better. One of the features added in version 5.3 is support for namespaces. If you’ve started trying to actually use PHP namespaces, you’ve probably run into some of the more obvious quirks that require you to either retrofit your code or implement a workaround. I started with the former before realizing that thanks to the namespace feature itself, a simple set of workarounds could do the job.

Specifically, I’m referring to the fact that namespace resolution happens at compile time, which means that if you attempt to reference a namespaced class or method as a string, it won’t be recognized unless that string explicitly includes the namespace name. The most apparent case is when calling class_exists(). For example, let’s say you define class Settings in namespace OssumCMS:

namespace OssumCMS;

class Settings {
  const MEMCACHED_ADDR = 'none';
  public static function do_something() {
  }
}

In your code, you might want to do some things like:

// ...
if (class_exists('Settings'))
  if (@constant('Settings::MEMCACHED_ADDR') != 'none')
    call_user_func(array('Settings', 'do_something'));

But that would never work because class Settings is defined within namespace OssumCMS. PHP resolves namespaces at compile time, so when class_exists(), constant(), and call_user_func() are called at run time, they won’t ever find class Settings because it’s checking only the global namespace. The obvious fix is to include the namespace name before every occurrence of the class name:

// ...
if (class_exists('OssumCMS\Settings'))
  if (@constant('OssumCMS\Settings::MEMCACHED_ADDR') != 'none')
    call_user_func(array('OssumCMS\Settings', 'do_something'));

Now PHP will know to check for the class within the namespace in which it’s actually defined. Small victory, especially when you’re dealing with thousands of lines of pre-existing code. You definitely don’t want to find every occurrence of every class and prefix it with the namespace. Another small step in the right direction would be to use a function to automatically prefix a string with the current namespace if needed by using a function:

namespace OssumCMS;

function ns($i_name) {
  if (!is_scalar($i_name))
    return $i_name;
  return strpos($i_name, '\\') !== false ? $i_name : (__NAMESPACE__ . '\\' . $i_name);
}

This would allow you to simply wrap all your class name strings in that function call, for example:

if (class_exists(ns($class_name)))
  do_something();


But that would still require you to traverse all line of all your source code files doing a search/replace. Wouldn’t it be easier if instead of manipulating your class name references, you could override the built-in PHP functions? In the old days, you wouldn’t be able to do something like that, but thanks to namespaces, it’s entirely possible. All we have to do is create our own class_exists(), constant(), and call_user_func() type functions within our own namespace and add to string arguments the namespace prefix whenever necessary, then call the corresponding global functions.

namespace OssumCMS;

function ns($i_name) {
  if (!is_scalar($i_name))
    return $i_name;
  return strpos($i_name, '\\') !== false ? $i_name : (__NAMESPACE__ . '\\' . $i_name);
}

function un_ns($i_name) {
  if (!is_scalar($i_name))
    return $i_name;
  return preg_replace('#^' . preg_quote(__NAMESPACE__, '#') . '\\#i', '', $i_name);
}

function constant($i_name)        { return @\constant(ns($i_name)); }
function class_exists($i_name)    { return \class_exists(ns($i_name)); }

function call_user_func_array($i_func, $i_args) {
  is_array($i_func)
    ? (!is_object($i_func[0]) && ($i_func[0] = ns($i_func[0])))
    : (!is_object($i_func) && !function_exists($i_func) && ($i_func = ns($i_func)));
  return \call_user_func_array($i_func, $i_args);
}

function call_user_func($i_func) {
  return call_user_func_array($i_func, array_shift($args = func_get_args()));
}

Given these overrides, our example block of code would be evaluated as follows

// ...
if (OssumCMS\class_exists(OssumCMS\ns('Settings')))
  if (OssumCMS\constant(OssumCMS\ns('Settings::MEMCACHED_ADDR')) != 'none')
    OssumCMS\call_user_func(array(OssumCMS\ns('Settings'), 'do_something'));

There are surely other functions that need overriding, but these are the most obvious suspects. This was a useful little hack for me and I hope you find it useful as well.

 
2 Comments

Posted in PHP

 

21,680 views

Tags: ,

Leave a Reply

 

 
  1. Gary A Mort

    November 20, 2012 at 2:58 pm

    Thanks for the tips! I ran into this issue when I added a namespace to the Joomla! framework. Initially I just added the __NAMESPACE__ constant to the loader…but as I dug deeper I found a lot of class_exists calls that this would not work for.

    I pulled all your functions into part of the stub file and now it works great!
    https://github.com/garyamort/packager

     
  2. rommel

    November 20, 2012 at 4:22 pm

    Excellent. Thanks for letting me know it was useful to you. :)