إطار Laravel

بناء حزم قابلة لإعادة الاستخدام

18 دقيقة الدرس 44 من 45

بناء حزم قابلة لإعادة الاستخدام

تسمح حزم Laravel بإضافة وظائف إلى تطبيقات Laravel بطريقة معيارية وقابلة لإعادة الاستخدام. سواء كنت تبني أدوات داخلية أو مكتبات مفتوحة المصدر، فإن فهم تطوير الحزم أمر أساسي للمطورين المتقدمين في Laravel.

هيكل الحزمة والإعداد

لماذا نُنشئ الحزم؟
  • إعادة الاستخدام: مشاركة الكود عبر مشاريع متعددة
  • التعيين: الحفاظ على الميزات منفصلة وقابلة للصيانة
  • المصدر المفتوح: المساهمة في نظام Laravel البيئي
  • التنظيم: تنظيم أفضل للكود للتطبيقات الكبيرة
  • الاختبار: أسهل لاختبار الوظائف المعزولة

إنشاء حزمة

// هيكل الحزمة الموصى به packages/ └── yourname/ └── package-name/ ├── src/ │ ├── PackageServiceProvider.php │ ├── Facades/ │ ├── Commands/ │ ├── Controllers/ │ ├── Models/ │ ├── Middleware/ │ └── config/ ├── resources/ │ ├── views/ │ ├── lang/ │ └── assets/ ├── database/ │ ├── migrations/ │ └── seeders/ ├── routes/ │ ├── web.php │ └── api.php ├── tests/ │ ├── Unit/ │ └── Feature/ ├── composer.json ├── README.md └── LICENSE // composer.json للحزمة الخاصة بك { "name": "yourname/package-name", "description": "حزمة Laravel لـ...", "type": "library", "license": "MIT", "authors": [ { "name": "اسمك", "email": "your@email.com" } ], "require": { "php": "^8.1", "illuminate/support": "^10.0|^11.0" }, "require-dev": { "orchestra/testbench": "^8.0|^9.0", "phpunit/phpunit": "^10.0" }, "autoload": { "psr-4": { "YourName\\PackageName\\": "src/" } }, "autoload-dev": { "psr-4": { "YourName\\PackageName\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "YourName\\PackageName\\PackageServiceProvider" ], "aliases": { "PackageName": "YourName\\PackageName\\Facades\\PackageName" } } }, "minimum-stability": "dev", "prefer-stable": true }

مزود الخدمة - قلب الحزمة الخاصة بك

// src/PackageServiceProvider.php namespace YourName\PackageName; use Illuminate\Support\ServiceProvider; use YourName\PackageName\Commands\InstallCommand; use YourName\PackageName\View\Components\Alert; class PackageServiceProvider extends ServiceProvider { /** * تسجيل الخدمات. */ public function register(): void { // دمج تكوين الحزمة مع تكوين التطبيق $this->mergeConfigFrom( __DIR__.'/../config/package-name.php', 'package-name' ); // تسجيل خدمات singleton $this->app->singleton('package-name', function ($app) { return new PackageClass($app['config']['package-name']); }); // تسجيل facades $this->app->alias('package-name', PackageName::class); } /** * تشغيل الخدمات. */ public function boot(): void { // نشر التكوين $this->publishes([ __DIR__.'/../config/package-name.php' => config_path('package-name.php'), ], 'config'); // نشر migrations $this->publishes([ __DIR__.'/../database/migrations/' => database_path('migrations'), ], 'migrations'); // تحميل migrations تلقائياً (اختياري) $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); // نشر views $this->publishes([ __DIR__.'/../resources/views' => resource_path('views/vendor/package-name'), ], 'views'); // تحميل views $this->loadViewsFrom(__DIR__.'/../resources/views', 'package-name'); // تحميل الترجمات $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'package-name'); // نشر الترجمات $this->publishes([ __DIR__.'/../resources/lang' => lang_path('vendor/package-name'), ], 'lang'); // تسجيل المسارات $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); // تسجيل الأوامر if ($this->app->runningInConsole()) { $this->commands([ InstallCommand::class, ]); } // تسجيل مكونات Blade $this->loadViewComponentsAs('package', [ Alert::class, ]); // نشر الأصول $this->publishes([ __DIR__.'/../resources/assets' => public_path('vendor/package-name'), ], 'public'); } }
أفضل ممارسات مزود الخدمة:
  • استخدم register() لربط الخدمات بالحاوية
  • استخدم boot() للإجراءات التي تعتمد على خدمات أخرى
  • قدم قيماً افتراضية معقولة في تكوينك
  • ضع علامات على الأصول القابلة للنشر بأسماء وصفية
  • اجعل migrations اختيارية - دع المستخدمين ينشرونها

إنشاء تكوين الحزمة

