Configuração por ambiente: por que e como organizar
Em um back-end com Slim, a mesma base de código precisa se comportar de forma diferente em dev, teste e produção. A configuração por ambiente é a prática de separar o que é “código” do que é “parâmetro de execução”, permitindo ajustar comportamento (logs, cache, CORS, timeouts, banco, exibição de erros) sem alterar o código-fonte.
Um modelo prático é trabalhar com camadas: defaults (valores seguros e comuns) + override por ambiente (apenas o que muda). Além disso, segredos (DSN, senhas, chaves) devem vir de variáveis de ambiente (ou secret manager), nunca versionados.
Objetivos da estratégia
- Reprodutibilidade: subir o app em qualquer ambiente com o mesmo artefato.
- Segurança: segredos fora do repositório e com menor superfície de exposição.
- Observabilidade: logs e níveis adequados por ambiente.
- Fail fast: validar configurações na inicialização e falhar cedo.
Estrutura recomendada de arquivos
Uma organização simples e eficaz:
project/ config/ defaults.php env/ dev.php test.php prod.php bootstrap/ config.php public/ index.phpdefaults.php contém o baseline. Cada arquivo em env/ sobrescreve apenas o necessário. O bootstrap/config.php carrega, mescla e valida.
Camadas de configuração: defaults + override por ambiente
1) Defaults (config/defaults.php)
Defaults devem ser conservadores e seguros. Evite colocar segredos aqui.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
<?php // config/defaults.php return [ 'app' => [ 'name' => 'Slim API', 'env' => 'prod', 'timezone' => 'UTC', ], 'http' => [ 'trusted_proxies' => [], 'timeout_seconds' => 10, ], 'errors' => [ 'display_error_details' => false, ], 'log' => [ 'level' => 'warning', 'channel' => 'app', ], 'cors' => [ 'enabled' => true, 'allowed_origins' => [], 'allowed_methods' => ['GET','POST','PUT','PATCH','DELETE','OPTIONS'], 'allowed_headers' => ['Content-Type','Authorization','X-Request-Id'], 'exposed_headers' => ['X-Request-Id'], 'allow_credentials' => false, 'max_age' => 600, ], 'cache' => [ 'enabled' => false, 'driver' => 'array', 'ttl_seconds' => 60, ], 'db' => [ 'driver' => 'pdo_pgsql', 'dsn' => null, 'user' => null, 'password' => null, 'options' => [ 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION', 'ATTR_DEFAULT_FETCH_MODE' => 'FETCH_ASSOC', 'ATTR_TIMEOUT' => 5, ], ], ];2) Overrides por ambiente (config/env/*.php)
Em dev, normalmente você quer mais verbosidade e menos cache. Em prod, o oposto.
config/env/dev.php
<?php return [ 'app' => [ 'env' => 'dev', ], 'errors' => [ 'display_error_details' => true, ], 'log' => [ 'level' => 'debug', ], 'cors' => [ 'allowed_origins' => ['http://localhost:3000','http://localhost:5173'], 'allow_credentials' => true, ], 'cache' => [ 'enabled' => false, ], ];config/env/test.php
<?php return [ 'app' => [ 'env' => 'test', ], 'errors' => [ 'display_error_details' => true, ], 'log' => [ 'level' => 'info', ], 'cache' => [ 'enabled' => false, ], 'db' => [ 'options' => [ 'ATTR_TIMEOUT' => 2, ], ], ];config/env/prod.php
<?php return [ 'app' => [ 'env' => 'prod', ], 'errors' => [ 'display_error_details' => false, ], 'log' => [ 'level' => 'warning', ], 'cors' => [ 'allowed_origins' => ['https://app.suaempresa.com'], 'allow_credentials' => true, ], 'cache' => [ 'enabled' => true, 'driver' => 'redis', 'ttl_seconds' => 300, ], 'http' => [ 'timeout_seconds' => 5, ], ];3) Carregamento e merge (bootstrap/config.php)
O merge deve respeitar a precedência: defaults < env file < variáveis de ambiente (para segredos e ajustes operacionais).
<?php // bootstrap/config.php function array_merge_recursive_distinct(array $base, array $override): array { foreach ($override as $key => $value) { if (is_array($value) && isset($base[$key]) && is_array($base[$key])) { $base[$key] = array_merge_recursive_distinct($base[$key], $value); } else { $base[$key] = $value; } } return $base; } $defaults = require __DIR__ . '/../config/defaults.php'; $env = getenv('APP_ENV') ?: ($defaults['app']['env'] ?? 'prod'); $envFile = __DIR__ . '/../config/env/' . $env . '.php'; $envConfig = file_exists($envFile) ? require $envFile : []; $config = array_merge_recursive_distinct($defaults, $envConfig); // Override final via env vars (segredos e parâmetros operacionais) $config['app']['env'] = $env; $config['db']['dsn'] = getenv('DB_DSN') ?: $config['db']['dsn']; $config['db']['user'] = getenv('DB_USER') ?: $config['db']['user']; $config['db']['password'] = getenv('DB_PASSWORD') ?: $config['db']['password']; $config['log']['level'] = getenv('LOG_LEVEL') ?: $config['log']['level']; $config['errors']['display_error_details'] = filter_var(getenv('DISPLAY_ERROR_DETAILS') ?: $config['errors']['display_error_details'], FILTER_VALIDATE_BOOL); $config['cors']['allowed_origins'] = getenv('CORS_ALLOWED_ORIGINS') ? array_map('trim', explode(',', getenv('CORS_ALLOWED_ORIGINS'))) : $config['cors']['allowed_origins']; $config['http']['timeout_seconds'] = getenv('HTTP_TIMEOUT_SECONDS') ? (int)getenv('HTTP_TIMEOUT_SECONDS') : $config['http']['timeout_seconds']; return $config;Tratamento de segredos: chaves, DSN e credenciais
Princípios práticos
- Nunca versionar segredos em
config/*.php. - Preferir variáveis de ambiente (
DB_PASSWORD,JWT_SECRET,REDIS_URL) ou um secret manager da infraestrutura. - Separar “configuração pública” (ex.:
cache.enabled) de “configuração sensível” (ex.: senha). - Evitar imprimir segredos em logs e mensagens de erro.
Exemplo de variáveis de ambiente esperadas
| Variável | Exemplo | Uso |
|---|---|---|
APP_ENV | dev, test, prod | Seleciona override por ambiente |
DB_DSN | pgsql:host=db;port=5432;dbname=app | Conexão com banco |
DB_USER | app_user | Usuário do banco |
DB_PASSWORD | (segredo) | Senha do banco |
LOG_LEVEL | debug, info, warning | Verbosidade de logs |
DISPLAY_ERROR_DETAILS | true/false | Detalhes de erro (nunca em prod) |
CORS_ALLOWED_ORIGINS | https://app.com,https://admin.app.com | Origens permitidas |
Níveis de log e exibição de erros por ambiente
Regras comuns:
dev:display_error_details=trueelog.level=debug.test:display_error_details=true(para facilitar diagnóstico) e logs moderados.prod:display_error_details=falseelog.level=warning(ouinfose você precisa de auditoria).
Mesmo em dev, evite logar payloads sensíveis. Se precisar, mascare campos (ex.: senha, token).
Exemplos práticos: CORS, cache, timeouts e banco
CORS: configuração orientada a ambiente
O ideal é permitir somente as origens necessárias. Em dev, liberar localhost; em prod, apenas domínios oficiais.
<?php // Exemplo de leitura da config para um middleware de CORS (pseudo) $cors = $config['cors']; // allowed_origins, allowed_methods, allowed_headers, allow_credentials, max_ageCuidados:
- Se
allow_credentials=true, não use*emallowed_origins. - Responda
OPTIONSrapidamente (preflight) e comAccess-Control-Max-Ageadequado.
Cache: habilitar em produção, desabilitar em dev/test
Cache costuma ser fonte de confusão em desenvolvimento. Por isso, defaults podem vir desabilitados e o override de prod habilita com driver real (ex.: Redis).
<?php // Exemplo de parâmetros de cache $cache = $config['cache']; // enabled, driver, ttl_secondsParâmetros úteis por ambiente:
dev:enabled=falsepara evitar “estado invisível”.prod:enabled=trueettl_secondsmaior para reduzir carga.
Timeouts: HTTP e banco
Timeouts devem ser mais agressivos em produção para evitar saturação. Em teste, podem ser menores para falhar rápido.
<?php // HTTP timeout (para clientes externos, se aplicável) $timeout = $config['http']['timeout_seconds']; // Banco (PDO) $pdoTimeout = $config['db']['options']['ATTR_TIMEOUT'] ?? 5;Boas práticas:
- Defina timeouts explícitos para conexões externas e banco.
- Evite timeouts muito altos em
prod; prefira retries controlados e circuit breaker (quando aplicável).
Banco de dados: DSN e opções por ambiente
Use DSN via variável de ambiente e mantenha opções sensatas no defaults. Em test, é comum apontar para um banco isolado e reduzir timeouts.
<?php // Exemplo de montagem de conexão a partir da config (sem container, apenas ilustrativo) $dsn = $config['db']['dsn']; $user = $config['db']['user']; $pass = $config['db']['password']; $options = []; foreach (($config['db']['options'] ?? []) as $k => $v) { $const = constant('PDO::' . $k); $options[$const] = constant('PDO::' . $v) ?: $v; } // Observação: o mapeamento acima depende de como você representa constantes; alternativa: guardar já como constantes. $pdo = new PDO($dsn, $user, $pass, $options);Uma alternativa mais simples é armazenar as opções já como constantes (inteiros) no config, evitando conversões.
Validação de configuração na inicialização (fail fast)
Falhar cedo evita que o app suba “meio quebrado” e só apresente erro sob carga. A validação deve rodar no bootstrap, antes de registrar rotas e iniciar o processamento de requests.
Checklist de validação
- Obrigatórios: DSN/credenciais em ambientes que usam banco.
- Consistência:
display_error_detailsnão pode estartrueemprod. - CORS: se
allow_credentials=true,allowed_originsnão pode conter*. - Cache: se
driver=redis, exigir URL/host do Redis (variável de ambiente). - Tipos: inteiros positivos para timeouts e TTL.
Implementação de validação (bootstrap/validate_config.php)
<?php function assert_true(bool $cond, string $message): void { if (!$cond) { throw new RuntimeException('Config inválida: ' . $message); } } function validate_config(array $config): void { $env = $config['app']['env'] ?? 'prod'; assert_true(in_array($env, ['dev','test','prod'], true), 'APP_ENV deve ser dev|test|prod'); // Erros em produção assert_true(!($env === 'prod' && ($config['errors']['display_error_details'] ?? false) === true), 'DISPLAY_ERROR_DETAILS não pode ser true em produção'); // Banco: exigir DSN/user/pass em prod (ajuste conforme seu caso) if ($env !== 'test') { assert_true(!empty($config['db']['dsn']), 'DB_DSN é obrigatório'); assert_true(!empty($config['db']['user']), 'DB_USER é obrigatório'); assert_true(!empty($config['db']['password']), 'DB_PASSWORD é obrigatório'); } // Timeouts assert_true(($config['http']['timeout_seconds'] ?? 0) > 0, 'HTTP_TIMEOUT_SECONDS deve ser > 0'); $ttl = $config['cache']['ttl_seconds'] ?? 0; assert_true($ttl >= 0, 'cache.ttl_seconds deve ser >= 0'); // CORS rules $cors = $config['cors'] ?? []; $origins = $cors['allowed_origins'] ?? []; if (($cors['allow_credentials'] ?? false) === true) { assert_true(!in_array('*', $origins, true), 'CORS: não use * com allow_credentials=true'); } // Cache driver rules if (($config['cache']['enabled'] ?? false) === true && ($config['cache']['driver'] ?? '') === 'redis') { $redisUrl = getenv('REDIS_URL'); assert_true(!empty($redisUrl), 'REDIS_URL é obrigatório quando cache.driver=redis'); } }Executando a validação no bootstrap
No ponto de entrada (ex.: public/index.php), carregue config e valide antes de seguir.
<?php $config = require __DIR__ . '/../bootstrap/config.php'; require __DIR__ . '/../bootstrap/validate_config.php'; validate_config($config); // A partir daqui, registre middlewares/rotas e rode o appPasso a passo prático para aplicar no projeto
Passo 1: criar defaults e arquivos por ambiente
- Crie
config/defaults.phpcom valores comuns e seguros. - Crie
config/env/dev.php,test.php,prod.phpsobrescrevendo apenas o necessário.
Passo 2: definir variáveis de ambiente para segredos
- Defina
APP_ENVe segredos (DB_DSN,DB_USER,DB_PASSWORDetc.) no runtime (Docker/Kubernetes/CI/servidor). - Se usar arquivo local de variáveis, garanta que ele não seja versionado.
Passo 3: implementar carregamento com precedência
- Carregue defaults.
- Carregue override do ambiente.
- Aplique overrides finais via variáveis de ambiente.
Passo 4: validar e falhar cedo
- Implemente
validate_config()com regras de obrigatoriedade e consistência. - Rode a validação antes de iniciar o app.
Passo 5: parametrizar CORS, cache, timeouts e banco via config
- Leia as chaves de config nos pontos de integração (middleware de CORS, cliente HTTP, camada de persistência, cache).
- Garanta que cada ambiente tenha valores coerentes com seu objetivo (debuggabilidade em dev, estabilidade em prod).