plesk/php-scoper

为文件或目录中的所有PHP命名空间添加前缀。

维护者

详细信息

github.com/plesk/php-scoper

源代码

资助包维护!
theofidry

0.15.0 2021-05-10 20:50 UTC

README

Package version Build Status Scrutinizer Code Quality Code Coverage Slack License

PHP-Scoper是一个工具,可以将任何代码块,包括所有依赖(如vendor目录),移动到一个新的、独特的命名空间中。

目标

PHP-Scoper的目标是确保项目中的所有代码都位于一个独特的PHP命名空间中。例如,当构建包含自身依赖项的PHAR时,这是必要的;

  • 捆绑自己的供应商依赖项;
  • 从具有类似依赖项的任意PHP项目中加载/执行代码

当存在一个(可能不同版本的)包,并且该包同时存在于PHAR和执行的代码中时,将使用来自PHAR的包。这意味着这些PHAR可能会在捆绑的供应商和它们交互的项目供应商之间引起冲突,导致由于不兼容或不受支持的包版本而难以调试的问题。

目录

安装

Phive

您可以使用Phive安装PHP-Scoper

$ phive install humbug/php-scoper --force-accept-unsigned

要升级php-scoper,请使用以下命令

$ phive update humbug/php-scoper --force-accept-unsigned

PHAR

首选的安装方法是使用PHP-Scoper PHAR,可以从最新的Github发布版下载。

Composer

您可以使用Composer安装PHP-Scoper

composer global require humbug/php-scoper

如果您因为依赖冲突而无法安装它,或者您更喜欢为项目安装它,我们建议您查看bamarni/composer-bin-plugin。示例

composer require --dev bamarni/composer-bin-plugin
composer bin php-scoper config minimum-stability dev
composer bin php-scoper config prefer-stable true 
composer bin php-scoper require --dev humbug/php-scoper

但是请注意,这个库不是设计用来扩展的。

使用方法

php-scoper add-prefix

这将在当前工作目录中找到的代码中的所有相关命名空间添加前缀。添加前缀的文件将在build文件夹中可用。然后您可以使用添加前缀的代码来构建您的PHAR。

警告:在添加前缀后,如果您依赖于Composer进行自动加载,则需要再次转储自动加载器。

有关更具体的示例,您可以在Makefile中查看PHP-Scoper的构建步骤,特别是如果您使用Composer,因为运行PHP-Scoper之前和之后都有一些步骤需要考虑。

请参考TBD,深入了解从PHP-Scoper makefile中提取的命名空间限制和构建PHAR的过程。

配置

如果您需要更细致的配置,可以通过运行命令 php-scoper init 创建一个 scoper.inc.php 文件。可以使用 --config 选项传递不同的文件或位置。

<?php declare(strict_types=1);

// scoper.inc.php

use Isolated\Symfony\Component\Finder\Finder;

return [
    'prefix' => null,                       // string|null
    'finders' => [],                        // Finder[]
    'patchers' => [],                       // callable[]
    'files-whitelist' => [],                // string[]
    'whitelist' => [],                      // string[]
    'expose-global-constants' => true,   // bool
    'expose-global-classes' => true,     // bool
    'expose-global-functions' => true,   // bool
    'exclude-constants' => [],             // string[]
    'exclude-classes' => [],               // string[]
    'exclude-functions' => [],             // string[]
];

前缀

用于隔离代码的前缀。如果提供 null''(空字符串),则会自动生成随机前缀。

查找器和路径

默认情况下,运行 php-scoper add-prefix 时,它将为当前工作目录中找到的所有相关代码添加前缀。但是,您可以通过配置中的 Finders 定义哪些文件应被隔离。

<?php declare(strict_types=1);

// scoper.inc.php

use Isolated\Symfony\Component\Finder\Finder;

return [
    'finders' => [
        Finder::create()->files()->in('src'),
        Finder::create()
            ->files()
            ->ignoreVCS(true)
            ->notName('/LICENSE|.*\\.md|.*\\.dist|Makefile|composer\\.json|composer\\.lock/')
            ->exclude([
                'doc',
                'test',
                'test_old',
                'tests',
                'Tests',
                'vendor-bin',
            ])
            ->in('vendor'),
        Finder::create()->append([
            'bin/php-scoper',
            'composer.json',
        ])
    ],
];

