65.9K
CodeProject 正在变化。 阅读更多。
Home

将项目升级到 PHP 命名空间:一个简单的解决方案

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2013年8月26日

CPOL

3分钟阅读

viewsIcon

19592

如何为您的项目添加可选的 PHP 命名空间

引言

在本文中,我们将解释为什么以及如何在我们自己的 Web 应用程序项目中实现 PHP 命名空间。由于几乎所有项目都基于此,因此实现是在 Scavix Web 开发框架中完成的,您可以在那里找到完整的代码。如果您不熟悉它,您可能需要先阅读我们的其他文章。

目标读者

具有扎实 PHP 知识的经验丰富的软件开发人员。

问题所在

您可能自己也经历过:您将一个第三方库包含到项目中,它使用了您在项目中的其他地方已经使用过的一些类名(例如“Logger”或“Model”)。
如果您无法更改类名,这很糟糕,因为...

  • ...向后兼容性。
  • ...数据库中的外部类引用。
  • ...

如果您无法更改第三方库,情况会变得更糟,因为它例如是闭源的(或者出于任何原因)。在这种情况下,您将无法使用此库。

解决方案

简而言之:在您的代码中使用 命名空间。更详细地说:如果您将代码封装到命名空间中,您可以使用任何不使用相同命名空间的库。当然,这种冲突的可能性要小得多,如果您明智地选择您的命名空间(例如 CompanyName\ProductName),它永远不会发生。

解决方案的问题

像往常一样,在您拥有干净的命名空间之前,存在一些障碍需要克服。如果您从零开始一个新项目,则没有任何问题,但请记住:如果您与第三方库发生冲突,那么您正在处理一个现有项目。如果您将命名空间添加到您的文件中,您需要为整个项目执行此操作。在您完成之前,它会崩溃。这当然不好,您可能会忘记某些地方,因此命名空间重新设计将花费您很长时间。但它真的值得。

解决解决方案中的问题

简而言之:向后兼容。当然,这并不那么简单。哦,等等:事实上在这种情况下是!哦,再等等:只有当您使用 自动加载时才是这样,但我们确信您这样做,所以我们将继续:)

基本过程是如果请求的类是普通的,但找到的类有一个限定名,则创建一个类别名。听起来很酷,是吧?让我们在下面的示例中看看

//file: myclass.php
namespace Scavix\SomeCoolProject;
class MyClass { /* some magic happens here */ }
这是类名的标准自动加载器机制
// 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);

参考资料__search_file_for_classspl_autoload_register

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

当然,在此示例中,会遇到“类未找到”异常。因此,为了使该代码正常工作,我们现在将扩展自动加载器以自动实现上述规则:如果请求的类是普通的,但找到的类具有限定名,则创建一个类别名。

// 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;
}

参考资料__search_file_for_classspl_autoload_registerget_declared_classesends_withends_iwithclass_alias

好的:比原来的多几行,但不太复杂,对吧? ;)。现在这会很好地工作

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

因此,现在您可以逐步将项目迁移到使用命名空间,而无需担心由于缺少命名空间声明而导致的崩溃。很抱歉放弃了您跳过命名空间的最后一个借口 :)

关于命名空间的一些说明

您可能已经注意到:我们喜欢命名空间!但是您将不得不以正确的方式进行操作,并考虑它在哪里有用。所以我们在我们的项目中遵循这些规则

  • 使用一个唯一的命名空间根目录
  • 每个文件只使用一个命名空间
  • 确保每个类/接口都在一个命名空间中
  • 每个类/接口放在其自己的文件中,并且不要在其中放置任何其他内容
  • 不要向命名空间添加函数

更改日志

  • 2013/08/26:初始发布
© . All rights reserved.