Blade šablony, direktivy a automatická ochrana proti XSS
Šablonovací systém Blade, kompilace do nativního PHP v storage/framework/views, eskapování výstupu {{ }} jako ochrana proti Cross-Site Scripting (XSS) a neescapovaný výstup {!! !!}.
Šablonovací engine Blade a jeho kompilace
Blade je nativní, neuvěřitelně rychlý šablonovací engine Laravelu. Na rozdíl od jiných PHP šablonovacích systémů (jako je např. Twig nebo Smarty) vás Blade neomezuje v používání čistého PHP kódu uvnitř šablon.
Všechny Blade šablony (.blade.php) jsou při prvním spuštění zkompilovány do čistého nativního PHP kódu a uloženy do adresáře storage/framework/views/. Při dalších požadavcích se již kód nekompiluje, ale přímo vykonává, což znamená nulový výkonnostní dopad na produkční běh.
Šablona (.blade.php) ──► Blade Kompilátor ──► Nativní PHP v storage/framework/views/
│
Rychlý běh
1. Zobrazení dat a neprůstřelná ochrana proti XSS
Nejčastější bezpečnostní zranitelností webových aplikací je Cross-Site Scripting (XSS) – situace, kdy útočník vloží do databáze škodlivý JavaScriptový kód <script>...</script>, který se následně spustí v prohlížeči ostatních návštěvníků.
Blade tuto hrozbu řeší automaticky:
A) Bezpečný escapovaný výpis {{ $data }}
Kdykoliv v Blade použijete dvojité složené závorky, Laravel hodnotu automaticky prožene nativní funkcí htmlspecialchars():
<h1>{{ $student->name }}</h1>
{{-- Pokud name obsahuje '<script>', vypíše se jako <script> a nespustí se! --}}
B) Neescapovaný surový výpis {!! $data !!}
Pouze v případě, kdy si jste 100% jisti, že obsah je bezpečný (například vyrenderovaný Markdown v naší aplikaci, který byl předem ošetřen):
<div class="prose">
{!! $renderedHtml !!}
</div>
Varování: Zápis {!! !!} nikdy nepoužívejte pro neověřené vstupy zadané uživatelem!
2. Praktické Blade direktivy v našem projektu
Blade nabízí elegantní zkratky pro běžné PHP konstrukce, které udržují kód čistý a čitelný:
- Podmínky:
@if($lesson->lifecycle_phase),@elseif,@else,@endif, případně@empty($variable)a@isset($variable). - Cykly:
@foreach($modules as $module), kde máte k dispozici magickou proměnnou$loop(např.$loop->first,$loop->last,$loop->iteration). - Ochrana formulářů:
@csrfvygeneruje skryté input pole s CSRF tokenem chránící proti zfalšování požadavku mezi weby. - Vkládání assetů:
@vite(['resources/css/app.css', 'resources/js/app.js'])automaticky zajistí správné cesty ke stylům a skriptům.
3. Rozbor layoutu a sekcí v resources/views/layouts/app.blade.php
V naší aplikaci slouží resources/views/layouts/app.blade.php jako centrální kostra celého HTML dokumentu. Obsahuje:
- Společné HTML hlavičky (
<head>), načtení fontů a CSS assetů. - Globální horní navigaci s logem, přepínačem modulů a anonymním profilem studenta.
- Flash alerty pro zobrazení zpráv ze session (
@if(session('success'))). - Vymezené místo pro hlavní obsah:
{{ $slot ?? '' }}a@yield('content').
{{-- resources/views/layouts/app.blade.php (Ukázka práce s Blade v naší aplikaci) --}}
<!DOCTYPE html>
<html lang="cs" class="h-full bg-slate-950">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{-- Prevence CSRF útoků pro asynchronní JavaScript požadavky --}}
<meta name="csrf-token" content="{{ csrf_token() }}">
{{-- Bezpečný escapovaný výpis titulku s výchozí hodnotou --}}
<title>{{ $title ?? 'Anatomie Laravelu 12 & Moderního PHP 8.4' }} — Živá výuková platforma</title>
{{-- Integrace moderní asset pipeline přes Vite --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="min-h-full flex flex-col bg-slate-950 text-slate-100 antialiased">
{{-- Flash zprávy se session notifikacemi --}}
@if(session('success'))
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-4">
<div class="rounded-xl bg-emerald-500/10 border border-emerald-500/20 p-4 text-emerald-400 text-sm">
{{ session('success') }}
</div>
</div>
@endif
{{-- Hlavní obsah: Podpora jak pro komponentový styl ($slot), tak pro klasické sekce (@yield) --}}
<main class="flex-1">
{{ $slot ?? '' }}
@yield('content')
</main>
@livewireScripts
</body>
</html>
🎯 Ověření znalostí z lekce
Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.
Jaký je zásadní bezpečnostní rozdíl mezi syntaxí {{ $data }} a {!! $data !!} v Blade?
Kde Laravel ukládá zkompilované Blade šablony a proč je tento přístup tak rychlý?
K čemu slouží direktiva @csrf uvnitř webového formuláře?
| 1 | <!DOCTYPE html> |
| 2 | <html lang="cs" class="h-full bg-slate-950"> |
| 3 | <head> |
| 4 | <meta charset="utf-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | <meta name="csrf-token" content="{{ csrf_token() }}"> |
| 7 | |
| 8 | <title>{{ $title ?? 'Anatomie Laravelu 12 & Moderního PHP 8.4' }} — Živá výuková platforma</title> |
| 9 | <meta name="description" content="Interaktivní výuková platforma Anatomie Laravelu 12 & Moderního PHP 8.4 na reálné živě běžící aplikaci."> |
| 10 | |
| 11 | <link rel="preconnect" href="https://fonts.bunny.net"> |
| 12 | <link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600,700|jetbrains-mono:400,500,600&display=swap" rel="stylesheet" /> |
| 13 | |
| 14 | @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) |
| 15 | @vite(['resources/css/app.css', 'resources/js/app.js']) |
| 16 | @endif |
| 17 | @livewireStyles |
| 18 | </head> |
| 19 | <body class="min-h-full flex flex-col bg-slate-950 text-slate-100 antialiased selection:bg-indigo-500 selection:text-white font-sans"> |
| 20 | @php |
| 21 | $student = $currentStudent ?? (app()->bound(\App\Services\StudentProgressService::class) ? app(\App\Services\StudentProgressService::class)->resolveProfile(request()->cookie(\App\Services\StudentProgressService::COOKIE_NAME ?? 'agy_student_token')) : null); |
| 22 | @endphp |
| 23 | |
| 24 | {{-- Horní navigace --}} |
| 25 | <header class="sticky top-0 z-50 border-b border-slate-800/80 bg-slate-950/90 backdrop-blur-md"> |
| 26 | <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| 27 | <div class="flex items-center justify-between h-16 gap-4"> |
| 28 | {{-- Logo & Název --}} |
| 29 | <div class="flex items-center gap-3"> |
| 30 | <a href="{{ route('course.index') }}" class="flex items-center gap-3 group focus:outline-none"> |
| 31 | <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 via-purple-600 to-amber-500 p-0.5 shadow-lg shadow-indigo-500/20 group-hover:shadow-indigo-500/40 transition"> |
| 32 | <div class="w-full h-full bg-slate-950 rounded-[10px] flex items-center justify-center font-mono font-bold text-base text-amber-400 group-hover:text-amber-300"> |
| 33 | </> |
| 34 | </div> |
| 35 | </div> |
| 36 | <div> |
| 37 | <div class="flex items-center gap-2"> |
| 38 | <span class="font-bold text-base tracking-tight text-white group-hover:text-amber-300 transition"> |
| 39 | Anatomie Laravelu 12 & PHP 8.4 |
| 40 | </span> |
| 41 | <span class="hidden sm:inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-indigo-500/10 text-indigo-400 ring-1 ring-inset ring-indigo-500/20"> |
| 42 | v12.x |
| 43 | </span> |
| 44 | </div> |
| 45 | <p class="text-xs text-slate-400"> |
| 46 | Živá výuková platforma · <span class="text-slate-300">laravel.heatsharing.cz</span> |
| 47 | </p> |
| 48 | </div> |
| 49 | </a> |
| 50 | </div> |
| 51 | |
| 52 | {{-- Hlavní menu --}} |
| 53 | <nav class="hidden md:flex items-center gap-1"> |
| 54 | <a href="{{ route('course.index') }}" |
| 55 | class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request()->routeIs('course.*') && !request()->routeIs('anatomy.*') ? 'bg-indigo-950/60 text-indigo-300 border border-indigo-800/50 shadow-sm' : 'text-slate-300 hover:text-white hover:bg-slate-900' }}"> |
| 56 | 📚 Osnova & Moduly |
| 57 | </a> |
| 58 | <a href="{{ route('anatomy.index') }}" |
| 59 | class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request()->routeIs('anatomy.*') ? 'bg-indigo-950/60 text-indigo-300 border border-indigo-800/50 shadow-sm' : 'text-slate-300 hover:text-white hover:bg-slate-900' }}"> |
| 60 | 🔬 Živá Anatomie & Request Lifecycle |
| 61 | </a> |
| 62 | </nav> |
| 63 | |
| 64 | {{-- Postup studenta & Přenosový kód (Alpine.js dropdown) --}} |
| 65 | <div class="flex items-center gap-3" x-data="{ open: false }"> |
| 66 | <div class="relative"> |
| 67 | <button @click="open = !open" |
| 68 | type="button" |
| 69 | class="flex items-center gap-2.5 px-3 py-1.5 rounded-lg bg-slate-900 border border-slate-800 hover:border-slate-700 text-xs font-medium text-slate-200 transition focus:outline-none focus:ring-2 focus:ring-indigo-500/50" |
| 70 | aria-expanded="false" |
| 71 | :aria-expanded="open.toString()"> |
| 72 | <span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> |
| 73 | <span class="hidden sm:inline text-slate-400">Profil:</span> |
| 74 | <span class="font-mono text-amber-300 font-semibold"> |
| 75 | {{ $student->transfer_code ?? 'LRV-POSTUP' }} |
| 76 | </span> |
| 77 | <svg class="w-3.5 h-3.5 text-slate-400 transition-transform duration-200" :class="{ 'rotate-180': open }" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 78 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> |
| 79 | </svg> |
| 80 | </button> |
| 81 | |
| 82 | {{-- Dropdown panel --}} |
| 83 | <div x-show="open" |
| 84 | @click.outside="open = false" |
| 85 | x-transition:enter="transition ease-out duration-150" |
| 86 | x-transition:enter-start="opacity-0 scale-95" |
| 87 | x-transition:enter-end="opacity-100 scale-100" |
| 88 | x-transition:leave="transition ease-in duration-100" |
| 89 | x-transition:leave-start="opacity-100 scale-100" |
| 90 | x-transition:leave-end="opacity-0 scale-95" |
| 91 | class="absolute right-0 mt-2 w-80 sm:w-96 rounded-xl bg-slate-900 border border-slate-800 shadow-2xl p-4 text-xs text-slate-300 z-50" |
| 92 | style="display: none;"> |
| 93 | <div class="flex items-center justify-between pb-3 border-b border-slate-800"> |
| 94 | <div> |
| 95 | <h4 class="font-semibold text-white text-sm">Anonymní profil studenta</h4> |
| 96 | <p class="text-[11px] text-slate-400">Postup je uložen v cookie a v PostgreSQL.</p> |
| 97 | </div> |
| 98 | <span class="px-2 py-0.5 rounded text-[10px] font-semibold bg-emerald-500/10 text-emerald-400 ring-1 ring-inset ring-emerald-500/20"> |
| 99 | Aktivní |
| 100 | </span> |
| 101 | </div> |
| 102 | |
| 103 | <div class="py-3 space-y-2"> |
| 104 | <span class="text-slate-400 font-medium block">Tvůj unikátní přenosový kód:</span> |
| 105 | <div class="flex items-center gap-2"> |
| 106 | <code class="flex-1 px-3 py-2 bg-slate-950 border border-slate-800 rounded-lg text-amber-300 font-mono text-sm tracking-wider select-all text-center font-bold"> |
| 107 | {{ $student->transfer_code ?? 'LRV-GENEROVANI' }} |
| 108 | </code> |
| 109 | </div> |
| 110 | <p class="text-[11px] text-slate-500"> |
| 111 | Tento kód si uložte, pokud budete chtít pokračovat ve výuce na jiném zařízení nebo v jiném prohlížeči. |
| 112 | </p> |
| 113 | </div> |
| 114 | |
| 115 | <div class="pt-3 border-t border-slate-800"> |
| 116 | <form action="{{ route('course.switch-profile') }}" method="POST" class="space-y-2"> |
| 117 | @csrf |
| 118 | <label for="transfer_code_input" class="block text-slate-300 font-medium"> |
| 119 | Načíst profil z jiného zařízení: |
| 120 | </label> |
| 121 | <div class="flex gap-2"> |
| 122 | <input type="text" |
| 123 | id="transfer_code_input" |
| 124 | name="transfer_code" |
| 125 | placeholder="LRV-XXXX-XXXX" |
| 126 | required |
| 127 | class="flex-1 px-3 py-1.5 rounded-lg bg-slate-950 border border-slate-700 text-white font-mono text-xs uppercase placeholder:text-slate-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500"> |
| 128 | <button type="submit" |
| 129 | class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg text-xs transition shadow-sm"> |
| 130 | Aktivovat |
| 131 | </button> |
| 132 | </div> |
| 133 | </form> |
| 134 | </div> |
| 135 | </div> |
| 136 | </div> |
| 137 | </div> |
| 138 | </div> |
| 139 | |
| 140 | {{-- Mobilní navigace --}} |
| 141 | <div class="flex md:hidden border-t border-slate-800/80 py-2 gap-2"> |
| 142 | <a href="{{ route('course.index') }}" |
| 143 | class="flex-1 text-center px-3 py-1.5 rounded text-xs font-medium {{ request()->routeIs('course.*') && !request()->routeIs('anatomy.*') ? 'bg-indigo-950 text-indigo-300' : 'text-slate-400 hover:text-white' }}"> |
| 144 | 📚 Osnova & Moduly |
| 145 | </a> |
| 146 | <a href="{{ route('anatomy.index') }}" |
| 147 | class="flex-1 text-center px-3 py-1.5 rounded text-xs font-medium {{ request()->routeIs('anatomy.*') ? 'bg-indigo-950 text-indigo-300' : 'text-slate-400 hover:text-white' }}"> |
| 148 | 🔬 Živá Anatomie |
| 149 | </a> |
| 150 | </div> |
| 151 | </div> |
| 152 | </header> |
| 153 | |
| 154 | {{-- Flash zprávy --}} |
| 155 | @if(session('success')) |
| 156 | <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4"> |
| 157 | <div class="rounded-xl bg-emerald-950/60 border border-emerald-800/80 p-4 text-emerald-200 flex items-start gap-3 shadow-lg"> |
| 158 | <svg class="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 159 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| 160 | </svg> |
| 161 | <div class="text-sm"> |
| 162 | <span class="font-semibold text-white">Úspěch:</span> {{ session('success') }} |
| 163 | </div> |
| 164 | </div> |
| 165 | </div> |
| 166 | @endif |
| 167 | |
| 168 | @if(session('error')) |
| 169 | <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-4"> |
| 170 | <div class="rounded-xl bg-rose-950/60 border border-rose-800/80 p-4 text-rose-200 flex items-start gap-3 shadow-lg"> |
| 171 | <svg class="w-5 h-5 text-rose-400 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 172 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| 173 | </svg> |
| 174 | <div class="text-sm"> |
| 175 | <span class="font-semibold text-white">Chyba:</span> {{ session('error') }} |
| 176 | </div> |
| 177 | </div> |
| 178 | </div> |
| 179 | @endif |
| 180 | |
| 181 | {{-- Hlavní obsah --}} |
| 182 | <main class="flex-1"> |
| 183 | {{ $slot ?? '' }} |
| 184 | @yield('content') |
| 185 | </main> |
| 186 | |
| 187 | {{-- Patička --}} |
| 188 | <footer class="mt-20 border-t border-slate-800/80 bg-slate-950 text-slate-400 text-xs"> |
| 189 | <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10"> |
| 190 | <div class="grid grid-cols-1 md:grid-cols-3 gap-8 pb-8 border-b border-slate-900"> |
| 191 | {{-- O platformě --}} |
| 192 | <div class="space-y-3"> |
| 193 | <div class="flex items-center gap-2 text-white font-semibold"> |
| 194 | <span class="text-amber-400 font-mono"></></span> |
| 195 | <span>Anatomie Laravelu 12 & PHP 8.4</span> |
| 196 | </div> |
| 197 | <p class="text-slate-400 leading-relaxed text-xs"> |
| 198 | Sebereferenční výuková platforma. Aplikace sama slouží jako exponát – kód, architektura i konfigurace, které studujete, pohání tento běžící web. |
| 199 | </p> |
| 200 | </div> |
| 201 | |
| 202 | {{-- Produkční infrastruktura --}} |
| 203 | <div class="space-y-3"> |
| 204 | <h5 class="text-white font-semibold">Infrastruktura & Produkční Stack</h5> |
| 205 | <ul class="space-y-1.5 text-slate-400"> |
| 206 | <li class="flex items-center gap-2"> |
| 207 | <span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span> |
| 208 | <span>FrankenPHP v1 + PHP 8.4 (Worker Mode, multi-stage Docker)</span> |
| 209 | </li> |
| 210 | <li class="flex items-center gap-2"> |
| 211 | <span class="w-1.5 h-1.5 rounded-full bg-indigo-400"></span> |
| 212 | <span>Laravel 12.x + Livewire 3 + Alpine.js + Tailwind CSS</span> |
| 213 | </li> |
| 214 | <li class="flex items-center gap-2"> |
| 215 | <span class="w-1.5 h-1.5 rounded-full bg-sky-400"></span> |
| 216 | <span>PostgreSQL 18 na Hetzner serveru (<code class="text-slate-300 font-mono">debian-hell</code> / 65.109.132.79)</span> |
| 217 | </li> |
| 218 | <li class="flex items-center gap-2"> |
| 219 | <span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span> |
| 220 | <span>Reverzní proxy Caddy v2 s automatickým TLS Let's Encrypt</span> |
| 221 | </li> |
| 222 | </ul> |
| 223 | </div> |
| 224 | |
| 225 | {{-- Rozcestník & Odkazy --}} |
| 226 | <div class="space-y-3"> |
| 227 | <h5 class="text-white font-semibold">Rychlé odkazy</h5> |
| 228 | <ul class="space-y-1.5 text-slate-400"> |
| 229 | <li> |
| 230 | <a href="{{ route('course.index') }}" class="hover:text-amber-300 transition">📚 Kompletní osnova (10 modulů, 30 lekcí)</a> |
| 231 | </li> |
| 232 | <li> |
| 233 | <a href="{{ route('anatomy.index') }}" class="hover:text-amber-300 transition">🔬 Živý Průzkumník anatomie</a> |
| 234 | </li> |
| 235 | <li> |
| 236 | <a href="https://heatsharing.cz" target="_blank" rel="noopener noreferrer" class="hover:text-amber-300 transition flex items-center gap-1"> |
| 237 | <span>🌐 Rozcestník projektů heatsharing.cz</span> |
| 238 | <svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 239 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> |
| 240 | </svg> |
| 241 | </a> |
| 242 | </li> |
| 243 | <li> |
| 244 | <a href="{{ route('api.status') }}" target="_blank" class="hover:text-amber-300 transition flex items-center gap-1 font-mono text-[11px] text-slate-500"> |
| 245 | <span>⚡ Status API: /api/status</span> |
| 246 | </a> |
| 247 | </li> |
| 248 | </ul> |
| 249 | </div> |
| 250 | </div> |
| 251 | |
| 252 | <div class="pt-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-[11px] text-slate-500"> |
| 253 | <p>© {{ date('Y') }} Heatsharing.cz · Anatomie Laravelu 12. Volně přístupné vzdělávací dílo.</p> |
| 254 | <p class="font-mono text-slate-500">PHP 8.4.x · Laravel 12.x · Zero Hallucinations Policy</p> |
| 255 | </div> |
| 256 | </div> |
| 257 | </footer> |
| 258 | |
| 259 | @livewireScripts |
| 260 | </body> |
| 261 | </html> |
| 262 | |