除了查找器外,您还可以通过命令添加任何路径

php-scoper add-prefix file1.php bin/file2.php

手动添加的路径将被追加到查找器找到的路径。

修补器

在隔离 PHP 文件时,可能会遇到一些被隔离的代码间接引用原始命名空间的情况。这包括字符串或字符串操作。PHP-Scoper 对此类字符串的预处理支持有限,因此您可能需要在 scoper.inc.php 配置文件中定义 patchers,一个或多个可调用的函数,用于替换一些被隔离的代码。

以下是一个简单的示例

  • 字符串中的类名。

您可以想象从基于已知命名空间和运行时选择的变量类名的变量中实例化一个类。也许代码类似这样

$type = 'Foo'; // determined at runtime
$class = 'Humbug\\Format\\Type\\' . $type;

如果我们把 Humbug 命名空间隔离为 PhpScoperABC\Humbug,那么上面的片段将失败,因为 PHP-Scoper 无法将其解释为命名空间类。为了成功完成隔离,必须 a) 定位问题,b) 替换违规行。

解决此问题的修补代码可能是

$type = 'Foo'; // determined at runtime
$scopedPrefix = array_shift(explode('\\', __NAMESPACE__));
$class = $scopedPrefix . '\\Humbug\\Format\\Type\\' . $type;

隔离后可能会出现此类和类似问题 可能,可以通过运行隔离代码并检查问题来调试。为此,建议进行一些端到端测试以验证隔离后的代码或 PHAR。

可以通过在 scoper.inc.php 中定义合适的修补器来实现此类更改

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'patchers' => [
        function (string $filePath, string $prefix, string $content): string {
            //
            // PHP-Parser patch conditions for file targets
            //
            if ($filePath === '/path/to/offending/file') {
                return preg_replace(
                    "%\$class = 'Humbug\\\\Format\\\\Type\\\\' . \$type;%",
                    '$class = \'' . $prefix . '\\\\Humbug\\\\Format\\\\Type\\\\\' . $type;',
                    $content
                );
            }
            return $content;
        },
    ],
];

白名单文件

files-whitelist 中列出的文件,在隔离过程中其内容将保持不变。

排除符号

可以按如下方式标记符号为排除

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'exclude-classes' => ['Stringeable'],
    'exclude-functions' => ['str_contains'],
    'exclude-constants' => ['PHP_EOL'],
];

这丰富了 PHP-Scoper 的 Reflector 考虑为“内部”符号的符号列表,即 PHP 引擎或扩展符号,并将完全保持不变。

应非常小心地使用此功能,因为它可能会轻松地破坏 Composer 自动加载,并且建议仅用于补偿 PHP-Scoper 随附的 PhpStorm 的 stubs 中缺失的符号。

白名单

PHP-Scoper 的目标是确保项目中的所有代码都位于一个独特的 PHP 命名空间中。然而,您可能希望将 PHAR 的捆绑代码与消费者代码之间的公共 API 保持共享。例如,如果您有一个包含隔离代码的 PHPUnit PHAR,您仍然希望 PHAR 能够理解 PHPUnit\Framework\TestCase 类。

全局命名空间中的常量、类和函数

默认情况下,PHP-Scoper 将为属于全局命名空间的用户定义的常量、类和函数添加前缀。但是,您可以更改此设置,使它们像通常一样添加前缀,除非明确列入白名单。

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'expose-global-constants' => false,
    'expose-global-classes' => false,
    'expose-global-functions' => false,
];

符号

您可以像这样列出白名单中的类、接口、常量和函数

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'whitelist' => [
        'PHPUnit\Framework\TestCase',
        'PHPUNIT_VERSION',
        'iter\count',
        'Acme\Foo*', // Warning: will not whitelist sub namespaces like Acme\Foo\Bar but will whitelist symbols like
                     // Acme\Foo or Acme\Fooo
    ],
];

注意事项

