Composer使用与原理分析

October 27, 2016

什么是composer

Composer 是 PHP 的一个依赖管理工具。它允许你申明项目所依赖的代码库,并根据所声明的依赖在项目中为你安装他们。composer不是包管理工具,因为真正的包位于packagist和github上面,composer相当于中间介质帮你安装你所声明的包,所以composer是一个依赖管理工具。

安装composer

composer需要php>=5.3.2,安装之前请确保你已经安装php,且版本不低于5.3.2。 composer支持多平台,这里只介绍linux上安装,我们使用composer中国全量镜像上提供的安装方式,执行以下命令: php -r "readfile('https://getcomposer.org/installer');" > composer-setup.php php composer-setup.php php -r "unlink('composer-setup.php');"

这时composer就已经下载好了,你会在当前目录找到一个composer.phar二进制文件,执行php composer.phar 即可看到composer相关信息和一些命令的用法,但是往往我们不会这么使用composer,而是通过composer + 命令这样使用,这里我们需要对composer进行全局安装,将composer.phar文件移动到PATH变量的某个目录中即可,如mv composer.phar /usr/local/bin/composer,这样你就可以在任意目录执行composer使用composer了。 composer相关信息

如何使用composer

  1. 创建composer.json文件
  2. 执行composer install安装依赖包
  3. 在php文件中引入依赖,require vendor/autoload.php

注:这里可以不用创建composer.json文件,直接通过composer require 供应商/包名即可安装依赖包,也推荐这样使用,更加方便。

配置资源镜像

由于默认的包是从国外的服务器上下载下来的,下载速度很慢,在使用composer之前请将资源下载地址配置到国内的镜像服务器上,执行: composer config -g repo.packagist composer https://packagist.phpcomposer.com 如果不加-g参数,请确保当前目录存在一个合法的composer.json文件,且该配置只对当前composer.json有效。 配置信息

demo

这里我用一个例子来演示下,比如我的项目中现在需要一个解析配置文件的通用方法,我在github上找到一个包hassankhan/config,它支持ini,xml,json,yaml,php等多种文件格式的解析(该包的具体使用方法请自行查看),下面我来安装这个包到我的项目中并解析获取一个ini配置文件的参数。 首先,在项目根目录下建立一个包含以下内容的composer.json文件,声明需要依赖的包。

{
    "require": {
        "hassankhan/config": "0.10.*"
    }
}

OK,执行composer install,然后再创建一个config.ini和index.php文件:

# config.ini
[mysql]
user=root
password=root
port=3306

[mongodb]
user=root
password=root
port=27017
<?php
// index.php
require ('vendor/autoload.php');
use Noodlehaus\Config;

$conf = Config::load('config.ini');
echo $config->get('mysql.port') . PHP_EOL;

在index.php中,我要获取config.ini中mysql区块下的port值,执行php index.php,得到以下结果,至此,我们成功的引用了这个包到我们的项目中,so easy! 3306

composer 的原理

通过上面的demo,我们已经知道如何使用composer了,但它是如何工作的呢?现在来分析下composer的原理,在执行composer install时究竟发生了什么? 安装过程 可以看出,composer先是安装依赖包,然后将安装的包信息写入lock锁文件,最后生成自动加载文件。查看当前目录下的内容,会发现多了一个composer.lock文件和一个vendor目录,这是composer自动生成的。 安装过程

composer.lock

composer.lock记录了安装的包的具体版本,包的信息,md5值等。注意:在你执行composer install时,composer会检查当前目录是否存在composer.lock文件,若存在,则根据该lock文件来安装依赖包而忽略composer.json声明的依赖;若不存在才根据composer.json中的声明来安装。

那么,如果我更改了composer.json需要用到其他的依赖包呢?执行composer update(或者通过composer require 供应商/包名称来引入)即可,update命令会同时更新composer.lock文件和vendor目录里面的内容。

vendor目录

再来看看vendor目录 vendor目录 autoload.php 需要引入的入口文件 composer 自动加载文件的目录 hasankhan 项目源代码目录

大家有没有想过,为什么只需要引入autoload.php就能调用这个包呢?composer是如何处理自动加载关系的呢?来看看autoload.php autoload 可以看到,它引入了composer/autoload_real.php文件,然后调用了这个类文件的getLoader()静态方法

<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInitba35a65e937248ede03565e79e5211fe
{
    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('ComposerAutoloaderInitba35a65e937248ede03565e79e5211fe', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInitba35a65e937248ede03565e79e5211fe', 'loadClassLoader'));

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInitba35a65e937248ede03565e79e5211fe::getInitializer($loader));
        } else {
            $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;
    }
}

看看getLoader()这个静态方法,前面几行spl_autoload_register之类的其实就是函数注册,不用细看,主要看后面几个require,它引入了几个autoload_*.php文件,composer加载依赖关系的精髓就在这几个文件中。

自动加载

composer是通过composer.json文件中的autoload字段来声明自动加载方式的。它提供了4种加载类型:

  • psr-0
  • psr-4
  • classmap
  • files

psr-0

