Fáze požadavku: 2. Bootstrap aplikace (bootstrap/app.php)
Modul 8 · Lekce 3

Bezpečná správa .env, config() vs env() a proč v produkci cachujeme konfiguraci

💡 Shrnutí lekce

Proč funkce env() smí být volána výhradně v konfiguračních souborech v config/, jak funguje php artisan config:cache a jak produkční entrypoint spouští optimalizace.

Správa konfigurace: Zlaté pravidlo config() vs env()

Jednou z nejčastějších začátečnických chyb v Laravelu je přímé volání pomocné funkce env() uvnitř Controllerů, Servisních tříd nebo Blade šablon (např. if (env('APP_DEBUG'))).

V produkčním prostředí takový kód povede k fatální chybě nebo nečekanému chování. Proč?

Jak funguje php artisan config:cache (a php artisan optimize)

Během lokálního vývoje Laravel při každém HTTP požadavku parsuje soubor .env pomocí knihovny vlucas/phpdotenv. To je pro vývoj pohodlné, ale na produkčním serveru to zbytečně zpomaluje běh (čtení ze souboru a parsování řetězců při každém requestu).

Proto v produkčním Docker kontejneru v docker/entrypoint.sh automaticky spouštíme:

php artisan optimize

Tento příkaz mimo jiné spustí php artisan config:cache, který:

  1. Načte všechny soubory z adresáře config/ (app.php, database.php, cache.php...).
  2. Spočítá všechny výrazy env(...) a dosadí jejich aktuální hodnoty.
  3. Sloučí celou konfiguraci do jediného obřího PHP pole a zapíše jej do souboru bootstrap/cache/config.php.
  4. Vypne načítání souboru .env! Proměnné prostředí se již do globálního superglobálního pole $_ENV nenačítají.

[!CAUTION] Jakmile je konfigurace nacachována, jakékoliv volání env('MOJE_PROMENNA') kdekoliv mimo adresář config/ vrátí hodnotu null!

Správný postup: Vše přes config(...)

  1. Definice v souboru v config/ (např. config/app.php):
return [
    'name' => env('APP_NAME', 'Anatomie Laravelu 12'),
    'env' => env('APP_ENV', 'production'),
    'debug' => (bool) env('APP_DEBUG', false),
    'key' => env('APP_KEY'),
];
  1. Čtení v aplikačním kódu (v controlleru, službě, šabloně):
// Správně: funguje v devu i v produkci s nacachovanou konfigurací
$appName = config('app.name');
$isDebug = config('app.debug');

Bezpečnost šifrovacího klíče APP_KEY a rotace klíčů

Proměnná APP_KEY je 32znakový náhodný řetězec generovaný příkazem php artisan key:generate. Slouží k:

  • Šifrování všech cookies (uživatel nemůže modifikovat obsah cookies na klientovi).
  • Šifrování podepsaných URL adres (Signed URLs).
  • Šifrování citlivých údajů přes fasádu Crypt::encryptString(...).

Všimněte si v config/app.php skvělé funkce moderního Laravelu pro rotaci šifrovacích klíčů:

'key' => env('APP_KEY'),

'previous_keys' => [
    ...array_filter(
        explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
    ),
],

Když potřebujete z bezpečnostních důvodů změnit produkční klíč, přesunete starý klíč do APP_PREVIOUS_KEYS. Laravel pak šifruje novým klíčem, ale staré existující cookies a tokeny dokáže stále bezchybně dešifrovat bez odhlášení uživatelů!

Orchestrace v docker/entrypoint.sh

V našem Docker kontejneru řídí start aplikace skript docker/entrypoint.sh:

  • Zajišťuje správná oprávnění pro storage/ a bootstrap/cache/ (uživatel www-data).
  • Při příznaku RUN_MIGRATIONS=1 vyčká na dostupnost PostgreSQL (php artisan db:monitor).
  • Spustí migrace php artisan migrate --force.
  • Provede php artisan optimize, čímž zkompiluje konfiguraci, routy i Blade pohledy do OPcache pro maximální rychlost FrankenPHP serveru.