不会 在特性上工作,并且此别名机制是不区分大小写的,即当传递 'iter\count' 时,如果找到一个类 Iter\Count,此类也将被列入白名单。

实施见解

类白名单

类别名机制如下所示

  • 通常对类或接口进行前缀
  • 在类/接口声明结束时添加一个 class_alias() 语句,将前缀符号链接到非前缀符号
  • 在注册自动加载器后立即添加一个 class_exists() 语句,以触发加载确保 class_alias() 语句执行的方法。

这样做是为了确保带前缀和已白名单的类可以共存而不破坏自动加载。 class_exists() 语句被丢弃在 vendor/scoper-autoload.php 中,不要忘记包含此文件而不是 vendor/autoload.php。但是,如果你使用的是 BoxPhpScoper 压缩器,这部分由 Box 处理。

所以,如果你有以下带有白名单类的文件

<?php

namespace Acme;

class Foo {}

作用域文件将如下所示

<?php

namespace Humbug\Acme;

class Foo {}

\class_alias('Humbug\\Acme\\Foo', 'Acme\\Foo', \false);

使用

<?php

// scoper-autoload.php @generated by PhpScoper

$loader = require_once __DIR__.'/autoload.php';

class_exists('Humbug\\Acme\\Foo');   // Triggers the autoloading of
                                    // `Humbug\Acme\Foo` after the
                                    // Composer autoload is registered

return $loader;
常量白名单

常量别名机制是通过将常量声明转换为 define() 语句来完成的,如果不是这样。请注意,这里有一个区别,因为 define() 在运行时定义常量,而 const 在编译时定义。有关差异的更多详细信息,请参阅 这里

以下文件带有白名单常量

<?php

namespace Acme;

const FOO = 'X';

作用域文件将如下所示

<?php

namespace Humbug\Acme;

\define('FOO', 'X');
函数白名单

函数别名机制是通过在 vendor/scoper-autoload.php 文件中将原始函数声明为前缀函数的别名来完成的。

给定以下带有函数声明的文件

<?php

// No namespace: this is the global namespace

if (!function_exists('dd')) {
    function dd() {}
}

文件将像往常一样作用域,以避免任何自动加载问题

<?php

namespace PhpScoperPrefix;

if (!function_exists('PhpScoperPrefix\dd')) {
    function dd() {...}
}

以下作为别名的函数将在 scoper-autoload.php 文件中声明

<?php

// scoper-autoload.php @generated by PhpScoper

$loader = require_once __DIR__.'/autoload.php';

if (!function_exists('dd')) {
    function dd() {
        return \PhpScoperPrefix\dd(...func_get_args());
    }
}

return $loader;

命名空间白名单

如果你想要更通用,并白名单整个命名空间,你可以这样做

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'whitelist' => [
        'PHPUnit\Framework\*',
    ],
];

现在,任何在 PHPUnit\Framework 命名空间下的内容都不会被前缀。注意,这也适用于全局命名空间

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'whitelist' => [
        '*',
    ],
];

请注意,这可能会导致自动加载问题。确实,如果你有以下包

{
    "autoload": {
        "psr-4": {
            "PHPUnit\\": "src"
        }
    }
}

并且白名单命名空间 PHPUnit\Framework\*,那么此包的自动加载将不正确且无法工作。为了使其正常工作,需要将整个包 PHPUnit\* 白名单化。

警告:以下 不是 命名空间白名单

<?php declare(strict_types=1);

// scoper.inc.php

return [
    'whitelist' => [
        'PHPUnit\F*',   // Will only whitelist symbols such as PHPUnit\F or PHPunit\Foo, but not symbols belonging to
                        // sub-namespaces like PHPunit\Foo\Bar
    ],
];

构建作用域 PHAR

使用Box

如果你使用 Box 来构建 PHAR,你可以使用现有的 PHP-Scoper 集成。Box 会为你处理大多数事情,所以你只需要根据你的需求调整 PHP-Scoper 配置。

不使用Box

步骤1:配置构建位置并准备供应商

假设你不需要任何开发依赖项,运行

composer install --no-dev --prefer-dist

这将允许你在作用域过程中节省时间,因为不需要处理不必要的文件。

