Fáze požadavku: 10. Pest testování & QA
Modul 9 · Lekce 1

Konzolové příkazy Artisan: Tvorba příkazu pro audit a synchronizaci kurzu

💡 Shrnutí lekce

Naučte se psát robustní konzolové příkazy v Laravelu 12 s injektovanými službami a přepínači. Prozkoumáme reálný příkaz VerifyCurriculumCommand, který hlídá naši aplikaci před halucinacemi.

Konzolové rozhraní Artisan v moderním Laravelu 12

Artisan je konzolové CLI rozhraní dodávané s Laravelem, postavené na robustní komponentě Symfony Console. V Laravelu 12 neslouží konzolové příkazy pouze k údržbě frameworku (jako migrate nebo cache:clear), ale představují primární nástroj pro automatizaci doménových procesů, synchronizaci dat, audit integrity a spouštění dávkových úloh.

Při návrhu profesionálních aplikací v PHP 8.4 píšeme příkazy jako tenké fasády nad doménovými službami. Samotný příkaz se stará o:

  1. Zpracování argumentů a přepínačů z příkazové řádky.
  2. Zobrazení formátovaného výstupu (tabulky, progress bary, barevné hlášky).
  3. Delegování byznys logiky do doménové vrstvy.
  4. Vrácení správného návratového kódu operačnímu systému (self::SUCCESS nebo self::FAILURE).

Anatomie podpisu příkazu ($signature)

Vlastnost $signature definuje název příkazu i jeho vstupní rozhraní. V našem projektu používáme příkaz curriculum:verify:

protected $signature = 'curriculum:verify {--db : Ověřit také přítomnost modulů a lekcí v databázi}';
  • curriculum:verify: Název příkazu s logickým jmenným prostorem (namespace oddělený dvojtečkou).
  • {--db}: Volitelný booleovský přepínač (flag/option). Pokud uživatel spustí php artisan curriculum:verify --db, metoda $this->option('db') vrátí true. Pokud přepínač nezadá, vrátí false.
  • : Ověřit také přítomnost...: Popis přepínače, který se automaticky zobrazí v nápovědě php artisan curriculum:verify --help.

Automatická injekce závislostí v metodě handle()

Jednou z nejelegantnějších vlastností Laravel kontejneru je Method Injection v metodě handle(). Nemusíte složitě konfigurovat konstruktor příkazu – stačí otipovat parametr v metodě handle() a Laravel IoC kontejner jej při spuštění automaticky vyřeší a dosadí:

public function handle(AnatomyInspectorService $inspectorService): int
{
    // $inspectorService je plně inicializovaná doménová služba
    $inspected = $inspectorService->inspectFile($cleanFile);
    // ...
}

Tento přístup udržuje kód testovatelný a čistý. Při testování příkazu můžete službu AnatomyInspectorService snadno nahradit mockem nebo testovacím dvojčetem.


Zero-Hallucination Audit v praxi naší platformy

V naší výukové platformě plní VerifyCurriculumCommand klíčovou roli v ochraně proti halucinacím. Příkaz načte soubor .agents/memory/curriculum_registry.json, který je jediným zdrojem pravdy (Single Source of Truth) pro osnovu kurzu, a pro každou lekci zkontroluje:

  1. Zda je definován platný a neprázdný slug.
  2. Zda existují všechny soubory uvedené v poli anatomy_files.
  3. Zda žádný soubor neporušuje bezpečnostní politiku (např. pokus o zobrazení .env nebo souborů mimo repozitář).
  4. Pokud je zadán parametr --db, ověří, že každý modul i lekce jsou zaindexovány v PostgreSQL a mají přiřazené kvízové otázky.

Pokud příkaz narazí na jedinou nesrovnalost, vrátí self::FAILURE (kód 1). Tento exit status okamžitě zastaví CI/CD pipeline nebo noční kontrolu a zabrání nasazení nekonzistentního obsahu.

💻 Klíčová konstrukce z praxe
// app/Console/Commands/VerifyCurriculumCommand.php
namespace App\Console\Commands;

use App\Models\CourseLesson;
use App\Models\CourseModule;
use App\Services\AnatomyInspectorService;
use Illuminate\Console\Command;

