Publisher

Publisher 库提供了使用强大的检测和错误检查在项目内复制文件的方法。

加载库

因为 Publisher 实例针对其源和目标,所以这个库不通过 Services 提供,而应该直接实例化或扩展。例如:

<?php

$publisher = new \CodeIgniter\Publisher\Publisher();

概念和用法

Publisher 解决了在后端框架中工作时的一些常见问题:

  • 我如何维护具有版本依赖性的项目资产?

  • 我如何管理上传和其他需要网络访问的“动态”文件?

  • 当框架或模块发生更改时,我如何更新我的项目?

  • 组件如何向现有项目注入新内容?

最基本意义上,发布就是将一个或多个文件复制到项目中。Publisher 扩展了 FileCollection 来执行流式样式的命令链,以读取、过滤和处理输入文件,然后将它们复制或合并到目标目标中。你可以根据需要在控制器或其他组件中使用 Publisher,或者通过扩展类并利用 spark publish 进行发现来规划发布。

按需使用

通过实例化该类的新实例直接访问 Publisher:

<?php

$publisher = new \CodeIgniter\Publisher\Publisher();

默认情况下,源和目标分别设置为 ROOTPATHFCPATH,使 Publisher 可以轻松获取项目中的任何文件并使其可通过 Web 访问。或者,你可以在构造函数中传递一个新的源或源和目标:

<?php

use CodeIgniter\Publisher\Publisher;

$vendorPublisher = new Publisher(ROOTPATH . 'vendor');
$filterPublisher = new Publisher('/path/to/module/Filters', APPPATH . 'Filters');

// Once the source and destination are set you may start adding relative input files
$frameworkPublisher = new Publisher(ROOTPATH . 'vendor/codeigniter4/codeigniter4');

// All "path" commands are relative to $source
$frameworkPublisher->addPath('app/Config/Cookie.php');

// You may also add from outside the source, but the files will not be merged into subdirectories
$frameworkPublisher->addFiles([
    '/opt/mail/susan',
    '/opt/mail/ubuntu',
]);
$frameworkPublisher->addDirectory(SUPPORTPATH . 'Images');

一旦所有文件都准备就绪,使用输出命令之一(copy()merge())将暂存的文件处理到它们的目标位置:

<?php

// Place all files into $destination
$frameworkPublisher->copy();

// Place all files into $destination, overwriting existing files
$frameworkPublisher->copy(true);

// Place files into their relative $destination directories, overwriting and saving the boolean result
$result = $frameworkPublisher->merge(true);

请参阅 库参考 以获取可用方法的完整描述。

自动化和发现

你可能有在应用程序部署或维护的一部分中嵌入了定期发布任务。Publisher 利用强大的 Autoloader 来定位任何准备发布的子类:

<?php

use CodeIgniter\CLI\CLI;
use CodeIgniter\Publisher\Publisher;

foreach (Publisher::discover() as $publisher) {
    $result = $publisher->publish();

    if ($result === false) {
        CLI::error(get_class($publisher) . ' failed to publish!', 'red');
    }
}

默认情况下, discover() 将在所有命名空间中搜索“Publishers”目录,但你可以指定不同的目录,它将返回找到的任何子类:

<?php

use CodeIgniter\Publisher\Publisher;

$memePublishers = Publisher::discover('CatGIFs');

大多数时候你不需要自己处理发现,只需使用提供的“publish”命令:

php spark publish

默认情况下,在你的类扩展上 publish() 将从你的 $source 添加所有文件并合并到你的目标位置,在冲突时覆盖。

安全性

为了防止模块向你的项目注入恶意代码, Publisher 包含一个配置文件,其中定义了允许作为目标的目录和文件模式。默认情况下,文件只能发布到你的项目中(以防止访问文件系统的其余部分), public/ 文件夹 (FCPATH) 只会接收以下扩展名的文件:

  • Web 资源:css、scss、js、map

  • 非可执行 Web 文件:htm、html、xml、json、webmanifest

  • 字体:ttf、eot、woff、woff2

  • 图像:gif、jpg、jpeg、tif、tiff、png、webp、bmp、ico、svg

如果你需要为项目添加或调整安全性,请更改 app/Config/Publisher.php 中的 Config\Publisher$restrictions 属性。

示例

这里有一些示例用例及其实现来帮助你开始发布。

文件同步示例

你想在主页上显示“每日照片”图像。你有每日照片的订阅源,但你需要将实际文件放入项目中可以浏览的位置,如 public/images/daily_photo.jpg。你可以设置 自定义命令 每天运行一次来处理此操作:

<?php

namespace App\Commands;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\Publisher\Publisher;
use Throwable;

class DailyPhoto extends BaseCommand
{
    protected $group       = 'Publication';
    protected $name        = 'publish:daily';
    protected $description = 'Publishes the latest daily photo to the homepage.';

