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

在创建 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 上会有一定的限制。

Issues & limitations

Too many dependencies

Dependencies with a package manager are great. It's a fantastic mechanism to re-use existing code and being able to update it easily. You should however be responsible about which and how many dependencies you are including. You are still including code and as such it can be buggy or insecure. You are becoming depend on something someone else has written and on which you may have no control over besides becoming subject to third-party problems. Packagist and GitHub are among others doing a fantastic job at reducing some of those risks, but the risk still exists nonetheless. What happen with the left-pad fiasco in the JavaScript community is a good example that things can go wrong and adding a package is not without consequences.

A second issue with dependencies is that they need to be compatible. That's Composer job. But as great as Composer is, there is some dependencies you cannot install together and the more dependencies you add the more likely you are to encounter a conflict.

TL:DR; Be responsible about which dependencies you are including and strive for few dependencies.

冲突

看一下这个示例:

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

这是两个静态分析工具包.它们不能同时安装,不然就会造成冲突,因为它们依赖于不同的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的另外一种工具,我在多个私人项目中使用了它,并且它工作得很好。

结论

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


Practice makes perfect.

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

译文地址:https://laravel-china.org/topics/7439/co...

本帖已被设为精华帖!
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 10

社区新功能?

10个月前

翻译?

10个月前
maxincai

貌似是个新功能

10个月前

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

10个月前

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

10个月前

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

10个月前

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

10个月前
lol173

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

10个月前

这个功能是真的赞!!

10个月前

  • 请注意单词拼写,以及中英文排版,参考此页
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`, 更多语法请见这里 Markdown 语法
  • 支持表情,使用方法请见 Emoji 自动补全来咯,可用的 Emoji 请见 :metal: :point_right: Emoji 列表 :star: :sparkles:
  • 上传图片, 支持拖拽和剪切板黏贴上传, 格式限制 - jpg, png, gif
  • 发布框支持本地存储功能,会在内容变更时保存,「提交」按钮点击时清空
  请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!