构建你自己的Laravel扩展包的流程步骤

2023-06-01 00:00:00 自己的 步骤 构建

共享代码变得前所未有的方便,安装 PHP 包变得更加方便;但是构建软件包?在本教程中,我将介绍如何启动和发布新的 Laravel 包。通过设置和工具,您可以使用来确保您的包质量,并且如果您构建和发布某些东西,那么您会做得很好。


那么我们要构建什么?我们可以创建什么包,它足够简单,您会发现该过程很容易学习,但有足够的部分来理解它。我们将使用 artisan 命令构建一个包,它允许我们在 Laravel 和 PHP 8.1 中创建数据传输对象,希望在 PHP 8.2 可用时尽快升级到它。除此之外,我们还将有一个用于水合数据传输对象的 Facade,这里称为 DTO。


那么,我们在构建新包时从哪里开始呢?我们的第一步应该是什么?首先,当我要创建一个包时,我喜欢做的是搜索 packageagist,以确保我没有构建一些已经可用或功能丰富的东西,以至于我会浪费我的时间。毕竟我们不想重新创建轮子。


一旦我确定我正在构建一些不存在的有用的东西,我就会考虑我的包需要什么。在我们的例子中,我们的要求相对简单。我们将要创建 3-4 个主要类,仅此而已。决定你的包的结构通常是你必须克服的第一步。您如何创建此代码以以人们习惯的方式与他人共享?幸运的是,Laravel 社区已经为您提供了相关信息。模板库可用于包骨架;你只需要搜索它们。Spatie 和 Beyond Code 等公司拥有一些功能齐全的最佳软件包框架,可为您节省大量时间。


但是,在本教程中,我不会使用骨架包,因为我觉得在使用工具为您完成工作之前学习如何完成任务是必不可少的。所以我们将从一张白纸开始。首先,您需要为您的包裹起一个名字。我最终将把我的“Laravel 数据对象工具”称为“Laravel 数据对象工具”,因为我想构建一个工具集,以便能够更轻松地在我的应用程序中使用 DTO。它告诉人们我的包的目的是什么,并允许我随着时间的推移扩展它的范围。


使用您的包名称创建一个新目录,然后在您选择的代码编辑器中打开它,以便我们开始设置。我对任何新包做的第一件事是将其初始化为 git 存储库,因此运行以下 git 命令:

git init

现在我们有了一个可以使用的存储库,我们知道我们将能够将内容提交到源代码控制并允许我们在适当的时候对我们的包进行版本控制。创建一个 PHP 包需要马上做一件事,一个composer.json告诉 Packagist 这个包是什么以及它需要运行什么的文件。您可以使用命令行作曲家工具或手动创建作曲家文件。我通常使用命令行composer init,因为它是一种交互式设置方式;但是,我将显示我的作曲家文件开头的输出,以便您可以看到结果:

{
  "name": "juststeveking/laravel-data-object-tools",
  "description": "A set of tools to make working with Data Transfer Objects easier in Laravel",
  "type": "library",
  "license": "MIT",
  "authors": [
    {
      "role": "Developer",
      "name": "Steve McDougall",
      "email": "[email protected]",
      "homepage": "https://www.juststeveking.uk/"
    }
  ],
  "autoload": {
    "psr-4": {
      "JustSteveKing\\DataObjects\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "JustSteveKing\\DataObjects\\Tests\\": "tests/"
    }
  },
  "require": {
    "php": "^8.1"
  },
  "require-dev": {},
  "minimum-stability": "dev",
  "prefer-stable": true,
  "config": {
    "sort-packages": true,
    "preferred-install": "dist",
    "optimize-autoloader": true
  }
}

这是我的大多数包的基础,无论是 Laravel 还是普通的 PHP 包,它都以一种我知道我会保持一致性的方式设置我。我们需要在我们的包中添加一些支持文件才能开始。首先,我们需要添加我们的.gitignore文件,以便我们可以告诉版本控制我们不想提交哪些文件和目录:

/vendor/
/.idea
composer.lock

