// ESTUDO · PHP 8.4 MODERNO · OPEN SOURCE

PhpModernGuide

Guia prático de PHP moderno (8.4) pra quem vem de .NET/Node: uma Task API real construída sobre Slim 4 + PHP-DI — de propósito didático, pra ver roteamento, DI e middleware sem framework escondendo a fiação — com PSR-4/7/11/15, tipagem estrita, readonly, enums, PHPStan nível 8 e testes Pest 3.

OPEN SOURCE PHP 8.4SLIM 4 / PHP-DIPSR-7/11/15PHPSTAN NÍVEL 8PEST 3

Motivação do estudo

O objetivo do PhpModernGuide é desmontar um preconceito comum em quem vem de .NET ou Node: a ideia de que PHP ainda é require_once em cascata, array associativo sem forma garantida e HTML misturado com lógica de negócio. Esse PHP existiu, mas não é o PHP de 2026 — o ecossistema profissional evoluiu para tipagem estática levada a sério, um gerenciador de pacotes à altura do NuGet/npm, padrões formais de interoperabilidade (PSR), containers de DI de verdade e análise estática comparável a um tsc --strict.

Em vez de só descrever isso em prosa, o repositório constrói uma Task API completa e funcional que aplica cada peça de verdade: Composer com autoload PSR-4, mensagens HTTP PSR-7 imutáveis, middleware PSR-15, injeção de dependência via container PSR-11, declare(strict_types=1) em todo arquivo, readonly, enum nativo com máquina de estados, PHPStan no nível mais alto do modo clássico e testes Pest cobrindo domínio e integração.

O que foi feito

  1. Task API real: CRUD completo (GET/POST /tasks, GET/PATCH/DELETE /tasks/{id}, GET /health) sobre PostgreSQL, com arquitetura em camadas — Domain / Application / Infrastructure / Http — uma versão enxuta de arquitetura hexagonal onde o domínio não sabe que PostgreSQL, PDO ou HTTP existem; ele só conhece TaskRepositoryInterface.
  2. Slim 4 + PHP-DI escolhidos de propósito didático: o roteamento, o container de DI e o middleware de erro são montados manualmente em bootstrap.php, sem nenhum deles escondido atrás de convenção de framework — ótimo pra aprender a mecânica, mas não é a recomendação para começar um projeto PHP real do zero. O próprio README é explícito: para produto de verdade a recomendação é Laravel (ou Symfony para times que querem controle arquitetural total), justamente porque eles resolvem por você o que aqui é montado à mão para ficar visível.
  3. Domínio com invariantes protegidas: o agregado Task só muta via métodos que revelam intenção (rename, changeStatus), nunca por setter público, e o enum TaskStatus carrega sua própria máquina de estados (canTransitionTo) — uma transição inválida (done → pending) é rejeitada com 422 antes de qualquer SQL rodar.
  4. DI real via PSR-11: dependencies.php é o único lugar do código que conhece a implementação concreta (PdoTaskRepository); todo o resto — controller, testes — depende só da interface TaskRepositoryInterface, o que permite trocar a implementação sem tocar em nenhuma outra classe.
  5. PHPStan nível 8 limpo: análise estática pega o que o “compilador” do PHP não pega em tempo de execução. Um exemplo real documentado no README: PDO::query() retorna PDOStatement|false — o PHPStan recusou até o false ser tratado explicitamente em PdoTaskRepository::findAll().
  6. 20 testes Pest passando: testes de unidade puros de domínio (Task, TaskStatus, sem I/O) e testes de feature que sobem a aplicação Slim real (o mesmo buildApp() usado em produção) e disparam requisições PSR-7 de verdade através de toda a pilha — roteamento → parsing de body → controller → repositório PDO → SQL → PostgreSQL de teste.
  7. Migrations com Phinx: schema versionado e reversível (composer migrate / migrate:rollback), independente de framework — o equivalente PHP de uma migration do Entity Framework ou do Knex/Prisma.

Como funciona

Visão geral do sistema

