Quer escrever PHP que não te deixa na mão em produção? Aqui vai um guia prático sobre os blocos básicos que fazem código durar: tipos consistentes, arquitetura simples, segurança que bloqueia falhas comuns, testes que evitam regressões e otimizações que contam. Sem receitas mágicas: só o que realmente impacta o teu dia a dia.
- TL;DR
- - Ativa strict_types e usa tipos em tudo (funções, propriedades, exceções). Reduz bugs cedo.
- - Segue PSR-12/PSR-4, usa Composer, DI simples e exceptions. Fica fácil manter.
- - Segurança: valida entradas, prepared statements, escaping e CSRF tokens. Padrão OWASP.
- - Testes: PHPUnit + cobertura pragmática; estático com PHPStan/Psalm no nível 6-8.
- - Desempenho: OPcache ligado, cachê (APCu/Redis), evita N+1 no SQL, mede antes de otimizar.
Fundamentos que escalam: tipos, padrões modernos e exemplos prontos
Se o teu PHP ainda parece 5.x, estás a perder produtividade. Desde o 8.0, a linguagem ficou muito mais expressiva: union types, nullsafe, match, attributes, enums (8.1) e melhorias no 8.2/8.3. O ganho? Menos ifs frágis, mais intenções claras.
Começa pelo básico que bloqueia bugs:
- Ativa tipos na primeira linha dos ficheiros críticos:
declare(strict_types=1);
- Especifica tipos de parâmetros e retornos em todas as funções públicas.
- Evita null quando possível; usa value objects (ex.:
Email
válido) para dar significado. - Trata erros com exceções tipadas; não retornes códigos mágicos.
declare(strict_types=1);
final class Email
{
public function __construct(private string $value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Email inválido');
}
}
public function value(): string { return $this->value; }
}
function enviarRecibo(Email $email): void
{
// ...
}
// Ajuda do nullsafe e coalescência
$nome = $user?->perfil?->nome ?? 'Convidado';
O operador nullsafe (->?
) e o null coalescing (??
) cortam boilerplate. E o match
aumenta legibilidade onde antes vivia um switch cansado.
function imposto(float $valor, string $pais): float
{
return match ($pais) {
'PT' => $valor * 0.23,
'DE' => $valor * 0.19,
'FR' => $valor * 0.20,
default => 0.0,
};
}
Enums (8.1+) são ouro para estados. Adeus strings soltas.
enum EstadoPedido: string { case Novo = 'novo'; case Pago = 'pago'; case Enviado = 'enviado'; }
final class Pedido
{
public function __construct(
private EstadoPedido $estado = EstadoPedido::Novo
) {}
public function pagar(): void
{
if ($this->estado !== EstadoPedido::Novo) {
throw new DomainException('Fluxo inválido');
}
$this->estado = EstadoPedido::Pago;
}
}
Composer + PSR-4 deixam o autoload certinho. O ficheiro composer.json
deve declarar o namespace da app e o diretório src/
.
{
"require": {
"php": "^8.1",
"ext-pdo": "*"
},
"autoload": {
"psr-4": { "App\\": "src/" }
}
}
Instala, roda e garante padrão de estilo com PSR-12 (recomendado pelo PHP-FIG) usando PHP-CS-Fixer ou PHPCS. Para análise estática, PHPStan ou Psalm ajudam a apanhar incoerências que não saltam à vista.
# Instalação local
composer require --dev phpstan/phpstan friendsofphp/php-cs-fixer phpunit/phpunit vlucas/phpdotenv monolog/monolog
# Scripts úteis (composer.json)
"scripts": {
"lint": "php-cs-fixer fix --allow-risky=yes --diff",
"stan": "phpstan analyse -l 7 src",
"test": "phpunit --colors=always"
}
Configuração por ambiente? Usa variáveis de ambiente e vlucas/phpdotenv
em dev. Nunca faz commit de credenciais.
// public/index.php
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->safeLoad();
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $_ENV['DB_HOST'], $_ENV['DB_NAME']);
Receita para acesso a dados seguro via PDO com prepared statements. Simples e blindado contra SQL injection (OWASP Top 10).
final class UserRepo
{
public function __construct(private PDO $pdo) {}
/** @return array{ id:int, email:string }|null */
public function findByEmail(Email $email): ?array
{
$stmt = $this->pdo->prepare('SELECT id, email FROM users WHERE email = :email');
$stmt->execute(['email' => $email->value()]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
return $user ?: null;
}
}
Bónus de produtividade: gerares coleções imutáveis com iteradores e generators quando os dados são grandes. Poupa memória.
function linhas(string $ficheiro): Generator
{
$h = fopen($ficheiro, 'r');
while (($l = fgets($h)) !== false) {
yield rtrim($l);
}
fclose($h);
}
foreach (linhas('clientes.csv') as $linha) {
// processa linha sem carregar o ficheiro todo
}
Heurísticas que não te falham:
- Prefere early return a nesting profundo.
- Uma função faz uma coisa. Nome claro, parâmetro tipado, retorno explícito.
- DTOs/Value Objects para dados com regras (Email, Dinheiro, IVA).
- Evita singletons; usa injeção de dependência (manual é suficiente na maioria dos casos).
E sim, guarda um glossário mental de truques PHP que realmente pagam a conta: tipos, exceptions, enums, match, nullsafe, generators, PDO preparado, OPcache e cache de aplicação.

Qualidade, segurança e testes sem drama: checklists e pit-stops
Qualidade começa com consistência. Padrões PSR do PHP-FIG (PSR-12 para estilo; PSR-4 para autoload; PSR-7/17 para HTTP se usares libs) dão linguagem comum. Em 2025, equipas que respeitam estes padrões integram mais rápido e gastam menos tempo a discutir formatação.
Checklist de qualidade antes do merge:
- CI roda: linters (PHPCS/PHP-CS-Fixer),
phpstan/psalm
nível 6-8, PHPUnit verde. - Funções públicas tipadas; não aceites
mixed
sem motivo. - Tratamento de erros com exceções específicas (ex.:
DomainException
,RuntimeException
customizadas). - Logs significativos com Monolog (níveis INFO/ERROR) e correlação de requisições.
- Sem credenciais no repo;
.env.example
atualizado.
Segurança não é opcional. Padrões básicos que evitam dores de cabeça, alinhados ao OWASP:
- Valida e normaliza entradas (backend) com filtros do PHP. Nunca confia no frontend.
- Escapa ao renderizar HTML:
htmlspecialchars($s, ENT_QUOTES, 'UTF-8')
. - SQL só com prepared statements (PDO).
- Autenticação:
password_hash()
epassword_verify()
com argon2id ou bcrypt. Rodapassword_needs_rehash()
periodicamente. - CSRF tokens em formulários ou cabeçalhos (mesmo com SameSite=Lax).
- Cookies:
HttpOnly
,Secure
eSameSite
definidos.
// Hash seguro
$hash = password_hash($senha, PASSWORD_ARGON2ID);
if (!password_verify($senhaDigitada, $hash)) { /* falha */ }
// Cookie seguro (exemplo com setcookie)
setcookie('sess', $id, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
CSRF em PHP puro fica simples com uma camada pequena.
function csrf_token(): string {
if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); }
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
return $_SESSION['csrf'];
}
function csrf_check(string $token): void {
if (!hash_equals($_SESSION['csrf'] ?? '', $token)) {
throw new RuntimeException('CSRF inválido');
}
}
Erros e logging: em produção, display_errors = Off
, log_errors = On
, nível de reporting alto (E_ALL
) e logs estruturados (JSON) para facilitar observabilidade. Integra com stack tipo ELK/OpenSearch ou um serviço gerido.
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
$logger = new Monolog\Logger('app');
$logger->pushHandler(new Monolog\Handler\StreamHandler(__DIR__.'/../var/app.log'));
$logger->info('Pedido criado', ['pedido_id' => $id]);
Testes, sem drama. Cobertura 100% é cara e, muitas vezes, pouco útil. Mira em cobrir regras de negócio e pontos de integração. Recomendo:
- Unitários para value objects e domínios (rápidos, sem I/O).
- Integração para repositórios e serviços (com DB de teste, fixtures).
- Contract tests se usares HTTP clients (PSR-18) para evitar rachar APIs externas.
final class EmailTest extends PHPUnit\Framework\TestCase
{
public function testNaoAceitaEmailInvalido(): void
{
$this->expectException(InvalidArgumentException::class);
new Email('x');
}
}
Controlo estático encontra armadilhas cedo. Configura PHPStan/Psalm com baseline para facilitar adoção e vai subindo o nível.
# phpstan.neon
parameters:
level: 7
paths: [src]
tmpDir: var/phpstan
Versões suportadas importam para segurança. Planeia upgrades com o calendário abaixo (baseado no cronograma oficial do PHP):
Versão | Lançamento | Suporte ativo até | Suporte de segurança até |
---|---|---|---|
8.1 | Nov 2021 | Nov 2023 | Nov 2025 |
8.2 | Dez 2022 | Dez 2024 | Dez 2025 |
8.3 | Nov 2023 | Nov 2025 | Nov 2026 |
8.4 (prev.) | Nov 2024 | Nov 2026 | Nov 2027 |
Fontes: Manual do PHP (tabela de versões suportadas) e notas de lançamento. Se estás abaixo de 8.1, trata a migração como prioridade de segurança.

