1、从文件引入谈起
在 PHP 5.3 之前,要在一个 PHP 脚本中引入另一个 PHP 脚本中定义的代码(通常是函数或者类),需要借助 include、require、include_once、require_once 等语句,include 和 require 都可以通过指定路径引入一个 PHP 脚本,区别是 include 没有找到对应路径脚本时发出警告(E_WARNING),而 require 会抛出致命错误(E_COMPILE_ERROR),include_once/require_once 也是用于引入指定路径 PHP 脚本,与 include/require 的区别是如果指定路径已经包含过,不会再次包含,换言之,只会包含一次同一路径脚本,include_once 和 require_once 的区别与 include/require 一样。
所以从性能角度说,使用 include_once/require_once 性能更好一些,至于使用 include_once 还是 require_once,取决于你对指定路径 PHP 脚本不存在的预期处理。
在前面的作业中,我们已经多次使用过它们来引入其他 PHP 脚本文件,比如在博客项目入口文件 index.php
中,我们通过如下代码引入 bootstrap.php
以便引入初始化函数 bootApp
进行调用:
<?php
require_once 'bootstrap.php';
// 新增一个 IoC 容器,通过依赖注入获取对象实例
$container = Container::getInstance();
bootApp($container);
...
然后在 bootstrap.php
中,又通过如下代码引入 Container
类定义:
<?php
require_once 'core/Container.php';
...
自动加载类文件
对于类文件的引入,如果你觉得反复编写 require_once
/include_once
语句太麻烦,还可以借助 spl_auto_register 函数注册自动加载器,实现系统未定义类或接口的自动加载。
比如我们将上述 bootstrap.php
中的通过 require_once
引入 Container
类代码调整为通过 spl_autoload_register
函数自动注册:
spl_autoload_register(function ($className) {
require_once 'core/' . $className. '.php';
});
这样,我们只需要通过 spl_autoload_register
全局注册这个匿名函数即可,当 Container
类找不到时,会根据这个自动加载器进行加载。
2、命名空间及其使用
结合 require_once
/include_once
和 spl_autoload_register
,已经可以很好地解决多个 PHP 脚本之间引入和组合的问题,从而构建出复杂系统,比如 Web 开发框架,或者第三方库等,事实上,在 PHP 5.3 之前,第三方框架和库就是这么做的,不过,细心的同学可能已经看出来,spl_autoload_register
这种自动类加载机制存在一个问题,那就是不同库/组件类名冲突问题,因此,从 PHP 5.3 开始,引入了命名空间的概念,通过命名空间,可以很好的解决这个问题,而且相较于前者,代码可读性更好。
在 PHP 中,通过 namespace
关键字声明当前脚本所在的命名空间,通常,一个 PHP 脚本文件归属于一个命名空间。我们在 php_learning
目录下新建一个 ns
子目录存放本篇教程代码,然后在 ns
目录下创建一个 Test.php
文件,编写一段简单的测试代码如下:
<?php
namespace App;
class Test
{
public static function print ()
{
printf("这是一个测试类: %sn", __CLASS__);
}
}
我们需要在 PHP 脚本的第一行代码声明代码所属的命名空间(必须是第一行,否则会报错):
代码语言:javascript复制namespace App;
表明这段脚本中的所有 PHP 常量、变量、类、函数都归属于这个命名空间,然后我们在这个命名空间中声明了一个 Test
类,以及一个静态方法 print
来打印类名。
接下来,我们在同一目录下创建一个 App.php
脚本来调用 Test::print()
方法:
App.php
和 Test.php
归属于同一个目录,所以声明了相同的命名空间,实际开发过程中,我们通常就是根据目录来组织并管理命名空间的。调用同一个命名空间中的类和函数,可以像上面代码这样直接调用,如果是不同命名空间的类和函数,则需要通过 use
关键字引入,我们在 ns
目录下新建一个 testing
子目录,并在该子目录下新建一个 Test.php
,在这个 PHP 脚本中,我们定义了一个继承自上级目录中定义的 Test
父类的同名子类:
这里,我们将该子类所属命名空间声明为 AppTesting
(同一个命名空间下不允许出现重名的类和函数),然后通过 use
关键字引入上级命名空间中的 Test
类,由于该类名与子类名同名,所以通过 as
关键字为其设置一个别名 BaseTest
,接下来,就可以通过 BaseTest
引用 Test
父类。
在 Test
子类中,我们重写了父类 BaseTest
的 print
方法。
最后,我们可以在 App.php
中这样调用这个子类:
<?php
namespace App;
use AppTestingTest as SubTest;
Test::print();
SubTest::print();
如果不存在类名冲突,则不需要设置别名:
代码语言:javascript复制<?php
namespace App;
use AppTestingTest;
Test::print();
此外,还可以不使用 use
关键字,直接引用包含完整命名空间的类名:
<?php
namespace App;
Test::print();
AppTestingTest::print();
或者这样,使用部分命名空间:
代码语言:javascript复制<?php
namespace App;
use AppTesting;
Test::print();
TestingTest::print();
但是,我们这个系列教程约定通过 use
引入完整命名空间,以避免代码的冗长,提高可读性。
注:学院君这里只是抛砖引玉,简单介绍了 PHP 命名空间的基本使用,更多细节请参考官方文档 或者现代 PHP 新特性系列(一) —— 命名空间这篇教程(链接地址:https://xueyuanjun.com/post/4221)。
自动加载命名空间类
当然,现在调用 php App.php
会报错,不论是 AppTest
还是 AppTestingTest
类都提示找不到:
-w999
要解决这个问题,可以借助上面提到的 spl_autoload_register
函数,将类名所属命名空间解析为对应的目录路径(这就是为什么要根据目录来组织命名空间),然后把通过 require_once
/include_once
引入,我们在 App.php
中加入如下这段代码:
<?php
namespace App;
use AppTestingTest as SubTest;
spl_autoload_register(function ($className) {
$path = explode('\', $className);
if ($path[0] == 'App') {
$base = __DIR__;
}
$filename = $path[count($path) - 1] . '.php';
$filepath = $base;
foreach ($path as $key => $val)
{
if ($key == 0 || $key == count($path) - 1) {
continue;
}
$filepath .= DIRECTORY_SEPARATOR . strtolower($val);
}
$filepath .= DIRECTORY_SEPARATOR . $filename;
require_once $filepath;
});
Test::print();
SubTest::print();
这样,我们就可以正常调用这段代码了:
3、通过 Composer 管理命名空间
实际项目开发时,手动编写这段 spl_autoload_register
代码有点麻烦,尤其是项目除了自己编写的代码外,还要引入各种第三方库,我们可以借助 PHP 的包管理工具 Composer 帮我们管理这种命名空间与目录路径的映射,在此之前,我们已经在 PHP 环境搭建篇中在本地系统中安装好了 Composer,因此,只需要在 ns
目录下运行 composer init
初始化 Composer 设置即可,按照向导一路往下走即可,最后会在项目根目录下生成一个 composer.json
配置文件:
如果项目有第三方库依赖,可以在 require
中进行配置,这里是一个测试项目,暂时还没有任何依赖,然后我们在其中配置 autoload
选项来设置类自动加载机制:
{
"name": "php/test",
"description": "A php namespace test project",
"type": "project",
"license": "Apache",
"authors": [
{
"name": "xueyuanjun",
"email": "yaojinbu@outlook.com"
}
],
"minimum-stability": "dev",
"require": {},
"autoload": {
"classmap": [
"."
]
}
}
这里,我们通过在 classmap
数组中添加 .
表示当前根目录作为类自动加载的入口目录,Composer 会从这里开始读取所有命名空间并建立目录映射关系。接下来执行 composer install
初始化依赖库和类自动加载设置:
初始化过程中,会在根目录下创建 vendor
用来存放第三方依赖包和类自动加载相关文件。初始化完成后,可以看到 vendor/composer/autoload_static.php
中已经包含了 App
及其子命名空间的目录映射了:
该文件会被 autoload_real.php
引用,autoload_real.php
又会被 vendor/autoload.php
引用:
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit991075cd5c3b2a6d389bb443802f7669::getLoader();
autoload_php
是所有 Composer 管理类自动加载的入口文件,所以我们只需要在代码中引入这个文件即可通过 Composer 来管理所有类的自动加载,在 App.php
中,修改示例代码如下:
<?php
include_once 'vendor/autoload.php';
use AppTest;
use AppTestingTest as SubTest;
Test::print();
SubTest::print();
比起之前手动编写 spl_autoload_register
进行类自动加载,现在的代码更加简单清晰,执行 php App.php
,运行结果如下:
实际上,Composer 底层也是通过 spl_autoload_register
函数实现类的自动加载的,只是在此之前,还会建立命令空间与类脚本路径的映射,更多细节,可以参考 Laravel 框架如何基于 Composer 实现类和文件的自动加载 这篇教程(链接地址:https://xueyuanjun.com/post/19890),当然,Composer 作为 PHP 的包管理工具,其功能远不止于此,其更强大的功能在于对第三方扩展包和库进行安装、维护和管理,限于篇幅,这里就不详细展开了,感兴趣的同学可以参考以下两篇教程:
- 漫谈 PHP 组件、框架、Composer 那些事(链接地址:https://xueyuanjun.com/post/4506)
- 聊聊 PHP 私有组件以及如何创建自己的 PHP 组件(链接地址:https://xueyuanjun.com/post/4545)
综上,有了命令空间和 Composer 加持,我们可以基于 PHP 轻松构建和维护复杂的、现代的大型项目,下篇教程开始,学院君将给大家演示如何从零开始构建一个 PHP Web 框架。