这是我们要忽略的文件的开始。我正在使用 PHPStorm,它将添加一个名为的元目录,该目录.idea将包含我的 IDE 理解我的项目所需的所有信息——我不想提交版本控制。接下来,我们需要添加一些 git 属性,以便版本控制知道如何处理我们的存储库。这被称为.gitattributes:

* text=auto
*.md diff=markdown
*.php diff=php
/.github export-ignore
/tests export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
CHANGELOG.md export-ignore
phpunit.xml export-ignore

创建版本时,我们会告诉源代码控制提供者我们想要忽略哪些文件以及如何处理差异。最后,我们的最后一个支持文件将是我们.editorconfig的文件,它告诉我们的代码编辑器如何处理我们正在编写的文件:

root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml,json}]
indent_size = 2

现在我们有了版本控制和编辑器的支持文件,我们可以开始考虑我们的包在依赖项方面需要什么。我们的包将依赖哪些依赖项,以及我们使用哪些版本?让我们开始吧。

当我们正在构建一个 Laravel 包时,我们首先需要的是 Laravel Support 包,所以使用以下 composer 命令安装它:

composer require illuminate/support

现在我们有一些东西要开始了,让我们看看我们的包需要的代码的第一个重要部分;服务提供者。服务提供者是任何 Laravel 包的关键部分,因为它告诉 Laravel 如何加载包以及可用的包。首先,我们想让 Laravel 知道我们有一个安装后可以使用的控制台命令。我打电话给我的服务提供商PackageServiceProvider,因为我没有想象力,而且命名很难。如果您愿意,请随意更改您自己的命名。我添加了我的服务提供者,src/Providers因为它熟悉 Laravel 应用程序。

declare(strict_types=1);
 
namespace JustSteveKing\DataObjects\Providers;
 
use Illuminate\Support\ServiceProvider;
use JustSteveKing\DataObjects\Console\Commands\DataTransferObjectMakeCommand;
 
final class PackageServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands(
                commands: [
                    DataTransferObjectMakeCommand::class,
                ],
            );
        }
    }
}

我通常将我知道不希望扩展的类作为最终类,因为这样做会改变我希望包的操作方式。您不需要这样做。这是您需要为自己做出的判断。所以我们现在注册了一个命令。我们应该考虑创建它。从命名中可以看出,它是一个将为我们生成其他类的命令——与典型的工匠命令略有不同。


我创建了一个名为 的类DataTransferObjectMakeCommand,它非常罗嗦,但解释了它在内部的作用src/Console/Commands。如您所见,在创建这些类时,我尝试反映 Laravel 开发人员熟悉的目录结构。这样做会使使用包变得更加容易。让我们看一下这个命令的代码:

declare(strict_types=1);
 
namespace JustSteveKing\DataObjects\Console\Commands;
 
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
 
final class DataTransferObjectMakeCommand extends GeneratorCommand
{
    protected $signature = "make:dto {name : The DTO Name}";
 
    protected $description = "Create a new DTO";
 
    protected $type = 'Data Transfer Object';
 
    protected function getStub(): string
    {
        $readonly = Str::contains(
            haystack: PHP_VERSION,
            needles: '8.2',
        );
 
        $file = $readonly ? 'dto-82.stub' : 'dto.stub';
 
        return __DIR__ . "/../../../stubs/{$file}";
    }
 
    protected function getDefaultNamespace($rootNamespace): string
    {
        return "{$rootNamespace}\\DataObjects";
    }
}

让我们通过这个命令来了解我们正在创建什么。我们的命令想要扩展,GeneratorCommand因为我们想要生成一个新文件。这有助于理解,因为关于如何执行此操作的文档很少。对于这个命令,我们唯一需要的是一个名为的方法getStub- 这是命令需要知道如何加载存根文件的位置以帮助生成文件的方法。我在包的根目录中创建了一个名为stubs,Laravel 应用程序熟悉的地方。您将在这里看到我正在检查已安装的 PHP 版本,以查看我们是否使用 PHP 8.2,如果是 - 我们希望加载正确的存根版本以利用只读类。现在发生这种情况的可能性非常低 - 但是,我们离我们并不遥远。这种方法有助于为特定的 PHP 版本生成文件,因此您可以确保支持您希望支持的每个版本。


