Search by

canhao43 / protheus-rest

Canhao43

Cliente PHP para a API REST do TOTVS Protheus: login OAuth2 com renovação de token, paginação e erros no padrão de API TOTVS.

Package info

github.com/Canhao43/protheus-rest-php

pkg:composer/canhao43/protheus-rest

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-11-13 03:29 UTC

This package is auto-updated.

Last update: 2026-11-13 03:34:40 UTC


README

CI Packagist

Cliente PHP para a API REST do TOTVS Protheus: faz o login OAuth2, renova o token sozinho (e pode guardá-lo entre processos), percorre coleções página por página, tenta de novo quando a leitura falha de um jeito passageiro e transforma os erros no formato de API da TOTVS em exceções que dá para tratar.

Construído com agentes de IA (Claude Code) e revisado por mim em cada PR, só a partir da documentação pública da TOTVS. As regras que os agentes seguem estão em CLAUDE.md.

Instalação

composer require canhao43/protheus-rest

PHP 8.2 ou mais novo. A versão é 0.x: tudo foi testado com respostas no formato da documentação, mas ainda não contra um Protheus de verdade, e a API pode mudar entre versões menores (CHANGELOG).

Uso

use Canhao43\ProtheusRest\ClienteProtheus;
use Canhao43\ProtheusRest\Credenciais;

$protheus = new ClienteProtheus(
    'https://erp.exemplo.com.br:8080/rest',
    new Credenciais(getenv('PROTHEUS_USUARIO'), getenv('PROTHEUS_SENHA')),
);

$recurso = $protheus->get('api/exemplo/v1/recursos/1');
$criado = $protheus->post('api/exemplo/v1/recursos', ['nome' => 'Exemplo']);
$protheus->delete('api/exemplo/v1/recursos/1');

Os caminhos são relativos à URL base do REST. get, post, put, patch e delete devolvem o JSON da resposta como array.

O login acontece na primeira chamada. Depois disso, o pacote:

  • manda Authorization: Bearer em cada chamada;
  • renova o token um minuto antes de vencer, pelo refresh_token, e só volta a usar a senha se o refresh for recusado;
  • se uma chamada voltar 401, renova e repete uma vez;
  • se uma leitura (GET) falhar de um jeito passageiro, espera e tenta de novo (abaixo).

As regras, com o motivo de cada uma, estão em docs/decisoes.

Token entre processos

Em PHP, cada requisição web e cada job da fila é um processo novo, e cada um faria o próprio login. Com um cache PSR-16, o token vale para todos:

$protheus = new ClienteProtheus(
    'https://erp.exemplo.com.br:8080/rest',
    new Credenciais(getenv('PROTHEUS_USUARIO'), getenv('PROTHEUS_SENHA')),
    cache: $cache, // no Laravel: app('cache.store'); no Symfony: new Psr16Cache($pool)
);
  • Uma entrada por servidor e usuário. A senha nunca vai para o cache.
  • O cache guarda tokens: quem lê o cache entra no Protheus com esse usuário até o token vencer. Use um cache que só a aplicação lê.
  • Se o cache cair, as chamadas seguem; só volta a haver um login por processo.

No Laravel

Um cliente só para a aplicação toda, com o cache do próprio Laravel:

// config/services.php
'protheus' => [
    'url' => env('PROTHEUS_URL'),
    'usuario' => env('PROTHEUS_USUARIO'),
    'senha' => env('PROTHEUS_SENHA'),
],

// app/Providers/AppServiceProvider.php, no register()
$this->app->singleton(ClienteProtheus::class, fn ($app) => new ClienteProtheus(
    config('services.protheus.url'),
    new Credenciais(config('services.protheus.usuario'), config('services.protheus.senha')),
    cache: $app->make('cache.store'),
));

Depois, é pedir ClienteProtheus no construtor de um controller, job ou comando.

Nova tentativa

Uma leitura (GET) que fica sem resposta ou volta 429, 502, 503 ou 504 é repetida, com espera que dobra a cada vez e um sorteio entre a metade e o teto. Se o servidor mandar Retry-After, vale o que ele pediu, até a espera máxima.

use Canhao43\ProtheusRest\Http\Repeticao;

new ClienteProtheus($url, $credenciais, repeticao: new Repeticao(tentativas: 5, esperaInicialMs: 200, esperaMaximaMs: 10_000));
new ClienteProtheus($url, $credenciais, repeticao: Repeticao::desligada());
  • Padrão: 3 tentativas no total, de 0,5 s a 5 s de espera.
  • POST, PUT, PATCH e DELETE nunca são repetidos: uma escrita que caiu no meio pode ter sido gravada, e repetir duplicaria no ERP. A decisão fica com quem chama.
  • 500 não é repetido: é erro do servidor, e repetir dá o mesmo erro.