主要用于带有命名空间的类的自动加载,对应了命名空间和目录的映射,可用于带有下划线的类。此规范已被FIG官方废弃,请使用psr-4代替。

{
    "autoload": {
        "psr-0": {
            "Monolog\\": "src/",
            "Vendor\\Namespace\\": "src/",
            "Vendor_Namespace_": "src/"
        }
    }
}

如若要查找Monolog\Logger这个class时,则其会去寻找src/Monolog/Logger.php,在生成自动加载文件时,会将命名空间与目录的映射以数组的形式写入vendor/composer/autoload_namespaces.php

psr-4

和psr-0作用一样,用于替代psr-0,但对于相同的命名空间与目录映射关系,psr-4对应的目录结构比psr-0浅,例如要查找Monolog\Logger这个class,则其会去寻找src/Logger.php,与psr-0相比少了一层命名空间目录,使用起来更加方便。 在生成自动加载文件时,会将命名空间与目录的映射以数组的形式写入vendor/composer/autoload_psr4.php

{
    "autoload": {
        "psr-4": {
            "Monolog\\": "src/"
        }
    }
}

注意:命名空间必须以\\结尾,第一个\是对第二个\进行转义,\分隔符是为了防止匹配到相似的命名空间,假设存在MonologTest这样的命名空间,若不加\\,在匹配Monolog时可能会匹配到MonologTest。

classmap

该自动加载类型用于声明不包含命名空间的类

{
    "autoload": {
        "classmap":["lib/","src/other.php"] 
    }
}

如上述classmap加载方式会包含lib目录下所有.php和.inc结尾的文件和src/other.php,在生成自动加载文件时,会将类名与文件路径的映射以数组的形式写入vendor/composer/autoload_classmap.php

files

主要用于声明全局函数的文件而不是类文件

{
    "autoload": {
        "files": ["src/functions.php"]
    }
}

这种加载方式使得src/functions.php中的函数可以在全局调用了,同样,在生成自动加载文件时,会将映射关系以数组的形式写入vendor/composer/autoload_files.php

自动加载演示

例如我的项目根目录test下的类库文件目录结构是这样的

test/
  src/
    lib/
      DB.php
    Main.php
    functions.php
  index.php
  composer.json

src目录下的内容是源代码,其代码分别如下

<?php
// DB.php
class DB
{
    static public function getInstance()
    {
        echo 'you are calling getInstance method' . PHP_EOL;
    }
}
<?php
// Main.php
namespace Ths;

class Main
{
    public function getData()
    {
        echo 'you are calling getData method' . PHP_EOL;
    }
}
<?php
// functions.php
function getCommon()
{
    echo 'you are calling getCommon method' . PHP_EOL;
}

根据类库文件,在composer.json中定义自动加载方式(其路径都是相对composer.json所在路径来定义的)

{
    "autoload": {
        "psr-4": { "Ths\\": "src/"
        },
        "classmap":["src/lib/"], 
        "files": ["src/functions.php"]
    }
}

然后执行composer install,执行完后进入vendor/composer/,查看autoload_*.php文件,均已生成自动加载对应的映射关系(因为并未定义psr-0,所以其返回的是一个空数组)。

<?php

// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Ths\\' => array($baseDir . '/src'),
);
<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'DB' => $baseDir . '/src/lib/DB.php',
);
<?php

// autoload_files.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    '5d80ba682afba25d348d62676196765b' => $baseDir . '/src/functions.php',
);
<?php

// autoload_namespaces.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
);

现在我们在index.php中调用这些文件里的几个方法,代码如下:

<?php
require 'vendor/autoload.php';
use Ths\Main;

$main = new Main();
$main->getData();   // 输出:you are calling getData method

DB::getInstance();  // 输出:you are calling getInstance method

getCommon();        // 输出:you are calling getCommon method

到这里,我们成功的定义了自动加载方式,并使用了它,我想现在大家应该很清楚了,composer的自动加载其实就是根据autoload的定义,将对应的映射关系写入各自的autoload_*.php中,然后require进来,就可以直接调用各个类中的方法了。建议大家亲自试试,对自动加载的更加容易理解。

那么,我现在需要修改类库,比如在lib/下新增了一个类文件,现在能直接调用吗?不能,因为我们的autoload_.php中并没有该类文件的映射,这时我们就要更新composer/下的autoload_.php了,执行composer dump-autoload命令即可,这样我们就可以调用新增的类库文件了。

composer.json架构

具体架构请参考官方文档,这里只讲解几个字段 autoload 自动加载 autoload-dev 开发环境自动加载 requrie 依赖声明 requrie-dev 开发环境依赖声明 比如在开发时,我们往往会写一个单元测试包,需要用到phpunit,这时可以在composer.json中这样定义

{
    "require-dev":{
        "phpunit/phpunit":"~4.0"
    },
    "autoload-dev": {
        "psr-4": { "Ths\\Test\\": "tests/"
        }
    }
}

其中,tests是自己写的单元测试。 如果composer.json包含require-dev字段,则composer install会默认安装dev声明的依赖包,如果不想这么做,在install命令后面加上–no-dev参数即可,即composer install --no-dev

(完)