Laravel المتقدم
بناء حزم Laravel
بناء حزم Laravel
تسمح لك حزم Laravel بإنشاء وظائف قابلة لإعادة الاستخدام يمكن مشاركتها عبر مشاريع متعددة أو توزيعها على المجتمع. في هذا الدرس، ستتعلم كيفية هيكلة وتطوير واختبار ونشر حزم Laravel.
هيكل الحزمة
تتبع حزمة Laravel النموذجية هذا الهيكل الدليلي:
my-package/
├── config/
│ └── my-package.php # تكوين الحزمة
├── database/
│ ├── migrations/ # ترحيلات الحزمة
│ └── seeders/ # بذور الحزمة
├── resources/
│ ├── views/ # عروض الحزمة
│ └── lang/ # ترجمات الحزمة
├── routes/
│ ├── web.php # مسارات الويب
│ └── api.php # مسارات API
├── src/
│ ├── Commands/ # أوامر Artisan
│ ├── Controllers/ # وحدات التحكم
│ ├── Models/ # نماذج Eloquent
│ ├── Middleware/ # Middleware
│ ├── Facades/ # Facades
│ ├── MyPackageServiceProvider.php
│ └── MyPackage.php # الفئة الرئيسية للحزمة
├── tests/
│ ├── Unit/ # اختبارات الوحدة
│ └── Feature/ # اختبارات الميزات
├── .gitignore
├── composer.json # بيانات الحزمة الوصفية
├── LICENSE
└── README.md
إنشاء حزمة
لنقم بإنشاء حزمة بسيطة لإدارة مفاتيح API:
# إنشاء هيكل دليل الحزمة
mkdir -p packages/vendor-name/api-keys/src
cd packages/vendor-name/api-keys
# تهيئة composer
composer init
# اتبع المطالبات:
# اسم الحزمة: vendor-name/api-keys
# الوصف: إدارة مفاتيح API لـ Laravel
# المؤلف: اسمك <your@email.com>
# الحد الأدنى من الاستقرار: stable
# الترخيص: MIT
# إضافة تبعيات Laravel
composer require illuminate/support
مزود الخدمة
مزود الخدمة هو نقطة الدخول لحزمتك:
<?php
// src/ApiKeysServiceProvider.php
namespace VendorName\ApiKeys;
use Illuminate\Support\ServiceProvider;
use VendorName\ApiKeys\Commands\GenerateApiKeyCommand;
use VendorName\ApiKeys\Commands\RevokeApiKeyCommand;
class ApiKeysServiceProvider extends ServiceProvider
{
/**
* تسجيل الخدمات
*/
public function register(): void
{
// دمج تكوين الحزمة مع تكوين التطبيق
$this->mergeConfigFrom(
__DIR__ . '/../config/api-keys.php',
'api-keys'
);
// تسجيل singleton
$this->app->singleton('api-keys', function ($app) {
return new ApiKeyManager($app['config']['api-keys']);
});
// تسجيل facade
$this->app->bind('api-keys-facade', function () {
return new ApiKeyManager();
});
}
/**
* تمهيد الخدمات
*/
public function boot(): void
{
// نشر التكوين
$this->publishes([
__DIR__ . '/../config/api-keys.php' => config_path('api-keys.php'),
], 'api-keys-config');
// نشر الترحيلات
$this->publishes([
__DIR__ . '/../database/migrations' => database_path('migrations'),
], 'api-keys-migrations');
// تحميل الترحيلات
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
// نشر العروض
$this->publishes([
__DIR__ . '/../resources/views' => resource_path('views/vendor/api-keys'),
], 'api-keys-views');
// تحميل العروض
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'api-keys');
// تحميل الترجمات
$this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'api-keys');
// تحميل المسارات
$this->loadRoutesFrom(__DIR__ . '/../routes/api.php');
// تسجيل الأوامر
if ($this->app->runningInConsole()) {
$this->commands([
GenerateApiKeyCommand::class,
RevokeApiKeyCommand::class,
]);
}
// تسجيل middleware
$router = $this->app['router'];
$router->aliasMiddleware('api.key', \VendorName\ApiKeys\Middleware\ValidateApiKey::class);
}
}
ملاحظة: يُستخدم طريقة
register() لربط الخدمات في الحاوية، بينما يُستخدم boot() للإجراءات التي تعتمد على الخدمات الأخرى المسجلة.
تكوين الحزمة
قم بإنشاء ملف تكوين لحزمتك:
<?php
// config/api-keys.php
return [
/*
|--------------------------------------------------------------------------
| إعدادات مفاتيح API
|--------------------------------------------------------------------------
*/
'table' => env('API_KEYS_TABLE', 'api_keys'),
'key_length' => env('API_KEY_LENGTH', 32),
'prefix' => env('API_KEY_PREFIX', 'sk_'),
'hash_algo' => env('API_KEY_HASH_ALGO', 'sha256'),
/*
|--------------------------------------------------------------------------
| تحديد المعدل
|--------------------------------------------------------------------------
*/
'rate_limiting' => [
'enabled' => env('API_KEY_RATE_LIMIT_ENABLED', true),
'requests_per_minute' => env('API_KEY_RATE_LIMIT_REQUESTS', 60),
],
/*
|--------------------------------------------------------------------------
| انتهاء الصلاحية
|--------------------------------------------------------------------------
*/
'expiration' => [
'enabled' => env('API_KEY_EXPIRATION_ENABLED', false),
'days' => env('API_KEY_EXPIRATION_DAYS', 365),
],
/*
|--------------------------------------------------------------------------
| القائمة البيضاء لـ IP
|--------------------------------------------------------------------------
*/
'ip_whitelist' => [
'enabled' => env('API_KEY_IP_WHITELIST_ENABLED', false),
],
];
الفئة الرئيسية للحزمة
قم بإنشاء فئة الوظيفة الرئيسية:
<?php
// src/ApiKeyManager.php
namespace VendorName\ApiKeys;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class ApiKeyManager
{
protected array $config;
public function __construct(array $config = [])
{
$this->config = $config ?: config('api-keys');
}
/**
* إنشاء مفتاح API جديد
*/
public function generate(
int $userId,
string $name,
?array $permissions = null,
?array $ipWhitelist = null,
?Carbon $expiresAt = null
): array {
$key = $this->generateRandomKey();
$hashedKey = $this->hashKey($key);
$apiKeyId = DB::table($this->config['table'])->insertGetId([
'user_id' => $userId,
'name' => $name,
'key_hash' => $hashedKey,
'permissions' => $permissions ? json_encode($permissions) : null,
'ip_whitelist' => $ipWhitelist ? json_encode($ipWhitelist) : null,
'expires_at' => $expiresAt,
'created_at' => now(),
'updated_at' => now(),
]);
return [
'id' => $apiKeyId,
'key' => $this->config['prefix'] . $key, // إرجاع مع البادئة
'name' => $name,
'created_at' => now(),
];
}
/**
* التحقق من صحة مفتاح API
*/
public function validate(string $key, ?string $ipAddress = null): ?array
{
// إزالة البادئة إن وجدت
$key = Str::startsWith($key, $this->config['prefix'])
? Str::after($key, $this->config['prefix'])
: $key;
$hashedKey = $this->hashKey($key);
$apiKey = DB::table($this->config['table'])
->where('key_hash', $hashedKey)
->where('is_active', true)
->first();
if (!$apiKey) {
return null;
}
// فحص انتهاء الصلاحية
if ($apiKey->expires_at && Carbon::parse($apiKey->expires_at)->isPast()) {
return null;
}
// فحص القائمة البيضاء لـ IP
if ($this->config['ip_whitelist']['enabled'] && $apiKey->ip_whitelist) {
$whitelist = json_decode($apiKey->ip_whitelist, true);
if ($ipAddress && !in_array($ipAddress, $whitelist)) {
return null;
}
}
// تحديث الطابع الزمني لآخر استخدام
DB::table($this->config['table'])
->where('id', $apiKey->id)
->update([
'last_used_at' => now(),
'usage_count' => DB::raw('usage_count + 1'),
]);
return (array) $apiKey;
}
/**
* إلغاء مفتاح API
*/
public function revoke(int $apiKeyId): bool
{
return DB::table($this->config['table'])
->where('id', $apiKeyId)
->update([
'is_active' => false,
'revoked_at' => now(),
]) > 0;
}
/**
* التحقق مما إذا كان مفتاح API لديه إذن
*/
public function hasPermission(array $apiKey, string $permission): bool
{
if (!$apiKey['permissions']) {
return true; // لا توجد أذونات = جميع الأذونات
}
$permissions = json_decode($apiKey['permissions'], true);
return in_array($permission, $permissions) || in_array('*', $permissions);
}
/**
* إنشاء مفتاح عشوائي
*/
protected function generateRandomKey(): string
{
return Str::random($this->config['key_length']);
}
/**
* تجزئة مفتاح للتخزين
*/
protected function hashKey(string $key): string
{
return hash($this->config['hash_algo'], $key);
}
}
نصيحة: قم دائمًا بتجزئة مفاتيح API قبل تخزينها في قاعدة البيانات، تمامًا مثل كلمات المرور. قم بتخزين التجزئة فقط وإرجاع المفتاح العادي للمستخدم مرة واحدة أثناء الإنشاء.
Middleware
قم بإنشاء middleware للتحقق من صحة مفاتيح API:
<?php
// src/Middleware/ValidateApiKey.php
namespace VendorName\ApiKeys\Middleware;
use Closure;
use Illuminate\Http\Request;
use VendorName\ApiKeys\Facades\ApiKeys;
class ValidateApiKey
{
/**
* معالجة طلب وارد
*/
public function handle(Request $request, Closure $next, ?string $permission = null)
{
$apiKey = $request->bearerToken() ?: $request->header('X-API-Key');
if (!$apiKey) {
return response()->json([
'error' => 'مفتاح API مطلوب',
], 401);
}
$validatedKey = ApiKeys::validate($apiKey, $request->ip());
if (!$validatedKey) {
return response()->json([
'error' => 'مفتاح API غير صالح أو منتهي الصلاحية',
], 401);
}
// فحص الإذن إذا تم تحديده
if ($permission && !ApiKeys::hasPermission($validatedKey, $permission)) {
return response()->json([
'error' => 'أذونات غير كافية',
], 403);
}
// إرفاق معلومات مفتاح API بالطلب
$request->attributes->set('api_key', $validatedKey);
return $next($request);
}
}
Facade
قم بإنشاء facade لسهولة الوصول إلى حزمتك:
<?php
// src/Facades/ApiKeys.php
namespace VendorName\ApiKeys\Facades;
use Illuminate\Support\Facades\Facade;
class ApiKeys extends Facade
{
/**
* الحصول على الاسم المسجل للمكون
*/
protected static function getFacadeAccessor(): string
{
return 'api-keys-facade';
}
}
أوامر Artisan
قم بإنشاء أوامر Artisan لحزمتك:
<?php
// src/Commands/GenerateApiKeyCommand.php
namespace VendorName\ApiKeys\Commands;
use Illuminate\Console\Command;
use VendorName\ApiKeys\Facades\ApiKeys;
use App\Models\User;
class GenerateApiKeyCommand extends Command
{
protected $signature = 'api-keys:generate
{user : معرف المستخدم أو البريد الإلكتروني}
{name : اسم مفتاح API}
{--permissions=* : الأذونات لهذا المفتاح}
{--expires-in= : انتهاء الصلاحية بالأيام}
{--ip=* : عناوين IP المدرجة في القائمة البيضاء}';
protected $description = 'إنشاء مفتاح API جديد للمستخدم';
public function handle(): int
{
$userIdentifier = $this->argument('user');
$user = is_numeric($userIdentifier)
? User::find($userIdentifier)
: User::where('email', $userIdentifier)->first();
if (!$user) {
$this->error('لم يتم العثور على المستخدم');
return self::FAILURE;
}
$name = $this->argument('name');
$permissions = $this->option('permissions') ?: null;
$ipWhitelist = $this->option('ip') ?: null;
$expiresAt = $this->option('expires-in')
? now()->addDays((int) $this->option('expires-in'))
: null;
$result = ApiKeys::generate(
$user->id,
$name,
$permissions,
$ipWhitelist,
$expiresAt
);
$this->info("تم إنشاء مفتاح API بنجاح!");
$this->line("");
$this->line("المفتاح: {$result['key']}");
$this->line("الاسم: {$result['name']}");
if ($expiresAt) {
$this->line("ينتهي: {$expiresAt->toDateTimeString()}");
}
$this->warn("\nقم بتخزين هذا المفتاح بشكل آمن. لن يظهر مرة أخرى!");
return self::SUCCESS;
}
}
اختبار حزمتك
قم بإعداد PHPUnit للاختبار:
<?php
// tests/TestCase.php
namespace VendorName\ApiKeys\Tests;
use Orchestra\Testbench\TestCase as Orchestra;
use VendorName\ApiKeys\ApiKeysServiceProvider;
abstract class TestCase extends Orchestra
{
protected function setUp(): void
{
parent::setUp();
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
}
protected function getPackageProviders($app): array
{
return [
ApiKeysServiceProvider::class,
];
}
protected function getEnvironmentSetUp($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
}
}
// tests/Feature/ApiKeyGenerationTest.php
namespace VendorName\ApiKeys\Tests\Feature;
use VendorName\ApiKeys\Tests\TestCase;
use VendorName\ApiKeys\Facades\ApiKeys;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ApiKeyGenerationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_can_generate_an_api_key()
{
$result = ApiKeys::generate(1, 'مفتاح اختبار');
$this->assertArrayHasKey('id', $result);
$this->assertArrayHasKey('key', $result);
$this->assertStringStartsWith('sk_', $result['key']);
}
/** @test */
public function it_can_validate_a_generated_key()
{
$result = ApiKeys::generate(1, 'مفتاح اختبار');
$validated = ApiKeys::validate($result['key']);
$this->assertNotNull($validated);
$this->assertEquals(1, $validated['user_id']);
}
/** @test */
public function it_rejects_invalid_keys()
{
$validated = ApiKeys::validate('invalid_key');
$this->assertNull($validated);
}
}
النشر إلى Packagist
قم بإعداد حزمتك للتوزيع:
# تحديث composer.json
{
"name": "vendor-name/api-keys",
"description": "إدارة مفاتيح API لـ Laravel",
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"VendorName\\ApiKeys\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"VendorName\\ApiKeys\\Tests\\": "tests/"
}
},
"require": {
"php": "^8.1",
"illuminate/support": "^10.0|^11.0"
},
"require-dev": {
"orchestra/testbench": "^8.0|^9.0",
"phpunit/phpunit": "^10.0"
},
"extra": {
"laravel": {
"providers": [
"VendorName\\ApiKeys\\ApiKeysServiceProvider"
],
"aliases": {
"ApiKeys": "VendorName\\ApiKeys\\Facades\\ApiKeys"
}
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
# خطوات النشر:
# 1. إنشاء حساب على packagist.org
# 2. دفع الكود إلى GitHub/GitLab
# 3. إرسال عنوان URL للحزمة إلى Packagist
# 4. إضافة webhook للتحديثات التلقائية
# 5. وسم الإصدارات مع إصدار دلالي (v1.0.0، v1.1.0، إلخ.)
# إنشاء وسم git
git tag -a v1.0.0 -m "الإصدار الأولي"
git push origin v1.0.0
تحذير: اختبر حزمتك بدقة دائمًا قبل النشر. بمجرد النشر، اتبع الإصدار الدلالي بدقة لتجنب التغييرات المدمرة للمستخدمين.
تمرين 1: أنشئ حزمة Laravel تسمى "notification-channels" التي تضيف دعمًا لإشعارات Telegram و Discord. قم بتضمين مزودي الخدمة والتكوين وقنوات الإشعارات والاختبارات. انشرها إلى مستودع GitHub خاص.
تمرين 2: قم ببناء حزمة لإدارة تفضيلات المستخدم (السمة، واللغة، والمنطقة الزمنية، وإشعارات البريد الإلكتروني). قم بتضمين الترحيلات والنماذج و facade وأوامر Artisan لاستيراد / تصدير التفضيلات و middleware الذي يطبق تفضيلات المستخدم على كل طلب.
تمرين 3: قم بتطوير حزمة "laravel-audit-log" التي تسجل تلقائيًا جميع تغييرات النموذج (إنشاء، تحديث، حذف) مع معلومات المستخدم والطوابع الزمنية. قم بتضمين عرض لوحة معلومات لتصفح سجلات المراجعة، وقدرات البحث / التصفية، والتنظيف التلقائي للسجلات القديمة. أضف اختبارات شاملة.
الملخص
في هذا الدرس، تعلمت:
- هيكل وتنظيم حزمة Laravel
- إنشاء مزودي الخدمة مع نشر التكوين والأصول
- بناء وظائف الحزمة مع facades و middleware
- كتابة أوامر Artisan لإدارة الحزم
- اختبار الحزم باستخدام Orchestra Testbench
- نشر الحزم إلى Packagist