404 Not Found

404 Not Found


nginx

وحدة تحكم Artisan في Laravel والأوامر المخصصة

Artisan هو "سكين الجيب السويسري" في Laravel — مع أكثر من 100 أمر مدمج يغطي جميع عمليات التشغيل والصيانة، والأوامر المخصصة تتيح لك التعامل مع المهام المتكررة بنقرة واحدة.

1. ما ستتعلمه


2. قصة حقيقية لمهندس عمليات

(1) المشكلة: تنفيذ 20 مهمة تشغيل يدويًا كل يوم

كل صباح، يجب على Charlie تنفيذ المهام التالية يدويًا: مسح الجلسات منتهية الصلاحية، وإنشاء التقارير اليومية، ومزامنة حالات الاشتراك، وإرسال رسائل البريد الإلكتروني للتذكير بانتهاء الصلاحية، والنسخ الاحتياطي لقاعدة البيانات... عشرون مهمة موزعة عبر خمس نوافذ طرفية — إذا فاته واحدة، سيدفع من جيبه. سأل Bob: "أليست هذه جميعها مهام مجدولة؟ لماذا يجب عليك تنفيذها يدويًا؟"

(2) حلول أوامر Artisan والجدولة

أوامر Artisan المخصصة تغلف المهام المتكررة في أمر واحد، ويقوم Schedule بتشغيلها تلقائيًا وفقًا لجدول — يحتاج Charlie فقط إلى إلقاء نظرة سريعة على سجل التنفيذ مرة واحدة في اليوم.

BASH
# أمر واحد يفعل كل شيء
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 الشائعة

BASH
# معلومات التطبيق
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

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

4. الأوامر المخصصة

(1) إنشاء أمر

BASH
php artisan make:command ProcessTenantAnalytics
# ينشئ: app/Console/Commands/ProcessTenantAnalytics.php

(2) صياغة Signature للأوامر

PHP
// 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

PHP
// 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();
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

5. وسيطات وخيارات الأوامر

(1) استرجاع الوسيطات

PHP
// الوسيطات
$name = $this->argument('name');        // وسيطة واحدة
$all = $this->arguments();              // جميع الوسيطات كمصفوفة

// الخيارات
$force = $this->option('force');        // خيار واحد (منطقي أو قيمة)
$all = $this->options();                // جميع الخيارات كمصفوفة

(2) الوسيطات المصفوفية

PHP
// 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 مع شريط تقدم

PHP
// 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;
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

6. جدولة الأوامر

(1) تعريف الجدولة

PHP
// 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

PHP
// 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

الناتج:

TEXT
// تم التنفيذ بنجاح

7. الأوامر التفاعلية

(1) طرق التفاعل

PHP
// طلب إدخال
$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) سير عمل تفاعلي شامل

PHP
// 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

PHP
// 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;
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

8. مثال شامل: مجموعة أوامر عمليات ShopMetrics

PHP
// ============================================
// شامل: أوامر عمليات 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. يُوصى بتحديد اسم أمر مخصص دائمًا.
س هل يجب أن أستخدم Cron أم Laravel Schedule لجدولة المهام؟
ج استخدم Laravel Schedule. يحتاج الخادم إلى مهمة Cron واحدة فقط: * * * * * php artisan schedule:run. جميع المهام محددة في الكود، مما يجعلها خاضعة للتحكم في الإصدارات وقابلة للاختبار. لا تقم بإعداد مهمة Cron منفصلة لكل مهمة.
س متى يجب استخدام onOneServer؟
ج عند النشر على خوادم متعددة، لمنع نفس المهمة المجدولة من العمل في وقت واحد على عدة أجهزة. يتطلب مشغل تخزين مؤقت Redis أو قاعدة بيانات للتنسيق. ليس ضروريًا في إعداد خادم واحد.
س كيف أختبر الأوامر المخصصة؟
ج استخدم $this->artisan('command:name', ['arg' => 'value']) لتنفيذ الأمر في اختبار، وتأكد من أن الإخراج ->expectsOutput('Done') أو تحقق من تغييرات قاعدة البيانات.
س ماذا يجب أن أفعل إذا كانت الأوامر بطيئة جدًا؟
ج عالج العمليات المستهلكة للوقت بشكل غير متزامن باستخدام طابور؛ الأوامر مسؤولة فقط عن توزيع المهام. لمجموعات البيانات الكبيرة، استخدم التقسيم مع شريط تقدم. أضف خيار --dry-run لمعاينة النتائج قبل التنفيذ.
س كيف أسجل إخراج الأوامر؟
ج استخدم ->appendOutputTo(storage_path('logs/command.log')) لإلحاق السجلات في المجدول؛ استخدم Log::info() داخل الأمر؛ أو استخدم emailOutputOnFailure() لإرسال إشعار بالبريد الإلكتروني عند الفشل.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ أمرًا باسم shopmetrics:tenant-stats يقبل وسيطة tenant ويخرج عدد المتاجر والطلبات وإجمالي الإيرادات لهذا المستأجر، بتنسيق جدول باستخدام أمر table.

  2. تمرين متقدم (⭐⭐): أنشئ أمر shopmetrics:daily-maintenance، والذي يتضمن معالجة التحليلات (مع شريط تقدم) + تنظيف البيانات منتهية الصلاحية + مزامنة الاشتراكات، وقم بتهيئة Schedule للتشغيل تلقائيًا في الساعة 2 صباحًا كل يوم.

  3. تحدي (⭐⭐⭐): نفّذ أمر shopmetrics:tenant-setup تفاعلي — أدخل اسم المستأجر والاسم المختصر والخطة وعنوان بريد المسؤول الإلكتروني بالتسلسل، مع تحقق في كل خطمة، ثم أكد الإنشاء في النهاية. تضمين اقتراحات إكمال تلقائي (anticipate) لاسم المستأجر.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%