final class VerifyCurriculumCommand extends Command
{
    // Konzolový podpis příkazu včetně volitelného přepínače --db
    protected $signature = 'curriculum:verify {--db : Ověřit také přítomnost modulů a lekcí v databázi}';
    protected $description = 'Ověří integritu osnovy kurzu v .agents/memory/curriculum_registry.json a existenci všech odkazovaných anatomy_files';

    // Laravel Service Container automaticky injektuje AnatomyInspectorService
    public function handle(AnatomyInspectorService $inspectorService): int
    {
        $this->info('🔍 Spouštím Zero-Hallucination audit osnovy a souborů anatomie...');

        $registryPath = base_path('.agents/memory/curriculum_registry.json');
        if (! file_exists($registryPath)) {
            $this->error("Chyba: Soubor osnovy '{$registryPath}' neexistuje!");
            return self::FAILURE;
        }

        // Kontrola existence všech anatomy_files přes inspektor
        // ...
        return self::SUCCESS;
    }
}

🎯 Ověření znalostí z lekce

Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.

Otázka 1 z 3

Jak Laravel 12 předává závislosti (Dependency Injection) do metody handle() vlastního Artisan příkazu?

Otázka 2 z 3

Co znamená definice {--db : Popis} ve vlastnosti $signature konzolového příkazu?

Otázka 3 z 3

Proč je důležité, aby metoda handle() vracela návratový kód self::SUCCESS (0) nebo self::FAILURE (1)?

