聊聊如何構建一個自己的 Laravel 包 ?

聊聊如何構建一個自己的 Laravel 包 ?

共享代碼變得前所未有的方便,安裝 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 工具或手動創建 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":?{ ??????"JustSteveKingDataObjects":?"src/" ????} ??}, ??"autoload-dev":?{ ????"psr-4":?{ ??????"JustSteveKingDataObjectsTests":?"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?JustSteveKingDataObjectsProviders;  use?IlluminateSupportServiceProvider; use?JustSteveKingDataObjectsconsoleCommandsDataTransferObjectMakeCommand;  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?JustSteveKingDataObjectsConsoleCommands;  use?IlluminateConsoleGeneratorCommand; use?IlluminateSupportStr;  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:

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

我們的 DTO 將實施一個契約來保證一致性——我喜歡盡可能多地使用這些類。此外,我們的 DTO 類是 final 類。我們可能不想擴展這個類,所以默認情況下將其設為 final 是一種明智的做法。現在讓我們看一下 PHP 8.2 版本:

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

這里唯一的區別是我們將 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?JustSteveKingDataObjectsTests;  use?JustSteveKingDataObjectsProvidersPackageServiceProvider; use?OrchestraTestbenchTestCase;  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?JustSteveKingDataObjectsTestsPackageTestCase;  uses(PackageTestCase::class)-&gt;in(__DIR__);

首先,要確保我們的命令可以運行并且運行成功。所以添加以下測試:

declare(strict_types=1);  use?JustSteveKingDataObjectsConsoleCommandsDataTransferObjectMakeCommand;  use?function?PHPUnitFrameworkssertTrue;  it('can?run?the?command?successfully',?function?()?{ ????$this ????????-&gt;artisan(DataTransferObjectMakeCommand::class,?['name'?=&gt;?'Test']) ????????-&gt;assertSuccessful(); });

我們正在測試當我們調用這個命令時,運行沒有錯誤。如果您問我,這是最關鍵的測試之一,如果它出錯,則意味著出現問題。

既然我們知道我們的測試可以運行,我們還想確保創建了類。所以讓我們接下來編寫這個測試:

declare(strict_types=1);  use?IlluminateSupportFacadesFile; use?JustSteveKingDataObjectsConsoleCommandsDataTransferObjectMakeCommand;  use?function?PHPUnitFrameworkssertTrue;  it('create?the?data?transfer?object?when?called',?function?(string?$class)?{ ????$this-&gt;artisan( ????????DataTransferObjectMakeCommand::class, ????????['name'?=&gt;?$class], ????)-&gt;assertSuccessful();  ????assertTrue( ????????File::exists( ????????????path:?app_path("DataObjects/$class.php"), ????????), ????); })-&gt;with('classes');

這里我們使用 Pest Dataset 來運行一些選項,有點像 PHPUnit Data Provider。我們遍歷每個選項并調用我們的命令,斷言文件存在。我們現在知道可以將名稱傳遞給我們的 artisan 命令并創建一個 DTO 供我們在應用程序中使用。

最后,我們想為我們的包構建一個 facade,以允許我們的 DTO 輕松水合。擁有 DTO 通常只是成功的一半,是的,我們可以向 DTO 本身添加一個方法來靜態調用 – 但我們可以大大簡化這個過程。我們將通過 pestPHP 在他的 pestPHP 中使用一個非常有用的包來促進這一點,稱為「對象保濕劑」。請運行以下 composer 命令安裝它:

composer?require?eventsauce/object-hydrator

是時候圍繞這個包構建一個包裝器,以便我們可以很好地使用它,所以讓我們在 src/Hydrator/Hydrate.php 下創建一個新類,如果需要,我們還將創建一個契約在任何時候交換實現。這將是src/Contracts/HydratorContract.php。讓我們從契約開始,了解我們想要它做什么。

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

我們所需要的只是一種水合對象的方法,因此我們使用對象的類名和一組屬性來返回一個數據對象。現在讓我們看一下實現:

declare(strict_types=1);  namespace?JustSteveKingDataObjectsHydrator;  use?EventSauceObjectHydratorObjectMapperUsingReflection; use?JustSteveKingDataObjectsContractsDataObjectContract; use?JustSteveKingDataObjectsContractsHydratorContract;  class?Hydrate?implements?HydratorContract { ????public?function?__construct( ????????private?readonly?ObjectMapperUsingReflection?$mapper?=?new?ObjectMapperUsingReflection(), ????)?{}  ????public?function?fill(string?$class,?array?$properties):?DataObjectContract ????{ ????????return?$this-&gt;mapper-&gt;hydrateObject( ????????????className:?$class, ????????????payload:?$properties, ????????); ????} }

我們有一個對象映射器傳遞給構造函數或在構造函數中創建 – 然后我們在填充方法中使用它。然后填充方法使用映射器來水合對象。它使用簡單干凈,如果我們將來選擇使用不同的保濕器,可以輕松復制。但是,使用這種方式,我們希望將水化器綁定到容器中,以允許我們使用依賴注入來解決它。將以下內容添加到 PackageServiceProvider 的頂部:

public?array?$bindings?=?[ ????HydratorContract::class?=&gt;?Hydrate::class, ];

現在我們有了 hydrator,我們需要創建一個 facade,以便我們可以在我們的應用程序中很好地調用它。現在讓我們在 src/Facades/Hydrator.php 下創建它

declare(strict_types=1);  namespace?JustSteveKingDataObjectsFacades;  use?IlluminateSupportFacadesFacade; use?JustSteveKingDataObjectsContractsDataObjectContract; use?JustSteveKingDataObjectsHydratorHydrate;  /** ?*?@method?static?DataObjectContract?fill(string?$class,?array?$properties) ?* ?*?@see?JustSteveKingDataObjectsHydratorHydrate; ?*/ final?class?Hydrator?extends?Facade { ????/** ?????*?@return?class-string ?????*/ ????protected?static?function?getFacadeAccessor():?string ????{ ????????return?Hydrate::class; ????} }

所以我們的外觀當前返回的是 Hydrator 的事件實現-這意味著我們無法從容器中解決這個問題,所以如果我們切換實現,我們將需要更改 facade。不過,就目前而言,這還不是什么大事。接下來,我們需要將此別名添加到我們的文件中,以便 Laravel 在我們安裝軟件包時知道它。

"extra":?{ ??"laravel":?{ ????"providers":?[ ??????"JustSteveKingDataObjectsProvidersPackageServiceProvider" ????], ????"aliases":?[ ??????"JustSteveKingDataObjectsFacadesHydrator" ????] ??} },

現在我們已經注冊了 Facade,我們需要測試它是否按預期工作。讓我們來看看如何測試它。在 tests/Facades/HydratorTest.php 下創建一個新的測試文件,讓我們開始吧:

declare(strict_types=1);  use?JustSteveKingDataObjectsFacadesHydrator; use?JustSteveKingDataObjectsTestsStubsTest;  it('can?create?a?data?transfer?object',?function?(string?$string)?{ ????expect( ????????Hydrator::fill( ????????????class:?Test::class, ????????????properties:?['name'?=&gt;?$string], ????????), ????)-&gt;toBeInstanceOf(Test::class)-&gt;toArray()-&gt;toEqual(['name'?=&gt;?$string]); })-&gt;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'?=&gt;?$string], ????);  ????$reflection?=?new?ReflectionClass( ????????objectOrClass:?$test, ????);  ????expect( ????????$reflection-&gt;getProperty( ????????????name:?'name', ????????)-&gt;isReadOnly() ????)-&gt;toBeTrue()-&gt;and( ????????$reflection-&gt;getProperty( ????????????name:?'name', ????????)-&gt;isPrivate(), ????)-&gt;toBeTrue()-&gt;and( ????????$reflection-&gt;getMethod( ????????????name:?'toArray', ????????)-&gt;hasReturnType(), ????)-&gt;toBeTrue(); })-&gt;with('strings');

我們現在可以確定我們的包按預期工作。我們需要做的最后一件事是關注代碼的質量。在我的大多數包中,我喜歡確保編碼風格和靜態分析都在運行,這樣我就有了一個值得信賴的可靠包。讓我們從代碼樣式開始。為此,我們將安裝一個名為 pestPHP 的相對較新的軟件包:

composer?require?--dev?laravel/pint

我喜歡使用 PSR-12 作為我的代碼風格,所以讓我們在包的根目錄中創建一個 pint.json 以確保我們配置 pint 以運行我們想要運行的標準:

{ ??"preset":?"psr12" }

現在運行 pint 命令來修復任何不符合 PSR-12 的代碼樣式問題:

./vendor/bin/pint

最后,我們可以安裝 pestPHP,這樣我們就可以靜態分析我們的代碼,以確保我們盡可能嚴格并與我們的類型保持一致:

composer?require?--dev?phpstan/phpstan

要配置 PHPStan,我們需要在包的根目錄中創建一個 phpstan.neon 以了解正在使用的配置。

parameters: ????level:?9 ????paths: ????????-?src

最后,我們可以運行 PHPStan 來分析我們的代碼

./vendor/bin/phpstan?analyse

如果一切順利,我們現在應該會看到 [OK] No errors

對于任何包的構建,我喜歡遵循的最后一步是編寫我的 README 并添加我希望在包上運行的 GitHub 操作。
我不會在這里添加它們,因為它們很長并且充滿了 YAML。
但是,您可以查看 pestPHP 以了解它們是如何創建的。

你是否構建了 Laravel 或 PHP 軟件包 并想讓我們了解 ?
你是如何開發你的軟件包的 ?
在 Twitter 上告訴我們 !

原文地址:https://laravel-news.com/building-your-own-laravel-packages譯文地址:https://learnku.com/laravel/t/70422

【相關推薦:pestPHP

? 版權聲明
THE END
喜歡就支持一下吧
點贊5 分享