- Published on
PHP Package Development (Part-2)
PHP နဲ့ Laravel Package တွေ ဖန်တီးတာကို knowledge sharing လုပ်ချင်ပါတယ်။ ဒီအပိုင်းမှာတော့ Laravel အတွက် package တစ်ခုဖန်တီးပါမယ်။
Requirements
Setup Composer Project
Project တစ်ခု create လုပ်ဖို့အတွက်
mkdir quiz-api
cd quiz-api
composer init
ဆိုပြီး run ပါမယ်။
အဲ့ဒါဆိုရင် အခုလို folder structure ရပါပြီ။
├── src
├── vendor
└── composer.json
composer.json
မှာ အခုလိုပြင်ပါမယ်။
{
"name": "pyaesoneaung/quiz-api",
"description": "Quiz Api Client",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
- "Pyaesoneaung\\QuizApi\\": "src/"
+ "PyaeSoneAung\\QuizApi\\": "src/"
}
},
"authors": [
{
"name": "Pyae Sone Aung",
"email": "[email protected]"
}
],
"minimum-stability": "dev",
+ "prefer-stable": true,
"require": {}
}
ဘာမှမရေးခင် အရင်ဆုံး ကိုယ့် package က Laravel version ဘယ်လောက်အနိမ့်ဆုံးလိုမယ်ဆိုတာ သတ်မှတ်ဖို့လိုပါတယ်။
composer.json
မှာ အခုလိုပြင်ပါမယ်။
"require": {
"illuminate/contracts": "^10.0|^11.0",
"pyaesoneaung/quiz-api-client": "^1.0"
},
"require-dev": {
"orchestra/testbench": "^9.0|^10.0",
}
"illuminate/contracts": "^10.0|^11.0" က Laravel 10 နဲ့ 11 မှာ သွင်းလို့ရမယ်လို့ ဆိုလိုတာပါ။
"pyaesoneaung/quiz-api-client": "^1.0" က ကျတော်တို့ part-1 မှာ ရေးထားတဲ့ package ကို install လုပ်မယ်ဆိုလိုတာပါ။
"orchestra/testbench": "^9.0|^10.0" က test ရေးဖို့ အတွက်ပါ။ ပြီးတော့ testbench မှာ development လုပ်နေတဲ့ အချိန်မှာလိုအပ်တဲ့ ServiceProvider တို့၊ Facade တို့ အပြင်တခြား Laravel ရဲ့ core class တွေကို တခါထဲသွင်းသွားတဲ့ အတွက် text editor တွေရဲ့ auto complete feature ကိုလည်း အသုံးပြုနိုင်မှာပါ။ ^9.0 က Laravel 10 အတွက်ဖြစ်ပြီး ^10.0 က Laravel 11 အတွက်ပါ။ အသေးစိတ်ကိုတော့ packages.tools မှာ ကြည့်လို့ရပါတယ်။
Setup Config
QuizApi ကို ခေါ်သုံးဖို့ အတွက် api key ရှိဖို့လိုပါတယ်။ Api key ကို config ကနေ တစ်ဆင့် .env
file ကနေ QUIZ_API_KEY
ဆိုတဲ့ key နဲ့ ယူပါမယ်။ အဲ့အတွက် project root folder မှာပဲ config
ဆိုပြီး folder တစ်ခု create လုပ်ပါမယ်။ ပြီးရင် အဲ့ folder အောက်မှာပဲ quiz-api.php
ဆိုပြီး php file တစ်ခု create လုပ်ပါမယ်။
├── config
│ ├── quiz-api.php
├── src
├── vendor
└── composer.json
quiz-api.php
မှာ အခုလိုရေးပါမယ်။
<?php
return [
'api_key' => env('QUIZ_API_KEY'),
];
ဒီ config file ကို Laravel framework က သိဖို့အတွက် ServiceProvider တစ်ခု create လုပ်ပြီး register လုပ်ပေးဖို့လိုပါတယ်။
src
folder အောက်မှာ QuizApiServiceProvider.php
ဆိုပြီး file တစ်ခု create လုပ်ပါမယ်။
├── config
│ ├── quiz-api.php
├── src
│ ├── QuizApiServiceProvider.php
├── vendor
└── composer.json
QuizApiServiceProvider.php
မှာ အခုလိုရေးပါမယ်။
<?php
namespace PyaeSoneAung\QuizApi;
use Illuminate\Support\ServiceProvider;
class QuizApiServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->mergeConfigFrom(
__DIR__ . '/../config/quiz-api.php',
'quiz-api'
);
}
}
ပြီးရင် QuizApiServiceProvider ကို composer.json
မှာ register လုပ်ပါမယ်။
"extra": {
"laravel": {
"providers": ["PyaeSoneAung\\QuizApi\\QuizApiServiceProvider"]
}
}
ဒါဆိုရင် config('quiz-api.api_key')
ဆိုပြီး ခေါ်သုံးလို့ရပါပြီ။
Service Container
Laravel Service Container ကို အကြမ်းဖျင်း နည်းနည်း ရှင်းပြချင်ပါတယ်။ Service Container မှာ အဓိက ၂ ပိုင်း ရှိပါတယ်။ Class တွေရဲ့ dependency တွေကို manage လုပ်တာနဲ့ dependency injection လုပ်ပေးတာပါ။
Binding
Dependency ကို manage လုပ်တယ်ဆိုတာက ဉပမာ ApiClient class တစ်ခုရဲ့ constructor မှာ $endpoint နဲ့ $apiKey လိုတယ်ဆိုရင် config ထဲက ဘယ် key တွေကို pass လိုက်ပါဆိုပြီး သတ်မှတ်တာကို ဆိုလိုတာပါ။
$this->app->bind('api-client', function () {
return new ApiClient('https://example.com', 'example-key');
});
ဒါဆိုရင် 'api-client' ကို resolve လုပ်ရင် ApiClient ရဲ့ constructor မှာ https://example.com
နဲ့ example-key
ကို laravel က pass ပေးသွားမှာပါ။ တကယ့် real world project မှာတော့ အပေါ်က code လို ရိုးရိုး မ bind ဘဲ singleton method ကို သုံးပြီး bind ပါတယ်။
$this->app->singleton('api-client', function () {
return new ApiClient('https://example.com', 'example-key');
});
ဘာကွာလဲဆိုရင် singleton method က တစ်ခါပဲ resolve လုပ်ပေးမှာ။ app('api-client')
ဆိုပြီး object တစ်ခါဆောက်ပြီးရင် နောက်တစ်ခါ နောက်တစ်နေရာမှာ app('api-client')
လို့ခေါ်လည်း object အသစ်မဆောက်ဘဲ ပထမ ဆောက်ထားတဲ့ object ပဲ return ပြန်မှာပါ။
Resolving
app()
ဆိုတာက dependency တွေကို reslove ဖို့သုံးတာပါ။ app('api-client')
လို့ ခေါ်လိုက်ရင် service container မှာ bind ထားတဲ့အတိုင်း ApiClient ရဲ့ constructor မှာ $endpoint နဲ့ $apiKey pass ပြီး create လုပ််ထားတဲ့ object ကိုရမှာပါ။
QuizApi Integration
QuizApi ကို object ဆောက်ပြီး ခေါ်သုံးဖို့ အတွက် QuizApiServiceProvider မှာ အခုလိုရေးပါမယ်။
<?php
namespace PyaeSoneAung\QuizApi;
use Illuminate\Support\ServiceProvider;
use PyaeSoneAung\QuizApiClient\QuizApi;
class QuizApiServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->mergeConfigFrom(
__DIR__ . '/../config/quiz-api.php',
'quiz-api'
);
$this->app->singleton(QuizApi::class, function () {
return new QuizApi(config('quiz-api.api_key'));
});
}
}
ဒါဆိုရင် app(QuizApi::class)
လို့ခေါ်ရင် api key ထည့်ပြီးသား QuizApi object ကို ရမှာပါ။ ဒီနေရာမှာ QuizApi::class
က "PyaeSoneAung\QuizApiClient\QuizApi" ဆိုတဲ့ string ကိုဆိုလိုတာပါ။ app('PyaeSoneAung\QuizApiClient\QuizApi')
လို့ခေါ်လည်း QuizApi object ကိုရမှာပါ။ ဒီနေရာမှာ QuizApi::class
လို့ သုံးတာက dependency resolve လုပ်တဲ့အချိန် တခြား package တွေနဲ့ နာမည်တူမျိုး မဖြစ်အောင်လို့ပါ။
ဒါပေမဲ့ Laravel ကနေ
use PyaeSoneAung\QuizApiClient\QuizApi;
app(QuizApi::class)->questions()->get();
ဆိုပြီး ခေါ်သုံးမှာ မဟုတ်ပါဘူး။ အခုလို ရှုပ်ထွေးတဲ့ object ဆောက်တာတွေကို Laravel Facade အောက်မှာ hide ပြီး Laravel ကနေ အခုလိုခေါ် သုံးမှာပါ။
use QuizApi;
QuizApi::questions()->get();
Facade ကို create လုပ်ဖို့ အတွက် src/Facades
folder အောက်မှာ QuizApi.php
ဆိုပြီး file တစ်ခု create လုပ်ပါမယ်။
QuizApi.php
<?php
namespace PyaeSoneAung\QuizApi\Facades;
use Illuminate\Support\Facades\Facade;
class QuizApi extends Facade
{
protected static function getFacadeAccessor(): string
{
return \PyaeSoneAung\QuizApiClient\QuizApi::class;
}
}
getFacadeAccessor()
function က app()
ထဲမှာ solve လုပ်မယ့် နာမည်ကို return ပြန်ပေးတာပါ။ ဒီနေရာမှာ "PyaeSoneAung\QuizApiClient\QuizApi" string ကို return ပြန်ပေးမှာပါ။
ပြီးရင်တော့ composer.json
မှာ QuizApi Facade ကို register လုပ်ပါမယ်။
"extra": {
"laravel": {
"providers": ["PyaeSoneAung\\QuizApi\\QuizApiServiceProvider"],
"aliases": {
"QuizApi": "PyaeSoneAung\\QuizApi\\Facades\\QuizApi"
}
}
}
ဒါဆိုရင်တော့ QuizApi::questions()->get()
ဆိုပြီး Laravel app ကနေ ခေါ်သုံးလို့ရပါပြီ။
Testing
ကျတော်တို့ package အလုပ်လုပ်မလုပ် စမ်းဖို့အတွက် tests တွေရှိဖို့လိုပါတယ်။ Test ရေးဖို့အတွက် Pest PHP
ကို install လုပ်ပါမယ်။
composer require pestphp/pest --dev --with-all-dependencies
ပြီးရင်တော့
./vendor/bin/pest --init
ဆိုပြီး run ပါမယ်။ ဒါဆိုရင် tests folder နဲ့ phpunit.xml file ကို generate လုပ်သွားပေးမှာပါ။ ဒါဆိုရင်
./vendor/bin/pest
ဆိုပြီး test တွေ run လို့ရပါပြီ။
tests
folder ထဲမှာ Feature နဲ့ Unit ဆိုပြီး folder ၂ခု ရှိပါတယ်။ Package တွေမှာတော့ Feature သပ်သပ်၊ Unit သပ်သပ်ဆိုပြီး test တွေရေးတာထက် အပြင်မှာ class တစ်ခု create လုပ်ပြီးရေးတာမျိုးက ပိုများပါတယ်။ Test ရေးဖို့ အတွက် Feature နဲ့ Unit folder နှစ်ခုလုံးကို ဖျက်လိုက်ပါမယ်။ ပြီးရင် TestCase.php
ရေးဖို့ အတွက် composer.json
မှာ အခုလိုပြင်ပါမယ်။
"autoload-dev": {
"psr-4": {
"PyaeSoneAung\\QuizApi\\Tests\\": "tests"
}
}
tests
folder ထဲက class တွေက PyaeSoneAung\QuizApi\Tests
ဆိုတဲ့ namespace အောက်မှာ ရှိတယ်ဆိုပြီး သတ်မှတ်လိုက်လိုက်တာပါ။
TestCase.php
<?php
namespace PyaeSoneAung\QuizApi\Tests;
use Illuminate\Support\Facades\Config;
use PyaeSoneAung\QuizApi\QuizApiServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;
class TestCase extends Orchestra
{
protected function setUp(): void
{
parent::setUp();
Config::set('quiz-api.api_key', 'foo');
}
protected function getPackageProviders($app)
{
return [
QuizApiServiceProvider::class,
];
}
}
setUp()
function မှာက testing လုပ်တဲ့ အချိန်မှာသုံးမယ့် api key ကို သတ်မှတ်လိုက်တာပါ။
getPackageProviders()
function မှာက QuizApiServiceProvider
ကို testing မှာ bootstrap လုပ်မယ်ဆိုပြီး သတ်မှတ်လိုက်တာပါ။
ပြီးရင် Pest.php
မှာ အခုလိုရေးပါမယ်။
Pest.php
<?php
use PyaeSoneAung\QuizApi\Tests\TestCase;
uses(TestCase::class)->in(__DIR__);
tests
folder အောက်က test တွေကို run ရင် TestCase
class ကို သုံးမယ်ဆိုပြီး သတ်မှတ်လိုက်တာပါ။ ပြီးရင်တော့ QuizApiTest.php
ဆိုပြီး file တစ်ခု create လုပ်ပြီး အခုလိုရေးပါမယ်။
QuizApiTest.php
<?php
use PyaeSoneAung\QuizApi\Facades\QuizApi;
use PyaeSoneAung\QuizApiClient\QuizApi as QuizApiClient;
use PyaeSoneAung\QuizApiClient\Resources\QuestionResource;
it('can get QuizApi client', function () {
expect(app(QuizApiClient::class))->toBeInstanceOf(QuizApiClient::class);
});
it('can get question resource', function () {
expect(QuizApi::questions())->toBeInstanceOf(QuestionResource::class);
});
can get QuizApi client
က ကျတော်ဆို့ app()
ဆိုပြီး ခေါ်သုံးတဲ့နေရာမှာ QuizApi client object ပြန်ရလားဆိုပြီး စစ်တာပါ။
can get question resource
က QuizApi::questions()
ဆိုပြီး facade ကို ခေါ်သုံးလို့ရလားဆိုပြီး စစ်တာပါ။
ဒါဆိုရင်
./vendor/bin/pest
ဆိုပြီး test ကို run ကြည့်လို့ရပါပြီ။
Continuous Integration
CI အတွက် .github/workflows/run-tests.yml
ဆိုပြီး file တစ်ခု create လုပ်ပါမယ်။
run-tests.yml
name: run-tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.2]
laravel: [10.*]
stability: [prefer-stable]
include:
- laravel: 10.*
testbench: 8.*
carbon: ^2.63
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
coverage: none
- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
- name: List Installed Dependencies
run: composer show -D
- name: Execute tests
run: vendor/bin/pest --ci
ဒီနေရာမှာ include အပိုင်းကို အနည်းငယ်ရှင်းပြချင်ပါတယ်။
include:
- laravel: 10.*
testbench: 8.*
carbon: ^2.63
Laravel 10 ကို test ရင် testbench ကို version 8.* ပဲ သုံးရမယ်လို့ သတ်မှတ်တာပါ။ Laravel 9 ဆိုရင် testbench: 7.*၊ Laravel 11 ဆိုရင် 9.* ကို သုံးမယ်ဆိုပြီး သတ်မှတ်ဖို့လိုပါတယ်။ အခု စာရေးနေတဲ့ အချိန်မှာတော့ Laravel 11 မထွက်သေးတဲ့ အတွက် Laravel 10 အတွက်ပဲ CI ရေးထားတာပါ။
ဒါဆိုရင် git init
လုပ်ပြီး GitHub ပေါ်တင်လို့ရပါပြီ။ git init
မလုပ်ခင် .gitignore
ဆိုပြီး file တစ်ခု create လုပ်ပြီး အခုလိုရေးပါမယ်။
vendor
composer.lock
vendor folder ရယ်၊ composer.lock ရယ် ကို git ထဲမထည့်ဘူး သတ်မှတ်တာပါ။
git init
git add .
git commit -m "initial commit"
ပြီးရင်တော့ GitHub မှာ repo တစ်ခု create လုပ်ပြီး push ပါမယ်။
ဒါဆိုရင် GitHub Repo ထဲက Actions tab မှာ အခုလို test တွေ success ဖြစ်နေတာကိုတွေ့မှာပါ။
Release
GitHub မှာ v1.0.0
ကို release လုပ်ပြီး packagist.org မှာ submit သွားလုပ်ပါမယ်။
ဒါပြီးရင်တော့
composer require pyaesoneaung/quiz-api
ဆိုပြီးတော့ ကျတော်တို့ package ကို composer ကနေ install လုပ်လို့ရပါပြီ။
Part-3 မှာတော့ package တစ်ခုကို အခုလို အစဆုံးရေးနေတာမျိုး မဟုတ်ဘဲ template တွေအသုံးပြုပြီး ဖန်တီးတာရယ်၊ spatie ရဲ့ package development tool package အကြောင်းရယ်၊ documentation အကြောင်းတွေကို ဆက်ပြီး knowledge sharing လုပ်သွားပါမယ်။
Source code ကို ဒီမှာ ကြည့်လို့ရပါတယ်။ သိချင်တာရှိရင်လည်း မေးလို့ရပါတယ် ခင်ဗျာ။