PHP 中的代码依赖管理(大量的 Composer 技巧来袭)

在创建 PHP 的应用程序或库时,通常有三种依赖关系:

  • 硬依赖性:你的应用程序/库需要此依赖才能够正常运行
  • 可选的依赖关系:例如一个 PHP 库可以为不同的框架提供一个功能
  • 开发依赖:调试工具,测试框架等...

    如何管理这些依赖关系?

oIypZMwgdG.png

硬依赖性:

{  
    "require": {  
        "acme/foo": "^1.0"  
    }  
}

可选的依赖关系:

{  
    "suggest": {  
        "monolog/monolog": "Advanced logging library",  
        "ext-xml": "Required to support XML"  
    }  
}

开发依赖:

{  
    "require-dev": {  
      "monolog/monolog": "^1.0",  
      "phpunit/phpunit": "^6.0"  
    }  
}

截止到目前还是很顺利。那么什么地方会出错呢? 主要在 require-dev 上会有一定的限制。

问题和限制

过多的依赖关系

使用包管理器解决依赖是非常好的。这种方式可以很好的更新和重用代码。但是,你得对你引入了哪些包、多少包负责。你引入的这些包会有产生 bug 和不安全的风险。 除了受到第三方问题的困扰之外,你正在变得依赖别人写下来的东西,而这些东西你可能无法控制。 PackagistGitHub 对于减少这些风险做了很好的工作,不过风险依然存在. 在 JavaScript 社区中 left-pad fiasco 是一个很好的例子,添加一个包并不是完全没有影响的,因为它会导致错误的发生。

依赖的第二个问题是它们需要兼容。这是 Composer 的工作。但是 Composer 就是这样, 有一些依赖你并不能同时安装,添加越多的依赖就越可能出现冲突。

觉得段落长,直接看这里: 对你引入的依赖进行负责,并且争取少依赖。

强关系冲突

让我们来看下面的例子:

{  
    "require-dev": {  
        "phpstan/phpstan": "^1.0@dev",  
        "phpmetrics/phpmetrics": "^2.0@dev"  
    }  
}

这两个包是 静态分析工具(static analysis tools) 并且他们不能同时安装, 即产生了一个冲突 因为他们依赖不同且不兼容的 PHP-Parser版本.

这可以称作 "愚蠢的" 冲突: 只有在你试图去包含与你现有应用不兼容的依赖的时候才会发生冲突. 这两个包不需要互相兼容, 你的应用程序不会直接使用他们且他们也不会执行你的应用程序代码.

另一个例子是你写了一个 SymfonyLaravel 的连接库. 你想要包含 Symfony 和 Laravel 这两个依赖去测试他们连接:

{  
    "require-dev": {  
        "symfony/framework-bundle": "^4.0",  
        "laravel/framework": "~5.5.0" # gentle reminder that Laravel  
                                      # packages are not semver  
    }  
}

在某些情况下可能会工作但是很可能大部分的情况下都会失败. 这种场景可能有点傻因为你不 可能在同一时间同时包含这两个包并且你更不可能想要去支持这个场景.

无法测试的依赖关系

请看示例 composer.json:

{  
    "require": {  
        "symfony/yaml": "^2.8 || ^3.0"  
    },  
    "require-dev": {  
        "symfony/yaml": "^3.0"  
    }  
}

