Até agora todos os exemplos couberam em um único arquivo. Na prática, um projeto real tem dezenas ou centenas de classes — e precisamos de uma forma de organizá-las em pastas, evitar conflitos de nomes e carregá-las automaticamente sem precisar escrever centenas de require. Três ferramentas resolvem isso juntas: namespaces, autoloading PSR-4 e Composer.
Este artigo cobre o que todo desenvolvedor PHP precisa saber antes de trabalhar com qualquer framework: como estruturar um projeto, como o PHP sabe onde encontrar uma classe, e como o Composer gerencia as dependências externas.
O problema sem namespaces
Antes dos namespaces (PHP 5.3), todas as classes compartilhavam o mesmo espaço de nomes global. Instalar duas bibliotecas que definiam uma classe Request causava conflito fatal. A solução era prefixar nomes com strings longas — Zend_Http_Client_Request — criando nomes impraticáveis. Namespaces resolvem isso de forma elegante:
<?php
// Problema: duas bibliotecas definem a mesma classe
// require 'vendor/framework-a/Request.php'; // define class Request
// require 'vendor/framework-b/Request.php'; // Fatal: Cannot redeclare class Request
// Com namespaces — cada biblioteca vive no seu próprio "espaço"
// FrameworkA\Http\Request e FrameworkB\Http\Request coexistem sem conflito
Declarando e usando namespaces
A declaração namespace deve ser a primeira instrução do arquivo (antes de qualquer código, incluindo espaços em branco). O separador \ cria sub-namespaces que convencionalmente espelham a estrutura de pastas:
src/Http/Request.php
<?php
declare(strict_types=1);
// namespace deve ser a primeira instrução do arquivo
namespace MeuApp\Http;
class Request
{
private array $dados;
public function __construct()
{
// Captura os dados da requisição HTTP atual
$this->dados = [
"method" => $_SERVER["REQUEST_METHOD"] ?? "GET",
"uri" => $_SERVER["REQUEST_URI"] ?? "/",
"input" => $_POST,
"query" => $_GET,
];
}
public function method(): string { return $this->dados["method"]; }
public function uri(): string { return $this->dados["uri"]; }
public function input(string $k): mixed { return $this->dados["input"][$k] ?? null; }
}
src/Http/Response.php
<?php
declare(strict_types=1);
namespace MeuApp\Http;
class Response
{
private int $status = 200;
private array $headers = [];
private string $corpo = "";
public function status(int $codigo): static
{
$this->status = $codigo;
return $this;
}
public function header(string $nome, string $valor): static
{
$this->headers[] = "{$nome}: {$valor}";
return $this;
}
public function json(array $dados): static
{
$this->header("Content-Type", "application/json");
$this->corpo = json_encode($dados, JSON_UNESCAPED_UNICODE);
return $this;
}
public function enviar(): void
{
http_response_code($this->status);
foreach ($this->headers as $h) header($h);
echo $this->corpo;
}
}
Importando com use
Em vez de escrever o nome completo MeuApp\Http\Request toda vez, usamos use para importar a classe. Isso vale apenas dentro do arquivo atual — não "inclui" o arquivo, apenas cria um alias para o nome completo:
src/Controllers/ProdutoController.php
<?php
declare(strict_types=1);
namespace MeuApp\Controllers;
// use importa a classe pelo nome completo — use depois de namespace
use MeuApp\Http\Request;
use MeuApp\Http\Response;
use MeuApp\Models\Produto;
use InvalidArgumentException; // classes nativas ficam no namespace global
// Agora pode usar só "Request" no lugar de "MeuApp\Http\Request"
class ProdutoController
{
public function listar(Request $req, Response $res): void
{
$produtos = Produto::todos();
$res->json($produtos)->enviar();
}
public function criar(Request $req, Response $res): void
{
$nome = $req->input("nome");
$preco = $req->input("preco");
if (!$nome || !$preco) {
throw new InvalidArgumentException("Nome e preço são obrigatórios.");
}
$produto = new Produto($nome, (float) $preco);
$res->status(201)->json(["id" => $produto->getId()])->enviar();
}
}
// Aliases — quando dois namespaces têm o mesmo nome de classe
// use MeuApp\Http\Request as HttpRequest;
// use OutraLib\Request as OutraRequest;
// Importação em grupo — PHP 7+
// use MeuApp\Http\{Request, Response, Middleware};
⚠️ Namespace global e classes nativas
Classes nativas do PHP comoDateTime,InvalidArgumentExceptionestdClassvivem no namespace global (sem prefixo). Dentro de um arquivo com namespace, você precisa ou importá-las comuseou prefixá-las com\— por exemplo\DateTime. Odeclare(strict_types=1)deve vir antes donamespace.
Estrutura de projeto padrão
A convenção da comunidade PHP para projetos modernos segue esta estrutura. O namespace raiz da aplicação (MeuApp) mapeia diretamente para a pasta src/:
meu-projeto/
├── src/ ← namespace raiz: MeuApp\
│ ├── Controllers/
│ │ └── ProdutoController.php ← MeuApp\Controllers\ProdutoController
│ ├── Models/
│ │ └── Produto.php ← MeuApp\Models\Produto
│ ├── Http/
│ │ ├── Request.php ← MeuApp\Http\Request
│ │ └── Response.php ← MeuApp\Http\Response
│ ├── Services/
│ │ └── PagamentoService.php ← MeuApp\Services\PagamentoService
│ └── Exceptions/
│ └── AppException.php ← MeuApp\Exceptions\AppException
├── tests/ ← namespace: MeuApp\Tests\
├── public/
│ └── index.php ← ponto de entrada da aplicação
├── vendor/ ← gerado pelo Composer — nunca editar
├── composer.json
└── composer.lock
💡 A regra PSR-4
A PSR-4 é uma especificação da PHP-FIG que define como o namespace de uma classe deve corresponder ao caminho do arquivo. Regra:VendorName\SubNamespace\ClassNamemapeia paravendor_dir/SubNamespace/ClassName.php. Um arquivo, uma classe. O nome do arquivo deve ser idêntico (inclusive capitalização) ao nome da classe.
PSR-4 — o mapeamento namespace → pasta
A PSR-4 formaliza a convenção que já usamos nos exemplos. O Composer implementa esse mapeamento automaticamente:
| Namespace completo da classe | Caminho do arquivo |
|---|---|
MeuApp\Http\Request |
src/Http/Request.php |
MeuApp\Controllers\ProdutoController |
src/Controllers/ProdutoController.php |
MeuApp\Models\Produto |
src/Models/Produto.php |
MeuApp\Exceptions\AppException |
src/Exceptions/AppException.php |
MeuApp\Tests\Models\ProdutoTest |
tests/Models/ProdutoTest.php |
Composer — gerenciador de dependências
O Composer é a ferramenta padrão para gerenciar dependências PHP. Ele faz três coisas essenciais: baixa e instala bibliotecas externas, garante compatibilidade de versões entre dependências, e gera o autoloader PSR-4 que carrega automaticamente qualquer classe do projeto.
composer.json — o coração do projeto
O composer.json descreve o projeto e suas dependências. O bloco autoload é o que conecta namespaces a pastas:
{
"name": "meuusuario/meu-projeto",
"description": "Minha aplicação PHP",
"type": "project",
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.0",
"vlucas/phpdotenv": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.0"
},
"autoload": {
"psr-4": {
"MeuApp\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"MeuApp\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"analyse": "phpstan analyse src --level=8"
},
"config": {
"sort-packages": true,
"optimize-autoloader": true
}
}
Os principais comandos do Composer
# Cria interativamente o composer.json de um novo projeto
composer init
# Instala as dependências listadas no composer.lock (ideal para CI/deploy)
composer install
# Adiciona uma dependência nova ao projeto e instala imediatamente
composer require guzzlehttp/guzzle
# Adiciona como dependência de desenvolvimento apenas
composer require --dev phpunit/phpunit
# Atualiza todas as dependências para a versão mais recente compatível
composer update
# Remove uma dependência do projeto
composer remove vlucas/phpdotenv
# Regenera o autoloader — necessário após adicionar classes ou mudar o autoload
composer dump-autoload
# Gera autoloader otimizado (classmap) para produção — mais rápido que PSR-4 puro
composer dump-autoload -o
Como o autoloading funciona
Quando você executa composer install ou composer dump-autoload, o Composer gera o arquivo vendor/autoload.php. Basta incluí-lo uma única vez no ponto de entrada da aplicação — depois disso, qualquer classe com namespace configurado é carregada automaticamente quando usada pela primeira vez:
public/index.php
<?php
declare(strict_types=1);
// Este é o ÚNICO require necessário em toda a aplicação
// O Composer cuida de carregar todas as outras classes automaticamente
require_once __DIR__ . "/../vendor/autoload.php";
use MeuApp\Http\Request;
use MeuApp\Http\Response;
use MeuApp\Controllers\ProdutoController;
// PHP tenta usar MeuApp\Http\Request pela primeira vez →
// autoloader busca src/Http/Request.php (PSR-4) →
// inclui o arquivo automaticamente — nenhum require manual necessário
$request = new Request();
$response = new Response();
$controller = new ProdutoController();
// Roteamento simples baseado na URI
match($request->uri()) {
"/produtos" => $controller->listar($request, $response),
"/produtos/criar" => $controller->criar($request, $response),
default => $response->status(404)->json(["erro" => "Rota não encontrada"])->enviar(),
};
O mecanismo interno do autoloader
Por baixo dos panos, o Composer registra uma função com spl_autoload_register(). Entender esse mecanismo ajuda quando você precisar criar autoloaders customizados ou depurar problemas de carregamento:
<?php
declare(strict_types=1);
// O que o Composer faz internamente — simplificado
// Normalmente você NÃO escreve isso — use composer dump-autoload
spl_autoload_register(function (string $classeCompleta): void {
// Prefixo raiz que este autoloader conhece
$prefixo = "MeuApp\\";
$pastaSrc = __DIR__ . "/src/";
// Ignora classes que não pertencem a este namespace
if (!str_starts_with($classeCompleta, $prefixo)) {
return;
}
// Remove o prefixo, troca \ por /, adiciona .php
$relativo = substr($classeCompleta, strlen($prefixo));
$arquivo = $pastaSrc . str_replace("\\", "/", $relativo) . ".php";
if (file_exists($arquivo)) {
require $arquivo;
}
});
// Passo a passo para "MeuApp\Http\Request":
// 1. str_starts_with → true (começa com "MeuApp\\")
// 2. substr remove "MeuApp\\" → "Http\Request"
// 3. str_replace "\\" por "/" → "Http/Request"
// 4. arquivo final: /caminho/src/Http/Request.php
// 5. file_exists → require o arquivo
Versionamento semântico e constraints
O Composer usa versionamento semântico (MAJOR.MINOR.PATCH) para controlar quais versões de uma dependência são compatíveis com o seu projeto. Entender as constraints evita surpresas ao atualizar dependências:
| Constraint | Significado | Exemplo de versões aceitas |
|---|---|---|
"^7.0" |
Caret — aceita MINOR e PATCH livres | 7.0, 7.1, 7.9 — não aceita 8.0 |
"~7.1" |
Tilde — aceita apenas incrementos de PATCH | 7.1, 7.2 — não aceita 8.0 |
">=7.0 <8.0" |
Intervalo explícito | 7.0 a 7.x — não aceita 8.0 |
"7.1.*" |
Wildcard — fixa MAJOR e MINOR | 7.1.0, 7.1.5 — não aceita 7.2 |
"7.1.3" |
Versão exata — não recomendado | apenas 7.1.3 |
"dev-main" |
Branch de desenvolvimento — instável | commit mais recente da branch main |
✅ composer.lock — sempre commitar no Git
Ocomposer.lockregistra as versões exatas instaladas. Quando outro desenvolvedor (ou o servidor de deploy) rodacomposer install, ele recebe exatamente as mesmas versões — não as mais recentes compatíveis com a constraint. Sempre commite ocomposer.lock. Só rodecomposer updatequando quiser atualizar intencionalmente as dependências e retestar a aplicação.
Projeto prático — biblioteca de domínio com PSR-4
Vamos montar um módulo de pedidos completo para consolidar namespaces, autoloading e boas práticas de organização. Este é o tipo de código que você verá em projetos Laravel, Symfony e sistemas próprios:
src/Pedidos/Exceptions/PedidoException.php
<?php
declare(strict_types=1);
namespace MeuApp\Pedidos\Exceptions;
use RuntimeException;
// Sub-namespace dentro de Pedidos — agrupa tudo relacionado a pedidos
class PedidoException extends RuntimeException {}
class ProdutoEsgotadoException extends PedidoException
{
public function __construct(
public readonly string $nomeProduto,
public readonly int $estoqueAtual,
) {
parent::__construct(
"Produto '{$nomeProduto}' com estoque insuficiente ({$estoqueAtual} disponíveis)."
);
}
}
src/Pedidos/ItemPedido.php
<?php
declare(strict_types=1);
namespace MeuApp\Pedidos;
final class ItemPedido
{
public function __construct(
public readonly string $nome,
public readonly float $precoUnitario,
public readonly int $quantidade,
) {}
public function subtotal(): float
{
return $this->precoUnitario * $this->quantidade;
}
}
src/Pedidos/Pedido.php
<?php
declare(strict_types=1);
namespace MeuApp\Pedidos;
use MeuApp\Pedidos\Exceptions\ProdutoEsgotadoException;
use DateTimeImmutable;
class Pedido
{
private array $itens = [];
private string $status = "rascunho";
private DateTimeImmutable $criadoEm;
public function __construct(
private readonly string $clienteNome,
) {
$this->criadoEm = new DateTimeImmutable();
}
public function adicionarItem(
string $nome,
float $preco,
int $qtd,
int $estoqueDisponivel,
): static {
if ($qtd > $estoqueDisponivel) {
throw new ProdutoEsgotadoException($nome, $estoqueDisponivel);
}
$this->itens[] = new ItemPedido($nome, $preco, $qtd);
return $this;
}
public function confirmar(): static
{
if (empty($this->itens)) {
throw new Exceptions\PedidoException("Pedido sem itens não pode ser confirmado.");
}
$this->status = "confirmado";
return $this;
}
public function total(): float
{
return array_sum(array_map(
fn(ItemPedido $i) => $i->subtotal(),
$this->itens
));
}
public function resumo(): string
{
$total = number_format($this->total(), 2, ",", ".");
return sprintf(
"Pedido de %s | %d item(ns) | Total: R$ %s | Status: %s",
$this->clienteNome,
count($this->itens),
$total,
$this->status
);
}
}
public/index.php — usando o módulo
<?php
declare(strict_types=1);
require_once __DIR__ . "/../vendor/autoload.php";
use MeuApp\Pedidos\Pedido;
use MeuApp\Pedidos\Exceptions\ProdutoEsgotadoException;
try {
$pedido = (new Pedido("Maria Silva"))
->adicionarItem("Teclado", 350.0, 1, 10)
->adicionarItem("Mouse", 180.0, 2, 5)
->confirmar();
echo $pedido->resumo();
// Pedido de Maria Silva | 2 item(ns) | Total: R$ 710,00 | Status: confirmado
} catch (ProdutoEsgotadoException $e) {
echo "Erro: " . $e->getMessage();
echo "Produto: " . $e->nomeProduto;
}
Boas práticas de organização
Um arquivo, uma classe, um namespace. Esta é a regra fundamental da PSR-1. Nunca declare múltiplas classes em um mesmo arquivo. O nome do arquivo deve ser exatamente igual ao nome da classe — capitalização incluída. ProdutoController.php, não produtocontroller.php.
Namespace espelha a estrutura de pastas. MeuApp\Services\PagamentoService deve estar em src/Services/PagamentoService.php. Isso não é apenas convenção — é o que permite o autoloader encontrar o arquivo. Desviar disso significa escrever configuração extra no composer.json.
Nunca edite a pasta vendor/. Tudo dentro de vendor/ é gerado e regenerado pelo Composer. Adicione vendor/ ao .gitignore e documente no README que é necessário rodar composer install após clonar o repositório.
Prefira composer install a composer update em produção. O install respeita o composer.lock e garante que você está instalando as versões testadas. O update atualiza para as versões mais recentes dentro das constraints e deve ser feito com intenção e retestado antes de ir para produção.
Resumo do artigo
| Conceito | O que aprendemos |
|---|---|
namespace |
Primeira instrução do arquivo — evita conflitos de nome entre classes |
use |
Importa classe pelo nome completo — cria alias local no arquivo |
use ... as |
Importa com alias — resolve conflito de dois nomes iguais |
use A\{B, C} |
Importação em grupo — PHP 7+ |
| PSR-4 | Convenção que mapeia namespace → pasta/arquivo |
composer.json |
Define dependências e mapeamento PSR-4 do projeto |
composer install |
Instala dependências conforme o composer.lock |
composer require |
Adiciona nova dependência |
composer dump-autoload |
Regenera o autoloader após mudanças |
vendor/autoload.php |
Único require necessário — carrega tudo automaticamente |
composer.lock |
Versões exatas instaladas — sempre commitar no Git |
| Versionamento semântico | ^ aceita MINOR livre; ~ aceita só PATCH |
🏠 Exercício da semana
- Crie um projeto do zero com
composer init. Configure o autoload PSR-4 mapeando o namespaceLoja\para a pastasrc/. Crie as classesLoja\Models\Produto,Loja\Models\CategoriaeLoja\Services\CatalogoServicecom namespace correto. Teste rodandophp public/index.php. - Instale o pacote
vlucas/phpdotenvcom Composer. Crie um arquivo.envcom variáveis de configuração (APP_NAME,DB_HOST,DB_PORT). Nopublic/index.php, carregue as variáveis e exiba-as com$_ENVougetenv(). - Crie um namespace
Loja\Exceptionscom uma hierarquia de exceções:LojaException(base),ProdutoNaoEncontradoExceptioneEstoqueInsuficienteException— cada uma com propriedades relevantes e mensagem descritiva. Use-as emCatalogoService. - Instale o
phpunit/phpunitcomo dependência de desenvolvimento. Configure oautoload-devnocomposer.jsonpara o namespaceLoja\Tests\apontando paratests/. Escreva um teste básico paraCatalogoService. - Desafio: crie um mini-framework de rotas com namespace próprio. Interface
MeuApp\Routing\RotaInterfacecom métodoexecutar(array $params): void, classeRoteadorque registra rotas e despacha com base na URI, e ao menos dois controllers (HomeController,ApiController). Tudo carregado via autoloader do Composer, zerorequiremanuais além dovendor/autoload.php.
Referências e leituras para aprofundar
- Namespaces — Manual oficial do PHP — Documentação completa de namespaces, resolução de nomes, importação com
usee namespaces aninhados. - PSR-4: Autoloader — PHP-FIG — A especificação oficial da PSR-4. Documento curto e preciso que descreve a regra de mapeamento namespace → arquivo com exemplos.
- PSR-1: Basic Coding Standard — PHP-FIG — Padrão básico que define: um arquivo uma classe, capitalização de nomes, uso de
declare(strict_types=1)e outras convenções fundamentais. - Composer — Documentação oficial — Documentação completa do Composer: instalação, comandos,
composer.json, estratégias de versionamento, scripts e otimização para produção. - Packagist — repositório central de pacotes PHP — O repositório oficial de pacotes PHP. Onde o Composer busca dependências por padrão — qualquer pacote com
composer require nome/pacotevem daqui. - PHP: The Right Way — Namespaces — Visão geral de boas práticas com namespaces, autoloading e uso de Composer no contexto do desenvolvimento PHP moderno.
- Semantic Versioning — semver.org — A especificação de versionamento semântico em português. Explica a semântica de MAJOR.MINOR.PATCH e quando cada número deve ser incrementado.