Zodpovězeno: 0 / 3
🔬 Živá anatomie v této aplikaci
2 soubory
Na co se v kódu zaměřit: V souboru app/Console/Commands/VerifyCurriculumCommand.php si všimni metody handle(AnatomyInspectorService $inspectorService), kde Laravel Service Container automaticky provádí Method Injection přímo při spuštění konzolového příkazu.
cesta: app/Console/Commands/VerifyCurriculumCommand.php
150 řádků · 5.6 KB
Artisan příkaz pro zero-hallucination verifikaci registry osnovy proti fyzickým souborům a databázi.
1 <?php
2
3 declare(strict_types=1);
4
5 namespace App\Console\Commands;
6
7 use App\Models\CourseLesson;
8 use App\Models\CourseModule;
9 use App\Services\AnatomyInspectorService;
10 use Illuminate\Console\Command;
11 use Illuminate\Support\Facades\DB;
12 use Throwable;
13
14 final class VerifyCurriculumCommand extends Command
15 {
16 /**
17 * Konzolový podpis příkazu.
18 *
19 * @var string
20 */
21 protected $signature = 'curriculum:verify {--db : Ověřit také přítomnost modulů a lekcí v databázi}';
22
23 /**
24 * Popis příkazu.
25 *
26 * @var string
27 */
28 protected $description = 'Ověří integritu osnovy kurzu v .agents/memory/curriculum_registry.json a existenci všech odkazovaných anatomy_files';
29
30 public function handle(AnatomyInspectorService $inspectorService): int
31 {
32 $this->info('🔍 Spouštím Zero-Hallucination audit osnovy a souborů anatomie...');
33
34 $registryPath = base_path('.agents/memory/curriculum_registry.json');
35
36 if (! file_exists($registryPath)) {
37 $this->error("Chyba: Soubor osnovy '{$registryPath}' neexistuje!");
38
39 return self::FAILURE;
40 }
41
42 /** @var array{course_title?: string, version?: string, modules?: array<int, array<string, mixed>>}|null $registry */
43 $registry = json_decode((string) file_get_contents($registryPath), true);
44
45 if (! is_array($registry) || ! isset($registry['modules']) || ! is_array($registry['modules'])) {
46 $this->error("Chyba: Soubor osnovy '{$registryPath}' neobsahuje platný JSON s polem 'modules'.");
47
48 return self::FAILURE;
49 }
50
51 $courseTitle = (string) ($registry['course_title'] ?? 'Neznámý kurz');
52 $this->line("Kurz: <fg=cyan>{$courseTitle}</> (verze: ".($registry['version'] ?? '1.0.0').')');
53
54 $errors = [];
55 $totalModules = count($registry['modules']);
56 $totalLessons = 0;
57 $checkedFiles = [];
58
59 foreach ($registry['modules'] as $module) {
60 $moduleNumber = $module['number'] ?? '?';
61 $moduleSlug = (string) ($module['slug'] ?? '');
62 $lessons = (array) ($module['lessons'] ?? []);
63
64 if (empty($moduleSlug)) {
65 $errors[] = "Modul #{$moduleNumber} nemá definován platný 'slug'.";
66 }
67
68 foreach ($lessons as $lesson) {
69 $totalLessons++;
70 $lessonSlug = (string) ($lesson['slug'] ?? 'neznama-lekce');
71 $anatomyFiles = (array) ($lesson['anatomy_files'] ?? []);
72
73 if (empty($anatomyFiles)) {
74 $errors[] = "Modul [{$moduleSlug}] lekce [{$lessonSlug}]: Pole 'anatomy_files' je prázdné!";
75 }
76
77 foreach ($anatomyFiles as $filePath) {
78 $cleanFile = ltrim((string) $filePath, '/');
79 $checkedFiles[$cleanFile] = true;
80
81 $inspected = $inspectorService->inspectFile($cleanFile);
82 if (! $inspected->exists) {
83 $errors[] = "Modul [{$moduleSlug}] lekce [{$lessonSlug}]: Odkazovaný anatomy_file '{$cleanFile}' neexistuje nebo je blokován bezpečnostní politikou!";
84 }
85 }
86 }
87 }
88
89 $uniqueFilesCount = count($checkedFiles);
90 $this->line("Auditováno <info>{$totalModules}</info> modulů, <info>{$totalLessons}</info> lekcí a <info>{$uniqueFilesCount}</info> unikátních souborů anatomie.");
91
92 $checkDb = (bool) $this->option('db');
93 if ($checkDb) {
94 $this->info('🗄️ Ověřuji integritu záznamů v databázi PostgreSQL...');
95
96 try {
97 DB::connection()->getPdo();
98 } catch (Throwable $e) {
99 $this->error('Připojení k databázi selhalo: '.$e->getMessage());
100
101 return self::FAILURE;
102 }
103
104 foreach ($registry['modules'] as $module) {
105 $moduleSlug = (string) ($module['slug'] ?? '');
106 $dbModule = CourseModule::where('slug', $moduleSlug)->first();
107
108 if (! $dbModule) {
109 $errors[] = "Databáze: Modul se slugem '{$moduleSlug}' nebyl nalezen v tabulce course_modules!";
110
111 continue;
112 }
113
114 $lessons = (array) ($module['lessons'] ?? []);
115 foreach ($lessons as $lesson) {
116 $lessonSlug = (string) ($lesson['slug'] ?? '');
117 $dbLesson = CourseLesson::where('course_module_id', $dbModule->id)
118 ->where('slug', $lessonSlug)
119 ->first();
120
121 if (! $dbLesson) {
122 $errors[] = "Databáze: Lekce se slugem '{$lessonSlug}' v modulu '{$moduleSlug}' nebyla nalezena v tabulce course_lessons!";
123
124 continue;
125 }
126
127 $quizCount = $dbLesson->quizQuestions()->count();
128 if ($quizCount === 0) {
129 $this->warn("Upozornění: Lekce '{$lessonSlug}' modulu '{$moduleSlug}' nemá v DB žádné kvízové otázky.");
130 }
131 }
132 }
133 }
134
135 if (! empty($errors)) {
136 $this->newLine();
137 $this->error('❌ Zjištěno '.count($errors).' chyb integrity osnovy:');
138 foreach ($errors as $err) {
139 $this->line(" <fg=red>•</> {$err}");
140 }
141
142 return self::FAILURE;
143 }
144
145 $this->newLine();
146 $this->info('✅ Všechny moduly, lekce i anatomy_files v curriculum_registry.json jsou 100% platné a bez halucinací!');
147
148 return self::SUCCESS;
149 }
150 }
151
cesta: routes/console.php
12 řádků · 0.4 KB
Soubor aplikační architektury v modulu HTTP Routing.
1 <?php
2
3 declare(strict_types=1);
4
5 use Illuminate\Support\Facades\Artisan;
6 use Illuminate\Support\Facades\Schedule;
7
8 Artisan::command('inspire', function (): void {
9 $this->comment('Anatomie Laravelu 12 & PHP 8.4 — Živá výuková platforma na laravel.heatsharing.cz');
10 })->purpose('Zobrazí motto výukové platformy');
11
12 Schedule::command('curriculum:verify')->dailyAt('03:00');
13
Zdroj: skutečné soubory této aplikace Plný průzkumník anatomie →