PHP

Namespaces, Autoloading, PSR-4 e Composer Já leu

15 min de leitura

Namespaces, Autoloading, PSR-4 e Composer
Até agora todos os exemplos couberam em um único arquivo. Na prática, um projeto real tem dezenas ou centenas de classes — e precisam

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 como DateTime, InvalidArgumentException e stdClass vivem no namespace global (sem prefixo). Dentro de um arquivo com namespace, você precisa ou importá-las com use ou prefixá-las com \ — por exemplo \DateTime. O declare(strict_types=1) deve vir antes do namespace.

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\ClassName mapeia para vendor_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
O composer.lock registra as versões exatas instaladas. Quando outro desenvolvedor (ou o servidor de deploy) roda composer install, ele recebe exatamente as mesmas versões — não as mais recentes compatíveis com a constraint. Sempre commite o composer.lock. Só rode composer update quando 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

  1. Crie um projeto do zero com composer init. Configure o autoload PSR-4 mapeando o namespace Loja\ para a pasta src/. Crie as classes Loja\Models\Produto, Loja\Models\Categoria e Loja\Services\CatalogoService com namespace correto. Teste rodando php public/index.php.
  2. Instale o pacote vlucas/phpdotenv com Composer. Crie um arquivo .env com variáveis de configuração (APP_NAME, DB_HOST, DB_PORT). No public/index.php, carregue as variáveis e exiba-as com $_ENV ou getenv().
  3. Crie um namespace Loja\Exceptions com uma hierarquia de exceções: LojaException (base), ProdutoNaoEncontradoException e EstoqueInsuficienteException — cada uma com propriedades relevantes e mensagem descritiva. Use-as em CatalogoService.
  4. Instale o phpunit/phpunit como dependência de desenvolvimento. Configure o autoload-dev no composer.json para o namespace Loja\Tests\ apontando para tests/. Escreva um teste básico para CatalogoService.
  5. Desafio: crie um mini-framework de rotas com namespace próprio. Interface MeuApp\Routing\RotaInterface com método executar(array $params): void, classe Roteador que registra rotas e despacha com base na URI, e ao menos dois controllers (HomeController, ApiController). Tudo carregado via autoloader do Composer, zero require manuais além do vendor/autoload.php.

Referências e leituras para aprofundar

Comentários

Mais em PHP

O que é PHP e por que ele ainda importa
O que é PHP e por que ele ainda importa

Quando voc&ecirc; acessa um site, preenche um formul&aacute;rio, faz login em...

Arrays em Profundidade
Arrays em Profundidade

Arrays s&atilde;o a estrutura de dados mais usada no PHP. Praticamente tudo q...

Estruturas de Controle
Estruturas de Controle

Um programa que executa sempre as mesmas instru&ccedil;&otilde;es na mesma or...