Fáze požadavku: 7. Doménová vrstva & DTO
Modul 1 · Lekce 1

Striktní typy (strict_types=1) a moderní typový systém

💡 Shrnutí lekce

Pochopte, jak direktiva declare(strict_types=1) chrání kód před tichými chybami z implicitní konverze typů a jak typový systém v PHP 8.4 garantuje spolehlivost doménové logiky.

Co je striktní typování a proč je v moderním PHP klíčové

Historicky bylo PHP známé svou extrémně benevolentní typovou konverzí (type coercion). Pokud funkce očekávala celé číslo int a vy jste jí předali řetězec "42" nebo boolean true, PHP hodnotu tiše přetypovalo bez jakéhokoliv varování. V enterprise aplikacích a komplexních systémech však toto chování vede k tichým chybám, které se často projeví až v produkci v podobě nekonzistentních dat.

Od PHP 7.0 máme k dispozici direktivu:

declare(strict_types=1);

Umístěním této direktivy na úplně první řádek souboru (hned za otevírací značku <?php) dáváte PHP enginu instrukci: „Při volání jakýchkoliv funkcí a metod v tomto souboru striktně vyžaduj shodu deklarovaných a předaných typů. Žádné automatické přetypování řetězců na čísla nebo null na prázdný řetězec není dovoleno.“

Pokud dojde k předání nekompatibilního typu, PHP okamžitě vyhodí fatální výjimku TypeError. Chyba je tak odhalena okamžitě během vývoje nebo při běhu automatizovaných testů.


Evoluce typového systému až k PHP 8.4

Typový systém moderního PHP patří k nejpropracovanějším mezi dynamickými jazyky. Podívejme se na klíčové milníky:

  • PHP 7.0–7.4: Skalární typy (int, float, string, bool), návratové typy, nullable typy (?string), void a typované vlastnosti tříd (public string $name).
  • PHP 8.0: Union types (int|float), typ mixed a návratový typ never pro funkce, které vždy vyhazují výjimku nebo ukončují běh (exit).
  • PHP 8.1: Intersection types (Countable&Traversable), návratový typ never a podpora final class constants.
  • PHP 8.2: DNF typy (Disjunctive Normal Form) – kombinace Union a Intersection typů, např. (HasTitle&HasSlug)|null.
  • PHP 8.4: Asymetrická viditelnost (asymmetric visibility), nové funkce pro práci s poli (array_find, array_any) a ještě striktnější kontroly zanořených typů.

Rozsah platnosti (Scope) direktivy strict_types

Zásadní vlastností direktivy declare(strict_types=1); je, že funguje per-file (souborově):

  1. Ovlivňuje volání, která se provádějí v souboru se zapnutým striktním režimem.
  2. Pokud soubor A.php má strict_types=1 a zavolá metodu třídy definované v B.php, parametry jsou striktně ověřeny.
  3. Pokud soubor C.php nemá strict_types=1 a zavolá stejnou metodu, PHP použije slabé typování pro argumenty tohoto volání.

Proto v našem projektu agy-laravel uplatňujeme nekompromisní architektonické pravidlo definované v AGENTS.md: Každý PHP soubor v app/, config/, database/, routes/ a tests/ musí začínat deklarací declare(strict_types=1);.


Praktický dopad na doménové objekty v agy-laravel

V souboru app/Data/LessonContentData.php využíváme striktní typy v konstruktoru DTO:

public function __construct(
    public string $moduleSlug,
    public string $lessonSlug,
    public string $title,
    public string $summary,
    public string $renderedHtml,
    public ?string $codeSnippet,
    public array $anatomyFiles,
    public ?string $anatomyNote,
    public LifecyclePhase $lifecyclePhase,
) {}

Všimněte si:

  • ?string $codeSnippet: Otazník explicitně vyjadřuje, že hodnota může být string nebo null. Pokud by tam otazník nebyl a předali jsme null, PHP okamžitě vyvolá TypeError.
  • LifecyclePhase $lifecyclePhase: Argument musí být instancí konkrétního enumu, nikoliv libovolný string.
💻 Klíčová konstrukce z praxe
<?php