最后,我已经为我的 DTO 设置了默认命名空间,所以我知道我希望它们放在哪里。毕竟我不想过度填充根命名空间。

让我们先快速浏览一下这些存根文件,默认存根:

<?php
 
declare(strict_types=1);
 
namespace {{ namespace }};
 
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
final class {{ class }} implements DataObjectContract
{
    public function __construct(
        //
    ) {}
 
    public function toArray(): array
    {
        return [];
    }
}

我们的 DTO 将实施一个契约来保证一致性——我喜欢尽可能多地使用这些类。此外,我们的 DTO 课程是最终课程。我们可能不想扩展这个类,所以默认情况下将其设为 final 是一种明智的做法。现在让我们看一下 PHP 8.2 版本:

<?php
 
declare(strict_types=1);
 
namespace {{ namespace }};
 
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
 
readonly class {{ class }} implements DataObjectContract
{
    public function __construct(
        //
    ) {}
 
    public function toArray(): array
    {
        return [];
    }
}

这里唯一的区别是我们将 DTO 类设为只读以利用该语言的新特性。

我们如何测试这个?首先,我们要安装一个测试包,以确保我们可以编写运行此命令的测试 - 我将为此使用pesPHP,但使用 PHPUnit 将以非常相似的方式工作。

composer require pestphp/pest --dev --with-all-dependencies

此命令将要求您允许 Pest 使用 Composer 插件,因此如果您需要 Pest 插件进行测试(例如并行测试),请确保您对此表示同意。接下来,我们需要一个允许我们在测试中使用 Laravel 的包,以确保我们的包有效地工作。这个包叫做 Testbench,是我在构建 Laravel 包时发誓的。

composer require --dev orchestra/testbench

在我们的包中初始化测试套件的最简单方法是使用pesPHP 为我们初始化它。运行以下控制台命令:

./vendor/bin/pest --init

这将生成phpunit.xml文件和tests/Pest.php我们用来控制和扩展害虫本身的文件。首先,我喜欢对 Pest 将使用的 PHPUnit 配置文件进行一些更改。我喜欢添加以下选项以使我的测试更容易:

stopOnFailure我设置为真
cacheResults我设置为假

我这样做是因为如果测试失败,我想立即知道它。早期的退货和失败有助于我们构建我们更有信心的东西。缓存结果可以加速你的包的测试。但是,我喜欢确保每次都从头开始运行我的测试套件,以确保它按我的预期工作。

现在让我们将注意力集中在一个默认测试用例上,我们需要我们的包测试来运行它。在下面创建一个新文件,tests/PackageTestCase.php以便我们可以更轻松地控制我们的测试。

declare(strict_types=1);
 
namespace JustSteveKing\DataObjects\Tests;
 
use JustSteveKing\DataObjects\Providers\PackageServiceProvider;
use Orchestra\Testbench\TestCase;
 
class PackageTestCase extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [
            PackageServiceProvider::class,
        ];
    }
}

我们PackageTestCase扩展了测试台TestCase,因此我们可以从包中借用行为来构建我们的测试套件。然后我们注册我们的包服务提供者,以确保我们的包被加载到测试应用程序中。

现在让我们看看如何测试它。在我们编写测试之前,我们要确保我们测试的内容涵盖了包的当前行为。到目前为止,我们所做的所有测试都是提供一个可以运行以创建新文件的命令。我们的测试目录结构将反映我们的包结构,所以在下面创建我们的第一个测试文件

tests/Console/Commands/DataTransferObjectMakeCommandTest.php,

让我们开始我们的第一个测试。

在我们编写第一个测试之前,我们需要编辑tests/Pest.php文件以确保我们的测试套件PackageTestCase正确使用我们的。

declare(strict_types=1);
 
use JustSteveKing\DataObjects\Tests\PackageTestCase;
 
uses(PackageTestCase::class)->in(__DIR__);

首先,我们要确保我们可以运行我们的命令并且它成功运行。所以添加以下测试:

declare(strict_types=1);
 
use JustSteveKing\DataObjects\Console\Commands\DataTransferObjectMakeCommand;
 
use function PHPUnit\Framework\assertTrue;
 
