Composer 概述
一开始,最吸引我的当属 Composer 了,因为之前从没用过 Composer。
Composer 是 PHP 中用来管理依赖关系的工具,你只需在自己的项目中声明所依赖的外部工具库,Composer 就会帮你安装这些依赖的库文件。运行 Composer 需要 PHP 5.3.2+ 以上版本。
使用 Composer
第一步,声明依赖关系。比方说,你正在创建的一个项目需要一个库来做日志记录。你决定使用monolog。为了将它添加到你的项目中,你所需要做的就是创建一个composer.json
文件,其中描述了项目的依赖关系。
{
"require": {
"monolog/monolog": "1.2.*"
}
}
第二步,使用 composer。在项目根目录,执行安装命令,执行完毕后,monolog 就会被下载到vendor/monolog/monolog
目录。
$ php composer.phar install
第三步,类的自动加载。除了库的下载,Composer 还准备了一个自动加载文件,它可以加载 Composer 下载的库中所有的类文件。 使用它,你只需要将下面这行代码添加到你项目的引导文件中:
require 'vendor/autoload.php';
这使得你可以很容易的使用第三方代码。例如:如果你的项目依赖 monolog,你就可 ��� 像这样开始使用这个类库,并且他们将被自动加载。
$log = new Monolog\Logger('name');
$log->pushHandler(
new Monolog\Handler\StreamHandler('app.log', Monolog\Logger::WARNING)
);
$log->addWarning('Foo');
Composer 自动加载探秘
在现实世界中使用工具时,如果理解了工具的工作原理,使用起来就会更加有底气。 对于一个第一次接触 Laravel,且是第一次接触 Composer 的新手来说,如果理解 Composer 是如何工作的,使用起来将会更加自如。
我的理解是,Composer 根据声明的依赖关系,从相关库的 源 下载代码文件,并根据依赖关系在 Composer 目录下生成供类自动加载的 PHP 脚本,
使用的时候,项目开始处引入 /vendor/autoload.php
文件,就可以直接实例化这些第三方类库中的类了。
那么,Composer 是如何实现类的自动加载的呢?接下来,我们从 Laravel 的入口文件开始顺藤摸瓜往里跟进,来一睹 Composer 自动加载的奥妙。
laravel/public/index.php
require __DIR__ . '/../bootstrap/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/start.php';
$app->run();
第一行先是引入了 laravel/bootstrap/autoload.php
,不做解释,打开该文件。
laravel/bootstrap/autoload.php
define('LARAVEL_START', microtime(true));
require __DIR__ . '/../vendor/autoload.php';
if (file_exists($compiled = __DIR__ . '/compiled.php')) {
require $compiled;
}
Patchwork\Utf8\Bootup::initMbstring();
第一行定义了程序开始执行的时间点。紧接着第二行,引入了 laravel/vendor/autoload.php
。
第七行,前面说过,引入 Composer 的 autoload.php 之后就可以直接使用第三方类库中的类了,这里就是直接使用的 Bootup 类。
下面来看看 /vendor/autoload.php
到底做了什么。
laravel/vendor/autoload.php
# autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit9b2a1b1cf01c9a870ab98748dc5f1256::getLoader();
到这里,马上就进入自动加在的大门了。
这个文件很简单,第 5 行的函数名是不是看的一头雾水?别被吓到了,他就是个类名而已。
这个类是在第 3 行引入的文件 laravel/vendor/composer/autoload_real.php
里头声明的,接下来打开该文件看getLoader()
;
laravel/vendor/composer/autoload_real.php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit9b2a1b1cf01c9a870ab98748dc5f1256
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit9b2a1b1cf01c9a870ab98748dc5f1256', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit9b2a1b1cf01c9a870ab98748dc5f1256', 'loadClassLoader'));
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
$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);
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $file) {
composerRequire9b2a1b1cf01c9a870ab98748dc5f1256($file);
}
return $loader;
}
}
function composerRequire9b2a1b1cf01c9a870ab98748dc5f1256($file)及 $loader->addClassMap()
{
require $file;
}
第 17 行,getLoader()
中先是判断当前类中的 $loader
值,如果不是 null
就返回,这个可以略过。接着实例化了 ClassLoader 类给 $loader ,laravel/vendor/composer/ClassLoader.php
这里引入了几个文件,这些文件是由 Composer 自动生成的,当依赖关系发生改变时不需要修改这些脚本,运行 Composer 重新生成即可。
- laravel/vendor/composer/autoloade_namespace.php
- laravel/vendor/composer/autoloade_prs4.php
- laravel/vendor/composer/autoloade_classmap.php
- laravel/vendor/composer/autoloade_files.php
在设置完一堆的 path 信息后,执行了 $loader->set()
和 $loader->setPsr4()
及 $loader->addClassMap()
,
然后进行了 $loader->register(true);
现在我们一个个来看。
laravel/vendor/composer/ClassLoader.php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0 class loader
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
public function getPrefixes()
{
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-0 base directories
* @param bool $prepend Whether to prepend the directories
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*/
public function setPsr4($prefix, $paths) {
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}
$loader->set($namespace, $path);
Psr0 标准,设置命名空间对应的路径,以便于随后自动加载相关类文件。
$loader->setPsr4($namespace, $path);
Psr4 标准,设置命名空间对应的路径,以便于随后自动加载相关类文件。
$loader->addClassMap($classMap);
设置类文件路径与类名的对应关系,以便于随后自动加载相关类文件。
$loader->register(true);
public function register($prepend = false){
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
这里设置了欲注册的自动装载函数 $this->loadClass()
,关于 spl_autoload_register
和 spl_autoload_unregister
的更多信息随后会有专门的解释。
现在打开 loadClass()
的定义
public function loadClass($class){
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
这里有个 findFile()
函数,如果相关类的声明所在文件的路径找到了,就包含并运行该文件,然后返回 true
。接着打开 findFile()
的定义
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
}
return $file;
}
先是判断类名是否以\
开始,如果是的话,清除开头的\
接着,检查当前类的名字是否在 类名与声明当前类的文件的路径的关系数组 中,如果存在,直接返回相关键值(类文件路径信息)
如果上一步没有返回路径信息,执行 findFileWithExtension($class, '.php')
继续查找类文件路径信息,findFileWithExtension 的定义后面将会列出。
如果仍未找到类的文件路径信息,返回值为 null 且定义了 HHVM_VERSION 信息,则用.hh
后缀继续查找类文件信息。
HHVM_VERSION 是 HHVM 版本信息? HHVM 是 Facebook 开发的高性能 PHP 虚拟机,宣称比官方的快 9 倍。
如果仍未找到,则返回 false 。Remember that this class does not exist.(这句注释很有喜感?哈哈)
代码清单 findFileWithExtension()
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
}
该函数唯一的目的就是根据刚才通过 $loader->set($namespace, $path)
和 $loader->setPsr4($namespace, $path)
方法设置的信息找出类文件的路径信息。
接下来,我们回到代码清单 laravel/vendor/composer/autoload_real.php
,为精简篇幅,这里只贴出片段(续上节的 $loader->register(true)
)。
// 续上节的 `$loader->register(true)`
$loader->register(true);
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $file) {
composerRequire9b2a1b1cf01c9a870ab98748dc5f1256($file);
}
return $loader;
}
}
function composerRequire9b2a1b1cf01c9a870ab98748dc5f1256($file)
{
require $file;
}
在经历了一番长途跋涉后,终于从 laravel/vendor/composer/ClassLoader.php
中出来了。
继 $loader->register(true)
之后,又引入了 laravel/vendor/composer/autoload_files.php
,
相比之下,这个文件要简单得多,只是个数组,列出了几个文件路径。
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
$vendorDir . '/ircmaxell/password-compat/lib/password.php',
$vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
$vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Random.php',
$vendorDir . '/laravel/framework/src/Illuminate/Support/helpers.php',
);
接着就是用 composerRequire9b2a1b1cf01c9a870ab98748dc5f1256()
函数 包含并运行 这几个文件。这四个文件的具体信息,随后会专门写博文来认识。
最后 , 返回 ClassLoader 类的实例 $loader
。
现在在回到 laravel/bootstrap/autoload.php 的第 7 行
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
if (file_exists($compiled = __DIR__.'/compiled.php'))
{
require $compiled;
}
Patchwork\Utf8\Bootup::initMbstring();
注意这里第 7 行,之所以可以直接像 Patchwork\Utf8\Bootup::initMbstring()
这么使用而不需要手动 required Bootup 类文件,
是因为 前面在 ClassLoader 中的 register() 函数用 spl_autoload_register() 对 Bootup 类进行了注册。下面说一下 spl_autoload_register 。
spl_autoload_register
要使用 spl_autoload_register ,请保证你的 PHP 版本(PHP 5 >= 5.1.2)。
www.php.net 对 spl_autoload_register
的解释如下:注册 __autoload()
函数,将函数注册到 SPL __autoload 函数栈中。如果该栈中的函数尚未激活,则激活它们。刚接触 PHP 的同学肯定觉得这个解释云里雾里的,看完依然不知道什么意思。要想理解这句话,首先要弄明白 __autoload()
是个什么东西。
__autoload()
__autoload()
的作用是尝试加载未定义的类,可以通过定义这个函数来启用类的自动加载。下面举个例子:
function __autoload($class){
echo '尝试加载的类的名字是:'.$class;
}
$say= @ new say();
上例会输出:"尝试加载的类的名字是 say "。由于最后一行引用了尚未定义的类 box ,所以 __autoload()
函数将被执行。
再看下面这段
class say
{
public function __construct()
{
echo 'say 类存在,并说出了hello,所以 __autoload 函数不会执行。';
}
}
function __autoload($class)
{
echo '尝试加载的类的名字是:'.$class;
}
$say= @ new say();
这将会输出 :say 类存在,并说出了 hello, 所以 __autoload()
函数不会执行。
理解完 __autoload 就好办了,再看上面:将函数注册到 SPL __autoload 函数栈中,意思是我们可以自定义尝试加载未定义的类时 使用的函数。现在返回代码片段
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
这下是不是很明白了,当实例化一个类的时候,如果这个类没有定义,就执行 ClassLoader 类中的 loadClass 函数。 loadClass 的定义前面我们说过了,就是找到声明相关类的文件,然后包含并运行该文件,随后加载相关类。
至此,composer 的自动加载机制学习完毕。