declare(strict_types=1);

namespace App\Data;

use App\Enums\LifecyclePhase;

// Ukázka striktně typovaného DTO z naší aplikace (app/Data/LessonContentData.php)
final readonly class LessonContentData
{
    /**
     * @param list<string> $anatomyFiles
     */
    public function __construct(
        public string $moduleSlug,
        public string $lessonSlug,
        public string $title,
        public string $summary,
        public string $renderedHtml,
        public ?string $codeSnippet,        // Nullable skalární typ: string nebo null
        public array $anatomyFiles,         // Pole cest k souborům
        public ?string $anatomyNote,        // Průvodce anatomií pro studenta
        public LifecyclePhase $lifecyclePhase, // Nativní PHP 8.1+ Enum
    ) {}
}

🎯 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

Co přesně způsobí uvedení direktivy declare(strict_types=1); na začátku PHP souboru?

Otázka 2 z 3

Jaký je rozsah platnosti (scope) direktivy declare(strict_types=1);?

Otázka 3 z 3

Co v moderním PHP vyjadřuje typový zápis ?string $codeSnippet?

Zodpovězeno: 0 / 3
🔬 Živá anatomie v této aplikaci
2 soubory
Na co se v kódu zaměřit: Otevři si soubor app/Data/LessonContentData.php a prohlédni si striktní signaturu konstruktoru. V app/Services/AnatomyInspectorService.php si všimni metody isAllowedPath(string $relativePath): bool, kde striktní typování parametrů i návratového typu chrání aplikaci před zranitelnostmi typu Path Traversal.
cesta: app/Data/LessonContentData.php
25 řádků · 0.5 KB
Readonly DTO s kompletními daty lekce pro bezpečné předávání mezi modely, controllery a šablonami.
1 <?php
2
3 declare(strict_types=1);
4
5 namespace App\Data;
6
7 use App\Enums\LifecyclePhase;
8
9 final readonly class LessonContentData
10 {
11 /**
12 * @param list<string> $anatomyFiles
13 */
14 public function __construct(
15 public string $moduleSlug,
16 public string $lessonSlug,
17 public string $title,
18 public string $summary,
19 public string $renderedHtml,
20 public ?string $codeSnippet,
21 public array $anatomyFiles,
22 public ?string $anatomyNote,
23 public LifecyclePhase $lifecyclePhase,
24 ) {}
25 }
26
cesta: app/Services/AnatomyInspectorService.php
305 řádků · 12.5 KB
Bezpečná introspekční služba aplikace čtoucí reálný zdrojový kód s ochranou proti path traversal a whitelistem cest.
1 <?php
2
3 declare(strict_types=1);
4
5 namespace App\Services;
6
7 use App\Data\AnatomyFileNodeData;
8 use App\Enums\LifecyclePhase;
9
10 final class AnatomyInspectorService
11 {
12 private const array ALLOWED_PREFIXES = [
13 'app/',
14 'bootstrap/',
15 'config/',
16 'database/',
17 'docker/',
18 'public/index.php',
19 'resources/',
20 'routes/',
21 'tests/',
22 'composer.json',
23 'package.json',
24 'vite.config.js',
25 'compose.yaml',
26 'compose.prod.yaml',
27 'deploy.sh',
28 'AGENTS.md',
29 '.agents/memory/curriculum_registry.json',
30 ];
31
32 private const array FORBIDDEN_FRAGMENTS = [
33 '.env',
34 '.git',
35 'vendor/',
36 'node_modules/',
37 'storage/',
38 '..',
39 ];
40
41 /**
42 * Ověří, zda je relativní cesta v souladu s bezpečnostní politikou
43 * a zda cílový soubor reálně existuje v kořenovém adresáři aplikace.
44 */
45 public function isAllowedPath(string $relativePath): bool
46 {
47 $cleanPath = ltrim(trim($relativePath), '/');
48
49 if ($cleanPath === '') {
50 return false;
51 }
52
53 foreach (self::FORBIDDEN_FRAGMENTS as $fragment) {
54 if (str_contains($cleanPath, $fragment)) {
55 return false;
56 }
57 }
58
59 $hasAllowedPrefix = false;
60 foreach (self::ALLOWED_PREFIXES as $prefix) {
61 if (str_starts_with($cleanPath, $prefix)) {
62 $hasAllowedPrefix = true;
63 break;
64 }
65 }
66
67 if (! $hasAllowedPrefix) {
68 return false;
69 }
70
71 $basePath = realpath(base_path());
72 if ($basePath === false) {
73 return false;
74 }
75
76 $fullPath = base_path($cleanPath);
77 $realPath = realpath($fullPath);
78
79 if ($realPath === false || ! is_file($realPath)) {
80 return false;
81 }
82
83 $baseWithSlash = rtrim($basePath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
84
85 return str_starts_with($realPath, $baseWithSlash);
86 }
87
88 /**
89 * Prozkoumá soubor a vrátí jeho DTO uzel s obsahem a metadaty.
90 */
91 public function inspectFile(string $relativePath): AnatomyFileNodeData
92 {
93 $cleanPath = ltrim(trim($relativePath), '/');
94
95 if (! $this->isAllowedPath($cleanPath)) {
96 return new AnatomyFileNodeData(
97 path: $cleanPath,
98 category: $this->determineCategory($cleanPath),
99 description: $this->determineDescription($cleanPath),
100 exists: false,
101 lineCount: 0,
102 sizeBytes: 0,
103 content: '// Přístup k souboru odepřen bezpečnostní politikou AnatomyInspectorService nebo soubor neexistuje.',
104 );
105 }
106
107 $fullPath = base_path($cleanPath);
108 $content = (string) file_get_contents($fullPath);
109 $lineCount = $content === '' ? 0 : substr_count($content, "\n") + (str_ends_with($content, "\n") ? 0 : 1);
110 $sizeBytes = strlen($content);
111
112 return new AnatomyFileNodeData(
113 path: $cleanPath,
114 category: $this->determineCategory($cleanPath),
115 description: $this->determineDescription($cleanPath),
116 exists: true,
117 lineCount: $lineCount,
118 sizeBytes: $sizeBytes,
119 content: $content,
120 );
121 }
122
123 /**
124 * Prozkoumá kolekci relativních cest.
125 *
126 * @param array<int, string> $relativePaths
127 * @return array<int, AnatomyFileNodeData>
128 */
129 public function inspectMany(array $relativePaths): array
130 {
131 $nodes = [];
132 foreach ($relativePaths as $path) {
133 $nodes[] = $this->inspectFile($path);
134 }
135
136 return $nodes;
137 }
138
139 /**
140 * Vrátí kurátorský výběr nejdůležitějších exponátů živé anatomie projektu.
141 *
142 * @return array<int, AnatomyFileNodeData>
143 */
144 public function getCuratedProjectCatalog(): array
145 {
146 $catalogPaths = [
147 'public/index.php',
148 'bootstrap/app.php',
149 'routes/web.php',
150 'app/Http/Middleware/EnsureStudentProfile.php',
151 'app/Http/Controllers/CourseController.php',
152 'app/Http/Controllers/AnatomyController.php',
153 'app/Services/AnatomyInspectorService.php',
154 'app/Services/StudentProgressService.php',
155 'app/Models/CourseModule.php',
156 'app/Models/CourseLesson.php',
157 'app/Models/QuizQuestion.php',
158 'app/Models/StudentProfile.php',
159 'app/Data/AnatomyFileNodeData.php',
160 'app/Data/LessonContentData.php',
161 'app/Enums/LifecyclePhase.php',
162 'app/Enums/ModuleDifficulty.php',
163 'app/Livewire/LessonQuiz.php',
164 'app/Livewire/AnatomyExplorer.php',
165 'app/Console/Commands/VerifyCurriculumCommand.php',
166 'database/migrations/0001_01_01_000010_create_course_tables.php',
167 'resources/views/layouts/app.blade.php',
168 'docker/Dockerfile',
169 'docker/Caddyfile',
170 'docker/entrypoint.sh',
171 'compose.yaml',
172 'compose.prod.yaml',
173 'deploy.sh',
174 'composer.json',
175 'AGENTS.md',
176 '.agents/memory/curriculum_registry.json',
177 ];
178
179 return $this->inspectMany($catalogPaths);
180 }
181
182 /**
183 * Určí architektonickou kategorii souboru.
184 */
185 public function determineCategory(string $path): string
186 {
187 if (str_starts_with($path, 'docker/') || in_array($path, ['compose.yaml', 'compose.prod.yaml', 'deploy.sh'], true)) {
188 return 'DevOps & Docker';
189 }
190
191 if (str_starts_with($path, 'app/Http/Controllers/')) {
192 return 'Controller';
193 }
194
195 if (str_starts_with($path, 'app/Http/Middleware/')) {
196 return 'Middleware';
197 }
198
199 if (str_starts_with($path, 'app/Services/')) {
200 return 'Service Layer';
201 }
202
203 if (str_starts_with($path, 'app/Models/')) {
204 return 'Eloquent Model';
205 }
206
207 if (str_starts_with($path, 'app/Data/')) {
208 return 'Data Transfer Object (DTO)';
209 }
210
211 if (str_starts_with($path, 'app/Enums/')) {
212 return 'Enum';
213 }
214
215 if (str_starts_with($path, 'app/Livewire/')) {
216 return 'Livewire Component';
217 }
218
219 if (str_starts_with($path, 'app/Console/')) {
220 return 'Artisan Command';
221 }
222
223 if (str_starts_with($path, 'database/migrations/')) {
224 return 'Database Migration';
225 }
226
227 if (str_starts_with($path, 'database/seeders/')) {
228 return 'Database Seeder';
229 }
230
231 if (str_starts_with($path, 'resources/views/')) {
232 return 'Blade View';
233 }
234
235 if (str_starts_with($path, 'routes/')) {
236 return 'HTTP Routing';
237 }
238
239 if (str_starts_with($path, 'bootstrap/')) {
240 return 'Bootstrap & Pipeline';
241 }
242
243 if (str_starts_with($path, 'config/')) {
244 return 'Configuration';
245 }
246
247 if (str_starts_with($path, 'public/')) {
248 return 'HTTP Entry Point';
249 }
250
251 if (str_starts_with($path, 'tests/')) {
252 return 'Pest PHP Tests';
253 }
254
255 if (str_starts_with($path, 'AGENTS.md') || str_starts_with($path, '.agents/')) {
256 return 'Agentic Architecture & SSOT';
257 }
258
259 if (in_array($path, ['composer.json', 'package.json', 'vite.config.js'], true)) {
260 return 'Project Manifest & Config';
261 }
262
263 return 'Source Code';
264 }
265
266 /**
267 * Poskytne český popis architektonické role souboru v naší aplikaci.
268 */
269 public function determineDescription(string $path): string
270 {
271 return match ($path) {
272 'public/index.php' => 'Vstupní bod HTTP požadavku pro FrankenPHP webserver; inicializuje Composer autoloader a načítá bootstrap aplikace.',
273 'bootstrap/app.php' => 'Nové jádro Laravel 12 pro registraci webových i konzolových rout, middleware pipeline a zachytávání výjimek bez Http Kernelu.',
274 'routes/web.php' => 'Definice webových tras, Route Model Binding pro moduly a lekce, mapování na Controllery a API status endpoint.',
275 'app/Http/Middleware/EnsureStudentProfile.php' => 'Middleware zajišťující anonymní UUID profil studenta v cookie s trvalým sledováním pokroku kurzem.',
276 'app/Http/Controllers/CourseController.php' => 'Resource controller obsluhující přehled modulů, detail lekce, markdown render a přepínání profilu studenta.',
277 'app/Http/Controllers/AnatomyController.php' => 'Controller interaktivní anatomie aplikace nabízející kurátorský katalog klíčových komponent a živý náhled kódu.',
278 'app/Services/AnatomyInspectorService.php' => 'Bezpečná introspekční služba aplikace čtoucí reálný zdrojový kód s ochranou proti path traversal a whitelistem cest.',
279 'app/Services/StudentProgressService.php' => 'Doménová služba pro správu anonymních profilů studentů, postupu lekcemi, výsledků kvízů a přenosových kódů.',
280 'app/Models/CourseModule.php' => 'Eloquent model tématického modulu kurzu s vazbou hasMany na lekce a přetypováním na enum ModuleDifficulty.',
281 'app/Models/CourseLesson.php' => 'Eloquent model lekce kurzu s obsahem v Markdownu, přetypováním JSON polí a enumem LifecyclePhase.',
282 'app/Models/QuizQuestion.php' => 'Eloquent model otázky kvízu s JSON možnostmi, správnou volbou a vysvětlením pro interaktivní prověření znalostí.',
283 'app/Models/StudentProfile.php' => 'Model anonymního studenta s UUID tokenem a přenosovým kódem pro synchronizaci postupu napříč zařízeními.',
284 'app/Data/AnatomyFileNodeData.php' => 'Neměnné PHP 8.4 readonly DTO reprezentující analyzovaný soubor anatomie aplikace (řádky, bajty, obsah, popis).',
285 'app/Data/LessonContentData.php' => 'Readonly DTO s kompletními daty lekce pro bezpečné předávání mezi modely, controllery a šablonami.',
286 'app/Enums/LifecyclePhase.php' => 'PHP 8.4 string enum definující 11 kroků životního cyklu požadavku v Laravelu 12 s pořadím a popisky.',
287 'app/Enums/ModuleDifficulty.php' => 'Enum určující obtížnost modulu (Začátečník, Pokročilý, Architekt) s Tailwind CSS badge třídami.',
288 'app/Livewire/LessonQuiz.php' => 'Reaktivní Livewire 3 komponenta pro interaktivní testování znalostí s okamžitým vyhodnocením a záznamem skóre.',
289 'app/Livewire/AnatomyExplorer.php' => 'Interaktivní průzkumník anatomických souborů s filtrováním fází a živým čtením přes AnatomyInspectorService.',
290 'app/Console/Commands/VerifyCurriculumCommand.php' => 'Artisan příkaz pro zero-hallucination verifikaci registry osnovy proti fyzickým souborům a databázi.',
291 'database/migrations/0001_01_01_000010_create_course_tables.php' => 'Migrační schéma pro moduly, lekce, kvízové otázky a anonymní profily studentů v PostgreSQL 18.',
292 'resources/views/layouts/app.blade.php' => 'Hlavní Blade layout aplikace s tmavým tématem, navigací, synchronizací profilu a Alpine.js reaktivitou.',
293 'docker/Dockerfile' => 'Multi-stage Dockerfile založený na dunglas/frankenphp:1-php8.4 s dev a prod cíli a instalací pgsql/opcache rozšíření.',
294 'docker/Caddyfile' => 'Konfigurace Caddy webserveru a FrankenPHP worker módu pro bleskovou obsluhu požadavků bez opakovaného bootu.',
295 'docker/entrypoint.sh' => 'Startovací skript kontejneru zajišťující práva storage, env soubory a spuštění Caddy/Octane.',
296 'compose.yaml' => 'Docker Compose konfigurace pro lokální vývoj s FrankenPHP a PostgreSQL 18 alpine.',
297 'compose.prod.yaml' => 'Produkční Docker Compose pro nasazení na Hetzner serveru s připojením na sdílenou síť db a caddy.',
298 'deploy.sh' => 'Produkční nasazovací skript pro Hetzner server s migracemi, cacheováním a zero-downtime reloadem.',
299 'composer.json' => 'Projektový manifest Composeru definující PHP 8.4, Laravel 12 závislosti a PSR-4 autoloading.',
300 'AGENTS.md' => 'Závazná ústava projektu pro AI agenty s pravidly nulové halucinace a bezpečnosti Hetzneru.',
301 '.agents/memory/curriculum_registry.json' => 'Jediný zdroj pravdy (SSOT) pro osnovu 10 modulů a všech lekcí celého kurzu.',
302 default => 'Soubor aplikační architektury v modulu ' . $this->determineCategory($path) . '.',
303 };
304 }
305 }
306
Zdroj: skutečné soubory této aplikace Plný průzkumník anatomie →