it('can run the command successfully', function () {
    $this
        ->artisan(DataTransferObjectMakeCommand::class, ['name' => 'Test'])
        ->assertSuccessful();
});

我们正在测试当我们调用这个命令时,它运行时没有错误。如果您问我,这是最关键的测试之一,如果它出错,则意味着出现问题。

既然我们知道我们的测试可以运行,我们还想确保创建了类。所以让我们接下来编写这个测试:

declare(strict_types=1);
 
use Illuminate\Support\Facades\File;
use JustSteveKing\DataObjects\Console\Commands\DataTransferObjectMakeCommand;
 
use function PHPUnit\Framework\assertTrue;
 
it('create the data transfer object when called', function (string $class) {
    $this->artisan(
        DataTransferObjectMakeCommand::class,
        ['name' => $class],
    )->assertSuccessful();
 
    assertTrue(
        File::exists(
            path: app_path("DataObjects/$class.php"),
        ),
    );
})->with('classes');

这里我们使用 Pest Dataset 来运行一些选项,有点像 PHPUnit Data Provider。我们遍历每个选项并调用我们的命令,断言文件存在。我们现在知道可以将名称传递给我们的 artisan 命令并创建一个 DTO 供我们在应用程序中使用。

最后,我们想为我们的包构建一个外观,以允许我们的 DTO 轻松水合。拥有 DTO 通常只是成功的一半,是的,我们可以向 DTO 本身添加一个方法来静态调用 - 但我们可以大大简化这个过程。我们将通过使用Frank de Jonge在他的Eventsauce 包中的一个非常有用的包来促进这一点,称为“对象水合器”。要安装它,请运行以下 composer 命令:

composer require eventsauce/object-hydrator

是时候围绕这个包构建一个包装器,以便我们可以很好地使用它,所以让我们在 下创建一个新类src/Hydrator/Hydrate.php,如果我们想在任何时候交换实现,我们还将创建一个合约。这将是src/Contracts/HydratorContract.php。让我们从合约开始,了解我们想要它做什么。

declare(strict_types=1);
 
namespace JustSteveKing\DataObjects\Contracts;
 
interface HydratorContract
{
    /**
     * @param class-string<DataObjectContract> $class
     * @param array $properties
     * @return DataObjectContract
     */
    public function fill(string $class, array $properties): DataObjectContract;
}

我们所需要的只是一种水合对象的方法,因此我们使用对象的类名和属性数组来返回一个数据对象。现在让我们看一下实现:

declare(strict_types=1);
 
namespace JustSteveKing\DataObjects\Hydrator;
 
use EventSauce\ObjectHydrator\ObjectMapperUsingReflection;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
use JustSteveKing\DataObjects\Contracts\HydratorContract;
 
class Hydrate implements HydratorContract
{
    public function __construct(
        private readonly ObjectMapperUsingReflection $mapper = new ObjectMapperUsingReflection(),
    ) {}
 
    public function fill(string $class, array $properties): DataObjectContract
    {
        return $this->mapper->hydrateObject(
            className: $class,
            payload: $properties,
        );
    }
}

我们有一个对象映射器传递给构造函数或在构造函数中创建 - 然后我们在填充方法中使用它。然后填充方法使用映射器来水合对象。它使用简单干净,如果我们将来选择使用不同的保湿剂,可以轻松复制。但是,使用它,我们希望将 hydrator 绑定到容器中,以允许我们使用依赖注入来解决它。将以下内容添加到您的顶部PackageServiceProvider:

public array $bindings = [
    HydratorContract::class => Hydrate::class,
];

现在我们有了 hydrator,我们需要创建一个外观,以便我们可以在我们的应用程序中很好地调用它。让我们现在在下面创建它src/Facades/Hydrator.php

declare(strict_types=1);
 
namespace JustSteveKing\DataObjects\Facades;
 
use Illuminate\Support\Facades\Facade;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
use JustSteveKing\DataObjects\Hydrator\Hydrate;
 
/**
 * @method static DataObjectContract fill(string $class, array $properties)
 *
 * @see \JustSteveKing\DataObjects\Hydrator\Hydrate;
 */