flowchart TB
    Client["🌐 Cliente HTTP\ncurl / navegador"]

    subgraph SlimApp["Slim 4 App — public/index.php → bootstrap.php"]
        PSR7["Mensagens PSR-7\nServerRequestInterface / ResponseInterface\nimutáveis"]
        BodyMW["addBodyParsingMiddleware()\nPSR-15"]
        RouteMW["addRoutingMiddleware()\nPSR-15"]
        ErrorMW["addErrorMiddleware()\nPSR-15"]
        Router["Router Slim\nroutes.php — /health, /tasks/*"]
    end

    Container[("Container PHP-DI\nPSR-11 — dependencies.php")]
    Controller["TaskController\nHttp/Controller — adapter HTTP fino"]

    subgraph Domain["Domínio — não conhece HTTP nem PostgreSQL"]
        Task["Task (aggregate)\nrename · changeStatus · invariantes"]
        TaskStatus["TaskStatus (enum)\nmáquina de estados"]
        RepoIface["TaskRepositoryInterface\n(porta)"]
    end

    Repo["PdoTaskRepository\n(adapter — Infrastructure)"]
    PG[("PostgreSQL\ntabela tasks")]
    Phinx["Phinx\nmigrations versionadas"]

    Client -->|HTTP request| PSR7
    PSR7 --> BodyMW --> RouteMW --> ErrorMW --> Router
    Router -->|resolve rota| Container
    Container -->|injeta TaskRepositoryInterface| Controller
    Controller --> Task
    Controller -->|via porta| RepoIface
    RepoIface -.implementada por.-> Repo
    Task --> TaskStatus
    Repo -->|SQL puro, prepared stmt| PG
    Phinx -->|aplica schema versionado| PG
    Controller -->|Response PSR-7| Client

Fluxo de request real: criar uma tarefa (POST /tasks)

sequenceDiagram
    actor C as Cliente (curl)
    participant App as Slim App
    participant MW as Middleware PSR-15\n(body parsing → routing → error)
    participant Ctrl as TaskController
    participant Task as Task (domínio)
    participant Repo as PdoTaskRepository
    participant DB as PostgreSQL

    C->>App: POST /tasks {"title": "...", "description": "..."}
    App->>MW: addBodyParsingMiddleware()
    MW->>MW: JSON → $request->getParsedBody()
    MW->>MW: addRoutingMiddleware() resolve /tasks → TaskController::create
    App->>Ctrl: create(Request, Response) via container PHP-DI

    Ctrl->>Ctrl: CreateTaskInput::fromArray($body)
    Ctrl->>Task: Task::create(title, description)
    alt título vazio
        Task-->>Ctrl: throw DomainException
        Ctrl-->>C: 422 {"error": "Task title cannot be empty."}
    else válido
        Task-->>Ctrl: Task (status=Pending, id=uuid gerado)
        Ctrl->>Repo: save(task)  — via TaskRepositoryInterface
        Repo->>DB: INSERT ... ON CONFLICT (id) DO UPDATE (upsert)
        DB-->>Repo: ok
        Repo-->>Ctrl: void
        Ctrl->>Ctrl: TaskData::fromTask(task)->toArray()
        Ctrl-->>C: 201 Created + JSON da tarefa
    end

Modelo de dados

erDiagram
    TASKS {
        uuid id PK
        string title
        string description "nullable"
        string status "pending | in_progress | done"
        timestamptz created_at
        timestamptz updated_at
    }

Schema deliberadamente simples — o ponto de estudo não é modelagem complexa, é a fiação em volta dela: como o id (UUID gerado por TaskId::generate()) nasce no domínio, não no banco, e como status persiste como string mas trafega como enum tipado (TaskStatus) em todo o código PHP.

Stack

  • ◆ BACKEND: PHP 8.4, Slim 4 (PSR-7/15), PHP-DI 7 (PSR-11), ramsey/uuid, vlucas/phpdotenv, Monolog.
  • ▤ PERSISTÊNCIA: PostgreSQL via PDO puro (sem ORM — SQL explícito, prepared statements), Phinx pra migrations.
  • ✓ QUALIDADE: PHPStan nível 8 (análise estática), Pest 3 (20 testes: unit de domínio + feature via app Slim real).
  • ⌂ INFRA: Docker Compose (API + PostgreSQL).

Como rodar

# 1. Subir os containers (API + PostgreSQL)
docker compose up -d --build

# 2. Instalar dependências PHP dentro do container
docker compose exec api composer install

# 3. Aplicar as migrations Phinx no banco de desenvolvimento
docker compose exec api php vendor/bin/phinx migrate -c migrations.php

# API em http://localhost:8098
# PostgreSQL em localhost:55432 (postgres/postgres, banco php_modern_guide)

# Análise estática (PHPStan nível 8)
docker compose exec api composer analyse

# Suíte de testes (Pest — precisa do banco de teste)
docker compose exec db psql -U postgres -c "CREATE DATABASE php_modern_guide_test;"
docker compose exec -e DB_NAME=php_modern_guide_test api php vendor/bin/phinx migrate -c migrations.php
docker compose exec -e DB_NAME=php_modern_guide_test api composer test

# Código aberto

O PhpModernGuide é open source, feito pra estudar e servir de referência de PHP moderno.