Coleções

Endpoints de coleção no padrão TOTVS recebem page e pageSize e respondem {"hasNext": ..., "items": [...]}.

use Canhao43\ProtheusRest\Colecoes\Consulta;

$consulta = new Consulta(
    filtros: ['ativo' => 'true'],
    ordem: ['nome', '-codigo'],      // "-" na frente: decrescente
    campos: ['codigo', 'nome'],      // só estes campos em cada item
    expandir: ['enderecos'],
);

// Todos os itens, uma página por vez: a próxima só é pedida quando a atual acaba.
foreach ($protheus->colecao('api/exemplo/v1/recursos', $consulta, tamanhoDaPagina: 50) as $item) {
    // ...
}

// Uma página só, para tela com "anterior" e "próxima".
$pagina = $protheus->pagina('api/exemplo/v1/recursos', numero: 2, tamanho: 20, consulta: $consulta);
$pagina->items;    // os itens desta página
$pagina->hasNext;  // há próxima?
$pagina->extras;   // o que mais a API mandou, como "total"
  • Sem tamanho, vale o padrão do endpoint.
  • Se o token vencer no meio da leitura, ele é renovado entre uma página e outra.
  • Uma resposta fora do formato, uma página vazia que diz ter próxima ou uma página igual à anterior param a leitura com RespostaInvalida, em vez de deixar o foreach rodar para sempre.
  • Paginação por número não é uma foto: se registros entrarem ou saírem durante a leitura, um item pode pular ou repetir. Para leitura consistente, ordene por uma chave estável.

Erros

Toda exceção estende ErroDoProtheus.

Exceção Quando O que ela traz
ErroDaApi O Protheus respondeu 4xx ou 5xx status e mensagemDeErro (code, message, detailedMessage, helpUrl, details), ou null se o corpo não estiver no padrão
ErroDeAutenticacao O login foi recusado O mesmo que ErroDaApi, de quem ela é filha
ErroDeConexao Não houve resposta: rede, DNS, certificado, tempo esgotado O motivo e o endereço, sem a query
RespostaInvalida Sucesso, mas o corpo não é um objeto JSON A mensagem
use Canhao43\ProtheusRest\Erros\ErroDaApi;

try {
    $protheus->get('api/exemplo/v1/recursos/42');
} catch (ErroDaApi $erro) {
    if ($erro->status === 404) {
        // $erro->mensagemDeErro?->message é o texto para o usuário, no padrão TOTVS
    }
}

Segurança

  • Usuário e senha vão em cabeçalho, só no login, como o TDN define. Nunca na URL.
  • Senha e tokens não aparecem em var_dump, print_r nem em mensagem de exceção. A mensagem também não leva a query da chamada.
  • Use https. O TDN recomenda fortemente para o endpoint de token, porque a senha passa por ele.

O que ainda não cobre

  • Requisição assíncrona (202 com Location), HATEOAS e filtro OData montado por objeto: o guia descreve, mas cada endpoint decide se usa. O $filter OData passa como filtro simples: filtros: ['$filter' => "..."].
  • MFA: o campo hasMFA do login é lido e fica em Token, mas o fluxo de MFA não está na página pública do token.
  • Basic Auth: existe no Protheus, mas o TDN o descreve como menos seguro que o token.

Fontes

Tudo o que o pacote supõe sobre o Protheus vem destas páginas públicas:

Os testes não falam com um Protheus de verdade: usam respostas gravadas no formato dessas páginas, com dados fictícios (tests/respostas).

Roteiro

  • M1 — Autenticação e erros: login OAuth2, renovação, 401, erros no padrão TOTVS.
  • M2 — Coleções: percorrer páginas (page, pageSize, hasNext, items), filtro, ordenação, campos e expansão.
  • M3 — Resiliência: nova tentativa com espera em falha passageira de leitura, token em cache entre processos.
  • M4 — Publicação: versão 0.1.0 no Packagist, CHANGELOG, exemplo para Laravel e o pacote distribuído conferido no CI.

O que vem depois: uso relatado contra um Protheus de verdade, para chegar ao 1.0.

Desenvolvimento

composer install
vendor/bin/phpunit
vendor/bin/pint --test

O CI roda os dois em PHP 8.2, 8.3 e 8.4, e confere o composer.json e o que vai no pacote distribuído.

Licença

MIT