Configuration Strategy — One Rule That Prevents Chaos
Series: Every Laravel Project Should Have These Building Blocks
Part: 2 of 35 | Level: Beginner | Prerequisites: Folder Structure
What You’ll Learn
- The single rule that prevents environment-related bugs
- How to organize
config/as your application grows - When to create new config files vs. extending
services.php - How to properly expose third-party service config
- How to validate that required config values are present
The One Rule
Never call
env()outside ofconfig/files.
That’s it. That’s the entire configuration strategy.
If you follow this one rule consistently, you eliminate an entire category of bugs. If you break it even once, you’ll spend an afternoon wondering why your config is returning null after running php artisan config:cache.
Here’s why: when you run config:cache, Laravel compiles all your config files into a single cached file and stops reading .env entirely. If your code calls env('STRIPE_KEY') directly, it will return null in a cached environment — which is every production server.
// ❌ Wrong — breaks after config:cache
class StripePaymentService
{
public function __construct()
{
$this->apiKey = env('STRIPE_SECRET_KEY'); // null in production!
}
}
// ✅ Correct — always works
class StripePaymentService
{
public function __construct()
{
$this->apiKey = config('services.stripe.secret'); // reads from cache
}
}
The config/ Folder Structure
Laravel ships with a set of standard config files. The ones you’ll interact with most are:
config/
├── app.php # Application name, timezone, locale, debug flag
├── auth.php # Guards, providers, password brokers
├── cache.php # Default cache store, TTLs
├── database.php # Database connections
├── filesystems.php # Disk definitions (local, s3, etc.)
├── logging.php # Log channels — covered in a dedicated article
├── mail.php # Mailer config, default from address
├── queue.php # Queue connections and default queue
├── services.php # ← Third-party service credentials
└── session.php # Session driver and TTL
The most important one to understand as your app grows is services.php.
config/services.php — Your Third-Party Service Registry
services.php is Laravel’s designated place for external service credentials. It ships mostly empty:
// config/services.php (default)
return [
'mailgun' => [...],
'postmark' => [...],
'ses' => [...],
];
Every third-party service your application uses — Stripe, Twilio, AWS, a custom API — should have its credentials registered here:
// config/services.php
return [
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
'whm' => [
'host' => env('WHM_HOST'),
'username' => env('WHM_USERNAME'),
'api_token' => env('WHM_API_TOKEN'),
'verify_ssl' => env('WHM_VERIFY_SSL', true),
],
'name_api' => [
'api_key' => env('NAME_API_KEY'),
'base_url' => env('NAME_API_URL', 'https://api.name.com/v4/'),
],
'ollama' => [
'api_url' => env('OLLAMA_API_URL', 'http://localhost:11434/api/chat'),
'model' => env('OLLAMA_MODEL', 'llama3.2'),
'timeout' => env('OLLAMA_TIMEOUT', 90),
'context_char_limit' => env('OLLAMA_CONTEXT_CHAR_LIMIT', 3000),
'temperature' => env('OLLAMA_TEMPERATURE', 0.1),
],
'admin' => [
'email' => env('ADMIN_EMAIL'),
],
];
Then access it in code:
config('services.stripe.secret')
config('services.whm.host')
config('services.ollama.model')
When to Create a New Config File
The rule is simple: create a new config file when the configuration for a feature is complex enough that it deserves its own namespace.
Don’t create a new file for simple key/value pairs. These belong in services.php:
// ❌ Overkill — this doesn't need its own file
// config/stripe.php
return ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET')];
// ✅ Just add it to services.php
'stripe' => ['key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET')],
Do create a new file when you have a domain with many settings that would clutter services.php. Good examples from real projects:
// config/logging.php — Laravel ships this by default; customize it for named channels
// config/error_mail.php — configuration for the ErrorReporter service
// config/knowledge.php — configuration for AI knowledge base feature
// config/error_mail.php
return [
'sensitive_fields' => [
'password',
'password_confirmation',
'token',
'card_number',
'cvv',
],
];
Validating Required Config at Boot
One underused feature: you can validate that required config values are present when the application boots, rather than getting cryptic errors at runtime.
In AppServiceProvider::boot():
public function boot(): void
{
$this->validateConfig();
}
private function validateConfig(): void
{
$required = [
'services.stripe.secret' => 'STRIPE_SECRET',
'services.whm.api_token' => 'WHM_API_TOKEN',
'mail.support.address' => 'MAIL_SUPPORT_ADDRESS',
];
foreach ($required as $configKey => $envKey) {
if (blank(config($configKey))) {
throw new \RuntimeException(
"Required configuration [{$configKey}] is missing. Set {$envKey} in your .env file."
);
}
}
}
This is especially useful for services that your app simply cannot function without. You’ll get a clear error immediately instead of a null-pointer exception buried in a stack trace.
The .env.example as Documentation
Your .env.example file is documentation. Every variable your application can use should be in it, with a comment explaining what it does:
# Application
APP_NAME="My App"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=
# Stripe Payment Gateway
# Get your keys from https://dashboard.stripe.com/apikeys
STRIPE_KEY=pk_test_
STRIPE_SECRET=sk_test_
STRIPE_WEBHOOK_SECRET=whsec_
# Error Reporting
# Comma-separated list of developer emails to receive exception alerts
MAIL_SUPPORT_ADDRESS=dev@yourcompany.com
A new developer should be able to read .env.example and understand every setting without asking anyone.
Environment-Specific Config Overrides
Sometimes you need a different configuration in testing vs. production. Use the config() helper within AppServiceProvider to override:
// In config/mail.php
'support' => [
'address' => env('MAIL_SUPPORT_ADDRESS', ''),
],
// In .env
MAIL_SUPPORT_ADDRESS=dev@company.com,senior@company.com
// In ErrorReporter — reads it as a comma-separated list
$emails = config('mail.support.address', []);
if (is_string($emails)) {
$emails = explode(',', $emails);
}
For test environments, use .env.testing:
# .env.testing
APP_ENV=testing
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
MAIL_SUPPORT_ADDRESS= # Empty — don't send real emails during tests
Key Takeaways
- Never call
env()outsideconfig/files — this is non-negotiable. It will break afterconfig:cache. - All third-party credentials go in
config/services.php— don’t create a new file unless you have 5+ related settings. - Access config via
config('key.subkey')everywhere in your application code. - Your
.env.exampleis documentation — keep it up to date and well-commented. - Validate required config at boot for services your app can’t live without.
Tips and Gotchas
⚠️ Warning:
env()called directly in application code (outsideconfig/) breaksphp artisan config:cache. When config is cached,env()always returnsnull. This is a silent production bug that works perfectly in development.
💡 Tip: Run
php artisan config:cachelocally occasionally to catchenv()misuse before it reaches production.
🔥 Expert Note: Group all third-party service credentials in
config/services.php— not separate per-service config files. One file to open, one place to look. Laravel’s built-inBroadcast, andSocialiteintegrations already follow this convention.
Further Reading
← Folder Structure | Next: AppServiceProvider — The Backbone →
One Comment