Fáze požadavku: 3. Service Container & Providers
Modul 2 · Lekce 3

Srdce frameworku: Service Container, Dependency Injection a Service Providers

💡 Shrnutí lekce

Pronikněte do fungování IoC kontejneru v Laravelu, pochopte automatické vstřikování závislostí (Dependency Injection) přes PHP reflexi a naučte se správně používat register() a boot() v Service Providerech.

Co je Service Container (IoC Kontejner)

Service Container je centrálním mozkem celého Laravelu. Jedná se o implementaci vzoru Inversion of Control (IoC), která řeší dvě klíčové úlohy:

  1. Správu závislostí: Ví, jak vytvořit jakoukoliv třídu a jaké další třídy k tomu potřebuje.
  2. Dependency Injection (vstřikování závislostí): Automaticky předává potřebné instance do konstruktorů a metod controllerů bez nutnosti manuálního new.

Díky PHP Reflection API Laravel disponuje Zero-Configuration Dependency Injection: pokud controller vyžaduje službu AnatomyInspectorService, kontejner prozkoumá její konstruktor, zjistí, jaké parametry potřebuje, sám je vytvoří a službu do controlleru injektuje.


Rozdíl mezi bind() a singleton()

V kontejneru můžeme služby registrovat dvěma základními způsoby:

  1. Transient binding ($this->app->bind()):

    • Při každém požadavku na danou třídu kontejner vytvoří novou, čerstvou instanci.
    • Vhodné pro lehké, bezstavové pomocníky nebo objekty vyžadující pokaždé unikátní konfiguraci.
  2. Singleton binding ($this->app->singleton()):

    • Třída se instanciována pouze jednou při prvním vyžádání.
    • Při všech dalších voláních v rámci stejného životního cyklu kontejner vrátí tutéž existující instanci.
    • V našem projektu takto registrujeme AnatomyInspectorService a StudentProgressService.
$this->app->singleton(AnatomyInspectorService::class);

Životní cyklus Service Provideru: register() vs. boot()

Service Providery (jako je AppServiceProvider) jsou místem, kde se aplikace konfiguruje a propojuje. Každý provider má dvě základní metody a je životně důležité chápat rozdíl mezi nimi:

Metoda register()

  • Slouží výhradně pro vazby do kontejneru (bind, singleton).
  • V této fázi ještě nejsou ostatní provideři nabootovaní!
  • Pravidlo: V metodě register() se nikdy nesmíte pokoušet volat byznys logiku jiných služeb, registrovat event listenery nebo používat Eloquent modely.

Metoda boot()

  • Volá se až poté, co proběhly metody register() úplně všech providerů v aplikaci.
  • Zde máte stoprocentní jistotu, že všechny služby jsou v kontejneru připraveny.
  • Slouží k registraci eventů, view composerů, routových vzorů a globální konfiguraci modelů.

V našem AppServiceProvider.php využíváme metodu boot() k vynucení striktního chování databázových modelů:

public function boot(): void
{
    Model::shouldBeStrict(! $this->app->isProduction());

    if ($this->app->isProduction()) {
        URL::forceScheme('https');
    }
}

Metoda Model::shouldBeStrict() v lokálním vývoji automaticky vyhodí výjimku, pokud:

  • Dojde k N+1 línému načítání relací (Lazy Loading Violation).
  • Pokusíte se naplnit nechráněný atribut (Mass Assignment Violation).
  • Přistoupíte k neexistujícímu sloupci modelu.
💻 Klíčová konstrukce z praxe
<?php

declare(strict_types=1);

namespace App\Providers;

use App\Services\AnatomyInspectorService;
use App\Services\StudentProgressService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;

// Reálný Service Provider naší aplikace (app/Providers/AppServiceProvider.php)
class AppServiceProvider extends ServiceProvider
{
    /**
     * 1. Fáze register: Pouze vazby do Service Containeru
     */
    public function register(): void
    {
        // Registrace služeb jako sdílených Singletonů
        $this->app->singleton(AnatomyInspectorService::class);
        $this->app->singleton(StudentProgressService::class);
    }

    /**
     * 2. Fáze boot: Konfigurace po nabootování všech providerů
     */
    public function boot(): void
    {
        // Zapnutí striktního režimu Eloquentu mimo produkci (detekce N+1 dotazů)
        Model::shouldBeStrict(! $this->app->isProduction());

        // Vynucení HTTPS schématu v produkčním prostředí za Caddy proxy
        if ($this->app->isProduction()) {
            URL::forceScheme('https');
        }
    }
}

🎯 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ý je zásadní rozdíl mezi metodami register() a boot() v Service Provideru?

Otázka 2 z 3

Co znamená registrace služby pomocí $this->app->singleton(MyService::class)?

Otázka 3 z 3

Co v praxi způsobí volání Model::shouldBeStrict(! $this->app->isProduction()) v metodě boot()?

Zodpovězeno: 0 / 3
🔬 Živá anatomie v této aplikaci
2 soubory
Na co se v kódu zaměřit: V app/Providers/AppServiceProvider.php si prohlédni metodu register() a boot(). Všimni si, jak je registrována služba AnatomyInspectorService. Následně se podívej do app/Services/AnatomyInspectorService.php na její metody isAllowedPath() a inspectFile().
cesta: app/Providers/AppServiceProvider.php
35 řádků · 0.8 KB
Soubor aplikační architektury v modulu Source Code.
1 <?php
2
3 declare(strict_types=1);
4
5 namespace App\Providers;
6
7 use App\Services\AnatomyInspectorService;
8 use App\Services\StudentProgressService;
9 use Illuminate\Database\Eloquent\Model;
10 use Illuminate\Support\Facades\URL;
11 use Illuminate\Support\ServiceProvider;
12
13 class AppServiceProvider extends ServiceProvider
14 {
15 /**
16 * Register any application services in the Service Container.
17 */
18 public function register(): void
19 {
20 $this->app->singleton(AnatomyInspectorService::class);
21 $this->app->singleton(StudentProgressService::class);
22 }
23
24 /**
25 * Bootstrap any application services.
26 */
27 public function boot(): void
28 {
29 Model::shouldBeStrict(! $this->app->isProduction());
30
31 if ($this->app->isProduction()) {
32 URL::forceScheme('https');
33 }
34 }
35 }
36
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 →