Plik .env jest jednym z pierwszych elementów, z którymi spotyka się każdy developer rozpoczynający pracę z Laravelem.
Znajdziemy w nim konfigurację bazy danych, cache, kolejek, mailera, zewnętrznych API czy ustawienia środowiska aplikacji.
Problem zaczyna się wtedy, gdy
Jak nie używać pliku .env w kodzie aplikacji Laravel
Plik .env jest jednym z pierwszych elementów, z którymi spotyka się każdy developer rozpoczynający pracę z Laravelem.
Znajdziemy w nim konfigurację bazy danych, cache, kolejek, mailera, zewnętrznych API czy ustawienia środowiska aplikacji.
Problem zaczyna się wtedy, gdy .env zaczynamy traktować jak globalny magazyn konfiguracji dostępny bezpośrednio z dowolnego miejsca aplikacji.
Na początku takie podejście wydaje się wygodne:
$apiKey = env('PAYMENTS_API_KEY');
Działa lokalnie.
Jest krótkie.
Jest czytelne.
Więc dlaczego miałoby być problemem?
Ponieważ w poprawnie skonfigurowanej aplikacji Laravel kod biznesowy nie powinien bezpośrednio korzystać z env().
Lepsza architektura wygląda następująco:
Environment Variables
↓
config/*.php
↓
config()
↓
kod aplikacji
Zobaczmy dlaczego.
.env nie jest konfiguracją aplikacji
Najważniejsze rozróżnienie jest następujące:
.env przechowuje wartości środowiskowe.
Natomiast katalog:
config/
definiuje konfigurację aplikacji.
To subtelna, ale bardzo ważna różnica.
Załóżmy, że integrujemy aplikację z zewnętrznym systemem płatności.
Możemy mieć:
PAYMENTS_API_URL=https://api.example.com
PAYMENTS_API_KEY=secret
PAYMENTS_TIMEOUT=10
Najgorszym rozwiązaniem byłoby później korzystanie z tych wartości bezpośrednio w serwisie:
class PaymentService
{
public function charge()
{
$url = env('PAYMENTS_API_URL');
$key = env('PAYMENTS_API_KEY');
// ...
}
}
Technicznie może to działać.
Architektonicznie jednak kod aplikacji został właśnie połączony bezpośrednio z mechanizmem zmiennych środowiskowych.
Lepszym rozwiązaniem jest utworzenie dedykowanego pliku konfiguracji.
// config/payments.php
return [
'url' => env('PAYMENTS_API_URL'),
'api_key' => env('PAYMENTS_API_KEY'),
'timeout' => (int) env('PAYMENTS_TIMEOUT', 10),
];
A następnie korzystanie z niego:
class PaymentService
{
public function charge()
{
$url = config('payments.url');
$key = config('payments.api_key');
// ...
}
}
To właśnie config() powinno być API konfiguracji dla reszty aplikacji.
Dlaczego nie powinieneś używać env() w kontrolerach i serwisach?
Najważniejszym powodem jest mechanizm cache'owania konfiguracji Laravela.
W środowisku produkcyjnym jednym ze standardowych kroków deploymentu jest:
php artisan config:cache
Laravel generuje wtedy skompilowaną konfigurację aplikacji.
Dzięki temu framework nie musi przy każdym żądaniu ponownie odczytywać wszystkich plików konfiguracyjnych.
Po wykonaniu cache konfiguracji plik .env nie jest normalnie ładowany przez aplikację.
Dlatego kod:
env('SOME_VARIABLE');
wywołany poza konfiguracją może zachowywać się inaczej niż podczas lokalnego developmentu.
To jeden z tych błędów, które często wyglądają tak:
lokalnie działa
↓
deployment
↓
config:cache
↓
produkcja przestaje działać
Dlatego bardzo dobra zasada brzmi:
env()używamy wconfig/*.php. W pozostałej części aplikacji używamyconfig().
Złe i dobre przykłady
Controller
Źle
class CheckoutController extends Controller
{
public function store()
{
$currency = env('PAYMENTS_CURRENCY');
// ...
}
}
Dobrze
class CheckoutController extends Controller
{
public function store()
{
$currency = config('payments.currency');
// ...
}
}
Service
Źle
class SmsService
{
public function send(string $phone, string $message)
{
$apiKey = env('SMS_API_KEY');
// ...
}
}
Dobrze
class SmsService
{
public function send(string $phone, string $message)
{
$apiKey = config('services.sms.api_key');
// ...
}
}
Konfiguracja:
// config/services.php
return [
'sms' => [
'api_key' => env('SMS_API_KEY'),
],
];
Job
Ten sam problem może pojawić się w kolejkach.
Źle
class SendInvoice implements ShouldQueue
{
public function handle()
{
$endpoint = env('INVOICE_API_URL');
// ...
}
}
Dobrze
class SendInvoice implements ShouldQueue
{
public function handle()
{
$endpoint = config('services.invoice.url');
// ...
}
}
Nie używaj .env jako miejsca na feature flags
Kolejny częsty błąd:
NEW_CHECKOUT_ENABLED=true
A następnie:
if (env('NEW_CHECKOUT_ENABLED')) {
// ...
}
Jeżeli ustawienie rzeczywiście zależy od środowiska, może znaleźć się w konfiguracji:
// config/features.php
return [
'new_checkout' => env('FEATURE_NEW_CHECKOUT', false),
];
A w aplikacji:
if (config('features.new_checkout')) {
// ...
}
Jednak w większym systemie warto zadać sobie jeszcze jedno pytanie:
Czy ta wartość rzeczywiście jest konfiguracją środowiska?
Jeżeli feature flag ma być zmieniany:
-
przez administratora,
-
bez deploymentu,
-
osobno dla użytkowników,
-
osobno dla organizacji,
-
dynamicznie podczas testów A/B,
to .env jest prawdopodobnie złym miejscem.
Takie dane powinny trafić do:
database
Redis
feature flag service
dedykowanego systemu konfiguracji
Nie umieszczaj konfiguracji biznesowej w .env
To jeden z częstszych problemów w rozwijających się projektach.
Na początku pojawiają się zmienne:
FREE_TRIAL_DAYS=14
MAX_PROJECTS=5
DEFAULT_DISCOUNT=10
INVOICE_PAYMENT_DAYS=30
Po kilku miesiącach .env zaczyna przypominać bazę danych.
Problem polega na tym, że nie każda wartość zmienna jest zmienną środowiskową.
Dobrym pytaniem jest:
Czy ta wartość zmienia się dlatego, że aplikacja działa na innym środowisku?
Przykładowo:
DB_HOST=
REDIS_HOST=
MAIL_HOST=
AWS_REGION=
mają sens jako konfiguracja środowiska.
Natomiast:
PREMIUM_PLAN_PRICE=
MAX_FREE_PROJECTS=
DEFAULT_COMMISSION=
mogą być częścią logiki biznesowej.
W zależności od projektu powinny więc znajdować się w:
config/
database
dedykowanym module ustawień
.env nie powinien trafiać do repozytorium
Plik .env często zawiera dane takie jak:
DB_PASSWORD=
AWS_SECRET_ACCESS_KEY=
MAIL_PASSWORD=
STRIPE_SECRET=
Dlatego właściwy plik .env nie powinien być wersjonowany razem z kodem aplikacji.
Do repozytorium można natomiast dodać:
.env.example
Przykład:
APP_NAME=
APP_ENV=
APP_KEY=
APP_DEBUG=
APP_URL=
DB_CONNECTION=
DB_HOST=
DB_PORT=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
REDIS_HOST=
REDIS_PORT=
PAYMENTS_API_KEY=
Nie powinno być tam prawdziwych sekretów.
.env.example pełni rolę dokumentacji wymaganej konfiguracji projektu.
Laravel również oficjalnie zaleca, aby właściwy plik .env nie był commitowany do systemu kontroli wersji.
.env nie jest też jedynym sposobem przekazywania zmiennych środowiskowych
To ważne szczególnie przy bardziej zaawansowanych deploymentach.
Nazwa .env może sugerować, że Laravel wymaga fizycznego pliku .env.
Nie jest to jednak pełny obraz.
W produkcji wartości środowiskowe mogą być dostarczane przez infrastrukturę.
Przykładowo:
Docker
Kubernetes Secrets
AWS
Laravel Cloud
CI/CD
systemd
platformę hostingową
secret manager
Aplikacja może więc otrzymać:
APP_ENV
DB_HOST
REDIS_HOST
STRIPE_SECRET
bez przechowywania klasycznego pliku .env na serwerze.
Z punktu widzenia kodu aplikacji nic nie powinno się jednak zmienić.
Nadal:
environment
↓
config
↓
application
Typowanie konfiguracji
.env przechowuje wartości tekstowe.
Dlatego warto zwracać uwagę na typy.
Przykładowo:
PAYMENTS_TIMEOUT=10
Konfiguracja:
return [
'timeout' => (int) env('PAYMENTS_TIMEOUT', 10),
];
Dzięki temu reszta aplikacji może oczekiwać konkretnego typu:
$timeout = config('payments.timeout');
Możemy też ustawić rozsądne wartości domyślne.
return [
'timeout' => (int) env('PAYMENTS_TIMEOUT', 10),
'retry_attempts' => (int) env('PAYMENTS_RETRY_ATTEMPTS', 3),
'currency' => env('PAYMENTS_CURRENCY', 'PLN'),
];
Kod biznesowy nie musi już wiedzieć, skąd te wartości pochodzą.
Jeszcze lepiej: Configuration Object
W większej aplikacji nawet ciągłe wywoływanie:
config('payments.api_url');
config('payments.api_key');
config('payments.timeout');
config('payments.currency');
może zacząć rozlewać szczegóły konfiguracji po całym kodzie.
Możemy więc zrobić krok dalej.
final readonly class PaymentConfig
{
public function __construct(
public string $apiUrl,
public string $apiKey,
public int $timeout,
public string $currency,
) {}
}
Następnie zarejestrować obiekt:
use App\Config\PaymentConfig;
$this->app->singleton(PaymentConfig::class, function () {
return new PaymentConfig(
apiUrl: config('payments.api_url'),
apiKey: config('payments.api_key'),
timeout: config('payments.timeout'),
currency: config('payments.currency'),
);
});
Serwis wygląda wtedy tak:
final class PaymentService
{
public function __construct(
private PaymentConfig $config,
) {}
public function charge(): void
{
$url = $this->config->apiUrl;
// ...
}
}
Daje nam to:
.env
↓
config/payments.php
↓
PaymentConfig
↓
PaymentService
Kod biznesowy nie wie już nawet o istnieniu helpera config().
W dużych, modularnych aplikacjach jest to bardzo wygodne podejście.
Konfiguracja a testowanie
Unikanie bezpośredniego env() ułatwia również testowanie.
Jeżeli mamy:
class CurrencyService
{
public function currency(): string
{
return env('DEFAULT_CURRENCY');
}
}
test zaczyna zależeć od środowiska procesu PHP.
Natomiast:
class CurrencyService
{
public function currency(): string
{
return config('app.currency');
}
}
możemy łatwo przetestować:
public function test_it_returns_configured_currency(): void
{
config([
'app.currency' => 'PLN',
]);
$service = new CurrencyService();
$this->assertSame(
'PLN',
$service->currency(),
);
}
Nie musimy modyfikować .env.
Nie musimy restartować procesu.
Nie musimy manipulować globalnym środowiskiem.
Co właściwie powinno znajdować się w .env?
Dobra heurystyka wygląda tak:
.env powinien zawierać wartości zależne od środowiska uruchomieniowego.
Na przykład:
APP_ENV=production
DB_HOST=database
REDIS_HOST=redis
MAIL_HOST=smtp.example.com
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
Natomiast aplikacja powinna korzystać z nich poprzez konfigurację:
config('database.default');
config('cache.default');
config('mail.default');
config('filesystems.disks.s3');
Prosty test architektury
Przy code review można zastosować bardzo prostą zasadę.
Jeżeli zobaczysz:
env(
w katalogach:
app/
routes/
resources/
database/
warto sprawdzić, dlaczego się tam znajduje.
W większości przypadków powinno być zastąpione przez:
config(
A samo env() powinno pozostać przede wszystkim w:
config/
Przykładowa struktura profesjonalnego projektu
Dla integracji płatności możemy mieć:
.env
│
├── PAYMENTS_API_URL
├── PAYMENTS_API_KEY
└── PAYMENTS_TIMEOUT
config/
└── payments.php
app/
├── Config/
│ └── PaymentConfig.php
│
├── Services/
│ └── PaymentService.php
│
└── Providers/
└── AppServiceProvider.php
Przepływ konfiguracji:
Environment
↓
config/payments.php
↓
PaymentConfig
↓
PaymentService
Dzięki temu odpowiedzialności są jasno rozdzielone.
Produkcyjny deployment
Podczas deploymentu aplikacji warto cache'ować konfigurację:
php artisan config:cache
Laravel udostępnia również:
php artisan optimize
które może przygotować kilka cache'y wykorzystywanych przez aplikację produkcyjną.
To kolejny argument za tym, aby traktować konfigurację jako osobną warstwę aplikacji, zamiast odczytywać wartości środowiskowe bezpośrednio z kodu biznesowego.
Podsumowanie
Plik .env nie jest problemem.
Problemem jest sposób, w jaki go używamy.
Najważniejsza zasada wygląda tak:
.env
↓
config/*.php
↓
config()
↓
application
Zamiast:
env('PAYMENTS_API_KEY');
w serwisie:
config('payments.api_key');
A w większych projektach możemy pójść jeszcze dalej:
Environment
↓
Laravel Config
↓
Typed Configuration Object
↓
Service
Takie podejście daje nam:
-
przewidywalne działanie
config:cache, -
łatwiejsze testowanie,
-
lepsze rozdzielenie odpowiedzialności,
-
prostszy deployment,
-
mniejsze sprzężenie kodu z infrastrukturą,
-
czytelniejszą konfigurację dużych aplikacji.
Najważniejsze jest więc nie to, żeby całkowicie pozbyć się .env.
Chodzi o to, aby nie pozwolić, żeby .env stał się częścią logiki aplikacji.
Środowisko powinno dostarczać wartości.
Konfiguracja powinna je interpretować.
A kod biznesowy powinien korzystać już tylko z gotowej konfiguracji.