# 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` ```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 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 ```php $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) => '

HTML

'); ``` Rotas ficam sob `/plugin/{slug}/…` e `/api/plugin/{slug}/…` — sem colisão com o núcleo. ### Gateway de pagamento ```php $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 ```php $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 ```php $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 ```php $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 ```php $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. - `Desativar` remove os hooks registrados na hora (hot reload), sem reiniciar. - `Recarregar` relê 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.