• Время чтения ~4 мин
  • 10.08.2022

Обмен кодом никогда не был более доступным, а установка пакетов PHP стала удобной; Однако сборка пакетов? В этом уроке я расскажу, как запустить и опубликовать новый пакет Laravel. Рассмотрим настройку и инструменты, которые вы можете использовать для обеспечения качества вашего пакета и того, что если вы создаете и публикуете что-то, вы делаете это хорошо.

Итак, что мы собираемся сделать строить?Какой пакет мы могли бы создать, который был бы достаточно простым, чтобы вам было легко изучить процесс, но имел бы достаточно частей, чтобы понять его. Мы создадим пакет с командой artisan, которая позволит нам создавать объекты передачи данных в Laravel и PHP 8.1, надеясь обновиться до PHP 8.2, как только он станет доступен.Наряду с этим у нас также будет Фасад для гидратации объектов передачи данных, именуемых здесь DTO.

Итак, с чего мы начнем при создании нового пакета? Каким должен быть наш первый шаг? Во-первых, когда я собираюсь создать пакет, мне нравится искать пакет, чтобы убедиться, что я не создаю что-то уже доступное или достаточно многофункциональное, чтобы тратить свое время.В конце концов, мы не хотим воссоздавать колесо.

Когда я уверен, что создаю что-то полезное, чего не существует, я думаю о том, что нужно моему пакету. В нашем случае наши требования относительно просты. У нас будет 3-4 основных класса, которые мы хотим создать, и все. Определение структуры вашего пакета обычно является одним из первых шагов, которые вы должны преодолеть.Как вы можете создать этот код, чтобы поделиться им с другими так, как люди привыкли? К счастью, сообщество Laravel рассказало вам об этом. Репозитории шаблонов доступны для скелетов пакетов; вам нужно только искать их. Такие компании, как Spatie и Beyond Code, предлагают одни из лучших полнофункциональных каркасов пакетов, которые сэкономят вам массу времени.

Однако в этом уроке я не буду использовать каркасный пакет, так как считаю важным научиться выполнять задачу, прежде чем использовать инструмент, который сделает эту работу за вас. Итак, мы начнем с чистого листа. Во-первых, вам нужно будет придумать имя для вашего пакета.Я собираюсь назвать свой «Инструменты объектов данных 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, поэтому установите его с помощью следующей команды композитора:

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. , так что я знаю, где я хочу, чтобы они жили. В конце концов, я не хочу переполнять корневое пространство имен.

Давайте сначала взглянем на эти файлы-заглушки, заглушку по умолчанию:

Наш DTO будет реализовывать контракт, чтобы гарантировать согласованность — то, что мне нравится делать с максимально возможным количеством классов. Кроме того, наш класс DTO является окончательным. Скорее всего, мы не захотим расширять этот класс, поэтому сделать его окончательным по умолчанию — разумный подход. Теперь давайте посмотрим на версию PHP 8.2:

<?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 доступным только для чтения, чтобы воспользоваться преимуществами новых возможностей языка.

<?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 [];
    }
}

Как мы можем это проверить? Во-первых, мы хотим установить тестовый пакет, чтобы убедиться, что мы можем написать тесты для запуска этой команды — я буду использовать pestPHP для это, но использование PHPUnit будет работать очень похожим образом.

Эта команда попросит вас разрешить вредителю использовать плагины композитора, поэтому убедитесь, что вы ответили «да», если вам нужны плагины вредителя для ваших тестов, таких как параллельное тестирование. . Далее нам понадобится пакет, который позволит нам использовать Laravel в наших тестах, чтобы убедиться, что наш пакет работает эффективно. Этот пакет называется Testbench, и я клянусь им при создании пакетов Laravel.

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

Самый простой способ инициализировать набор тестов в нашем пакете — это использовать pestPHP для его инициализации за нас. Выполните следующую консольную команду:

composer require --dev orchestra/testbench

Это создаст файл phpunit.xml и tests/Pest.php файл, который мы используем для контроля и распространения самого вредителя. Во-первых, я хотел бы внести несколько изменений в файл конфигурации PHPUnit, который будет использовать вредитель.Мне нравится добавлять следующие параметры, чтобы упростить тестирование:

./vendor/bin/pest --init

Я делаю это, потому что, если тест терпит неудачу, я хочу знать об этом немедленно. Ранние возвраты и неудачи — это то, что помогает нам создавать то, в чем мы больше уверены. Кэширование результатов ускоряет тестирование вашего пакета.Тем не менее, мне нравится каждый раз запускать свой набор тестов с нуля, чтобы убедиться, что он работает так, как я ожидаю.

stopOnFailure I set to true
cacheResults I set to false

Давайте теперь обратим наше внимание на тест по умолчанию. случай, когда нам нужно, чтобы наши тесты пакетов запускались. Создайте новый файл в разделе tests/PackageTestCase.php, чтобы нам было проще управлять нашими тестами.

Наш PackageTestCase расширяет тестовый стенд TestCase, чтобы мы могли позаимствовать поведение из пакета для создания нашего набора тестов. Затем мы регистрируем нашего поставщика службы пакетов, чтобы убедиться, что наш пакет загружен в тестовое приложение.

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,
        ];
    }
}

Теперь давайте посмотрим, как мы можем проверить это. Прежде чем мы напишем наши тесты, мы хотим убедиться, что то, что мы тестируем, охватывает текущее поведение пакета.Пока что все, что делает наш тест, — это предоставляет команду, которую можно запустить для создания нового файла. Наша структура каталогов тестов будет отражать структуру нашего пакета, поэтому создайте наш первый тестовый файл в папке 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();
});

