Gong Yong的Blog

使用composer中的autoload

composer是php的包管理工具,类似于node的npm和ruby的bundle,本意是用于取代pear,对pear不了解,就不妄作评论了。实际上我对composer也不胜了解,相对于php,这货出来得太晚了,写了多年php的人,很难习惯composer所定义的一套规则。

从包管理的角度出发,composer是很有价值的,使用compser可以方便的发布包和管理包的版本,而且也解决了包的依赖问题,这样有利于包的分享,而且对于php开发者而言,即可以把包发布到packagist这种公共平台,也可以自己搭建一个私有的包管理服务器来管理整个团队的包。

我在使用composer过程中碰到一个比较麻烦的问题就是怎么使用它提供的autoload,以及怎么在代码中使用命名空间,虽然这个问题很简单,但苦于找不到完整的文档和示例而不得不折腾了一阵子,这篇文章就是对这次折腾的总结,文中的所有示例代码可以在我的github上找到。

composer的autoload

创建名为一个composer.json的文件,在文件中加上我们要依赖的包,然后在当前目录下执行composer install命令。这个命令执行完后会在当前目录中生成一个vendor文件夹,这个文件夹中包含了所有我们需要的包,以及包含命令行工具的bin文件夹。此外,还有一个重要的文件夹是composer,今天要讲的东西就是跟这个文件夹中的内容相关的,我们可以先看下它里面有些什么东西(我的composer版本是1.0-dev,不同的版本可能会有些一些差别):

ClassLoader.php
autoload_classmap.php
autoload_namespaces.php
autoload_psr4.php
autoload_real.php
include_paths.php
installed.json

如果你按照我上面说的步骤操作的话,估计最先注意的会是vendor里面的autoload.php,而且你可能会觉得这个文件中的内容会更重要,毕竟它的名字就叫autoload嘛。我们先看看它的内容:

<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInitf7848bde2b9487edbeec90379f6deab5::getLoader();

实际上它所干的事情并不多,它只是一个入口。通常我们只需要在代码中require这个文件,就可以使用composer提供的autoload了,先看一个composer官网上提供的示例:

require 'vendor/autoload.php';
$log = new Monolog\Logger('name');
$log->pushHandler(new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING));
$log->addWarning('Foo');

上面的代码首先假设你composer.json中require了monolog这个包,并且已经下载到了你的本地。这段代码的第一行先加载了vender/autoload.php,下面就可以使用Monolog\Logger这个类了,我们没有使用include或者require语句先加载这个类的php文件,这就是autoload所干的事情。

回到vendor/autoload.php,我们看到它首先加载了vender/composer/autoload_real.php,它也是vendor/composer下的一个文件,这个文件中有一个类,它的签名为:

<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitf7848bde2b9487edbeec90379f6deab5
{ ... }

这个类名很长,而且每次运行composer dump-autoload命令后都会生成新的文件名,composer这么搞也许是为了防止重名吧。这个类中有一个名为getLoader的静态方法,而vendor/autoload.php做的唯一一件事情就是调用了这个方法,所以魔法可能就是在这个方法中实现的,我们看看它的代码:

public static function getLoader(){
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitf7848bde2b9487edbeec90379f6deab5', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitf7848bde2b9487edbeec90379f6deab5', 'loadClassLoader'));

$includePaths = require __DIR__ . '/include_paths.php';
array_push($includePaths, get_include_path());
set_include_path(join(PATH_SEPARATOR, $includePaths));

$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}

$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}

$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}

$loader->register(true);
return $loader;
}

上面代码中的成员变量$loader是composer/ClassLoader.php的ClassLoader类的实例。ClassLoader类以及上面代码中调用的spl_autoload_register和spl_autoload_unregister两个函数都是实现自动加载机制的,这不在本文的范围内,有兴趣的同学可以自行搜索相关文档。我们重点关注的是这个函数中的几处require语句:

$includePaths = require __DIR__ . '/include_paths.php';
$map = require __DIR__ . '/autoload_namespaces.php';
$map = require __DIR__ . '/autoload_psr4.php';
$classMap = require __DIR__ . '/autoload_classmap.php';

每个require语句都加载了vendor/composer下的一个php文件,它们分别对应4种不同的加载方式,在composer的文档中它们分别被命名为:

psr-4 => autoload_psr4.php
psr-0 => autoload_namespaces.php
classmap => autoload_classmap.php
files => include_paths.php

我们用一个示例来说明它们的用法,虽然这个示例很简单,也没什么实用价值,但基本上可讲清楚这几种不同的加载方法,可以在这里下载完整的源代码

示例

假设我们要做一个小学的管理系统,我们要新开发两个类,Student和Classroom,但是之前的一个家伙已经开发了一个名为DB类,这个类专门负责数据库操作,它有一个方法返回所有学生的数据,这个类在项目根目录下classes文件夹中。我们想把新开发的两个类放到src目录下,并且使用命名空间,而且我们打算遵从psr-4规范,与国际潮流接轨。此外我们还有一套自己的积累的工具函数,在多年的php职业生涯中,这些工具函数一直陪伴着我们,我们成长了,它也成长了,唯一遗憾的是,因为年代久远,这些都只是函数,没有封装成类,而我们对它有特殊的感情,也不打算在没有问题的情况下修改它们,我们会把它放在在common目录下,文件名为util.php。现在看看这个项目的目录结构:

/autoload-test
     /classes
          -DB.php
     /common
          -util.php
     /src
          -Student.php
          -Classroom.php
     /vendor
         -autoload.php
         /composer
     composer.json
     index.php 

我们现在要做的事情是在index.php中调用DB的getAllStudent()方法,它会返回学生数据的数组。然后遍历这个数组,调用Classroom的addStudent()把学生加到班级中,这个方法会为每个学生创建一个Student对象,然后存到Classroom对象的students数组中。最后再遍历Classrooms中的所有学生,调用util.php的hello()方法,这个方法会返回一个字符串。下面是最终的index.php的代码:

<?php
require "vendor/autoload.php";

use school\Student;
use school\Classroom;

$data = DB::getAllStudents();
$classroom = new Classroom();
foreach($data as $s) {
$student = $classroom->addStudent($s);
}

$students = $classroom->getAllStudents();
foreach($students as $student) {
echo hello($student->getName());
}

我们看到这个代码中没有使用任何include和require语句来加载上面列出的php文件,只是使用use声明了两个命名空间,它们是命名空间school下面的类。这是怎么实现的呢?一切都在composer.json中:

{
"name" : "autoload-test",
"autoload" : {
"classmap" : ["classes/"],
"files" : ["common/util.php"],
"psr-4" : {
"school\\" : "src/"
}
}
}

这种用到了三种autoload,在此没有使用psr-0,因为psr-0跟psr-4非常类似,而且php-fig的官网上也推荐使用psr-4的规范,psr-0可能会被废除。composer的autoload部分的文档也推荐使用psr-4,所以我们还是先讲psr-4吧。

psr-4

psr-0psr-4都是autoload的规范,也可以说是对定义命名空间的规范,在composer.json中的autoload部分添加了psr-4部分后,使用composer dump-autoload命令就可以生成相关的autoload文件,按照我们前面的说明psr-4对应的文件是vendor/composer/autoload_psr4.php,我们看看这个文件的内容:

<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'school\\' => array($baseDir . '/src'),
);

这个文件中有一个数组,它的key是我们定义的命名空间的名称"school\\",这里要特别注意后面的两个斜线,php中的命名空间以反斜线分隔的,使用两个斜线是为了防止它被解析为转义符,你可以把两个反斜线理解为一个反斜线字符,之所以要在后面加一个反斜线字符是为了防止跟命名空间同名的类冲突,如果不使用"\\"结尾的话,运行的时候会报错。

在这里src目录下面的所有类都是在school命名空间下面的,如果在程序是使用这个命名空间,那么就自动会到这个目录下面去找对应的类,另外一点要注意的是,对于src中的类也必须使用namespace关键字声明它的命名空间,在我们这个示例中Student.php和Classroom.php这两个类都必须在第一哥个非空行写上namespace school这个语句:

<?php
namespace school;

对于psr-4,composer的文档中还介绍了一些其他的用法,例如将同一个命名空间设置为多个目录,或者是在composer.json中不设置命名空间,关于这些就不细述了,看看文档,写写代码就可以搞清楚。

classmap

在composer.json的autoload字段中key名为classmap字段的值是一个数组,这个数组可以是文件夹,也可以是文件,文件的扩展名必须是.php和.inc,而且composer也只会处理文件夹下扩展名为.php和.inc的文件。它会将所有.php和.inc文件中的类提出出来然后以类名作为key,类的路径作为值保存在vendor/composer/autoload_classmap.php中:

<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'DB' => $baseDir . '/classes/DB.php',
);

在我们的示例中加入到classmap中的就是classes文件夹下的所有类,我们可以在autoload_classmap.php中看到以DB为key,以它的路径为值的字段,所以当我们使用DB类的时候,composer的autoload就是加载它对应的类文件。

files

php的自动加载只能加载类,所以当你想使用全局函数的时候,就必须显示的使用include或者require,或者是把它们封装到一个类中,从面相对象的角度而言,封装到类中是一个不错的选择,但如果这些函数是一些遗留的系统中的函数,我们觉得没必要封装,这个时候就可以使用composer的autoload中的files了,它们的值是一个数组,每个元素必须是一个文件,而不能使用文件夹,我们看看它对应的vendor/composer/autoload_files.php这个文件:

<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
$baseDir . '/common/util.php',
);

它返回一个数组,这个数组中会保存我们所设置的文件的路径,composer会把这些文件都include进来。

psr-0

我们没有再这个示例中使用psr-0的autload,上面在psr-4中已经说明了原因,这里也并不打算做过多的讲解,只是提示一点,psr-0跟psr-4最大的不同就是命名空间的规范,psr-0支持用下划线区分命名空间的路径,如果你有兴趣的话,可以到php-fig上看看相关的文档。