在 PHP 中使用自动加载和命名空间

PHP 自动加载和命名空间提供了方便的便利性,并具有巨大的优势。
1 位读者喜欢这篇文章。
women programming

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

在 PHP 语言中,自动加载是一种在代码中自动包含项目类文件的方法。假设你有一个复杂的面向对象的 PHP 项目,其中包含一百多个 PHP 类。你需要确保在使用所有类之前加载它们。本文旨在帮助你理解 PHP 中自动加载、命名空间和 use 关键字的概念、原因和使用方法。

什么是自动加载?

在一个复杂的 PHP 项目中,你可能会使用数百个类。如果没有自动加载,你可能需要手动包含每个类。你的代码可能看起来像这样:

<?php

// manually load every file the whole project uses
require_once __DIR__.'/includes/SomeClass1.php';
require_once __DIR__.'/includes/SomeClass2.php';
require_once __DIR__.'/includes/SomeClass3.php';
require_once __DIR__.'/includes/SomeClass4.php';
require_once __DIR__.'/includes/SomeClass5.php';
require_once __DIR__.'/includes/SomeClass6.php';
require_once __DIR__.'/includes/SomeClass7.php';
require_once __DIR__.'/includes/SomeClass8.php';
require_once __DIR__.'/includes/SomeClass9.php';
require_once __DIR__.'/includes/SomeClass10.php';
// ... a hundred more times

$my_example = new SomeClass1();

这往好听了说是繁琐,往难听了说是难以维护。

如果可以,让 PHP 在你需要时自动加载类文件,那会怎么样?你可以通过自动加载来实现。

PHP 自动加载 101

创建自动加载器只需两个步骤。

  1. 编写一个函数来查找需要包含的文件。
  2. 使用核心 PHP 函数 spl_autoload_register() 注册该函数。

以下是如何为上面的示例执行此操作:

<?php
/**
 * Simple autoloader
 *
 * @param $class_name - String name for the class that is trying to be loaded.
 */
function my_custom_autoloader( $class_name ){
    $file = __DIR__.'/includes/'.$class_name.'.php';

    if ( file_exists($file) ) {
        require_once $file;
    }
}

// add a new autoloader by passing a callable into spl_autoload_register()
spl_autoload_register( 'my_custom_autoloader' );

$my_example = new SomeClass1(); // this works!

就这样。你不再需要手动 require_once 项目中的每个类文件。相反,通过你的自动加载器,系统会在使用类时自动 require 文件。

为了更好地理解这里发生了什么,请逐步了解上面的代码:

  1. 函数 my_custom_autoloader 期望一个名为 $class_name 的参数。给定一个类名,该函数会查找具有该名称的文件并加载该文件。
     
  2. PHP 中的 spl_autoload_register() 函数期望一个可调用参数。可调用参数可以是多种类型,例如函数名、类方法,甚至是匿名函数。在本例中,它是一个名为 my_custom_autoloader 的函数。
     
  3. 因此,代码能够实例化名为 SomeClass1 的类,而无需首先 require 其 PHP 文件。

那么,当运行此脚本时会发生什么?

  1. PHP 意识到尚未加载名为 SomeClass1 的类,因此它会执行已注册的自动加载器。
     
  2. PHP 执行自定义自动加载函数 (my_custom_autoloader),并将字符串 SomeClass1 作为 $class_name 的值传递给它。
     
  3. 自定义函数将文件定义为 $file = __DIR__.'/includes/SomeClass1.php';,查找其是否存在 (file_exists()),然后(只要找到该文件)使用 require_once __DIR__.'/includes/SomeClass1.php'; 将其标记为 required。因此,类的 PHP 文件会自动加载。

万岁!你现在有了一个非常简单的自动加载器,它可以在首次实例化类时自动加载类文件。在一个中等规模的项目中,你已经避免了编写数百行代码。

什么是 PHP 命名空间?