Здесь мы используем набор данных Pest для запуска некоторых параметров, что немного похоже на поставщика данных PHPUnit. Мы перебираем каждую опцию и вызываем нашу команду, утверждая, что файл существует.Теперь мы знаем, что можем передать имя нашей команде artisan и создать DTO для использования в нашем приложении.

Наконец, мы хотим построить фасад для наш пакет, позволяющий легко гидратировать наши DTO. Наличие DTO часто является лишь половиной дела, и да, мы могли бы добавить метод к самому DTO для статического вызова, но мы можем значительно упростить этот процесс.Мы облегчим это, используя действительно полезный пакет Франка де Йонге в его Пакет Eventsauce, называемый "гидратор объекта". Чтобы установить это, выполните следующую команду композитора:

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');

Пришло время создать оболочку вокруг этого пакета, чтобы мы могли его использовать, поэтому давайте создадим новый класс в src/Hydrator/Hydrate.php, и мы также создадим контракт вместе с этим, если мы хотите поменять реализацию в любой момент. Это будет src/Contracts/HydratorContract.php. Давайте начнем с контракта, чтобы понять, что мы хотим сделать.

Все, что нам нужно, — это способ гидратации объекта, поэтому мы берем имя класса объекта и массив свойств, чтобы вернуть объект данных. Теперь давайте посмотрим на реализацию:

composer require eventsauce/object-hydrator

У нас есть объектный преобразователь, переданный в конструктор или созданный в конструкторе, который мы затем используем внутри метода заполнения. Затем метод fill использует картограф для гидратации объекта.Он прост и чист в использовании, и его можно легко воспроизвести, если в будущем мы решим использовать другой гидратор. Однако, используя это, мы хотим привязать гидратор к контейнеру, чтобы разрешить его с помощью внедрения зависимостей. Добавьте следующее в начало вашего PackageServiceProvider:

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;
}

Теперь, когда у нас есть гидратор, нам нужно создать фасад, чтобы мы могли красиво вызывать его в наших приложениях. Давайте создадим это сейчас в src/Facades/Hydrator.php

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,
        );
    }
}

Итак, наш Фасад в настоящее время возвращает реализацию гидратора в виде соуса событий, которая означает, что мы не можем решить это из контейнера, поэтому, если мы поменяем реализацию, нам нужно будет изменить фасад.Это не массовая сделка на данный момент. Затем нам нужно добавить этот псевдоним в наш файл composer.json, чтобы Laravel знал об этом при установке пакета.

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

Теперь что мы зарегистрировали наш фасад, нам нужно проверить, работает ли он как положено. Давайте рассмотрим, как мы можем проверить это. Создайте новый тестовый файл в папке tests/Facades/HydratorTest.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;
    }
}

Мы создали новый набор данных под названием strings, который возвращает массив случайных строк для использования. Мы передаем это в наш тест и пытаемся вызвать метод заполнения на нашем фасаде. Пройдя тестовый класс, мы можем создать массив свойств для гидратации. Затем мы проверяем, создан ли экземпляр и соответствует ли он нашим ожиданиям, когда мы вызываем метод toArray в DTO.Мы можем использовать API отражения, чтобы убедиться, что наш DTO создан, как и ожидалось, для нашего финального теста.

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

Теперь мы можем быть уверены, что наш пакет работает должным образом. Последнее, что нам нужно сделать, это сосредоточиться на качестве нашего кода. В большинстве своих пакетов я предпочитаю следить за тем, чтобы и стиль кодирования, и статический анализ работали, чтобы у меня был надежный пакет, которому я могу доверять.Начнем со стиля кода. Для этого мы установим относительно новый пакет под названием Laravel Pint:

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');

Мне нравится использовать PSR-12 для моего стиля кода, поэтому давайте создадим pint.json в корне нашего пакета, чтобы убедиться, что мы настроили pint для запуска стандарт, который мы хотим запустить:

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');

Теперь запустите команду pint, чтобы исправить любые проблемы со стилем кода, которые не соответствуют PSR-12:

composer require --dev laravel/pint

Наконец мы можем установить PHPStan, чтобы мы могли проверить статический анализ нашей базы кода, чтобы убедиться, что мы максимально строго и последовательно используем наши типы:

{
  "preset": "psr12"
}

Чтобы настроить PHPStan, нам нужно создать phpstan.neonв корне нашего пакета, чтобы узнать используемую конфигурацию.

./vendor/bin/pint

Наконец, мы можем запустить PHPStan, чтобы убедиться, что мы хорошо выглядим с точки зрения типов.

composer require --dev phpstan/phpstan

Если все прошло хорошо, мы должны увидеть сообщение "[OK] Нет ошибок".

parameters:
    level: 9

    paths:
        - src

Окончательный Шаги, которым я предпочитаю следовать для любой сборки пакета, — это написать мой README и добавить любые конкретные действия GitHub, которые я, возможно, захочу запустить в пакете.Я не буду добавлять их сюда, потому что они длинные и полны YAML. Однако вы можете сами заглянуть в репозиторий, чтобы увидеть, как они были созданы.

./vendor/bin/phpstan analyse

Создали ли вы какие-либо пакеты Laravel или PHP, о которых вы хотите, чтобы мы знали? Как вы подходите к разработке вашего пакета? Дайте нам знать в Твиттере!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297