// config/package-name.php return [ /* |-------------------------------------------------------------------------- | الإعدادات الافتراضية |-------------------------------------------------------------------------- */ 'enabled' => env('PACKAGE_ENABLED', true), 'api_key' => env('PACKAGE_API_KEY'), 'cache' => [ 'driver' => env('PACKAGE_CACHE_DRIVER', 'redis'), 'ttl' => env('PACKAGE_CACHE_TTL', 3600), ], /* |-------------------------------------------------------------------------- | الخيارات المتقدمة |-------------------------------------------------------------------------- */ 'features' => [ 'feature_one' => true, 'feature_two' => false, ], 'routes' => [ 'prefix' => 'package', 'middleware' => ['web', 'auth'], ], ]; // الوصول إلى التكوين في حزمتك namespace YourName\PackageName; class PackageClass { public function __construct(protected array $config) { } public function isEnabled(): bool { return config('package-name.enabled', true); } public function getApiKey(): ?string { return config('package-name.api_key'); } }

مسارات ووحدات التحكم في الحزمة

// routes/web.php use Illuminate\Support\Facades\Route; use YourName\PackageName\Http\Controllers\PackageController; Route::prefix(config('package-name.routes.prefix', 'package')) ->middleware(config('package-name.routes.middleware', ['web'])) ->name('package.') ->group(function () { Route::get('/', [PackageController::class, 'index'])->name('index'); Route::get('/settings', [PackageController::class, 'settings'])->name('settings'); Route::post('/settings', [PackageController::class, 'updateSettings'])->name('settings.update'); }); // src/Http/Controllers/PackageController.php namespace YourName\PackageName\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Routing\Controller; class PackageController extends Controller { public function index() { return view('package-name::index', [ 'title' => __('package-name::messages.welcome'), ]); } public function settings() { return view('package-name::settings', [ 'config' => config('package-name'), ]); } public function updateSettings(Request $request) { $validated = $request->validate([ 'api_key' => 'required|string', 'enabled' => 'boolean', ]); // منطق تحديث الإعدادات return redirect() ->route('package.settings') ->with('success', __('package-name::messages.settings_updated')); } }

عروض ومكونات الحزمة

// resources/views/index.blade.php <!DOCTYPE html> <html> <head> <title>{{ $title }}</title> <link href="{{ asset('vendor/package-name/css/style.css') }}" rel="stylesheet"> </head> <body> <h1>{{ $title }}</h1> <x-package::alert type="success"> الحزمة تعمل بشكل صحيح! </x-package::alert> @yield('content') <script src="{{ asset('vendor/package-name/js/script.js') }}"></script> </body> </html> // src/View/Components/Alert.php namespace YourName\PackageName\View\Components; use Illuminate\View\Component; class Alert extends Component { public function __construct( public string $type = 'info', public ?string $title = null, ) {} public function render() { return view('package-name::components.alert'); } } // resources/views/components/alert.blade.php <div class="alert alert-{{ $type }}" role="alert"> @if($title) <h4 class="alert-heading">{{ $title }}</h4> @endif {{ $slot }} </div>

أوامر الحزمة

// src/Commands/InstallCommand.php namespace YourName\PackageName\Commands; use Illuminate\Console\Command; class InstallCommand extends Command { protected $signature = 'package:install {--force : الكتابة فوق الملفات الموجودة} {--without-migrations : تخطي نشر migrations}'; protected $description = 'تثبيت حزمة Package Name'; public function handle() { $this->info('جارٍ تثبيت Package Name...'); // نشر التكوين $this->call('vendor:publish', [ '--provider' => 'YourName\\PackageName\\PackageServiceProvider', '--tag' => 'config', '--force' => $this->option('force'), ]); // نشر migrations if (!$this->option('without-migrations')) { $this->call('vendor:publish', [ '--provider' => 'YourName\\PackageName\\PackageServiceProvider', '--tag' => 'migrations', '--force' => $this->option('force'), ]); } // نشر الأصول $this->call('vendor:publish', [ '--provider' => 'YourName\\PackageName\\PackageServiceProvider', '--tag' => 'public', '--force' => $this->option('force'), ]); // تشغيل migrations if ($this->confirm('هل تريد تشغيل migrations الآن؟')) { $this->call('migrate'); } $this->info('تم تثبيت Package Name بنجاح!'); $this->comment('يُرجى تشغيل: php artisan package:install لإكمال الإعداد'); } }
اعتبارات migrations الحزمة:
  • استخدم أسماء migration وصفية مع طوابع زمنية
  • اجعل migrations idempotent (يمكن تشغيلها عدة مرات)
  • قدم طرق down() للتراجع
  • فكر في جعل migrations اختيارية عبر النشر
  • وثق أي خطوات يدوية مطلوبة

اختبار حزمتك