步骤2:运行PHP-Scoper

PHP-Scoper 在前缀过程中将代码复制到新的位置,留下你的原始代码未受影响。默认位置是 ./build。你可以使用 --output-dir 选项更改默认位置。默认情况下,它还生成一个随机的前缀字符串。你可以使用 --prefix 选项设置特定的前缀字符串。如果你在自动化构建,可以设置 --force 选项以覆盖输出目录中存在的任何代码,而无需确认。

以下是基本命令,假设默认选项来自项目的根目录

bin/php-scoper add-prefix

由于没有路径参数,当前工作目录将完全作用域到 ./build。当然,实际的加前缀限制在 PHP 文件或 PHP 脚本上。其他文件保持不变,尽管我们还需要作用域某些 Composer 相关的文件。

说到作用域 Composer 相关的文件...下一步是如果依赖它,则转储 Composer 自动加载器,以确保一切按预期工作

composer dump-autoload --working-dir build --classmap-authoritative

建议

处理独立PHAR时需要管理3件事

  • 依赖项:您正在分发哪些依赖项?是使用composer.lock精细控制的依赖项,还是您总是分发带有最新依赖项的应用程序?
  • PHAR格式:存在一些不兼容性,例如realpath(),由于路径不是虚拟的,因此将不再适用于PHAR内部的文件。
  • 隔离代码:由于PHP的动态特性,隔离依赖项永远不会是一件简单的事,因此您应该有一些端到端测试来确保您的隔离代码运行正常。您可能还需要配置白名单修补程序

因此,您应该至少为您的发布PHAR进行端到端测试。

由于一次处理上述3个问题可能很繁琐,强烈建议为每个步骤进行几个测试。

例如,您可以同时为您的非隔离PHAR和隔离PHAR进行测试,这样您就可以知道哪个步骤存在问题。如果隔离PHAR无法正常工作,您可以尝试在PHAR外直接测试隔离代码,以确保作用域过程不是问题所在。

要检查隔离代码是否正确工作,您有多种解决方案

请记住,将代码捆绑到PHAR中也不保证立即工作。实际上,存在许多问题,例如

因此,您应该也

限制

动态符号

PHP-Scoper尽可能尝试为字符串添加前缀。但是,存在一些情况是不可能的,例如

  • 正则表达式中的字符串,例如/^Acme\\\\Foo/
  • 连接的字符串,例如
    • $class = 'Symfony\\Component\\'.$name;
    • const X = 'Symfony\\Component' . '\\Yaml\\Ya_1';

日期符号

您的代码可能使用日期字符串格式的约定,这可能会被误认为是类,例如

const ISO8601_BASIC = 'Ymd\THis\Z';

在这种情况下,PHP-Scoper无法判断字符串'Ymd\THis\Z'是否指的是符号,而是日期格式。在这种情况下,您将不得不依赖修补程序。请注意,PHP-Scoper将能够处理一些情况,例如,请参阅date-spec

HereDoc值

如果您考虑以下代码

<?php

<<<PHP_HEREDOC
<?php

use Acme\Foo;

PHP_HEREDOC;

PHP_HEREDOC的内容不会添加前缀。未来可能添加一些部分支持,但由于heredoc的动态性质,这将是非常有限的。例如,如果您考虑以下内容

<?php

<<<EOF
<?php

{$namespaceLine}

// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.

if (\\class_exists(\\Container{$hash}\\{$options['class']}::class, false)) {
    // no-op
} elseif (!include __DIR__.'/Container{$hash}/{$options['class']}.php') {
    touch(__DIR__.'/Container{$hash}.legacy');
    return;
}

if (!\\class_exists({$options['class']}::class, false)) {
    \\class_alias(\\Container{$hash}\\{$options['class']}::class, {$options['class']}::class, false);
}

return new \\Container{$hash}\\{$options['class']}(array(
    'container.build_hash' => '$hash',
    'container.build_id' => '$id',
    'container.build_time' => $time,
), __DIR__.\\DIRECTORY_SEPARATOR.'Container{$hash}');

EOF;

将很难正确地作用域相关的类。