final class Hydrator extends Facade
{
    /**
     * @return class-string
     */
    protected static function getFacadeAccessor(): string
    {
        return Hydrate::class;
    }
}

所以我们的 Facade 当前正在返回 hydrator 的事件酱实现——这意味着我们无法从容器中解决这个问题,所以如果我们切换实现,我们将需要更改外观。不过,目前这并不是一笔大买卖。接下来,我们需要将此别名添加到我们的composer.json文件中,以便 Laravel 在我们安装包时知道它。

"extra": {
  "laravel": {
    "providers": [
      "JustSteveKing\\DataObjects\\Providers\\PackageServiceProvider"
    ],
    "aliases": [
      "JustSteveKing\\DataObjects\\Facades\\Hydrator"
    ]
  }
},

现在我们已经注册了 Facade,我们需要测试它是否按预期工作。让我们来看看如何测试它。在 下新建一个测试文件tests/Facades/HydratorTest.php,我们开始吧:

declare(strict_types=1);
 
use JustSteveKing\DataObjects\Facades\Hydrator;
use JustSteveKing\DataObjects\Tests\Stubs\Test;
 
it('can create a data transfer object', function (string $string) {
    expect(
        Hydrator::fill(
            class: Test::class,
            properties: ['name' => $string],
        ),
    )->toBeInstanceOf(Test::class)->toArray()->toEqual(['name' => $string]);
})->with('strings');

我们创建了一个名为字符串的新数据集,它返回一个随机字符串数组供我们使用。我们将它传递给我们的测试并尝试在我们的外观上调用填充方法。传入一个测试类,我们可以创建一组属性来进行水合。然后,我们测试该实例是否已创建,并且当我们toArray在 DTO 上调用该方法时它是否符合我们的预期。我们可以使用反射 API 来确保为最终测试按预期创建 DTO。

it('creates our data transfer object as we would expect', function (string $string) {
    $test = Hydrator::fill(
        class: Test::class,
        properties: ['name' => $string],
    );
 
    $reflection = new ReflectionClass(
        objectOrClass: $test,
    );
 
    expect(
        $reflection->getProperty(
            name: 'name',
        )->isReadOnly()
    )->toBeTrue()->and(
        $reflection->getProperty(
            name: 'name',
        )->isPrivate(),
    )->toBeTrue()->and(
        $reflection->getMethod(
            name: 'toArray',
        )->hasReturnType(),
    )->toBeTrue();
})->with('strings');

我们现在可以确定我们的包按预期工作。我们需要做的最后一件事是关注代码的质量。在我的大多数包中,我喜欢确保编码风格和静态分析都在运行,这样我就有了一个值得信赖的可靠包。让我们从代码样式开始。为此,我们将安装一个名为Laravel Pint的包,它相对较新:

composer require --dev laravel/pint

我喜欢使用 PSR-12 作为我的代码风格,所以让我们pint.json在包的根目录中创建一个,以确保我们配置 pint 以运行我们想要运行的标准:

{
  "preset": "psr12"
}

现在运行 pint 命令来修复任何不符合 PSR-12 的代码样式问题:

./vendor/bin/pint

最后,我们可以安装PHPStan,以便我们可以检查代码库的静态分析,以确保我们尽可能严格并与我们的类型保持一致:

composer require --dev phpstan/phpstan

要配置 PHPStan,我们需要phpstan.neon在包的根目录中创建一个以了解正在使用的配置。

parameters:
    level: 9
    paths:
        - src

最后,我们可以运行 PHPStan 以确保从类型的角度来看我们看起来不错。

./vendor/bin/phpstan analyse

如果一切顺利,我们现在应该会看到一条消息“[OK] No errors”。

对于任何包构建,我喜欢遵循的最后一步是编写我的 README 并添加我可能希望在包上运行的任何特定 GitHub 操作。我不会在这里添加它们,因为它们很长并且充满了 YAML。但是,您可以自己查看存储库以了解它们是如何创建的。

你是否构建了任何你想让我们知道的 Laravel 或 PHP 包?你如何处理你的包开发?在推特上告诉我们!


转:

https://laravel-news.com/building-your-own-laravel-packages

相关文章