Plugin SDK
Toda funcionalidade nova entra como plugin. O núcleo dispara hooks; o plugin se conecta a eles. Nenhum plugin precisa (nem deve) alterar arquivos do núcleo.
Anatomia
plugins/meu-plugin/
plugin.json manifesto
Plugin.php classe que implementa PluginInterface
src/ classes extras (autoload: Plugins\MeuPlugin\Sub\Classe)
views/ templates próprios
plugin.json
{
"name": "Meu Plugin",
"version": "1.2.0",
"description": "O que ele faz, em uma frase.",
"author": "Seu Nome",
"entry": "Plugin.php",
"class": "Plugins\\MeuPlugin\\Plugin",
"priority": 50,
"permissions": ["hooks", "routes", "gateways", "alerts", "ai", "queue", "database"],
"requires": {
"php": "8.1",
"core": "1.0.0",
"extensions": ["curl"],
"plugins": { "outro-plugin": ">=2.0" }
},
"default_config": { "api_key": "" }
}
Permissões são obrigatórias: usar um recurso sem declará-lo lança exceção.
| Permissão | Libera |
|---|---|
hooks | on(), filter() |
routes | route(), apiRoute() |
gateways | gateway() |
alerts | alertChannel() |
ai | aiTool(), aiContext() |
queue | job() |
database | acesso direto ao banco |
Plugin.php
<?php
namespace Plugins\MeuPlugin;
use App\Core\Plugin\PluginInterface;
use App\Core\Plugin\Sdk;
class Plugin implements PluginInterface
{
public function boot(Sdk $sdk): void { /* roda a cada requisição */ }
public function install(Sdk $sdk): void { /* uma vez, na instalação */ }
public function upgrade(Sdk $sdk, string $fromVersion): void { /* ao subir versão */ }
public function uninstall(Sdk $sdk): void { /* limpeza */ }
}
O upgrade() roda sozinho quando a versão do plugin.json for maior que a instalada. boot() precisa ser barato — só registra; não faça I/O pesado.
Referência do SDK
Eventos e filtros
$sdk->on('order.approved', fn(array $order) => /* … */);
$sdk->filter('metrics.derive', fn(array $m) => $m + ['minha_metrica' => 1]);
$sdk->emit('meu.evento', $dados);
Eventos disponíveis
| Evento | Quando | Recebe |
|---|---|---|
app.booted | Fim do boot | Kernel |
tracking.event (filtro) | Antes de enfileirar a visita | array $event |
tracking.ingested | Visita gravada | array $event |
tracking.geo (filtro) | Resolução geográfica | array $geo, string $ip |
order.before_save (filtro) | Antes de gravar o pedido | array $data |
order.attribution (filtro) | Após resolver a atribuição | array $attr |
order.created | Pedido novo | array $order |
order.status_changed | Mudou de status | array $order, ?string $anterior |
order.approved / .refused / .refunded / .chargeback | Por status | array $order |
order.reattributed | Atribuição corrigida | array $order, array $attr |
alert.before_dispatch (filtro) | Antes de enviar (devolva null para cancelar) | array $data |
alert.dispatched | Alerta criado | int $id, int $projectId, string $type |
anomaly.detected | Anomalia encontrada | int $id, int $projectId, string $kind |
anomaly.signals (filtro) | Sinais coletados | array $signals |
report.generated | Relatório pronto | int $id, int $projectId |
ai.answered | Assistente respondeu | int $projectId, string $q, array $result |
plugin.installed / .enabled / .disabled / .uninstalled | Ciclo de vida | string $slug |
Filtros de interface
| Filtro | Modifica |
|---|---|
admin.menu | Itens do menu lateral |
dashboard.widgets | Blocos do dashboard |
ranking.rows / ranking.highlights | Linhas e destaques do ranking |
metrics.derive | Métricas calculadas |
ai.tool_definitions / ai.tool_result / ai.system_context | Comportamento da IA |
gateways.all / alerts.channels / pixels.available | Registros disponíveis |
cron.tasks | Tarefas agendadas |
http.response | Resposta HTTP final |
Rotas e interface
$sdk->route('GET', '/painel', fn($request) => Response::view('meu-plugin::painel'));
$sdk->apiRoute('POST', '/sync', fn($request) => ['ok' => true]);
$sdk->views('views');
$sdk->menu('Meu Plugin', '/plugin/meu-plugin/painel', '★', 70);
$sdk->widget('resumo', 'Meu resumo', fn($project, $period) => '<p>HTML</p>');
Rotas ficam sob /plugin/{slug}/… e /api/plugin/{slug}/… — sem colisão com o núcleo.
Gateway de pagamento
$sdk->gateway('meugateway', 'Meu Gateway',
fn(array $payload) => OrderNormalizer::generic([
'order_id' => $payload['id'],
'status' => $payload['situacao'],
'total' => $payload['valor'], // na unidade da moeda
// 'total' => OrderNormalizer::centsToCents($payload['valor_centavos']),
'tracking' => ['vid' => $payload['custom']['utmi_vid'] ?? null],
]),
[
'fields' => [['name' => 'secret', 'label' => 'Segredo', 'type' => 'password']],
'verify' => fn($payload, $raw, $headers, $config) =>
hash_equals(hash_hmac('sha256', $raw, $config['secret']), $headers['x-signature'] ?? ''),
]
);
A URL do webhook passa a ser /webhook/meugateway/{chave-secreta-do-projeto}.
Canal de alerta
$sdk->alertChannel('slack', 'Slack',
function (array $alert, array $config): bool {
$r = Http::postJson($config['webhook_url'], ['text' => $alert['title']]);
if ($r['status'] !== 200) {
throw new \RuntimeException('Slack respondeu ' . $r['status']);
}
return true; // lançar exceção faz a fila tentar de novo com backoff
},
[['name' => 'webhook_url', 'label' => 'Webhook', 'type' => 'url', 'required' => true]]
);
Ferramenta de IA
$sdk->aiTool(
'get_estoque',
'Consulta o estoque atual dos produtos. Use quando a pergunta for sobre '
. 'disponibilidade, ruptura ou produto esgotado.',
['type' => 'object', 'properties' => [
'produto_id' => ['type' => 'string', 'description' => 'Filtra por produto.'],
]],
fn(array $input, array $context) => Estoque::consultar($context['project_id'], $input)
);
A descrição é o que faz o modelo escolher (ou não) a ferramenta: diga quando usá-la, não só o que ela faz. O $context traz project_id, project, currency e user_id. Devolva arrays — o SDK serializa. Valores monetários: converta para a unidade da moeda antes de devolver.
Fila e agendamento
$sdk->job('sincronizar', fn(array $payload) => /* … */);
$sdk->dispatch('sincronizar', ['id' => 1], Queue::LOW, 60); // 60s de atraso
$sdk->schedule('diaria', 'daily_at:03:00', fn() => /* … */);
Frequências: every_minute, every_5_minutes, every_10_minutes, every_15_minutes, every_30_minutes, hourly, daily, daily_at:HH:MM, weekly_on:N:HH:MM (1 = segunda), monthly_on:D:HH:MM.
Dados e configuração
$tabela = $sdk->table('registros'); // plugin_meu_plugin_registros
$valor = $sdk->config('api_key');
$sdk->setConfig(['api_key' => 'novo']);
$sdk->log('mensagem', 'info', ['contexto' => 1]);
Tabelas do plugin devem usar o prefixo de $sdk->table() — é o que permite remover tudo no uninstall() sem tocar em dados do núcleo.
Ciclo de vida e segurança
- Um plugin que lança exceção no boot é desativado automaticamente; o resto
- do sistema continua no ar e o erro aparece no Plugin Manager.
- Exceções dentro de um listener são capturadas e registradas — um plugin com
- defeito não derruba o fluxo de vendas.
Desativarremove os hooks registrados na hora (hot reload), sem reiniciar.Recarregarrelê o plugin do disco — útil durante o desenvolvimento.
Publicando
Distribua a pasta do plugin (zip ou git). O usuário coloca em /plugins, abre Plugins no painel e instala em um clique. Versione com SemVer: subir a version no plugin.json dispara o upgrade() na próxima ativação.