可调用

如果您考虑以下两个值

['Acme\Foo', 'bar'];
'Acme\Foo::bar';

那里使用的类将不会被作用域。虽然应该不是不可能添加对该功能的支持,但目前尚未支持。请参阅#286

字符串值

PHP-Scoper尽可能尝试为字符串添加前缀

class_exists('Acme\Foo');

// Will be prefixed into:

\class_exists('Humbug\Acme\Foo');

PHP-Scoper 使用正则表达式来判断字符串是否是必须添加前缀的类名。但难免会有令人困惑的情况。例如:

  • 如果你有一个普通的字符串 'Acme\Foo',它与类无关,PHP-Parser 将无法识别,并将其添加前缀。

  • 属于全局作用域的类: 'Foo''Acme_Foo',因为除了少数方法外,无法知道它是否是类名还是随机字符串。

  • class_alias()

  • class_exists()

  • define()

  • defined()

  • function_exists()

  • interface_exists()

  • is_a()

  • is_subclass_of()

  • trait_exists()

本地函数和常量阴影

在以下示例中

<?php

namespace Foo;

is_array([]);

没有使用use语句来声明函数 is_array。这意味着PHP将尝试加载函数 \Foo\is_array,如果失败,将回退到 \is_array(请注意,PHP只为函数和常量这样做:不是类)。

为了进行一些性能优化,调用将仍然以 \is_array 的形式添加前缀。这 将会 使你的代码崩溃,如果你依赖 \Foo\is_array。然而,这种情况应该 非常罕见,所以如果发生这种情况,你有两个解决方案:使用 补丁程序 或更简单地使用use语句(在添加前缀的上下文中不需要)来消除任何歧义。

<?php

namespace Foo;

use function Foo\is_array;

is_array([]);

常量的情况也是如此。

分组常量白名单

当给出以下分组常量声明时

const X = 'foo', Y = 'bar';

PHP-Scoper 无法将 XY 加入白名单。上面的语句应该被多个常量语句替换。

const X = 'foo';
const Y = 'bar';

Composer自动加载器

PHP-Scoper 不支持对导出的Composer自动加载器和自动加载文件进行前缀添加。这就是为什么在给应用程序添加前缀后,你必须手动重新导出自定义加载器。

注意:当使用Box时,Box可以为你处理这一步骤。

PHP-Scoper 也不能处理Composer的静态文件自动加载器。这是由于Composer根据从包名和相对文件路径生成的哈希值加载文件。有关解决方法的说明,请参阅 #298

Composer插件

Composer插件不受支持。问题是,对于 白名单符号,PHP-Scoper 依赖于你应该加载 vendor/scoper-autoload.php 文件而不是 vendor/autoload.php 来触发正确类及其类别名加载。但是,Composer 并不这样做,因此接口例如 Composer\Plugin\Capability\Capable 被添加前缀,但别名没有注册。

这不容易改变,因此现在当你在使用隔离的Composer版本时,你需要使用 --no-plugins 选项。

PSR-0部分支持

截至目前,给定以下目录结构

src/
  JsonMapper.php
  JsonMapper/
    Exception.php

与以下配置

{
  "autoload": {
     "psr-0": {"JsonMapper": "src/"}
  }
}

自动加载将不会工作。实际上,PHP-Scoper 试图通过将其转换为 PSR-4 来支持 PSR-0,即在上面的情况中

{
  "autoload": {
     "psr-4": {"PhpScoperPrefix\\JsonMapper": "src/"}
  }
}

如果这适用于 src/JsonMapper/ 下的类,那么它将不适用于 JsonMapper.php

WordPress

截至目前,PHP-Scoper 不容易允许完全保留未包含的第三方代码,这给WordPress插件带来了一些障碍。关于提供更好的开箱即用集成的现有问题(见 #303),但你可以使用 WP React Starter,这是一个WordPress插件的样板。

如果您无法迁移,您可以查看解决方案本身,了解更多信息请点击此处 #303 (评论)

贡献

贡献指南

致谢

项目最初由:Bernhard Schussek (@webmozart) 创建,现在已被转移到 Humbug 旗下