命名空间是一种封装类似功能或属性的方法。一个简单(且实用)的类比是操作系统的目录结构。文件 foo.txt 可以同时存在于目录 /home/greg/home/other 中,但是两个 foo.txt 的副本不能共存于同一目录中。

此外,要访问 /home/greg 目录之外的 foo.txt 文件,你必须使用目录分隔符将目录名称前置到文件名,才能得到 /home/greg/foo.txt

你可以在 PHP 文件的顶部使用 namespace 关键字定义命名空间:

<?php

namespace Jonathan;

function do_something() {
    echo "this function does a thing";
}

在上面的示例中,我将 do_something() 函数封装在 Jonathannamespace 中。这意味着很多事情——最重要的是,这些事情都不会与全局范围内同名的函数冲突。

例如,假设你将上面的代码放在名为 jonathan-stuff.php 的文件中。在另一个文件中,你有这个:

<?php
require_once "jonathan-stuff.php";

function do_something(){  echo "this function does a completely different thing"; }

do_something();
// this function does a completely different thing

没有冲突。你有两个名为 do_something() 的函数,它们能够彼此共存。

现在你所要做的就是弄清楚如何访问命名空间方法。这是通过与目录结构非常相似的语法完成的,使用反斜杠:

<?php

\Jonathan\do_something();
// this function does a thing

此代码执行位于 Jonathan 命名空间内的名为 do_something() 的函数。

此方法也(更常见地)用于类。例如:

?php

namespace Jonathan;

class SomeClass { }

可以像这样实例化:

<?php

$something = new \Jonathan\SomeClass();

借助命名空间,非常大的项目可以包含许多同名的类,而不会发生任何冲突。很棒,对吧?

命名空间解决了哪些问题?

要了解命名空间提供的优势,你只需回顾一下没有命名空间的 PHP。在 PHP 5.3 版本之前,你无法封装类,因此它们始终面临与另一个同名类冲突的风险。前缀类名曾经(并且在某种程度上仍然)很常见:

<?php

class Jonathan_SomeClass { }

你可以想象,代码库越大,类越多,前缀就越长。如果你打开一些旧的 PHP 项目,并发现类名超过 60 个字符长,例如:

<?php 

class Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator { }

编写像这样的长类名和编写像 \Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator 这样的长类名有什么区别?这是一个很好的问题,答案在于在给定上下文中多次使用该类的简易程度。想象一下,你不得不在单个 PHP 文件中多次使用一个长类名。目前,你有两种方法可以做到这一点。

没有命名空间:

<?php

class Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator { }
    
$a = new Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator();
$b = new Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator();
$c = new Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator();
$d = new Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator();
$e = new Jonathan_SomeEntity_SomeBundle_SomeComponent_Validator();

哎哟,这要输入很多东西。这是使用命名空间的版本:

<?php
namespace Jonathan\SomeEntity\SomeBundle\SomeComponent;

class Validator { }

在代码的其他地方:

<?php

$a = new \Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator();
$b = new \Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator();
$c = new \Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator();
$d = new \Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator();
$e = new \Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator();

这当然没有好多少。幸运的是,还有第三种方法。你可以利用 use 关键字来引入命名空间。

use 关键字

use 关键字将给定的命名空间导入到当前上下文中。这允许你使用其内容,而无需每次使用它时都引用其完整路径。

<?php

namespace Jonathan\SomeEntity\SomeBundle\SomeComponent;

class Validator { }

现在你可以这样做:

<?php

use Jonathan\SomeEntity\SomeBundle\SomeComponent\Validator;

$a = new Validator();
$b = new Validator();
$c = new Validator();
$d = new Validator();
$e = new Validator();

除了封装之外,导入是命名空间的真正力量。

现在你已经了解了自动加载和命名空间的概念,你可以将它们结合起来,创建一种可靠的项目文件组织方式。

PSR-4:PHP 自动加载和命名空间的标准

PHP 标准建议 (PSR) 4 是一种常用的模式,用于组织 PHP 项目,使类的命名空间与该类文件的相对文件路径相匹配。