// tests/TestCase.php namespace YourName\PackageName\Tests; use Orchestra\Testbench\TestCase as Orchestra; use YourName\PackageName\PackageServiceProvider; abstract class TestCase extends Orchestra { protected function setUp(): void { parent::setUp(); // تحميل migrations $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); // إعداد إضافي } protected function getPackageProviders($app) { return [ PackageServiceProvider::class, ]; } protected function getEnvironmentSetUp($app) { // إعداد بيئة الاختبار $app['config']->set('database.default', 'testing'); $app['config']->set('package-name.enabled', true); } } // tests/Feature/PackageTest.php namespace YourName\PackageName\Tests\Feature; use YourName\PackageName\Tests\TestCase; class PackageTest extends TestCase { /** @test */ public function it_can_access_package_routes() { $response = $this->get(route('package.index')); $response->assertStatus(200); $response->assertSee('الحزمة تعمل'); } /** @test */ public function it_loads_configuration_correctly() { $this->assertTrue(config('package-name.enabled')); $this->assertEquals('package', config('package-name.routes.prefix')); } } // tests/Unit/PackageClassTest.php namespace YourName\PackageName\Tests\Unit; use YourName\PackageName\PackageClass; use YourName\PackageName\Tests\TestCase; class PackageClassTest extends TestCase { /** @test */ public function it_can_instantiate_package_class() { $package = app('package-name'); $this->assertInstanceOf(PackageClass::class, $package); } } // تشغيل الاختبارات ./vendor/bin/phpunit // أو مع التغطية ./vendor/bin/phpunit --coverage-html coverage

النشر إلى Packagist

// 1. جهز حزمتك // - تأكد من أن composer.json كامل // - أضف README.md مع تعليمات التثبيت والاستخدام // - أضف ملف LICENSE (MIT شائع) // - أضف CHANGELOG.md // - ضع علامة على إصدار // 2. أنشئ مستودعاً على GitHub git init git add . git commit -m "Initial commit" git remote add origin https://github.com/yourname/package-name.git git push -u origin main // 3. ضع علامة على أول إصدار لك git tag -a v1.0.0 -m "First release" git push origin v1.0.0 // 4. أرسل إلى Packagist // - اذهب إلى https://packagist.org // - انقر على "Submit" // - أدخل عنوان URL لمستودع GitHub الخاص بك // - سيقوم Packagist تلقائياً بجلب التحديثات على العلامات الجديدة // 5. أعد التحديث التلقائي (اختياري) // أضف webhook لـ GitHub في إعدادات مستودعك: // URL: https://packagist.org/api/github?username=YOUR_USERNAME // نوع المحتوى: application/json // الأحداث: حدث الدفع فقط // مثال README.md # اسم الحزمة [![أحدث إصدار على Packagist](https://img.shields.io/packagist/v/yourname/package-name.svg)](https://packagist.org/packages/yourname/package-name) [![إجمالي التنزيلات](https://img.shields.io/packagist/dt/yourname/package-name.svg)](https://packagist.org/packages/yourname/package-name) وصف حزمتك. ## التثبيت ```bash composer require yourname/package-name ``` ## التكوين انشر ملف التكوين: ```bash php artisan vendor:publish --provider="YourName\PackageName\PackageServiceProvider" --tag="config" ``` ## الاستخدام ```php use YourName\PackageName\Facades\PackageName; PackageName::doSomething(); ``` ## الاختبار ```bash composer test ``` ## سجل التغيير يُرجى الاطلاع على [CHANGELOG](CHANGELOG.md) لمزيد من المعلومات. ## المساهمة يُرجى الاطلاع على [CONTRIBUTING](CONTRIBUTING.md) للتفاصيل. ## الشكر - [اسمك](https://github.com/yourname) ## الرخصة رخصة MIT. يُرجى الاطلاع على [ملف الرخصة](LICENSE.md) لمزيد من المعلومات.
تمرين 1: أنشئ حزمة تحليلات بسيطة:
  1. أنشئ هيكل حزمة مع مزود الخدمة
  2. أضف middleware لتتبع مشاهدات الصفحة
  3. أنشئ migrations لتخزين بيانات التحليلات
  4. ابن وحدة تحكم لوحة المعلومات مع الرسوم البيانية
  5. أضف مكونات Blade لعرض المقاييس
  6. اكتب اختبارات لوظيفة التتبع
تمرين 2: ابن حزمة مركز الإشعارات:
  1. أنشئ نماذج للإشعارات مع أنواع (معلومات، تحذير، خطأ)
  2. أضف مزود خدمة مع التكوين
  3. أنشئ أوامر artisan لإرسال الإشعارات
  4. ابن نقاط نهاية API لجلب الإشعارات
  5. أضف مكون Blade لقائمة الإشعارات المنسدلة
  6. انشر الحزمة إلى GitHub وضع علامة v1.0.0
تمرين 3: طوّر حزمة إدارة الإعدادات:
  1. أنشئ مزود خدمة مع تكوين قابل للنشر
  2. ابن migrations لتخزين الإعدادات في قاعدة البيانات
  3. أضف facade للوصول السهل إلى الإعدادات
  4. أنشئ واجهة مستخدم لإدارة الإعدادات
  5. أضف التخزين المؤقت لأداء أفضل
  6. اكتب اختبارات شاملة ووثائق