💻 Klíčová konstrukce z praxe
// config/app.php:18-45, 100-108
'name' => env('APP_NAME', 'Laravel'),
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false),
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
    ...array_filter(
        explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
    ),
],

// docker/entrypoint.sh:32
php artisan optimize

🎯 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 se stane, pokud zavoláte funkci env('MOJE_HODNOTA') uvnitř kontroleru v produkci, kde byl spuštěn příkaz php artisan config:cache?

Otázka 2 z 3

K čemu slouží pole "previous_keys" v konfiguračním souboru config/app.php?

Otázka 3 z 3

Jaký je účel příkazu "php artisan optimize" spouštěného v produkčním docker/entrypoint.sh?

Zodpovězeno: 0 / 3
🔬 Živá anatomie v této aplikaci
3 soubory
Na co se v kódu zaměřit: Prozkoumej config/app.php a config/database.php, kde env() poskytuje bezpečné výchozí hodnoty. Poté se podívej do docker/entrypoint.sh na řádek 32, kde příkaz php artisan optimize (včetně config:cache) v produkci načte konfiguraci do jediného PHP pole, čímž env() přestane mimo config/ vracet hodnoty.
cesta: config/app.php
128 řádků · 4.2 KB
Soubor aplikační architektury v modulu Configuration.
1 <?php
2
3 declare(strict_types=1);
4
5 return [
6
7 /*
8 |--------------------------------------------------------------------------
9 | Application Name
10 |--------------------------------------------------------------------------
11 |
12 | This value is the name of your application, which will be used when the
13 | framework needs to place the application's name in a notification or
14 | other UI elements where an application name needs to be displayed.
15 |
16 */
17
18 'name' => env('APP_NAME', 'Laravel'),
19
20 /*
21 |--------------------------------------------------------------------------
22 | Application Environment
23 |--------------------------------------------------------------------------
24 |
25 | This value determines the "environment" your application is currently
26 | running in. This may determine how you prefer to configure various
27 | services the application utilizes. Set this in your ".env" file.
28 |
29 */
30
31 'env' => env('APP_ENV', 'production'),
32
33 /*
34 |--------------------------------------------------------------------------
35 | Application Debug Mode
36 |--------------------------------------------------------------------------
37 |
38 | When your application is in debug mode, detailed error messages with
39 | stack traces will be shown on every error that occurs within your
40 | application. If disabled, a simple generic error page is shown.
41 |
42 */
43
44 'debug' => (bool) env('APP_DEBUG', false),
45
46 /*
47 |--------------------------------------------------------------------------
48 | Application URL
49 |--------------------------------------------------------------------------
50 |
51 | This URL is used by the console to properly generate URLs when using
52 | the Artisan command line tool. You should set this to the root of
53 | the application so that it's available within Artisan commands.
54 |
55 */
56
57 'url' => env('APP_URL', 'http://localhost'),
58
59 /*
60 |--------------------------------------------------------------------------
61 | Application Timezone
62 |--------------------------------------------------------------------------
63 |
64 | Here you may specify the default timezone for your application, which
65 | will be used by the PHP date and date-time functions. The timezone
66 | is set to "UTC" by default as it is suitable for most use cases.
67 |
68 */
69
70 'timezone' => 'UTC',
71
72 /*
73 |--------------------------------------------------------------------------
74 | Application Locale Configuration
75 |--------------------------------------------------------------------------
76 |
77 | The application locale determines the default locale that will be used
78 | by Laravel's translation / localization methods. This option can be
79 | set to any locale for which you plan to have translation strings.
80 |
81 */
82
83 'locale' => env('APP_LOCALE', 'en'),
84
85 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
86
87 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
88
89 /*
90 |--------------------------------------------------------------------------
91 | Encryption Key
92 |--------------------------------------------------------------------------
93 |
94 | This key is utilized by Laravel's encryption services and should be set
95 | to a random, 32 character string to ensure that all encrypted values
96 | are secure. You should do this prior to deploying the application.
97 |
98 */
99
100 'cipher' => 'AES-256-CBC',
101
102 'key' => env('APP_KEY'),
103
104 'previous_keys' => [
105 ...array_filter(
106 explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
107 ),
108 ],
109
110 /*
111 |--------------------------------------------------------------------------
112 | Maintenance Mode Driver
113 |--------------------------------------------------------------------------
114 |
115 | These configuration options determine the driver used to determine and
116 | manage Laravel's "maintenance mode" status. The "cache" driver will
117 | allow maintenance mode to be controlled across multiple machines.
118 |
119 | Supported drivers: "file", "cache"
120 |
121 */
122
123 'maintenance' => [
124 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
125 'store' => env('APP_MAINTENANCE_STORE', 'database'),
126 ],
127
128 ];
129
cesta: config/database.php
186 řádků · 6.8 KB
Soubor aplikační architektury v modulu Configuration.
1 <?php
2
3 declare(strict_types=1);
4
5 use Illuminate\Support\Str;
6 use Pdo\Mysql;
7
8 return [
9
10 /*
11 |--------------------------------------------------------------------------
12 | Default Database Connection Name
13 |--------------------------------------------------------------------------
14 |
15 | Here you may specify which of the database connections below you wish
16 | to use as your default connection for database operations. This is
17 | the connection which will be utilized unless another connection
18 | is explicitly specified when you execute a query / statement.
19 |
20 */
21
22 'default' => env('DB_CONNECTION', 'sqlite'),
23
24 /*
25 |--------------------------------------------------------------------------
26 | Database Connections
27 |--------------------------------------------------------------------------
28 |
29 | Below are all of the database connections defined for your application.
30 | An example configuration is provided for each database system which
31 | is supported by Laravel. You're free to add / remove connections.
32 |
33 */
34
35 'connections' => [
36
37 'sqlite' => [
38 'driver' => 'sqlite',
39 'url' => env('DB_URL'),
40 'database' => env('DB_DATABASE', database_path('database.sqlite')),
41 'prefix' => '',
42 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
43 'busy_timeout' => null,
44 'journal_mode' => null,
45 'synchronous' => null,
46 'transaction_mode' => 'DEFERRED',
47 ],
48
49 'mysql' => [
50 'driver' => 'mysql',
51 'url' => env('DB_URL'),
52 'host' => env('DB_HOST', '127.0.0.1'),
53 'port' => env('DB_PORT', '3306'),
54 'database' => env('DB_DATABASE', 'laravel'),
55 'username' => env('DB_USERNAME', 'root'),
56 'password' => env('DB_PASSWORD', ''),
57 'unix_socket' => env('DB_SOCKET', ''),
58 'charset' => env('DB_CHARSET', 'utf8mb4'),
59 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
60 'prefix' => '',
61 'prefix_indexes' => true,
62 'strict' => true,
63 'engine' => null,
64 'options' => extension_loaded('pdo_mysql') ? array_filter([
65 (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
66 ]) : [],
67 ],
68
69 'mariadb' => [
70 'driver' => 'mariadb',
71 'url' => env('DB_URL'),
72 'host' => env('DB_HOST', '127.0.0.1'),
73 'port' => env('DB_PORT', '3306'),
74 'database' => env('DB_DATABASE', 'laravel'),
75 'username' => env('DB_USERNAME', 'root'),
76 'password' => env('DB_PASSWORD', ''),
77 'unix_socket' => env('DB_SOCKET', ''),
78 'charset' => env('DB_CHARSET', 'utf8mb4'),
79 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
80 'prefix' => '',
81 'prefix_indexes' => true,
82 'strict' => true,
83 'engine' => null,
84 'options' => extension_loaded('pdo_mysql') ? array_filter([
85 (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
86 ]) : [],
87 ],
88
89 'pgsql' => [
90 'driver' => 'pgsql',
91 'url' => env('DB_URL'),
92 'host' => env('DB_HOST', '127.0.0.1'),
93 'port' => env('DB_PORT', '5432'),
94 'database' => env('DB_DATABASE', 'laravel'),
95 'username' => env('DB_USERNAME', 'root'),
96 'password' => env('DB_PASSWORD', ''),
97 'charset' => env('DB_CHARSET', 'utf8'),
98 'prefix' => '',
99 'prefix_indexes' => true,
100 'search_path' => 'public',
101 'sslmode' => env('DB_SSLMODE', 'prefer'),
102 ],
103
104 'sqlsrv' => [
105 'driver' => 'sqlsrv',
106 'url' => env('DB_URL'),
107 'host' => env('DB_HOST', 'localhost'),
108 'port' => env('DB_PORT', '1433'),
109 'database' => env('DB_DATABASE', 'laravel'),
110 'username' => env('DB_USERNAME', 'root'),
111 'password' => env('DB_PASSWORD', ''),
112 'charset' => env('DB_CHARSET', 'utf8'),
113 'prefix' => '',
114 'prefix_indexes' => true,
115 // 'encrypt' => env('DB_ENCRYPT', 'yes'),
116 // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
117 ],
118
119 ],
120
121 /*
122 |--------------------------------------------------------------------------
123 | Migration Repository Table
124 |--------------------------------------------------------------------------
125 |
126 | This table keeps track of all the migrations that have already run for
127 | your application. Using this information, we can determine which of
128 | the migrations on disk haven't actually been run on the database.
129 |
130 */
131
132 'migrations' => [
133 'table' => 'migrations',
134 'update_date_on_publish' => true,
135 ],
136
137 /*
138 |--------------------------------------------------------------------------
139 | Redis Databases
140 |--------------------------------------------------------------------------
141 |
142 | Redis is an open source, fast, and advanced key-value store that also
143 | provides a richer body of commands than a typical key-value system
144 | such as Memcached. You may define your connection settings here.
145 |
146 */
147
148 'redis' => [
149
150 'client' => env('REDIS_CLIENT', 'phpredis'),
151
152 'options' => [
153 'cluster' => env('REDIS_CLUSTER', 'redis'),
154 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
155 'persistent' => env('REDIS_PERSISTENT', false),
156 ],
157
158 'default' => [
159 'url' => env('REDIS_URL'),
160 'host' => env('REDIS_HOST', '127.0.0.1'),
161 'username' => env('REDIS_USERNAME'),
162 'password' => env('REDIS_PASSWORD'),
163 'port' => env('REDIS_PORT', '6379'),
164 'database' => env('REDIS_DB', '0'),
165 'max_retries' => env('REDIS_MAX_RETRIES', 3),
166 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
167 'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
168 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
169 ],
170
171 'cache' => [
172 'url' => env('REDIS_URL'),
173 'host' => env('REDIS_HOST', '127.0.0.1'),
174 'username' => env('REDIS_USERNAME'),
175 'password' => env('REDIS_PASSWORD'),
176 'port' => env('REDIS_PORT', '6379'),
177 'database' => env('REDIS_CACHE_DB', '1'),
178 'max_retries' => env('REDIS_MAX_RETRIES', 3),
179 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
180 'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
181 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
182 ],
183
184 ],
185
186 ];
187
cesta: docker/entrypoint.sh
35 řádků · 0.8 KB
Startovací skript kontejneru zajišťující práva storage, env soubory a spuštění Caddy/Octane.
1 #!/bin/sh
2 set -e
3
4 APP_ROOT="${APP_ROOT:-/app}"
5 PHP_OPCACHE_VALIDATE_TIMESTAMPS="${PHP_OPCACHE_VALIDATE_TIMESTAMPS:-1}"
6 export APP_ROOT
7 export PHP_OPCACHE_VALIDATE_TIMESTAMPS
8
9 cd "$APP_ROOT"
10
11 if [ "$(id -u)" = "0" ]; then
12 mkdir -p storage/framework/cache/data \
13 storage/framework/sessions \
14 storage/framework/views \
15 storage/logs \
16 bootstrap/cache
17 chown -R www-data:www-data storage bootstrap/cache
18 chmod -R 775 storage bootstrap/cache
19 fi
20
21 if [ "${RUN_MIGRATIONS:-0}" = "1" ]; then
22 until php artisan db:monitor --max=1 >/dev/null 2>&1; do
23 sleep 2
24 done
25
26 php artisan migrate --force
27
28 if [ "${RUN_SEEDERS:-0}" = "1" ]; then
29 php artisan db:seed --force
30 fi
31
32 php artisan optimize
33 fi
34
35 exec "$@"
36
Zdroj: skutečné soubory této aplikace Plný průzkumník anatomie →