例如,你在一个使用 PSR-4 的项目中工作,并且你有一个名为 \Jonathan\SomeBundle\Validator(); 的命名空间类。你可以确定该类的文件可以在文件系统中的这个相对位置找到:<relative root>/Jonathan/SomeBundle/Validator.php

为了进一步强调这一点,以下是使用 PSR-4 的项目中类的 PHP 文件存在的更多示例:

  • 命名空间和类: \Project\Fields\Email\Validator()
    • 文件位置: <relative root>/Project/Fields/Email/Validator.php
       
  • 命名空间和类: \Acme\QueryBuilder\Where
    • 文件位置: <relative root>/Acme/QueryBuilder/Where.php
       
  • 命名空间和类: \MyFirstProject\Entity\EventEmitter
    • 文件位置: <project root>/MyFirstProject/Entity/EventEmitter.php

这实际上并非 100% 准确。项目的每个组件都有自己的相对根目录,但不要忽视此信息:了解 PSR-4 意味着类的文件位置可以帮助你轻松找到大型项目中的任何类。

PSR-4 如何工作?

PSR-4 工作的原因是它通过自动加载器函数实现。查看一个 PSR-4 示例自动加载器函数:

<?php

spl_autoload_register( 'my_psr4_autoloader' );

/**
 * An example of a project-specific implementation.
 *
 * @param string $class The fully-qualified class name.
 * @return void
 */
function my_psr4_autoloader($class) {
    // replace namespace separators with directory separators in the relative 
    // class name, append with .php
    $class_path = str_replace('\\', '/', $class);
    
    $file =  __DIR__ . '/src/' . $class_path . '.php';

    // if the file exists, require it
    if (file_exists($file)) {
        require $file;
    }
}

现在假设你刚刚实例化了 new \Foo\Bar\Baz\Bug(); 类。

  1. PHP 使用字符串值 $class = "\Foo\Bar\Baz\Bug" 执行带有 $class 参数的自动加载器。
     
  2. 使用 str_replace() 将所有反斜杠更改为正斜杠(像大多数目录结构一样使用),将命名空间转换为目录路径。
     
  3. <relative root>/src/Foo/Bar/Baz/Bug.php 位置查找该文件是否存在。
     
  4. 如果找到该文件,则加载它。

换句话说,你将 Foo\Bar\Baz\Bug 更改为 /src/Foo/Bar/Baz/Bug.php,然后找到该文件。

Composer 和自动加载

Composer 是一个命令行 PHP 包管理器。你可能已经看到一个项目的根目录中有一个 composer.json 文件。此文件告诉 Composer 关于项目的信息,包括项目的依赖项。

这是一个简单的 composer.json 文件示例:

{
    "name": "jonathan/example",
    "description": "This is an example composer.json file",
    "require": {
        "twig/twig": "^1.24"
    }
}

此项目名为 "jonathan/example",并且有一个依赖项:Twig 模板引擎(版本 1.24 或更高版本)。

安装 Composer 后,你可以使用 JSON 文件下载项目的依赖项。这样做时,Composer 会生成一个 autoload.php 文件,该文件会自动处理所有依赖项中类的自动加载。

Screenshot of nested drop down menus highlighting the path example - vendor - twig - autoload.php

(Jonathan Daggerheart,CC BY-SA 4.0)

如果将此新文件包含在项目中,则会在需要时自动加载依赖项中的所有类。

PSR 使 PHP 更好

由于 PSR-4 标准及其广泛采用,Composer 可以生成一个自动加载器,该自动加载器可以在你在项目中实例化依赖项时自动处理加载它们。下次编写 PHP 代码时,请记住命名空间和自动加载。

Jonathan Daggerhart
Jonathan Daggerhart 是一位 Drupal 和 WordPress 开发人员,他与这两个系统合作并为之贡献了十多年。Daggerhart Lab 的所有者和架构师。他曾在多个 Drupal 和 WordPress 大会上就面向初学者和专家开发人员的主题发表演讲。

评论已关闭。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.