    public function run(array $params)
    {
        $publisher = new Publisher('/path/to/photos/', FCPATH . 'assets/images');

        try {
            $publisher->addPath('daily_photo.jpg')->copy(true); // `true` to enable overwrites
        } catch (Throwable $e) {
            $this->showError($e);
        }
    }
}

现在运行 spark publish:daily 将使你的主页图像保持更新。如果照片来自外部 API 呢?你可以使用 addUri() 代替 addPath() 来下载远程资源并发布它:

<?php

$publisher->addUri('https://example.com/feeds/daily_photo.jpg')->copy(true);

资产依赖项示例

你想将前端库“Bootstrap”集成到你的项目中,但频繁的更新使跟踪它变得很麻烦。你可以在项目中创建发布定义来通过扩展 Publisher 来同步前端资产。所以 app/Publishers/BootstrapPublisher.php 可能如下所示:

<?php

namespace App\Publishers;

use CodeIgniter\Publisher\Publisher;

class BootstrapPublisher extends Publisher
{
    /**
     * Tell Publisher where to get the files.
     * Since we will use Composer to download
     * them we point to the "vendor" directory.
     *
     * @var string
     */
    protected $source = VENDORPATH . 'twbs/bootstrap/';

    /**
     * FCPATH is always the default destination,
     * but we may want them to go in a sub-folder
     * to keep things organized.
     *
     * @var string
     */
    protected $destination = FCPATH . 'bootstrap';

    /**
     * Use the "publish" method to indicate that this
     * class is ready to be discovered and automated.
     */
    public function publish(): bool
    {
        return $this
            // Add all the files relative to $source
            ->addPath('dist')

            // Indicate we only want the minimized versions
            ->retainPattern('*.min.*')

            // Merge-and-replace to retain the original directory structure
            ->merge(true);
    }
}

备注

在执行命令之前,必须先创建目录 $destination

现在通过 Composer 添加依赖项并调用 spark publish 来运行发布:

composer require twbs/bootstrap
php spark publish

… 然后你会在项目中得到类似下面的结果:

public/.htaccess
public/favicon.ico
public/index.php
public/robots.txt
public/
    bootstrap/
        css/
            bootstrap.min.css
            bootstrap-utilities.min.css.map
            bootstrap-grid.min.css
            bootstrap.rtl.min.css
            bootstrap.min.css.map
            bootstrap-reboot.min.css
            bootstrap-utilities.min.css
            bootstrap-reboot.rtl.min.css
            bootstrap-grid.min.css.map
        js/
            bootstrap.esm.min.js
            bootstrap.bundle.min.js.map
            bootstrap.bundle.min.js
            bootstrap.min.js
            bootstrap.esm.min.js.map
            bootstrap.min.js.map

模块部署示例

你希望允许使用你流行的身份验证模块的开发者能够扩展 Migration、Controller 和 Model 的默认行为。你可以为应用程序创建自己的模块“发布”命令来注入这些组件以供使用:

<?php

namespace Math\Auth\Commands;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\Publisher\Publisher;
use Throwable;

class AuthPublish extends BaseCommand
{
    protected $group       = 'Auth';
    protected $name        = 'auth:publish';
    protected $description = 'Publish Auth components into the current application.';

    public function run(array $params)
    {
        // Use the Autoloader to figure out the module path
        $source = service('autoloader')->getNamespace('Math\\Auth')[0];

        $publisher = new Publisher($source, APPPATH);

        try {
            // Add only the desired components
            $publisher->addPaths([
                'Controllers',
                'Database/Migrations',
                'Models',
            ])->merge(false); // Be careful not to overwrite anything
        } catch (Throwable $e) {
            $this->showError($e);

            return;
        }

        // If publication succeeded then update namespaces
        foreach ($publisher->getPublished() as $file) {
            // Replace the namespace
            $contents = file_get_contents($file);
            $contents = str_replace('namespace Math\\Auth', 'namespace ' . APP_NAMESPACE, $contents);
            file_put_contents($file, $contents);
        }
    }
}

现在当你的模块用户运行 php spark auth:publish 时,会向他们的项目添加以下内容:

app/Controllers/AuthController.php
app/Database/Migrations/2017-11-20-223112_create_auth_tables.php.php
app/Models/LoginModel.php
app/Models/UserModel.php

库参考

备注

PublisherFileCollection 的扩展,因此可以访问读取和过滤文件的所有这些方法。

支持方法

[static] discover(string $directory = ‘Publishers’): Publisher[]

发现指定命名空间目录中的所有 Publishers 并返回。例如,如果 app/Publishers/FrameworkPublisher.phpmyModule/src/Publishers/AssetPublisher.php 都存在并扩展了 Publisher,那么 Publisher::discover() 会返回每个的一个实例。