如上的示例… 只有指定的版本的Symfony YAML 组件可被安装 (可用的symfony/yaml包版本是 [3.0.0, 4.0.0[.

在一个应用中, 你大多数情况不需要关心这些.但是作为一个组件库,这可能是一个问题。事实上,这意味着,你无法在你的组件库中测试 symfony/yaml [2.8.0, 3.0.0[.

这是否确实是一个问题,很大程度上取决于你的情况。 要知道这种情况是可能发生的,并且没有有效的方法来杜绝它。 上面的例子很简单,但是如果这个symfony / yaml:^ 3.0在依赖关系树中隐藏的更深,例如:

{  
    "require": {  
        "symfony/yaml": "^2.8 || ^3.0"  
    },  
    "require-dev": {  
        "acme/foo": "^1.0"  # requires symfony/yaml ^3.0  
    }  
}

至少现在你是无法知道的。

解决方案

不使用依赖包

亲,没关系,毕竟您不是真的需要这个依赖包

PHARs

PHARs (PHP 档案)是将应用程序打包为单一文件的一种方法,如果您想了解更多,建议您查阅 PHP官方网站.

以此为例 PhpMetrics, 一个静态分析工具:

    $ wget  -o phpmetrics.phar

    $ chmod +x phpmetrics.phar  
    $ mv phpmetrics.phar /usr/local/bin/phpmetrics  
    $ phpmetrics --version  
    PhpMetrics, version 1.9.0

    # or if you want to keep the PHAR close and do not mind the .phar  
    # extension:

    $ phpmetrics.phar --version  
    PhpMetrics, version 1.9.0

警告: 将代码打包为PHAR并不像Java中的JARs将代码隔离起来,即便如此 这里有一个正在开发中的项目PHP-Scoper 去解决这个问题.

接下来让我们举个栗子来说明这个问题. 你构建了一个控制台应用程序 myapp.phar 依赖 Symfony YAML 2.8.0 执行给定的PHP脚本:


    $ myapp.phar myscript.php

您的脚本 myscript.php 正使用由Composer 引入的 Symfony YAML 4.0.0.

可能发生的是PHAR加载了一个Symfony YAML类,例如,SymfonyYamlYaml 执行您的脚本,您的脚本也依赖于SymfonyYamlYaml ,但是猜猜看,这个类早已经被加载了。这个 问题就是加载的是symfony / yaml 2.8.0这个包,而不是你的脚本需要的4.0.0。 因此,如果API不同,这将会很难打破。

TL:DR; PHARs是很好的适应于这些静态分析工具像PhpStan 或者PhpMetrics 但是由于一些依赖性的冲突,一旦代码运行是就变的不那么可靠了 (至少目前是这样!).

使用PHAR时还有一些其他的事情需要记住:

  • 它们很难跟踪,因为在Composer 中对于它们没有原生的支持。然而,存在一些解决方案例如这个Composer 插件tooly-composer-script 或者PhiVe 一个PHAR 安装程序
  • 如何管理版本取决于项目。 一些项目提供了一个具有不同稳定通道的“自我更新”命令,一些项目提供了独特的下载端点和最新版本,一些项目使用了GitHub发行版,并为每个版本发布了一个PHAR。

使用多个存储库

迄今为止最流行的技术之一。因此,我们不需要在一个 composer.json 中要求所有的桥依赖关系,而是将这个包分解到多个存储库中。

如果我们把前面的例子叫做 acme/foo,那么我们将为Symfony创建另一个包 acme/foo-bundle,为Laravel创建 acme/foo-provider

请注意,所有东西实际上仍然可以放在单个存储库中,并且只有像 Symfony 这样的其他软件包的只读存储库。

这种方法的主要优点是,它仍然相对简单,不需要任何额外的工具,除了最终存储库分配器像 splitsh,例如 Symfony,Laravel 和 PhpBB。缺点是你现在有多个包来维护,而不是一个。

调整配置

还有一种方法是使用更高级的安装和测试脚本。对比上一个例子,我们可以做一些其他的事:

#!/usr/bin/env bash  
# bin/tests.sh

# 测试核心库  
vendor/bin/phpunit --exclude-group=laravel,symfony

# 测试 Symfony 框架
composer require symfony/framework-bundle:^4.0  
vendor/bin/phpunit --group=symfony  
composer remove symfony/framework-bundle

# 测试 Laravel 框架  
composer require laravel/framework:~5.5.0  
vendor/bin/phpunit --group=symfony  
composer remove laravel/framework

在我的经验中,它是有效的,但这导致了臃肿的测试脚本,在运行中它们相对缓慢,难以维护,对新的贡献者也不太友好.

使用多个composer.json

这种方法相对来说是比较新的(在 PHP 中),主要是因为所需的工具不是现成的,所以我将在这个解决方案上进一步说明。

这个想法比较简单,例如下边:

    {  
        "autoload": {...},  
        "autoload-dev": {...},

        "require": {...},  
        "require-dev": {  
            "phpunit/phpunit": "^6.0",  
            "phpstan/phpstan": "^1.0@dev",  
            "phpmetrics/phpmetrics": "^2.0@dev"  
        }  
    }

我们将安装 phpstan/phpstanphpmetrics/phpmetrics 使用不同的 composer.json 文件。但是这首先会有一个疑问:我们把它们放在哪里?采用哪种结构?

composer-bin-plugin 便应运而生。一个非常简单的 Composer 插件,它允许您以不同的目录与一个 composer.json 进行交互。因此我们先假设我们有一个 composer.json 根文件

    {  
        "autoload": {...},  
        "autoload-dev": {...},

        "require": {...},  
        "require-dev": {  
            "phpunit/phpunit": "^6.0"  
        }  
    }

我们能够安装这个插件:

    $ composer require --dev bamarni/composer-bin-plugin

现在插件已经安装好了,每当您执行 composer bin acme smth ,它就会在子目录 vendor-bin / acme 中执行composer smth命令。 所以我们现在可以像这样安装PhpStan和PhpMetrics:


    $ composer bin phpstan require phpstan/phpstan:^1.0@dev  
    $ composer bin phpmetrics require phpmetrics/phpmetrics:^2.0@dev

这将创建以下目录结构:

    ... # projects files/directories  
    composer.json  
    composer.lock  
    vendor/  
    vendor-bin/  
        phpstan/  
            composer.json  
            composer.lock  
            vendor/  
        phpmetrics/  
            composer.json  
            composer.lock  
            vendor/

其中 vendor-bin / phpstan / composer.json 看起来像这样:

    {  
        "require": {  
            "phpstan/phpstan": "^1.0"  
        }  
    }

并且 vendor-bin/phpmetrics/composer.json 看起来像这样:

    {  
        "require": {  
            "phpmetrics/phpmetrics": "^2.0"  
        }  
    }

所以现在我们可以调用 vendor-bin / phpstan / vendor / bin / phpstanvendor-bin / phpmetrics / vendor / bin / phpstan 来轻松地使用PhpStan和PhpMetrics。

现在我们更进一步的以一个库在不同框架的引用为例

    {  
        "autoload": {...},  
        "autoload-dev": {...},

        "require": {...},  
        "require-dev": {  
            "phpunit/phpunit": "^6.0",  
            "symfony/framework-bundle": "^4.0",  
            "laravel/framework": "~5.5.0"  
        }  
    }

因此,同上 Symfony 引用的 vendor-bin/symfony/composer.json 文件:

    {  
        "autoload": {...},  
        "autoload-dev": {...},

        "require": {...},  
        "require-dev": {  
            "phpunit/phpunit": "^6.0",  
            "symfony/framework-bundle": "^4.0"  
        }  
    }

Laravel 引用的 vendor-bin/laravel/composer.json 文件:

    {  
        "autoload": {...},  
        "autoload-dev": {...},

        "require": {...},  
        "require-dev": {  
            "phpunit/phpunit": "^6.0",  
            "laravel/framework": "~5.5.0"  
        }  
    } 

我们的根 composer.json 现在应该是这样的:

    {  
        "autoload": {...},  
        "autoload-dev": {...},

        "require": {...},  
        "require-dev": {  
            "bamarni/composer-bin-plugin": "^1.0"  
            "phpunit/phpunit": "^6.0"  
        }  
    }

为了测试核心库之间的引用关系,你需要创建3个不同的单元测试文件,其中每一个都有 autoload.php(例如: Symfony 的引用文件 vendor-bin/symfony/vendor/autoload.php)。

如果你真的试试,你将会发现这种方法的一个主要缺点: 冗余配置。 确定你需要重复的根配置文件 composer.json 到其他两个 vendor-bin/{symfony,laravel}/composer.json, 调整自动加载变化的文件路径,当你需要一个新的依赖,你需要在其他的composer.json包含它。这是不可控的,所以 composer-inheritance-plugin出现了。

这个小包装插件composer-merge-pluginvendor-bin/symfony/composer.json内容合并到根composer.json。所以不是如下:

{  
    "autoload": {...},  
    "autoload-dev": {...},

    "require": {...},  
    "require-dev": {  
        "phpunit/phpunit": "^6.0",  
        "symfony/framework-bundle": "^4.0"  
    }  
}

现在是这样:

{  
    "require-dev": {  
        "symfony/framework-bundle": "^4.0",  
        "theofidry/composer-inheritance-plugin": "^1.0"  
    }  
}

其他的配置,自动加载和依赖将被包含在根 composer.json 。没有配置的, composer-inheritance-plugin是一个瘦小的包composer-merge-plugin来预配置任何使用composer-bin-plugin

您可以检查安装它需要的依赖,通过:

$ composer bin symfony show

我在很多项目中使用这个方法,像alice,不同于PhpStan和PHP-CS-Fixer这样的静态分析工具和框架桥接器。另一个例子是alice-data-fixtures,其中有很多不同的ORM桥持久层(Doctrine ORM, Doctrine ODM, Eloquent ORM,等)和框架的整合。

作为替代phars的另外一种工具,我在多个私人项目中使用了它,并且它工作得很好。

结论

我相信有些人会发现一些奇怪的方法或不推荐使用它们。 这里的目标不是判断或者推荐一个特殊的东西,而是列出一些可能的方法来管理一些依赖关系,以及每个依赖关系的优点和缺点。 所以,根据你的问题和你的个人喜好,选择一个最适合你的。 正如人们所说,没有解决办法,只有权衡。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/@tfidry/managing-your...

译文地址:https://learnku.com/laravel/t/7439/code-...

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 10

翻译最好能有一个原文对照的功能

6年前 评论

社区新功能?

6年前 评论

翻译最好能有一个原文对照的功能

6年前 评论

我擦,这个功能牛逼了。。Summer站长一出手,全体敬礼啊。。

6年前 评论

觉得应该出一个规范..要不"您"和"你"就不统一...

6年前 评论

看了两段,往下一翻,果然是翻译;读起来还是有点生硬.

6年前 评论
lol173

要是可以中英文切换就更棒了,社区越来越好了,哈哈哈

6年前 评论

这个功能是真的赞!!

6年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!