وحدة تحكم Artisan في Laravel والأوامر المخصصة
Artisan هو "سكين الجيب السويسري" في Laravel — مع أكثر من 100 أمر مدمج يغطي جميع عمليات التشغيل والصيانة، والأوامر المخصصة تتيح لك التعامل مع المهام المتكررة بنقرة واحدة.
1. ما ستتعلمه
- القائمة الكاملة للأوامر المدمجة: migrate/cache/config/route/list
- إنشاء أوامر مخصصة:
make:commandوصياغة Signature - وسيطات وخيارات الأوامر: تعريف {argument} / {--option}
- جدولة الأوامر: مهام Cron في app/Console/Kernel.php
- الأوامر التفاعلية: confirm/choice/ask/anticipate
2. قصة حقيقية لمهندس عمليات
(1) المشكلة: تنفيذ 20 مهمة تشغيل يدويًا كل يوم
كل صباح، يجب على Charlie تنفيذ المهام التالية يدويًا: مسح الجلسات منتهية الصلاحية، وإنشاء التقارير اليومية، ومزامنة حالات الاشتراك، وإرسال رسائل البريد الإلكتروني للتذكير بانتهاء الصلاحية، والنسخ الاحتياطي لقاعدة البيانات... عشرون مهمة موزعة عبر خمس نوافذ طرفية — إذا فاته واحدة، سيدفع من جيبه. سأل Bob: "أليست هذه جميعها مهام مجدولة؟ لماذا يجب عليك تنفيذها يدويًا؟"
(2) حلول أوامر Artisan والجدولة
أوامر Artisan المخصصة تغلف المهام المتكررة في أمر واحد، ويقوم Schedule بتشغيلها تلقائيًا وفقًا لجدول — يحتاج Charlie فقط إلى إلقاء نظرة سريعة على سجل التنفيذ مرة واحدة في اليوم.
# أمر واحد يفعل كل شيء
php artisan shopmetrics:daily-maintenance
# أو دع المجدول يشغله تلقائيًا في الساعة 2 صباحًا
php artisan schedule:run
(3) النتيجة
بعد أن أعد Charlie الجدولة باستخدام Artisan، عملت جميع المهام العشرين تلقائيًا، وانخفض معدل الفشل من 5% إلى 0%.
3. القائمة الكاملة للأوامر المدمجة
(1) فئات الأوامر الشائعة
| الفئة | الأمر | الوصف |
|---|---|---|
| التطبيق | about |
نظرة عامة على معلومات البيئة |
down / up |
تبديل وضع الصيانة | |
env |
عرض تهيئة .env | |
| قاعدة البيانات | migrate |
تنفيذ الترحيل |
migrate:rollback |
التراجع عن الترحيل | |
migrate:fresh |
إعادة بناء قاعدة البيانات | |
db:seed |
تعبئة البيانات | |
db:show |
معلومات قاعدة البيانات | |
| التخزين المؤقت | cache:clear |
مسح التخزين المؤقت |
config:cache / clear |
تخزين مؤقت للتهيئة | |
route:cache / clear |
تخزين مؤقت للمسارات | |
view:cache / clear |
تخزين مؤقت للعروض | |
| المسارات | route:list |
قائمة جميع المسارات |
| الطوابير | queue:work |
بدء العامل |
queue:failed |
عرض المهام الفاشلة | |
queue:retry |
إعادة محاولة المهام الفاشلة | |
| الإنشاء | make:model |
إنشاء نموذج |
make:controller |
إنشاء متحكم | |
make:migration |
إنشاء ترحيل | |
make:command |
إنشاء أمر |
(1) ▶ مثال: مرجع سريع لأوامر Artisan الشائعة
# معلومات التطبيق
php artisan about
php artisan env
# عمليات قاعدة البيانات
php artisan migrate --seed
php artisan db:show --counts
# إدارة التخزين المؤقت
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize # تخزين مؤقت للتهيئة + المسارات + العروض
# وضع الصيانة
php artisan down --secret="maintenance-token" # السماح بالوصول باستخدام ?secret=
php artisan up
# إدارة الطوابير
php artisan queue:work --queue=high,default
php artisan queue:failed
php artisan queue:retry all
# فحص المسارات
php artisan route:list --path=api
php artisan route:list --columns=method,uri,name
الناتج:
# تم تنفيذ الأمر بنجاح
4. الأوامر المخصصة
(1) إنشاء أمر
php artisan make:command ProcessTenantAnalytics
# ينشئ: app/Console/Commands/ProcessTenantAnalytics.php
(2) صياغة Signature للأوامر
// app/Console/Commands/ProcessTenantAnalytics.php
class ProcessTenantAnalytics extends Command
{
// صياغة Signature: {وسيطة} {--خيار}
protected $signature = 'shopmetrics:analytics
{tenant? : معرّف المستأجر أو الاسم المختصر (اختياري)}
{--type=monthly : نوع التقرير (daily|weekly|monthly)}
{--force : إعادة الحساب إجباريًا}
{--format=csv : صيغة الإخراج (csv|json)}';
protected $description = 'Process analytics for tenants';
public function handle(): int
{
$tenant = $this->argument('tenant');
$type = $this->option('type');
$force = $this->option('force');
if ($tenant) {
$this->processSingleTenant($tenant, $type, $force);
} else {
$this->processAllTenants($type, $force);
}
return self::SUCCESS;
}
}
(3) قواعد صياغة Signature
| الصياغة | الوصف | مثال |
|---|---|---|
{name} |
وسيطة مطلوبة | {tenant} |
{name?} |
وسيطة اختيارية | {tenant?} |
{name=default} |
بقيمة افتراضية | {type=monthly} |
{--option} |
خيار منطقي | --force |
{--option=default} |
خيار بقيمة | --format=csv |
{--O|shortcut} |
خيار مختصر | --force|f |
(1) ▶ مثال: أوامر إدارة المستأجرين في ShopMetrics
// app/Console/Commands/ManageTenant.php
class ManageTenant extends Command
{
protected $signature = 'shopmetrics:tenant
{action : الإجراء المطلوب تنفيذه (list|suspend|activate|stats)}
{tenant? : معرّف المستأجر أو الاسم المختصر}
{--with-users : تضمين إحصائيات المستخدمين}';
protected $description = 'Manage ShopMetrics tenants';
public function handle(): int
{
$action = $this->argument('action');
match ($action) {
'list' => $this->listTenants(),
'suspend' => $this->suspendTenant(),
'activate' => $this->activateTenant(),
'stats' => $this->showTenantStats(),
default => $this->error("Unknown action: {$action}"),
};
return self::SUCCESS;
}
private function listTenants(): void
{
$tenants = Tenant::withCount(['shops', 'users'])->get();
$this->table(
['ID', 'Name', 'Slug', 'Status', 'Shops', 'Users'],
$tenants->map(fn ($t) => [
$t->id, $t->name, $t->slug, $t->status,
$t->shops_count, $t->users_count,
])
);
}
private function suspendTenant(): void
{
$identifier = $this->argument('tenant') ?? $this->ask('Enter tenant ID or slug:');
$tenant = $this->resolveTenant($identifier);
$tenant->update(['status' => 'suspended']);
$this->info("Tenant {$tenant->name} has been suspended.");
}
private function resolveTenant(string $identifier): Tenant
{
return is_numeric($identifier)
? Tenant::findOrFail($identifier)
: Tenant::whereSlug($identifier)->firstOrFail();
}
}
الناتج:
// تم التنفيذ بنجاح
5. وسيطات وخيارات الأوامر
(1) استرجاع الوسيطات
// الوسيطات
$name = $this->argument('name'); // وسيطة واحدة
$all = $this->arguments(); // جميع الوسيطات كمصفوفة
// الخيارات
$force = $this->option('force'); // خيار واحد (منطقي أو قيمة)
$all = $this->options(); // جميع الخيارات كمصفوفة
(2) الوسيطات المصفوفية
// Signature مع وسيطة مصفوفية
protected $signature = 'shopmetrics:report
{tenants* : معرّف مستأجر واحد أو أكثر}
{--type=monthly}';
// الاستخدام
php artisan shopmetrics:report 1 2 3 --type=weekly
// الوصول
$tenants = $this->argument('tenants'); // [1, 2, 3]
(3) طرق الإخراج
| الطريقة | الوصف | نموذج الإخراج |
|---|---|---|
info() |
معلومات خضراء | ✓ تم |
error() |
خطأ أحمر | ✗ فشل |
warn() |
تحذير أصفر | ⚠ تحذير |
line() |
نص عادي | نص عادي |
table() |
جدول | جدول منسق |
progressBar() |
شريط تقدم | ██████░░ 60% |
(1) ▶ مثال: أمر تنظيف بيانات ShopMetrics مع شريط تقدم
// app/Console/Commands/CleanupExpiredData.php
class CleanupExpiredData extends Command
{
protected $signature = 'shopmetrics:cleanup
{--days=90 : حذف بيانات أقدم من N يوم}
{--dry-run : عرض ما سيتم حذفه}';
protected $description = 'Clean up expired data (old reports, expired tokens)';
public function handle(): int
{
$days = $this->option('days');
$dryRun = $this->option('dry-run');
$cutoff = now()->subDays($days);
// تنظيف الرموز منتهية الصلاحية
$expiredTokens = Sanctum::$personalAccessTokenModel::where('last_used_at', '<', $cutoff);
$this->info(($dryRun ? 'Would delete' : 'Deleting') . " {$expiredTokens->count()} expired tokens.");
// تنظيف التقارير القديمة من S3
$bar = $this->output->createProgressBar(Tenant::count());
$deletedFiles = 0;
Tenant::chunk(100, function ($tenants) use ($cutoff, $dryRun, &$deletedFiles, $bar) {
foreach ($tenants as $tenant) {
$files = Storage::disk('s3')->allFiles("reports/{$tenant->slug}");
foreach ($files as $file) {
if (Storage::disk('s3')->lastModified($file) < $cutoff->timestamp) {
if (!$dryRun) Storage::disk('s3')->delete($file);
$deletedFiles++;
}
}
$bar->advance();
}
});
$bar->finish();
$this->newLine();
$this->info(($dryRun ? 'Would delete' : 'Deleted') . " {$deletedFiles} old report files.");
if (!$dryRun) {
$expiredTokens->delete();
}
return self::SUCCESS;
}
}
الناتج:
// تم التنفيذ بنجاح
6. جدولة الأوامر
(1) تعريف الجدولة
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('shopmetrics:analytics --type=daily')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping()
->emailOutputOnFailure('admin@shopmetrics.io');
Schedule::command('shopmetrics:cleanup --days=90')
->weekly()
->sundays()
->at('03:00')
->onOneServer();
Schedule::command('shopmetrics:sync-subscriptions')
->dailyAt('06:00')
->onOneServer()
->withoutOverlapping();
Schedule::job(new ProcessMonthlyAnalyticsJob)
->monthlyOn(1, '00:00')
->onOneServer();
(2) خيارات تكرار الجدولة
| الطريقة | التكرار | ما يعادلها في Cron |
|---|---|---|
everyMinute() |
كل دقيقة | * * * * * |
everyFiveMinutes() |
كل 5 دقائق | */5 * * * * |
hourly() |
كل ساعة | 0 * * * * |
daily() |
منتصف الليل كل يوم | 0 0 * * * |
dailyAt('14:00') |
الساعة 2 ظهرًا يوميًا | 0 14 * * * |
weekly() |
منتصف ليل كل أحد | 0 0 * * 0 |
monthly() |
منتصف ليل أول كل شهر | 0 0 1 * * |
cron('...') |
Cron مخصص | أي |
(3) قيود الجدولة
| الطريقة | الوصف |
|---|---|
onOneServer() |
التشغيل على خادم واحد فقط |
withoutOverlapping() |
عدم السماح بالتنفيذ المتداخل |
runInBackground() |
التشغيل في الخلفية |
when(Closure) |
تنفيذ شرطي |
environments('prod') |
بيئة محددة |
emailOutputOnFailure() |
إشعار بالبريد الإلكتروني عند الفشل |
(1) ▶ مثال: تهيئة جدولة كاملة في ShopMetrics
// routes/console.php
Schedule::command('shopmetrics:analytics --type=daily')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping(60)
->emailOutputOnFailure('ops@shopmetrics.io');
Schedule::command('shopmetrics:cleanup --days=90')
->weeklyOn(Schedule::SUNDAY, '03:00')
->onOneServer();
Schedule::command('shopmetrics:send-expiring-notifications')
->dailyAt('08:00')
->when(fn () => Subscription::expiringSoon()->exists());
Schedule::command('queue:prune-failed --hours=168')
->daily();
Schedule::command('queue:prune-batches --hours=168')
->daily();
// إدخال Cron على الخادم
// * * * * * cd /var/www/shopmetrics && php artisan schedule:run >> /dev/null 2>&1
الناتج:
// تم التنفيذ بنجاح
7. الأوامر التفاعلية
(1) طرق التفاعل
// طلب إدخال
$name = $this->ask('What is the tenant name?');
// طلب إدخال بقيمة افتراضية
$email = $this->ask('Email address?', 'admin@example.com');
// إدخال سري (كلمات المرور)
$password = $this->secret('Enter password:');
// تأكيد (نعم/لا)
if ($this->confirm('Do you wish to continue?', true)) {
// الافتراضي: نعم
}
// اختيار (اختيار واحد)
$type = $this->choice(
'Select report type',
['daily', 'weekly', 'monthly'],
0 // الفهرس الافتراضي
);
// توقع (إكمال تلقائي)
$name = $this->anticipate('Tenant name', Tenant::pluck('name')->toArray());
(2) سير عمل تفاعلي شامل
// app/Console/Commands/SetupTenant.php
class SetupTenant extends Command
{
protected $signature = 'shopmetrics:tenant-setup';
protected $description = 'Interactive tenant setup wizard';
public function handle(): int
{
$this->info('=== ShopMetrics Tenant Setup Wizard ===');
$name = $this->ask('Tenant name');
$slug = $this->anticipate('Slug', [Str::slug($name)]);
$domain = $this->ask('Custom domain (optional)', $slug . '.shopmetrics.io');
$plan = $this->choice('Select plan', ['Starter', 'Pro', 'Enterprise'], 1);
$ownerEmail = $this->ask('Owner email');
$this->table(
['Field', 'Value'],
[['Name', $name], ['Slug', $slug], ['Domain', $domain], ['Plan', $plan], ['Owner', $ownerEmail]],
);
if (!$this->confirm('Create this tenant?', true)) {
$this->warn('Cancelled.');
return self::FAILURE;
}
$tenant = Tenant::create(compact('name', 'slug', 'domain'));
User::factory()->create([
'tenant_id' => $tenant->id,
'email' => $ownerEmail,
'role' => 'tenant_owner',
]);
$this->info("Tenant {$name} created successfully!");
return self::SUCCESS;
}
}
(1) ▶ مثال: أمر تصدير بيانات تفاعلي في ShopMetrics
// app/Console/Commands/ExportData.php
class ExportData extends Command
{
protected $signature = 'shopmetrics:export';
protected $description = 'Interactive data export tool';
public function handle(): int
{
$type = $this->choice('What to export?', [
'orders' => 'Orders',
'products' => 'Products',
'analytics' => 'Analytics Report',
]);
$tenant = $this->anticipate('Tenant (leave blank for all)', Tenant::pluck('name')->push('All')->toArray());
$format = $this->choice('Format?', ['csv', 'xlsx', 'json'], 0);
$dateFrom = $this->ask('Date from (Y-m-d, optional)');
$dateTo = $this->ask('Date to (Y-m-d, optional)');
$this->info("Exporting {$type} for {$tenant} in {$format} format...");
$query = match ($type) {
'orders' => Order::query(),
'products' => Product::query(),
'analytics' => AnalyticsReport::query(),
};
if ($tenant !== 'All') {
$tenantModel = Tenant::whereName($tenant)->firstOrFail();
$query->where('tenant_id', $tenantModel->id);
}
if ($dateFrom) $query->where('created_at', '>=', $dateFrom);
if ($dateTo) $query->where('created_at', '<=', $dateTo);
$count = $query->count();
$this->info("Found {$count} records.");
if (!$this->confirm("Export {$count} records?", true)) {
return self::FAILURE;
}
$path = ExportService::export($query, $format);
$this->info("Export saved to: {$path}");
return self::SUCCESS;
}
}
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: مجموعة أوامر عمليات ShopMetrics
// ============================================
// شامل: أوامر عمليات ShopMetrics
// يغطي: signature، الخيارات، شريط التقدم، الجدولة، التفاعل
// ============================================
// app/Console/Commands/DailyMaintenance.php
class DailyMaintenance extends Command
{
protected $signature = 'shopmetrics:daily-maintenance
{--skip-analytics : تخطي معالجة التحليلات}
{--skip-cleanup : تخطي تنظيف البيانات}
{--notify : إرسال إشعار عند الانتهاء}';
protected $description = 'Run daily maintenance tasks';
public function handle(): int
{
$this->info('Starting daily maintenance...');
if (!$this->option('skip-analytics')) {
$this->processAnalytics();
}
if (!$this->option('skip-cleanup')) {
$this->cleanupExpiredData();
}
$this->syncSubscriptions();
if ($this->option('notify')) {
$this->sendCompletionNotification();
}
$this->info('Daily maintenance completed.');
return self::SUCCESS;
}
private function processAnalytics(): void
{
$this->info('Processing daily analytics...');
$tenants = Tenant::active()->get();
$bar = $this->output->createProgressBar($tenants->count());
foreach ($tenants as $tenant) {
GenerateReportJob::dispatch($tenant, 'daily', 'json');
$bar->advance();
}
$bar->finish();
$this->newLine();
}
private function cleanupExpiredData(): void
{
$this->call('shopmetrics:cleanup', ['--days' => 90]);
}
private function syncSubscriptions(): void
{
$this->call('shopmetrics:sync-subscriptions');
}
private function sendCompletionNotification(): void
{
$admin = User::where('role', 'super_admin')->first();
$admin?->notify(new DailyMaintenanceCompleted());
}
}
// جدولة الأمر
// routes/console.php
Schedule::command('shopmetrics:daily-maintenance --notify')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping()
->emailOutputOnFailure('ops@shopmetrics.io');
❓ أسئلة شائعة
make:command و make:command --command؟--command=xxx اسم أمر مخصص: php artisan make:command SendEmails --command=emails:send. إذا تم حذفه، يُستخدم الاسم الافتراضي app:console-commands:send-emails. يُوصى بتحديد اسم أمر مخصص دائمًا.* * * * * php artisan schedule:run. جميع المهام محددة في الكود، مما يجعلها خاضعة للتحكم في الإصدارات وقابلة للاختبار. لا تقم بإعداد مهمة Cron منفصلة لكل مهمة.$this->artisan('command:name', ['arg' => 'value']) لتنفيذ الأمر في اختبار، وتأكد من أن الإخراج ->expectsOutput('Done') أو تحقق من تغييرات قاعدة البيانات.--dry-run لمعاينة النتائج قبل التنفيذ.->appendOutputTo(storage_path('logs/command.log')) لإلحاق السجلات في المجدول؛ استخدم Log::info() داخل الأمر؛ أو استخدم emailOutputOnFailure() لإرسال إشعار بالبريد الإلكتروني عند الفشل.📖 ملخص
- Artisan يتضمن أكثر من 100 أمر مدمج يغطي عمليات مثل الترحيل والتخزين المؤقت والطوابير والمسارات
- الأوامر المخصصة تستخدم صياغة Signature لتحديد الوسيطات والخيارات
- استخدم Laravel Schedule لأتمتة جدولة الأوامر بدلاً من مهام Cron اليدوية
- الأوامر التفاعلية تستخدم
askوconfirmوchoiceلتوفير تجربة موجهة - إخراج الجدول وشريط التقدم يجعل الأوامر تبدو أكثر احترافية
- onOneServer/withoutOverlapping: يمنع تشغيل مثيلات متعددة في وقت واحد
📝 تمارين
-
تمرين أساسي (⭐): أنشئ أمرًا باسم
shopmetrics:tenant-statsيقبل وسيطةtenantويخرج عدد المتاجر والطلبات وإجمالي الإيرادات لهذا المستأجر، بتنسيق جدول باستخدام أمرtable. -
تمرين متقدم (⭐⭐): أنشئ أمر
shopmetrics:daily-maintenance، والذي يتضمن معالجة التحليلات (مع شريط تقدم) + تنظيف البيانات منتهية الصلاحية + مزامنة الاشتراكات، وقم بتهيئة Schedule للتشغيل تلقائيًا في الساعة 2 صباحًا كل يوم. -
تحدي (⭐⭐⭐): نفّذ أمر
shopmetrics:tenant-setupتفاعلي — أدخل اسم المستأجر والاسم المختصر والخطة وعنوان بريد المسؤول الإلكتروني بالتسلسل، مع تحقق في كل خطمة، ثم أكد الإنشاء في النهاية. تضمين اقتراحات إكمال تلقائي (anticipate) لاسم المستأجر.