Desempenho e produtividade: do local ao deploy (com FAQ e troubleshooting)
Regra de ouro: mede antes de otimizar. Usa Xdebug profiler local ou serviços como Blackfire para perceber gargalos. Tenta estes ganhos rápidos:
- OPcache ligado em produção. É obrigatório hoje.
- Cache de aplicação (APCu/Redis) para resultados caros (ex.: listas frequentes).
- Evita N+1 queries; carrega relações com joins ou duas queries explícitas.
- Paginado sempre que lista grande. Sem paginação, a conta de memória explode.
- Generators para streams/ficheiros grandes; não carregues tudo em arrays.
// Cache simples com APCu
function cache_get(string $key, callable $compute, int $ttl = 60): mixed {
$ok = false; $val = apcu_fetch($key, $ok);
if ($ok) return $val;
$val = $compute();
apcu_store($key, $val, $ttl);
return $val;
}
Micro-otimizações só depois de resolver arquitetura e I/O. Trocar array_map
por foreach
raramente move a agulha; reduzir queries duplicadas e serializações desnecessárias sim.
Heurísticas práticas de performance:
- Ficheiros: prefere
SplFileObject
ou generators afile()
. - Strings:
json_validate()
(8.3+) evita decodificar lixo antes dojson_decode
. - Custos: rede e disco jogam mais que CPU na maioria das apps web em PHP.
- JIT (desde 8.0) ajuda pouco em I/O pesado; nota ganhos em computação pura.
// Exemplo com json_validate (PHP 8.3)
if (!json_validate($payload)) {
throw new InvalidArgumentException('JSON inválido');
}
$data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR);
Produtividade do fluxo local→produção:
- Ferramentas dev: makefile com alvos curtos (
make test
,make qa
,make fix
). - CI: roda
composer install --no-dev
para build de produção; valida versões de PHP iguais às do servidor. - Docker para paridade de ambiente (extensões, ini, timezone).
- Migrações de DB com tool do teu framework ou
phinx
/doctrine/migrations
. - Feature flags para releases seguros.
Checklist de "pronto para produção":
display_errors=Off
,error_reporting=E_ALL
, logs rodando.- OPcache on;
opcache.validate_timestamps=0
(em containers imutáveis). - HTTPS obrigatório; HSTS no front (Nginx/Apache).
- Backups do DB testados; restore ensaiado.
- Monitorização: uptime, erros por segundo, latência p95, consumo de memória.
Mini-FAQ
- Qual versão usar em 2025?
Recomendo 8.3 em novos projetos. Se já estás em 8.1/8.2, planeia subir para 8.3 no próximo ciclo. Acompanha o calendário oficial. - JIT vale a pena?
Para APIs e apps web comuns, o ganho é pequeno. Para tarefas CPU-bound (processamento numérico), pode valer testar com um micro-benchmark. - PDO ou MySQLi?
PDO. Interface consistente, prepared statements em todos os drivers e flexibilidade se mudares de SGBD. - Framework ou microframework ou "vanilla"?
Depende do teu contexto. Para CRUDs e APIs REST, Laravel ou Symfony aceleram. Micro frameworks (Slim) servem apps pequenas. Vanilla é ok para serviços focados, desde que mantenhas disciplina de organização. - Preciso de DI container?
Não no início. Injeção manual cobre 80% dos casos. Introduz um container quando a aplicação crescer e o wiring ficar repetitivo.
Troubleshooting rápido
- "Headers already sent": há saída antes de
header()
ousetcookie()
. Verifica BOM/whitespace e incluições. Solução: remove espaços fora de<?php ?>
e usaob_start()
apenas se necessário. - "Allowed memory size exhausted": algum loop/array gigante. Usa generators, paginação, ou aumenta
memory_limit
temporariamente enquanto resolves a causa. - Conflitos de Composer: executa
composer why-not pacote versao
. Ajusta constraints nocomposer.json
e tenta um upgrade menor primeiro. - Timezone estranho: define
date.timezone
nophp.ini
oudate_default_timezone_set('Europe/Lisbon')
no bootstrap. - Unicode/acentos a falhar: garante UTF-8 em tudo (conexão DB com
charset=utf8mb4
, cabeçalhos HTTP e ficheiros guardados em UTF-8 sem BOM). - CSRF a bloquear formulário legítimo: confirma persistência de sessão, rotação de sessão no login e que o token está no POST certo.
Decisões sólidas tendem a ser simples: tipos, PSR, exceptions, testes pragmáticos, PDO preparado, OPcache e cache de aplicação. Foca nisto antes de mexer em micro-otimizações. Quando precisares de mais, mede, escolhe a ferramenta certa e segue em frente.