valb-mig / teller
Scanner de docblocks @teller que mapeia regra de negocio documentada em PHP num call graph navegavel.
Requires
- php: ^8.2
- nikic/php-parser: ^5.0
- phpdocumentor/reflection-docblock: ^6.0
Requires (Dev)
- laravel/pint: ^1.29
README
Scanner de docblocks @teller que mapeia regra de negócio documentada em PHP num call graph
navegável — visual (cards + timeline + setas) e em texto (narrativa "Se X: chama Y — senão
chama Z", pronta pra colar numa conversa com IA).
Baseado em call graph real (via nikic/php-parser), não em estrutura estática tipo UML — segue quem chama quem de verdade, incluindo branches condicionais.
Por que
Comentário de negócio junto do código sempre existiu. O que falta é uma forma de navegar
essa documentação seguindo o fluxo real de execução, em vez de abrir arquivo por arquivo. O
Teller lê a tag @teller (curadoria deliberada — não documenta tudo, só o que importa),
monta o grafo de chamadas de verdade a partir da AST, e apresenta os dois num viewer só.
Instalação
composer require --dev valb-mig/teller
Isso instala vendor/bin/teller e deixa vendor/valb-mig/teller/public/ pronto pro viewer.
A tag @teller
Segue a convenção padrão de PHPDoc: primeira linha = título (summary), resto = descrição
longa em markdown (parseado via phpdocumentor/reflection-docblock, o mesmo parser que
o próprio phpDocumentor usa por baixo).
/** * @teller Calcula multa de atraso * * Aplica multa fixa de 2% quando o pagamento passa de 5 dias úteis do vencimento — * regra definida com o setor financeiro. * * - Sem atraso: valor original, sem alteração * - Com atraso: `valor * 1.02` */ public function calcularMulta(float $valor, int $diasAtraso): float { ... }
Funciona em docblock de classe/interface e de método. Também funciona solto, colado em qualquer statement de primeiro nível do corpo de um método (não desce em bloco aninhado) — útil pra documentar uma regra no meio do fluxo sem precisar extrair um método só pra isso:
public function executar(...): string { /** * @teller Regra de atraso * * Acima de 90 dias, o boleto some pra negativação em vez de seguir o fluxo normal. */ if ($diasAtraso > 90) { // ... } }
A tag é obrigatória — sem ela, o Teller não mostra o método/comentário, de propósito: sem
isso todo docblock técnico (@param, IDE helper, etc) viraria nó no grafo e a curadoria (o
diferencial da ferramenta) se perde.
Quando usar (e quando não usar)
Tagueia quando pelo menos um destes for verdade:
- Regra de negócio de verdade — decisão que veio de discussão de produto/jurídico/ comercial, não só "como" o código funciona tecnicamente.
- Sintaxe complexa demais pra intenção ficar óbvia — quem ler o código não vai entender o porquê só olhando a implementação.
- Regra extensa — vários passos/condições que só fazem sentido juntos, difícil de guardar na cabeça lendo linha por linha.
- Nomenclatura não clara — nome de variável/método/classe não deixa o significado óbvio (termo específico do domínio, abreviação, nome legado).
Não tagueia: getter/setter, CRUD simples, método técnico óbvio (formatação, cast, log), qualquer coisa auto-explicativa pelo nome.
Uso
"Arquivo" é qualquer classe — usecase, service, repository, o que for. Nenhum tratamento especial por tipo, qualquer classe serve de ponto de partida.
# a partir de um arquivo (busca por nome, segue classes referenciadas via PSR-4) vendor/bin/teller scan --file=CalculadoraJuros # a partir de uma classe (FQCN) vendor/bin/teller scan --class=App\\Services\\CalculadoraJuros # codebase inteira sob um path (recomendado pra usar o command palette do viewer) vendor/bin/teller scan --all --path=app # limpar o banco local (storage/data.sqlite, nunca precisa ser commitado) vendor/bin/teller clear # exportar relatório HTML standalone do subgrafo de um arquivo vendor/bin/teller export --file=CalculadoraJuros --out=relatorio.html
Por padrão a raiz do projeto escaneado é o diretório de onde você roda o comando — use
--project-root=/caminho se precisar rodar de outro lugar.
Viewer
O viewer é estático — sem API, o browser lê o .sqlite direto via
sql.js (WebAssembly). Qualquer servidor HTTP simples serve:
vendor/bin/teller scan --all --path=app php -S localhost:8090 -t vendor/valb-mig/teller/public
Ou via Docker, se preferir não instalar PHP local pra isso:
docker run --rm -p 8090:8090 \ -v "$(pwd)/vendor/valb-mig/teller/public:/var/www/html:ro" \ -v "$(pwd)/vendor/valb-mig/teller/storage/data.sqlite:/var/www/html/data.sqlite:ro" \ php:8.3-cli-alpine php -S 0.0.0.0:8090 -t /var/www/html
Abre em http://localhost:8090. Depois de rodar scan de novo (ou clear + scan),
recarregue a página — o browser relê o arquivo a cada load.
Tela limpa, sem toolbar fixa: Ctrl/Cmd + K abre o command palette pra buscar
arquivo/classe por nome, título @teller ou FQCN, ranqueado por quantas referências @teller
tem na árvore de chamada — escolher um já carrega o trace (BFS client-side, sem bater no
banco de novo).
Cada arquivo vira um card (nome do arquivo + título/descrição da classe — clicar no
header abre no VSCode). Dentro, uma timeline vertical lista métodos e comentários soltos
na ordem em que aparecem no arquivo: métodos mágicos (__construct, __invoke) em verde,
métodos comuns em cinza, comentário solto em triângulo amarelo. Documentados aparecem
destacados (título+descrição, cyan), sem tag aparecem apagados (só o nome). Cada item tem
dropdown pra colapsar e abre um modal com mais detalhe (namespace, linha, fqcn, botão
abrir no VSCode) ao clicar.
Cards ficam em colunas por profundidade (BFS a partir do arquivo escolhido — quem ele chama fica na coluna seguinte), com setas conectando o método/comentário exato que chama ao método exato chamado do outro lado, numeradas na ordem real do código-fonte. Canto inferior direito: zoom in/out/fit + exportar PNG (captura tudo via html2canvas).
Chamadas alternativas (if/elseif/else no código) convergem num losango de decisão
entre origem e destinos — a origem aponta só pro losango (rótulo = número do passo, ex. "4"),
o losango se abre pra cada braço (rótulo = letra, "a"/"b"). Hover na origem acende todos os
caminhos; hover num destino acende só o caminho dele.
Sidebar esquerda (clique no › no canto pra abrir/fechar) narra o trace inteiro em texto,
na mesma ordem/numeração das setas, traduzindo estrutura de controle pra português ("Se
X: chama Y — senão chama Z"). Botão Copiar pro Claude copia essa narrativa pra área de
transferência — cola numa conversa com IA pra pedir revisão, dúvida sobre a regra, etc.
Limitações da v1
- Chamada via dependência injetada resolve pro tipo declarado (constructor promoted property ou propriedade tipada), não pelo container real — se a interface tiver várias implementações, o grafo não sabe qual roda em runtime.
- Chamadas a
parent::não são seguidas. - Comentário
@tellersolto só é achado em statements de primeiro nível do corpo do método — não desce em bloco aninhado (se dentro de outro if, por exemplo). - Narrativa da sidebar só entende
if/elseif/else—try/catch,switch, loop não viram texto especial (aparecem como chamada linear normal). - "Abrir no código" usa o esquema
vscode://file/— preso ao VSCode. - Só resolve classes dentro dos diretórios PSR-4 do próprio projeto; chamadas pra vendor/framework não viram edge.
Licença
MIT — veja LICENSE.