publish(): bool

处理完整的输入-过程-输出链。默认情况下,这相当于调用 addPath($source)merge(true),但子类通常会提供自己的实现。在运行 spark publish 时,会在所有发现的 Publisher 上调用 publish()。返回成功或失败。

getScratch(): string

返回临时工作区,如有必要则创建它。某些操作使用中间存储来暂存文件和更改,这提供了一个瞬态的可写目录的路径,你也可以使用它。

getErrors(): array<string, Throwable>

返回最后一次写入操作的任何错误。数组的键是导致错误的文件,值是捕获的 Throwable。使用 Throwable 的 getMessage() 来获取错误消息。

addPath(string $path, bool $recursive = true)

添加相对于 $source 的实际文件或目录指示的所有文件。如果相对路径解析为目录,则 $recursive 将包含子目录。

addPaths(array $paths, bool $recursive = true)

添加相对于 $source 的实际文件或目录指示的所有文件。如果相对路径解析为目录,则 $recursive 将包含子目录。

addUri(string $uri)

使用 CURLRequest 下载 URI 的内容到临时工作区,然后将结果文件添加到列表中。

addUris(array $uris)

使用 CURLRequest 将 URI 的内容下载到临时工作区,然后将结果文件添加到列表中。

备注

所做的 CURL 请求是一个简单的 GET,并使用响应主体作为文件内容。某些远程文件可能需要自定义请求才能正确处理。

输出文件

wipe()

$destination 中删除所有文件、目录和子目录。

重要

想清楚再使用。

copy(bool $replace = true): bool

将所有文件复制到 $destination 中。这不会重新创建目录结构,因此来自当前列表的每个文件最终都会结束在同一目标目录中。使用 $replace 会导致文件在已经存在现有文件时被覆盖。返回成功或失败,使用 getPublished()getErrors() 来排查故障。要注意基本名称冲突,例如:

<?php

use CodeIgniter\Publisher\Publisher;

$publisher = new Publisher('/home/source', '/home/destination');
$publisher->addPaths([
    'pencil/lead.png',
    'metal/lead.png',
]);

// This is bad! Only one file will remain at /home/destination/lead.png
$publisher->copy(true);

merge(bool $replace = true): bool

将所有文件以适当的相对子目录复制到 $destination 中。与 $source 匹配的任何文件都将被放置到 $destination 中的等效目录中,从而有效地创建一个“镜像”或“rsync”操作。使用 $replace 会导致文件在已经存在现有文件时被覆盖;由于目录已合并,这不会影响目标中的其他文件。返回成功或失败,使用 getPublished()getErrors() 来排查故障。

例子:

<?php

use CodeIgniter\Publisher\Publisher;

$publisher = new Publisher('/home/source', '/home/destination');
$publisher->addPaths([
    'pencil/lead.png',
    'metal/lead.png',
]);

// Results in "/home/destination/pencil/lead.png" and "/home/destination/metal/lead.png"
$publisher->merge();

修改文件

replace(string $file, array $replaces): bool

在 4.3.0 版本加入.

替换 $file 内容。第二个参数 $replaces 数组指定要搜索的字符串作为键,替换为值。

<?php

use CodeIgniter\Publisher\Publisher;

$source    = service('autoloader')->getNamespace('CodeIgniter\\Shield')[0];
$publisher = new Publisher($source, APPPATH);

$file = APPPATH . 'Config/Auth.php';

$publisher->replace(
    $file,
    [
        'use CodeIgniter\Config\BaseConfig;' . "\n" => '',
        'class App extends BaseConfig'              => 'class App extends \Some\Package\SomeConfig',
    ]
);

addLineAfter(string $file, string $line, string $after): bool

在 4.3.0 版本加入.

在包含特定字符串 $after 的行之后添加 $line

<?php

use CodeIgniter\Publisher\Publisher;

$source    = service('autoloader')->getNamespace('CodeIgniter\\Shield')[0];
$publisher = new Publisher($source, APPPATH);

$file = APPPATH . 'Config/App.php';

$publisher->addLineAfter(
    $file,
    '    public int $myOwnConfig = 1000;', // Adds this line
    'public bool $CSPEnabled = false;'     // After this line
);

addLineBefore(string $file, string $line, string $after): bool

在 4.3.0 版本加入.

在包含特定字符串 $after 的行之前添加 $line

<?php

use CodeIgniter\Publisher\Publisher;

$source    = service('autoloader')->getNamespace('CodeIgniter\\Shield')[0];
$publisher = new Publisher($source, APPPATH);

$file = APPPATH . 'Config/App.php';

$publisher->addLineBefore(
    $file,
    '    public int $myOwnConfig = 1000;', // Add this line
    'public bool $CSPEnabled = false;'     // Before this line
);