怎么构建一个自己的Laravel包

这篇“怎么构建一个自己的Laravel包”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“怎么构建一个自己的Laravel包”文章吧。

创新互联公司专注于韶关企业网站建设,成都响应式网站建设,商城建设。韶关网站建设公司,为韶关等地区提供建站服务。全流程定制网站开发,专业设计,全程项目跟踪,创新互联公司专业和态度为您提供的服务

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

git init

现在我们有了一个可以使用的存储库,我们将能够将内容提交到历史版本,并允许在适当的时候对包进行版本控制。

创建一个 PHP 包需要马上做一件事:一个 composer.json 文件,它会告诉 Packagist 这个包是什么以及它需要运行什么。你可以使用命令行 Composer 工具或手动创建 Composer 文件。我通常使用命令行 composer init,因为它是一种交互式的设置方式;但是,我将显示我的 Composer 文件开头的输出,以便你可以看到结果:

{
  "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": "juststevemcd@gmail.com",
      "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 支持包,所以使用以下 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 设置了默认命名空间,所以我知道我希望它们放在哪里。毕竟我不想过度填充根命名空间。

先来快速了解一下这些存根文件,默认的命名空间为 stub:

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

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

我们如何测试这个?首先,我们要安装一个测试包,以确保我们可以编写运行此命令的测试 - 我将为此使用 pestPHP,使用 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。首先,我喜欢对 Pest 要使用的 PHPUnit 配置文件进行一些更改。我喜欢添加以下选项以使我的测试更容易:

stopOnFailure 我设置为 true
cacheResults 我设置为 false

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

现在让我们将注意力集中在一个默认测试用例上,我们需要我们的包测试来运行它。在 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 供我们在应用程序中使用。

最后,我们想为我们的包构建一个 facade,以允许我们的 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 $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,
        );
    }
}

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

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

现在我们有了 hydrator,我们需要创建一个 facade,以便我们可以在我们的应用程序中很好地调用它。现在让我们在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;
    }
}

所以我们的外观当前返回的是 Hydrator 的事件实现-这意味着我们无法从容器中解决这个问题,所以如果我们切换实现,我们将需要更改 facade。不过,就目前而言,这还不是什么大事。接下来,我们需要将此别名添加到我们的文件中,以便 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');

我们创建了一个名为 strings 的新数据集,它返回一个随机字符串数组供我们使用。我们将它传递给我们的测试并尝试在我们的 facade 上调用填充方法。传入一个测试类,我们可以创建一组属性来进行水合。然后,当我们在 DTO 上调用toArray 方法时,我们会测试该实例是否已创建以及它是否符合我们的预期。我们可以使用反射 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

以上就是关于“怎么构建一个自己的Laravel包”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注创新互联行业资讯频道。


网页名称:怎么构建一个自己的Laravel包
文章位置:http://azwzsj.com/article/ppejpj.html