kb: KB-PLUGIN-031 runbook de workflow dev/deploy de plugins + commit dos registros 021-030 pendentes

- Novo runbook KB-PLUGIN-031: nascimento do plugin ate validacao E2E no
  GLPI dev (scaffold, Forgejo local, deploy CT100, console, bootstrap
  Kernel para testes CLI). Validado de ponta a ponta com o assetinherit.
- Registros KB-PLUGIN-021..030 existiam apenas no disco (drift) e foram
  incluidos no versionamento; index.json sincronizado via kb-fix.
- Ignora lixo AppleDouble/.DS_Store do macOS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Rodolpho Lopes 2026-06-11 17:59:47 +00:00
parent 60060dce60
commit 200cd6c2ce
13 changed files with 2164 additions and 1 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@
*.swp
*.tmp
*.bak
._*
.DS_Store

188
index.json Normal file → Executable file
View file

@ -1,6 +1,6 @@
{
"version": "1.0.0",
"last_updated": "2026-05-26",
"last_updated": "2026-06-11",
"records": [
{
"id": "KB-INFRA-001",
@ -384,6 +384,192 @@
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-020-mcprotocol-bff-roadmap.md",
"summary": "id: KB-PLUGIN-020"
},
{
"id": "KB-PLUGIN-021",
"title": "mcprotocol — Roadmap de Resources e caso de uso N1",
"domain": "plugin-dev",
"tags": [
"mcp",
"resources",
"n1",
"knowledge-base",
"roadmap"
],
"status": "draft",
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-021-mcprotocol-resources-n1-roadmap.md",
"summary": "id: KB-PLUGIN-021"
},
{
"id": "KB-PLUGIN-022",
"title": "mcprotocol — Padrões de validação e ergonomia em tool inputs",
"domain": "plugin-dev",
"tags": [
"mcp",
"tools",
"validation",
"ergonomics",
"llm",
"patterns"
],
"status": "active",
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-022-mcprotocol-tool-input-patterns.md",
"summary": "id: KB-PLUGIN-022"
},
{
"id": "KB-PLUGIN-023",
"title": "mcprotocol — InputValidation extraída como classe utilitária compartilhada",
"domain": "plugin-dev",
"tags": [
"mcp",
"tools",
"validation",
"refactor",
"patterns"
],
"status": "active",
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-023-mcprotocol-inputvalidation-extracted.md",
"summary": "id: KB-PLUGIN-023"
},
{
"id": "KB-PLUGIN-024",
"title": "mcprotocol — Bug duplo no glpi_delete_item (sucesso silencioso + ProjectTask não exposto em v2)",
"domain": "plugin-dev",
"tags": [
"mcp",
"bug",
"glpi",
"rest-api",
"delete",
"error-handling"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-024-mcprotocol-delete-item-silent-success-bug.md",
"summary": "id: KB-PLUGIN-024"
},
{
"id": "KB-PLUGIN-025",
"title": "mcprotocol — Comportamento de timezone em datas sem hora explícita",
"domain": "plugin-dev",
"tags": [
"mcp",
"tools",
"validation",
"timezone",
"gotcha"
],
"status": "active",
"severity": "low",
"path": "records/plugin-dev/KB-PLUGIN-025-mcprotocol-date-timezone-handling.md",
"summary": "id: KB-PLUGIN-025"
},
{
"id": "KB-PLUGIN-026",
"title": "mcprotocol — Snapshot de roadmap v1.2 (pós-ProjectTask + InputValidation)",
"domain": "plugin-dev",
"tags": [
"mcp",
"roadmap",
"snapshot",
"planning"
],
"status": "active",
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-026-mcprotocol-roadmap-snapshot-v1.2.md",
"summary": "id: KB-PLUGIN-026"
},
{
"id": "KB-PLUGIN-027",
"title": "mcprotocol — Fluxo de release Dev → Prod (manual)",
"domain": "plugin-dev",
"tags": [
"mcp",
"release",
"workflow",
"forgejo",
"mindplace"
],
"status": "active",
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-027-mcprotocol-dev-prod-release-flow.md",
"summary": "id: KB-PLUGIN-027"
},
{
"id": "KB-PLUGIN-028",
"title": "GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile",
"domain": "plugin-dev",
"tags": [
"glpi11",
"plugin",
"profile",
"rights",
"menu",
"sidebar",
"gotcha"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-028-glpi11-plugin-rights-and-menu-visibility.md",
"summary": "id: KB-PLUGIN-028"
},
{
"id": "KB-PLUGIN-029",
"title": "GLPI 11 — Itemtypes administrativos vs. ativos (semântica de vínculo com Ticket)",
"domain": "plugin-dev",
"tags": [
"glpi11",
"plugin",
"design",
"architecture",
"ticket",
"contract",
"taxonomy"
],
"status": "active",
"severity": "medium",
"path": "records/plugin-dev/KB-PLUGIN-029-glpi11-administrative-vs-asset-itemtypes.md",
"summary": "id: KB-PLUGIN-029"
},
{
"id": "KB-PLUGIN-030",
"title": "\"GLPI 11 — Criação de tabelas exige classe Migration (Executing direct queries is not allowed!)\"",
"domain": "plugin-dev",
"tags": [
"glpi11",
"plugin",
"install",
"database",
"ddl",
"migration",
"gotcha"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-030-glpi11-migration-class-required-for-ddl.md",
"summary": "id: KB-PLUGIN-030"
},
{
"id": "KB-PLUGIN-031",
"title": "\"Runbook — Workflow de desenvolvimento e deploy DEV de plugins GLPI\"",
"domain": "plugin-dev",
"tags": [
"runbook",
"workflow",
"dev",
"deploy",
"forgejo",
"scaffold",
"validation",
"console"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md",
"summary": "id: KB-PLUGIN-031"
}
]
}

View file

@ -0,0 +1,87 @@
---
id: KB-PLUGIN-021
title: mcprotocol — Roadmap de Resources e caso de uso N1
domain: plugin-dev
tags:
- mcp
- resources
- n1
- knowledge-base
- roadmap
status: draft
severity: medium
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-019
- KB-PLUGIN-020
---
# mcprotocol — Roadmap de Resources e caso de uso N1
## Contexto
A spec MCP define 3 primitivos: **tools**, **resources** e **prompts**. Hoje o plugin mcprotocol implementa apenas `tools`. A evolução natural mais valiosa pro caso GLPI é adicionar **resources**, especialmente para habilitar **LLM como N1** (atendimento de primeiro nível).
## Por que Resources e não Prompts
- **Resources (alto valor)** — GLPI tem KB articles, tickets com histórico, anexos e CIs. Tudo isso é "documento que o LLM deveria ler", não "função que ele chama". Resources transforma N chamadas de tool em uma anexação direta no contexto.
- **Prompts (baixo valor)** — ITSM tem workflows mas usuários já falam linguagem natural. ROI marginal.
## Caso de uso central: LLM como N1
Fluxo tradicional N1:
1. Recebe chamado
2. Procura KB
3. Lê artigos
4. Aplica procedimento ou escala
Com resources + tools:
- Cliente MCP anexa KB articles relevantes como resources
- LLM lê no contexto sem fazer N chamadas de tool
- Usa tools (`glpi_ticket_*`) pra agir no chamado
- Caso de "N1 autônomo": webhook → busca KB+tickets similares → resources → LLM decide aplicar ou escalar
## Resources prioritárias
| URI pattern | Origem | Caso de uso |
|---|---|---|
| `kb://{id}` | `glpi_knowbaseitems` | Artigos da base de conhecimento |
| `ticket://{id}` | `glpi_tickets` (+ followups, solutions) | Ticket completo com histórico |
| `document://{id}` | `glpi_documents` | Anexos (PDFs, prints, manuais) |
| `computer://{id}` | `glpi_computers` (+ items) | CI técnico do ativo |
## Implementação sugerida
Criar `src/KBResources.php` (e companheiros) seguindo o padrão `ToolRegistry`:
```php
class KBResources {
public static function getResources(): array { /* ... */ }
public static function readResource(string $uri) { /* ... */ }
}
```
Adicionar `ResourceRegistry` análogo ao `ToolRegistry`. Em `Server.php`, implementar:
- `resources/list` — lista URIs disponíveis (paginar para KB grandes)
- `resources/read` — retorna conteúdo por URI
## Evolução futura (fora deste escopo)
- **SSE / Streamable HTTP completo** — notificações server-push (novo ticket, SLA estourando)
- **Prompts** — depois de resources, talvez 2-3 prompts simbólicos (triagem, fechamento padrão)
- **Multi-tenancy / escopo OAuth2 fino** — separar permissões por escopo (tools_ticket, tools_inventory, etc.)
- **stdio→HTTP bridge** — pacote NPM `@mindplace/glpi-mcp-bridge` para compatibilidade com clientes que só falam stdio
## Status
**Roadmap** — não implementado. Prioridade após conclusão do roadmap de tools (KB-PLUGIN-020).
## Referências
- KB-PLUGIN-019 — Arquitetura BFF do mcprotocol
- KB-PLUGIN-020 — Roadmap de tools pendentes
- Spec MCP: https://spec.modelcontextprotocol.io

View file

@ -0,0 +1,167 @@
---
id: KB-PLUGIN-022
title: mcprotocol — Padrões de validação e ergonomia em tool inputs
domain: plugin-dev
tags:
- mcp
- tools
- validation
- ergonomics
- llm
- patterns
status: active
severity: medium
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-019
- KB-PLUGIN-020
---
# mcprotocol — Padrões de validação e ergonomia em tool inputs
## Contexto
Tools do MCP são chamadas por LLMs que **não conhecem IDs internos** do GLPI nem o formato exato esperado pelos campos. Sem padrões de entrada bem desenhados, o LLM precisa fazer várias chamadas exploratórias (listar estados → encontrar ID → criar projeto), gastando turns e tokens. Pior: quando erra, recebe mensagens de erro do ORM crípticas (`SQL error`, `boolean false`) e não consegue se autocorrigir.
Este registro documenta os padrões implementados no `ProjectTools::handleCreate` que resolvem esses problemas, **aplicáveis a qualquer tool de criação/atualização** no plugin.
## Padrões implementados
### 1. `resolveReference` — Aceita ID ou nome
**Problema:** LLM não sabe que `projectstates_id=2` é "Processing".
**Solução:** helper que aceita int (validando existência) ou string (resolvendo por nome).
```php
private static function resolveReference(
string $table, // ex: 'glpi_projectstates'
$value, // int OU string
string $field, // nome do campo (pra mensagem de erro)
array &$errors // acumulador de erros
): ?int
```
**Caso int / string numérica:**
- Valida existência via `SELECT id FROM {$table} WHERE id = ?`
- Se não existe: `"{$field}: ID {$id} não existe em {$table}."`
**Caso string (nome):**
- Busca via `SELECT id FROM {$table} WHERE name = ?`
- Se não encontra: lista até 20 nomes disponíveis na mensagem de erro
**Exemplo de erro útil pra LLM:**
```
projectstates_id: nome 'AbacaxiDoido' não encontrado em glpi_projectstates.
Disponíveis: New, Processing, Closed.
```
A LLM lê isso e **se autocorrige** na próxima chamada — sem precisar de uma tool separada `glpi_projectstate_list`.
### 2. `normalizeDate` — Normaliza formato
**Problema:** Datas vêm em formatos variados (`2026-06-01`, `2026-06-01 00:00:00`, ISO 8601, etc.). GLPI espera `Y-m-d H:i:s`.
**Solução:** helper aceita os 2 formatos mais comuns e normaliza:
```php
private static function normalizeDate(
string $value,
string $field,
array &$errors
): ?string
```
- `YYYY-MM-DD` → preenche `00:00:00`
- `YYYY-MM-DD HH:MM:SS` → mantém
- Qualquer outro: erro claro com formato esperado
### 3. Validação prévia agregada (fail-loud-once)
**Problema:** Validar campo por campo lançando exception faz a LLM corrigir 1 erro por chamada — N chamadas pra N erros.
**Solução:** acumula erros num array e lança UMA exception no final:
```php
$errors = [];
// ... várias validações que appendam em $errors
if (!empty($errors)) {
throw new \Exception("Falha na validação: " . implode(" | ", $errors));
}
```
A LLM recebe **todos os problemas de uma vez**, corrige tudo e tenta de novo.
### 4. Response inclui `applied` (debug & confiança)
**Problema:** depois de resolver nomes → IDs, normalizar datas, etc., a LLM não sabe o que efetivamente foi gravado.
**Solução:** resposta de sucesso inclui o input final aplicado no ORM:
```php
return [
'status' => 'success',
'id' => $newId,
'message' => 'Projeto criado com sucesso.',
'applied' => $input // mostra o que realmente entrou no DB
];
```
A LLM confirma `projectstates_id: 2` (resolvido de "Processing"), `plan_start_date: 2026-06-01 00:00:00` (normalizado), e segue confiante.
### 5. Schema usa `oneOf` pra polimorfismo
Campos que aceitam int ou string declaram isso explicitamente no `inputSchema`:
```php
'projectstates_id' => [
'description' => 'Estado. Aceita ID (int) ou nome (ex: "New", "Processing", "Closed").',
'oneOf' => [
['type' => 'integer'],
['type' => 'string']
]
]
```
A descrição já lista exemplos comuns — a LLM começa certo na primeira tentativa.
## Quando aplicar
**Sempre que uma tool tem campo que referencia outra tabela GLPI:**
- `users_id``glpi_users` (login ou ID)
- `groups_id``glpi_groups` (nome ou ID)
- `entities_id``glpi_entities` (nome ou ID)
- `itilcategories_id``glpi_itilcategories`
- `locations_id``glpi_locations`
- `tickets_states`, `priority`, `urgency`, `impact` (esses já são enums numéricos GLPI — pode aceitar string como atalho: "Alta"=4)
**Sempre que aceitar data:** usar `normalizeDate`.
**Sempre que houver múltiplas validações:** agregar erros.
**Sempre na resposta de sucesso:** retornar `applied`.
## Anti-padrões a evitar
| Anti-padrão | Por quê é ruim |
|---|---|
| Aceitar só ID int | LLM precisa fazer N chamadas exploratórias |
| Mensagem de erro do tipo "Erro ao criar projeto" | LLM não sabe o que corrigir |
| Lançar exception no primeiro erro | LLM corrige 1 problema por vez |
| Não validar antes do ORM | Erros de SQL/ORM são incompreensíveis pra LLM |
| Não retornar input aplicado | LLM perde rastreabilidade do que foi gravado |
## Referências de implementação
- `src/ProjectTools.php` — handler `handleCreate` (referência canônica deste padrão)
- `src/ToolRegistry.php` — registro central de tools
- Spec MCP — Tool Input Schemas: https://spec.modelcontextprotocol.io/specification/server/tools
## Próxima evolução
Extrair os helpers (`resolveReference`, `normalizeDate`) para uma classe utilitária `src/InputValidation.php` quando o segundo handler precisar do mesmo padrão (provavelmente `glpi_project_update` ou `glpi_ticket_create` com `users_id`/`itilcategories_id`).

View file

@ -0,0 +1,85 @@
---
id: KB-PLUGIN-023
title: mcprotocol — InputValidation extraída como classe utilitária compartilhada
domain: plugin-dev
tags:
- mcp
- tools
- validation
- refactor
- patterns
status: active
severity: medium
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-022
- KB-PLUGIN-019
---
# mcprotocol — InputValidation extraída como classe utilitária compartilhada
## Contexto
KB-PLUGIN-022 documentou os padrões `resolveReference`, `normalizeDate`, validação agregada e response com `applied` como métodos privados em `ProjectTools`. Esse KB previu que **na introdução do segundo handler** os helpers seriam extraídos para uma classe utilitária. Esse momento chegou ao implementar `glpi_projecttask_create`.
## Decisão
Extraído `src/InputValidation.php` (namespace `GlpiPlugin\Mcprotocol`) com métodos **estáticos públicos** reutilizáveis em qualquer handler de tool:
| Método | Responsabilidade |
|---|---|
| `resolveReference($table, $value, $field, &$errors, $nameCol='name')` | Aceita int ou string e devolve ID validado. Suporta coluna de busca customizada (ex: `name` em `glpi_users`). |
| `normalizeDate($value, $field, &$errors)` | Aceita `YYYY-MM-DD` ou `YYYY-MM-DD HH:MM:SS`, normaliza pro formato GLPI. |
| `validateDateRange($start, $end, $startField, $endField, &$errors)` | Garante `end >= start` quando ambos informados. |
| `validateIntRange($value, $field, $min, $max, &$errors)` | Valida inteiro em intervalo (ex: `percent_done` 0-100). |
## Mudanças realizadas
- **Novo arquivo:** `src/InputValidation.php`
- **Refatorado:** `src/ProjectTools.php` — removeu helpers privados, agora chama `InputValidation::*`. Também ganhou método `buildInputFromArgs($args, $isUpdate)` que centraliza a montagem do input para create/update (DRY).
- **Estendido:** `glpi_project_update` agora aceita os mesmos campos do create (datas, estado, tipo) — antes só aceitava name/content/percent_done.
- **Novo arquivo:** `src/ProjectTaskTools.php` — usa `InputValidation` desde o início, com 2 tools: `glpi_projecttask_create` e `glpi_projecttask_get`.
- **Atualizado:** `src/ToolRegistry.php` — registra `ProjectTaskTools::getTools()` no merge.
## Convenções de uso
1. **Acumular erros** — qualquer handler que valida múltiplos campos deve usar a assinatura `&$errors` e lançar UMA exception ao final. Nunca múltiplas.
2. **Coluna de busca por nome**`resolveReference` aceita `$nameCol`. Para tabelas onde o campo descritivo não se chama `name`, passar explicitamente:
```php
InputValidation::resolveReference('glpi_users', $args['users_id'], 'users_id', $errors, 'name');
```
3. **Validação prévia ao ORM** — todos os helpers rodam ANTES de `$obj->add()` ou `$obj->update()`. Se houver erro, exception é lançada e o ORM nunca é chamado. Evita erros de SQL/ORM crípticos.
4. **Response com `applied`** — handlers devem sempre retornar o input efetivamente aplicado:
```php
return ['status'=>'success', 'id'=>$newId, 'message'=>'...', 'applied'=>$input];
```
## Tabelas comuns para `resolveReference`
| Tabela | Coluna de nome | Uso típico |
|---|---|---|
| `glpi_projectstates` | `name` | Estado de projeto/tarefa |
| `glpi_projecttypes` | `name` | Tipo de projeto |
| `glpi_projecttasktypes` | `name` | Tipo de tarefa |
| `glpi_users` | `name` (login) | Responsável |
| `glpi_groups` | `name` | Grupo |
| `glpi_entities` | `name` | Entidade |
| `glpi_itilcategories` | `name` | Categoria de chamado |
| `glpi_locations` | `name` | Localização |
## Próximos handlers candidatos a usar `InputValidation`
- `glpi_ticket_create``users_id_recipient`, `users_id_lastupdater`, `itilcategories_id`, datas
- `glpi_ticket_update` — idem + estados (1=New, 2=Processing, etc. já são enums numéricos GLPI)
- Futuras tools de Computer/Asset (`glpi_computer_create`, etc.)
## Validação end-to-end
Padrão validado em produção criando o projeto VALGROUP (28 tarefas em hierarquia) através das tools. Erros de validação retornados claramente, datas normalizadas, estados resolvidos por nome ("New" → 1, "Processing" → 2). Aplicado conforme esperado.

View file

@ -0,0 +1,163 @@
---
id: KB-PLUGIN-024
title: mcprotocol — Bug duplo no glpi_delete_item (sucesso silencioso + ProjectTask não exposto em v2)
domain: plugin-dev
tags:
- mcp
- bug
- glpi
- rest-api
- delete
- error-handling
status: active
severity: high
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-019
- KB-PLUGIN-016
---
# mcprotocol — Bug duplo no glpi_delete_item (sucesso silencioso + ProjectTask não exposto em v2)
## Resumo
Ao tentar deletar uma `ProjectTask` via `glpi_delete_item`, a tool retorna sucesso ao cliente mas o item **continua existindo no banco**. Investigação revelou dois bugs sobrepostos.
## Evidência
Comando que reproduz:
```json
{"method":"tools/call","params":{
"name":"glpi_delete_item",
"arguments":{"itemtype":"ProjectTask","id":30,"force_purge":true}
}}
```
Resposta MCP: aparenta sucesso (sem JSON-RPC error).
Banco: registro ID 30 segue em `glpi_projecttasks` com `is_deleted=0`.
## Causa raiz #1 — REST API v2 do GLPI não expõe ProjectTask
Verificado por probes:
| Endpoint | HTTP |
|---|---|
| `GET /api.php/v2/Project` | 200 ✅ |
| `GET /api.php/v2/ProjectTask` | 404 ❌ |
| `DELETE /api.php/v2/Project/4/ProjectTask/30` | 404 ❌ |
| `DELETE /api.php/v2/Project/ProjectTask/30` | 404 ❌ |
| `DELETE /api.php/v2/Assistance/ProjectTask/30` | 404 ❌ |
`ProjectTask` simplesmente **não está no roteamento público da v2** desta build do GLPI 11. Não há caminho documentado. Pode ser limitação da própria release (não confirmado se evolui em versões futuras).
## Causa raiz #2`makeRequest` não valida HTTP status
Em `src/Server.php`, o handler de `glpi_delete_item` faz:
```php
$response = $this->makeRequest('DELETE', "/$itemtype/{$args['id']}$purge");
break;
```
O método `makeRequest()` retorna o body da resposta sem checar o código HTTP. Quando a API retorna `{"status":"ERROR_ITEM_NOT_FOUND"}` com HTTP 404, esse JSON vira o `content[0].text` da resposta MCP — que do ponto de vista do JSON-RPC parece sucesso (não tem campo `error` no envelope JSON-RPC).
O cliente (Python, LLM, etc.) que checa apenas `if "error" in response` é enganado.
## Impacto
- **Alto:** silenciosamente perde operações de delete sem alertar o usuário/LLM
- LLM acredita que removeu, segue trabalhando com base nessa premissa falsa
- Afeta qualquer itemtype que não esteja na v2 do GLPI (ProjectTask confirmado; outros podem estar afetados — `KnowbaseItem`, `Document_Item`, etc., precisam ser testados)
## Mitigações temporárias
1. **Não usar `glpi_delete_item` para ProjectTask** — usar SQL direto ou criar tool de domínio (`glpi_projecttask_delete`) com ORM `(new ProjectTask())->delete(['id' => $id], true)`.
2. **Cliente checa `applied`** — tools de domínio do plugin retornam `applied`. Para tools genéricas, o cliente deve fazer `GET` após `DELETE` pra confirmar remoção (verificação ativa).
## Correções recomendadas
### Correção #1 (rápida, alta prioridade) — Validar HTTP status no `makeRequest`
Em `src/Server.php`, modificar `makeRequest` para propagar 4xx/5xx como exception:
```php
private function makeRequest(string $method, string $path, ?array $body = null): array {
// ... cURL setup ...
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($response, true) ?? [];
if ($httpCode >= 400) {
$msg = $decoded['title'] ?? $decoded['status'] ?? 'Unknown error';
$detail = $decoded['detail'] ?? '';
throw new \Exception("GLPI API {$method} {$path} → HTTP {$httpCode}: {$msg}" . ($detail ? " ({$detail})" : ''));
}
return $decoded;
}
```
Assim qualquer 404/422/500 da API vira erro JSON-RPC visível para o cliente.
### Correção #2 (evolução) — Criar tools de domínio para ProjectTask
Adicionar em `src/ProjectTaskTools.php`:
```php
'glpi_projecttask_delete' => [
'description' => 'Remove uma tarefa de projeto. Bypassa REST API v2 (não exposta).',
'inputSchema' => [...],
'handler' => [self::class, 'handleDelete']
],
```
Handler usa ORM diretamente (não a REST API):
```php
public static function handleDelete(array $args) {
$task = new \ProjectTask();
if (!$task->can($args['id'], DELETE)) throw new \Exception("Acesso Negado.");
if (!$task->delete(['id' => $args['id']], (bool)($args['force_purge'] ?? true))) {
throw new \Exception("Erro ao remover tarefa.");
}
return ['status'=>'success', 'id'=>$args['id'], 'message'=>'Tarefa removida.'];
}
```
### Correção #3 (defensiva) — Probe na lista de tools
Documentar em `glpi_delete_item` quais itemtypes são conhecidamente quebrados na v2. Ou no startup do plugin, fazer um `OPTIONS` em itemtypes comuns e marcar quais funcionam.
## Como reproduzir
```bash
# 1. criar uma ProjectTask
curl -s -X POST .../mcp.php -H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"glpi_projecttask_create","arguments":{"projects_id":4,"name":"X"}}}'
# 2. tentar deletar (parece sucesso)
curl -s -X POST .../mcp.php -H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"glpi_delete_item","arguments":{"itemtype":"ProjectTask","id":<NEW_ID>,"force_purge":true}}}'
# 3. confirmar que o registro continua
docker exec glpi11-mariadb mariadb -uglpi -pglpi_local_dev glpi \
-e "SELECT id,name,is_deleted FROM glpi_projecttasks WHERE id=<NEW_ID>;"
```
## Status
- Bug **identificado** e **documentado**
- Correção #1 (`makeRequest`) priorizada — afeta TODAS as tools genéricas REST
- Correção #2 (`glpi_projecttask_delete`) pode ser implementada junto da próxima rodada do roadmap
- Correção #3 (probe/documentação) — backlog
## Lições
1. **Tools wrapper REST nunca confiam em corpo da resposta sem checar HTTP status** — sempre validar.
2. **Tools de domínio (ORM) são mais robustas** — não dependem do estado da REST API, usam o GLPI diretamente em PHP.
3. **`applied` em response** ajuda o cliente confiar — mas não substitui checagem ativa pós-delete.
4. **REST API v2 do GLPI tem buracos** — não cobre todos os itemtypes. Validar caso a caso antes de assumir que `glpi_*_item` genérico funciona.

View file

@ -0,0 +1,150 @@
---
id: KB-PLUGIN-025
title: mcprotocol — Comportamento de timezone em datas sem hora explícita
domain: plugin-dev
tags:
- mcp
- tools
- validation
- timezone
- gotcha
status: active
severity: low
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-022
- KB-PLUGIN-023
---
# mcprotocol — Comportamento de timezone em datas sem hora explícita
## Resumo
`InputValidation::normalizeDate` aceita `YYYY-MM-DD` e expande para `YYYY-MM-DD 00:00:00` **sem informação de timezone**. O PHP do container GLPI está em `date.timezone=UTC`, então o valor enviado ao MariaDB é tratado como UTC e gravado como tal em colunas `timestamp`.
Para usuários sem TZ pessoal configurado (caso padrão do GLPI), esse comportamento é **transparente** — a UI exibe a data exatamente como recebida. Para usuários com TZ regional configurado (ex: `America/Sao_Paulo`), a data aparece deslocada nas horas correspondentes (`-03:00` → "24/05 21:00" em vez de "25/05 00:00").
## Detalhamento técnico
Cadeia de TZ no ambiente atual:
| Camada | TZ |
|---|---|
| Container app (`glpi11-app`) — clock | `-03` (São Paulo) |
| PHP (`date.timezone`) | `UTC` |
| Container MariaDB (`glpi11-mariadb`) — clock | `-03` |
| MariaDB session (`@@session.time_zone`) | `SYSTEM` (= `-03`) |
| GLPI config (`glpi_configs.timezone`) | `0` (UTC default) |
| Usuário `glpi.timezone` | `NULL` (sem override) |
Quando `normalizeDate('2026-05-25')` retorna `2026-05-25 00:00:00`:
1. PHP envia a string para o ORM do GLPI
2. ORM passa adiante para MariaDB
3. MariaDB com sessão em `SYSTEM` interpretaria a string como horário local — mas o GLPI executa `SET time_zone='+00:00'` ao conectar (verificado em `Glpi\System\Diagnostic`), então a sessão fica UTC
4. Valor gravado: `2026-05-25 00:00:00 UTC`
Confirmado por probe:
```sql
SET @@session.time_zone='+00:00';
SELECT plan_start_date FROM glpi_projecttasks WHERE id=33;
-- 2026-05-25 00:00:00 ← correto
```
REST API v2 retorna com sufixo explícito:
```json
"plan_start_date": "2026-05-25T00:00:00+00:00"
```
## Gotcha — Usuários com TZ regional
Cenário problemático:
1. Admin cria usuário "joao" e seta `joao.timezone = 'America/Sao_Paulo'` no perfil
2. LLM cria tarefa via plugin: `plan_start_date: "2026-05-25"`
3. Banco grava: `2026-05-25 00:00:00 UTC`
4. UI do João renderiza convertendo pra SP: `24/05/2026 21:00:00`
O João vê **dia 24** em vez de **dia 25**. Confusão garantida.
## Mitigações
### Curto prazo — documentação
Adicionar nota na descrição do schema dos campos `plan_start_date`/`plan_end_date`:
> "Datas sem hora são interpretadas como 00:00 UTC. Para usuários em fusos
> regionais, recomenda-se enviar com hora explícita no fuso de interesse,
> ex: '2026-05-25 03:00:00' para meia-noite em São Paulo (UTC-3)."
### Médio prazo — normalização inteligente
Estender `InputValidation::normalizeDate` para aceitar um TZ contextual e, quando data vier sem hora, expandir como meia-noite naquele TZ convertido para UTC:
```php
public static function normalizeDate(
string $value,
string $field,
array &$errors,
?string $tz = null // novo parâmetro opcional
): ?string {
$tz = $tz ?? self::resolveContextTz();
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
$dt = new \DateTimeImmutable("$value 00:00:00", new \DateTimeZone($tz));
return $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
}
// ... resto igual
}
private static function resolveContextTz(): string {
// Ordem: user preference → glpi config → UTC
$userTz = $_SESSION['glpi_tz'] ?? null;
if ($userTz) return $userTz;
global $CFG_GLPI;
return $CFG_GLPI['timezone'] ?? 'UTC';
}
```
Assim a data "2026-05-25" enviada por um LLM operando em nome de um usuário com TZ=SP resultaria em `2026-05-25 03:00:00 UTC`, que renderiza corretamente como `25/05/2026 00:00` em SP.
### Longo prazo — schema MCP com TZ embutido
Para datas com hora, padronizar input em ISO 8601 com TZ explícito:
`"2026-05-25T00:00:00-03:00"` ou `"2026-05-25T00:00:00Z"`.
`normalizeDate` aceitaria também esse formato e converteria para UTC.
## Diagnóstico de campo
Comando para auditar o estado real vs. UI:
```bash
# 1. Confirma TZ do user atual
SELECT name, timezone FROM glpi_users WHERE name = 'X';
# 2. Lê data forçando UTC (valor real armazenado)
SET @@session.time_zone='+00:00';
SELECT plan_start_date FROM <tabela> WHERE id=N;
# 3. Lê data no TZ do user (o que a UI vai mostrar)
SET @@session.time_zone='America/Sao_Paulo';
SELECT plan_start_date FROM <tabela> WHERE id=N;
```
## Status
- **Comportamento atual:** correto para o caso padrão (sem TZ regional)
- **Não há bug** — é uma gap de UX prevista da extensão de schema
- **Mitigação curto prazo:** documentação no schema
- **Mitigação ideal:** TZ contextual no `normalizeDate` (backlog do plugin)
## Lições
1. `timestamp` no MariaDB sempre armazena em UTC, independente da sessão
2. Strings de data sem TZ explícito dependem do `@@session.time_zone` para interpretação
3. GLPI já normaliza a sessão para UTC ao conectar — bom comportamento
4. Mas o **input** do plugin precisa de TZ contextual para ser semanticamente correto

View file

@ -0,0 +1,147 @@
---
id: KB-PLUGIN-026
title: mcprotocol — Snapshot de roadmap v1.2 (pós-ProjectTask + InputValidation)
domain: plugin-dev
tags:
- mcp
- roadmap
- snapshot
- planning
status: active
severity: medium
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
related_records:
- KB-PLUGIN-019
- KB-PLUGIN-020
- KB-PLUGIN-021
- KB-PLUGIN-022
- KB-PLUGIN-023
- KB-PLUGIN-024
- KB-PLUGIN-025
---
# mcprotocol — Snapshot de roadmap v1.2 (pós-ProjectTask + InputValidation)
## Contexto
Esta KB é um **ponto de salvamento** do estado do plugin mcprotocol após a rodada de evolução de tools de Project + ProjectTask + extração da camada de validação. Serve de referência canônica para o estado atual e backlog priorizado.
## Estado atual das tools
### Tools de domínio (ORM direto)
| Tool | Status | Notas |
|---|---|---|
| `glpi_ticket_get` | ✅ | |
| `glpi_ticket_create` | ✅ | |
| `glpi_ticket_update` | ✅ | |
| `glpi_ticket_add_followup` | ✅ | |
| `glpi_ticket_add_solution` | ✅ | |
| `glpi_project_get` | ✅ | Retorna tarefas com `projecttasks_id` (hierarquia) |
| `glpi_project_create` | ✅ | Aceita estado/tipo por nome, datas, validação prévia |
| `glpi_project_update` | ✅ | **Estendido** com os mesmos campos do create + `percent_done` |
| `glpi_projecttask_get` | ✅ NOVO | |
| `glpi_projecttask_create` | ✅ NOVO | Suporta hierarquia (parent task), responsável, duração, estado |
| `glpi_ticket_search_by_status` | ✅ | |
### Tools genéricas (REST API v2)
| Tool | Status | Observação |
|---|---|---|
| `glpi_get_items` | ⚠️ | Funcional, mas confiável só para itemtypes expostos na v2 |
| `glpi_get_item` | ⚠️ | Idem |
| `glpi_add_item` | ⚠️ | Idem |
| `glpi_update_item` | ⚠️ | Idem |
| `glpi_delete_item` | ❌ | Bug do "silent success" — ver KB-PLUGIN-024 |
### Infraestrutura
| Componente | Status |
|---|---|
| `src/Server.php` | ✅ Funcional. **Pendente:** validar HTTP status em `makeRequest` (KB-024 Correção #1) |
| `src/ToolRegistry.php` | ✅ Registra Ticket + Project + ProjectTask |
| `src/InputValidation.php` | ✅ NOVO — helpers compartilhados (KB-PLUGIN-023) |
| `src/TicketTools.php` | ✅ Ainda usa padrão antigo — candidato a migrar pro `InputValidation` |
| `src/ProjectTools.php` | ✅ Refatorado pra usar `InputValidation` |
| `src/ProjectTaskTools.php` | ✅ NOVO |
| `ajax/mcp.php` | ✅ Endpoint stateless com JWT |
## Casos reais executados
| Cliente | Projeto | Tarefas | Resultado |
|---|---|---|---|
| **VALGROUP** | Implantação GLPI | 7 macros + 21 subs (hierarquia) | ✅ Criado end-to-end via plugin |
| **TCERJ** | Implantação GLPI | 6 tarefas (datas replanejadas) | ✅ Criado end-to-end via plugin |
Ambos comprovam o padrão de tools nativo funcionando em fluxo real de comercial → projeto operacional.
## Bugs conhecidos
| ID | Severidade | Resumo |
|---|---|---|
| KB-PLUGIN-024 | Alta | `glpi_delete_item` retorna sucesso silencioso quando API retorna 404 |
| KB-PLUGIN-024 | Média | `ProjectTask` não exposto na REST v2 do GLPI (precisa tool de domínio) |
| KB-PLUGIN-025 | Baixa | Datas sem hora explícita podem desalinhar em UI de usuários com TZ regional |
## Roadmap priorizado
### Próxima rodada (P1)
1. **Correção #1 do KB-024**`makeRequest` deve validar HTTP status e propagar 4xx/5xx como exception. Afeta todas as tools genéricas REST. Impacto alto, esforço baixo.
2. **`glpi_projecttask_delete`** — Correção #2 do KB-024. Tool de domínio bypassa REST API, usa ORM direto.
3. **`glpi_projecttask_update`** — completar o CRUD de tarefas. Necessário para casos como "atualizar percent_done", "remarcar datas".
4. **`glpi_project_get_open_tasks`** — Tool semântica para casos de uso N1/PM. Filtra tarefas com `percent_done < 100` e `projectstates_id != Closed`.
### Médio prazo (P2)
5. **Migrar `TicketTools` para `InputValidation`** — eliminar dívida técnica, padronizar erros, ergonomia de nomes vs IDs em campos como `users_id_recipient`, `itilcategories_id`.
6. **Tools do roadmap original (KB-020)** ainda pendentes:
- `glpi_ticket_get_pending_approvals`
- `glpi_ticket_assign_to_me`
- `glpi_ticket_add_private_note`
- `glpi_computer_get_unassigned`
- `glpi_user_get_my_assets`
7. **TZ contextual em `normalizeDate`** — Correção média do KB-025, expande data sem hora no TZ do GLPI/usuário.
### Longo prazo (P3 — visão estratégica)
8. **Resources MCP** (KB-PLUGIN-021) — habilitar caso de uso "LLM como N1":
- `kb://{id}` — artigos da KB do GLPI
- `ticket://{id}` — ticket completo com followups/solutions
- `document://{id}` — anexos
- `computer://{id}` — CI técnico
9. **SSE / Streamable HTTP completo** — notificações push (novo ticket, SLA estourando)
10. **OAuth2 com escopos finos** — separar permissões por escopo de tool
## Métricas atuais
- **Tools registradas:** 16 (11 domínio + 5 genéricas)
- **Casos reais validados:** 2 projetos, 34 tarefas
- **KBs do plugin:** 14 (incluindo este snapshot)
- **Bugs ativos:** 3 (1 alta, 1 média, 1 baixa)
- **Versão atual do plugin:** 1.1.0 (próxima release: 1.2.0)
## Próxima versão proposta — v1.2.0
Conteúdo já no branch `main` do dev (a fazer push):
- Adicionada `src/InputValidation.php` com helpers compartilhados
- Adicionada `src/ProjectTaskTools.php` com `glpi_projecttask_get` e `glpi_projecttask_create`
- Estendida `src/ProjectTools.php``update` agora aceita datas, estado, tipo
- Atualizado `src/ToolRegistry.php` registrando `ProjectTaskTools`
- KBs documentando padrões (022, 023), bugs (024, 025) e snapshot (026)
Conteúdo para v1.2.1+ (correções dos bugs documentados):
- `Server.php::makeRequest` validando HTTP status
- `ProjectTaskTools::handleDelete` (`glpi_projecttask_delete`)
- `glpi_projecttask_update`

View file

@ -0,0 +1,172 @@
---
id: KB-PLUGIN-027
title: mcprotocol — Fluxo de release Dev → Prod (manual)
domain: plugin-dev
tags:
- mcp
- release
- workflow
- forgejo
- mindplace
status: active
severity: medium
created_at: 2026-05-26
updated_at: 2026-05-26
applies_to:
- glpi-11
- mcprotocol-plugin
- mindplace-marketplace
related_records:
- KB-PLUGIN-013
- KB-PLUGIN-018
- KB-PLUGIN-026
---
# mcprotocol — Fluxo de release Dev → Prod (manual)
## Contexto
O ecossistema Mindplace usa **dois Forgejos**: um de **desenvolvimento** (lab) e um de **produção** (Mindtek). O fluxo de release é **híbrido**: push automático para dev via `git push` normal, push para prod via script semi-automatizado.
A versão do plugin é a **mesma** entre dev e prod — não há suffixo `-dev`, `-rc`, etc. A diferença é qual Forgejo recebeu o push.
## Topologia
```
┌─────────────────────────────┐
│ Workspace local (CT100) │
│ /opt/projects/GLPI11/ │
│ docker/glpi/ │
│ marketplace/mcprotocol │
└──────────────┬──────────────┘
│ git push origin main
┌─────────────────────────────────────────┐
│ Forgejo DEV (lab, CT101) │
│ git@192.168.100.101: │
│ administrador/mcprotocol.git │
│ Uso: desenvolvimento, homologação │
└──────────────┬──────────────────────────┘
│ homologação OK ─► bin/mindplace-release.sh
┌─────────────────────────────────────────┐
│ Forgejo PROD (Mindtek) │
│ https://servicedesk.mindtek.com.br/git │
│ /rodolpho.lopes/mcprotocol │
│ Uso: release oficial, marketplace │
└──────────────┬──────────────────────────┘
│ Mindplace catálogo lê do prod
┌─────────────────────────────────────────┐
│ Mindplace (catálogo) │
│ plugins.json no repo "mindplace" │
│ + Banco Postgres /api/admin/sync │
└─────────────────────────────────────────┘
```
## Fluxo passo a passo
### 1. Desenvolvimento
- Editar código no plugin (`src/`, `setup.php`, etc.)
- Testar localmente contra `https://glpi.lab.coretoai.com/`
### 2. Push em Dev
```bash
cd /opt/projects/GLPI11/docker/glpi/marketplace/mcprotocol
git add <arquivos>
git commit -m "feat/fix/chore: ..."
git push origin main # vai pro Forgejo lab (192.168.100.101)
```
### 3. Homologação
- Testar end-to-end via curl ou cliente MCP apontando para o GLPI lab
- Iterar até estável
### 4. Bump de versão (quando for liberar release)
```php
// setup.php
define('PLUGIN_MCPROTOCOL_VERSION', 'X.Y.Z'); // semver
```
Commit + push para dev primeiro.
### 5. Push em Prod (manual, via script)
```bash
cd /opt/projects/MindPlace
./bin/mindplace-release.sh ./docker/glpi/plugins/mcprotocol
# Ou se o plugin estiver em outro caminho:
./bin/mindplace-release.sh /opt/projects/GLPI11/docker/glpi/marketplace/mcprotocol
```
O script executa 5 fases:
1. Cria/valida repo no Forgejo de prod (`servicedesk.mindtek.com.br/git/rodolpho.lopes/mcprotocol`)
2. `git push` do código pro Forgejo de prod
3. Gera ZIP do plugin (vide KB-PLUGIN-018 sobre wrapper conventions)
4. Cria release `vX.Y.Z` + upload do ZIP como release asset
5. Atualiza `plugins.json` no repo `mindplace` (catálogo central)
Pré-requisitos:
- `FORGEJO_TOKEN` em `/opt/projects/MindPlace/bin/.forgejo-token` (gitignored)
- Token com escopo `write:repository, write:user` no Forgejo de prod
- Acesso ao repo `mindplace` (catálogo)
### 6. Detecção pelo Mindplace
- **Hoje:** `GET /api/admin/sync` chama o endpoint do Mindplace, que faz `upsert` na tabela `App` (Prisma) para cada repo do Forgejo de prod.
- **Em prod**: este fluxo funciona corretamente — o catálogo reflete os plugins.
- **Gaps conhecidos** (vide diagnóstico em CT100, lab):
- Sync é pull manual, não tem trigger por webhook
- Schema `App` não tem campo `version` — não detecta release nova
- `forgejoService.getLatestRelease()` existe mas o endpoint `/sync` não o usa
- Não há sistema de notificação de update
## Convenções
| Item | Convenção |
|---|---|
| Versão | SemVer (`MAJOR.MINOR.PATCH`) |
| Dev e prod com versão idêntica | Sim — mesma versão em ambos os ambientes |
| Branch principal | `main` em ambos Forgejos |
| Mensagem de commit | Prefixos `feat:`, `fix:`, `chore:`, `docs:` (Conventional Commits) |
| Bump de versão | Commit isolado tipo `chore(release): bump X.Y.Z → X.Y.W` |
| Tag de release | Criada **só** no Forgejo de prod, pelo script (`vX.Y.Z`) |
## Quando usar dev vs prod
| Situação | Forgejo |
|---|---|
| Desenvolvimento em andamento, testes da lab | Dev (192.168.100.101) |
| Validação com cliente piloto interno | Dev |
| Pronto pra ir pra marketplace (catálogo público) | Prod (servicedesk.mindtek.com.br) |
| Hotfix urgente em cliente | Pula homologação? Avaliar caso a caso |
## Recuperação
### Reescrita de histórico no dev
- Forçar push com `--force-with-lease` (preferível) ou `--force` (irreversível para colaboradores)
- Sempre comunicar a equipe se há mais de 1 colaborador no repo
### Rollback de release em prod
1. Reverter o commit problemático no repo do plugin
2. Bump de PATCH (ex: 1.1.1 → 1.1.2 com fix)
3. Re-rodar `mindplace-release.sh`
4. **Não deletar release antiga** — Mindplace pode ter clientes apontando para ela
## Roadmap para automação da Fase 4 (futuro)
Gaps que valem ser fechados quando houver bandwidth:
1. **Webhook Forgejo → Mindplace** — disparar sync quando push acontece no prod
2. **Schema `App.version`** — capturar versão atual + permitir comparação
3. **Notificação de update** — e-mail/push pra clientes com licenças ativas quando há release nova
4. **Cron de sync periódico** — backup pra caso webhook falhe (ex: a cada 15min)
Estes gaps estão documentados também no diagnóstico do plugin (KB-PLUGIN-026, seção "Próximos passos do Mindplace").
## Status atual
- **Fluxo 1-3 (dev):** ✅ funcionando, automação simples via `git push`
- **Fluxo 5 (prod):** ✅ funcionando em produção, validado com plugins anteriores
- **Fluxo 6 (Mindplace catalog):** ✅ funcionando em produção, embora seja pull manual
- **Detecção automática de versão / notificação:** ⏳ backlog

View file

@ -0,0 +1,543 @@
---
id: KB-PLUGIN-028
title: GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile
domain: plugin-dev
tags:
- glpi11
- plugin
- profile
- rights
- menu
- sidebar
- gotcha
status: active
severity: high
created_at: 2026-06-01
updated_at: 2026-06-01
applies_to:
- glpi-11
- estimate-plugin
- webapplications-plugin
related_records:
- KB-PLUGIN-013
---
# GLPI 11 — Plugin com menu próprio precisa registrar rights via Profile
## Sintoma
Plugin instalado, ativo e habilitado no marketplace, classes carregando corretamente via autoload, **mas o menu não aparece** na sidebar do GLPI (nem o item dentro de Gerência / Helpdesk / Administração).
Não há erro no log — o menu é simplesmente omitido silenciosamente.
## Causa raiz
GLPI executa `Session::haveRight($rightname, READ)` antes de renderizar qualquer entrada de menu de um itemtype. Se o **right não existir** na tabela `glpi_profilerights` (jamais foi cadastrado pelo plugin), `haveRight()` retorna `0` e o menu é ocultado.
Verificação:
```sql
SELECT COUNT(*) FROM glpi_profilerights WHERE name LIKE '%plugin_meuplugin%';
-- retorna 0 → você está com o problema
```
## Solução padrão GLPI 11
Plugins que expõem itemtypes próprios DEVEM ter uma classe `Profile` que:
1. Estende `\Profile`
2. Expõe `getAllRights()` listando os rights
3. Implementa `initProfile()` para registrar os rights em `glpi_profilerights`
4. Implementa `createFirstAccess($profile_id)` para conceder full access ao perfil que instalou
### Esqueleto da classe `src/Profile.php`
```php
<?php
namespace GlpiPlugin\Meuplugin;
use CommonGLPI;
use DbUtils;
use ProfileRight;
class Profile extends \Profile
{
public static $rightname = "profile";
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
{
if ($item->getType() === 'Profile' && $item->getField('interface') === 'central') {
return self::createTabEntry(MeuItemtype::getTypeName(2));
}
return '';
}
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
{
if ($item->getType() === 'Profile') {
self::addDefaultProfileInfos($item->getID(), [
'plugin_meuplugin_x' => 0,
]);
(new self())->showForm($item->getID());
}
return true;
}
public static function getAllRights($all = false): array
{
return [
[
'itemtype' => MeuItemtype::class,
'label' => MeuItemtype::getTypeName(2),
'field' => 'plugin_meuplugin_x',
],
];
}
public static function initProfile(): void
{
$dbu = new DbUtils();
foreach ((new self())->getAllRights(true) as $data) {
if ($dbu->countElementsInTable('glpi_profilerights', ['name' => $data['field']]) === 0) {
ProfileRight::addProfileRights([$data['field']]);
}
}
}
public static function createFirstAccess($profiles_id): void
{
self::addDefaultProfileInfos($profiles_id, [
'plugin_meuplugin_x' => READ + CREATE + UPDATE + DELETE + PURGE,
], true);
}
public static function addDefaultProfileInfos($profiles_id, array $rights, bool $drop_existing = false): void
{
$dbu = new DbUtils();
$profileRight = new ProfileRight();
foreach ($rights as $name => $value) {
$exists = $dbu->countElementsInTable('glpi_profilerights', [
'profiles_id' => $profiles_id, 'name' => $name,
]) > 0;
if ($exists && $drop_existing) {
$profileRight->deleteByCriteria(['profiles_id' => $profiles_id, 'name' => $name]);
$exists = false;
}
if (!$exists) {
$profileRight->add([
'profiles_id' => $profiles_id,
'name' => $name,
'rights' => $value,
]);
}
}
}
}
```
### Registro em `setup.php`
```php
$PLUGIN_HOOKS['change_profile']['meuplugin'] = [
'GlpiPlugin\\Meuplugin\\Profile', 'initProfile'
];
Plugin::registerClass('GlpiPlugin\\Meuplugin\\Profile', [
'addtabon' => ['Profile']
]);
```
### Chamada no `hook.php` (install)
```php
function plugin_meuplugin_install(): bool
{
// ... criação de tabelas ...
\GlpiPlugin\Meuplugin\Profile::initProfile();
if (isset($_SESSION['glpiactiveprofile']['id'])) {
\GlpiPlugin\Meuplugin\Profile::createFirstAccess(
(int) $_SESSION['glpiactiveprofile']['id']
);
}
return true;
}
```
## Gotcha — Instalação via CLI
`bin/console glpi:plugin:install -f <plugin>` roda **sem sessão GLPI**, então `$_SESSION['glpiactiveprofile']['id']` é `null` e `createFirstAccess()` não é executado. Resultado: rights são registrados na tabela mas com valor `0` (sem permissão real).
### Mitigação A — Web-first install
Recomendado: ativar o plugin pela primeira vez **via interface web** logado como Super-Admin. A sessão está ativa e o `createFirstAccess()` concede full access automaticamente.
### Mitigação B — Forçar via SQL após CLI install
Quando install foi via CLI e você precisa destravar rapidamente:
```sql
UPDATE glpi_profilerights
SET rights = 31 -- READ+CREATE+UPDATE+DELETE+PURGE
WHERE name IN ('plugin_meuplugin_x', 'plugin_meuplugin_y')
AND profiles_id IN (SELECT id FROM glpi_profiles WHERE interface='central');
```
Depois faça logout/login no GLPI pra a sessão recarregar os rights.
### Mitigação C — install.php que concede pra todos os admins
Pode-se estender o `install` pra dar full access automaticamente a todos os profiles com `interface='central'`:
```php
foreach ($DB->request(['FROM' => 'glpi_profiles', 'WHERE' => ['interface' => 'central']]) as $p) {
\GlpiPlugin\Meuplugin\Profile::createFirstAccess((int) $p['id']);
}
```
Mas isso pode ser intrusivo — perfis de Observer/Read-Only ganhariam create/delete por padrão. Avaliar caso a caso.
## Conflito de nomes — `Profile` colide com dropdown
Se o plugin já tem uma classe `Profile` pra outro conceito (ex: catálogo de perfis de executor, perfis de licenciamento, etc.), há colisão com a classe `Profile` exigida pra rights.
### Solução
Renomear a classe que NÃO é a de rights pra algo semanticamente mais claro. Exemplos do plugin Estimate:
- `Profile` (executor) → `ExecutorProfile`
- Tabela mantém `glpi_plugin_estimate_profiles` via override `getTable()`
```php
class ExecutorProfile extends CommonDropdown
{
public static function getTable($classname = null)
{
return 'glpi_plugin_estimate_profiles';
}
}
```
A classe `Profile` (extends `\Profile`) **sempre** fica reservada pra gestão de rights.
## Checklist de validação
Antes de jogar a culpa no menu/cache, confira:
```sql
-- 1. Right existe?
SELECT name, COUNT(*) c FROM glpi_profilerights
WHERE name LIKE '%plugin_meuplugin%' GROUP BY name;
-- 2. Profile atual tem o right ativo (>0)?
SELECT pr.name, pr.rights
FROM glpi_profilerights pr
JOIN glpi_profiles p ON p.id = pr.profiles_id
WHERE p.id = ? AND pr.name LIKE '%plugin_meuplugin%';
```
Se ambos retornam valores válidos (>0), o menu **vai aparecer** após logout/login (sessão precisa recarregar o cache de rights).
## Lições
1. **Menu silenciosamente oculto é sintoma clássico de right ausente.**
2. CLI install é parcial — sempre validar com web install ou SQL update.
3. Classe `Profile` (extends `\Profile`) é **convenção rígida** do GLPI — qualquer conceito de "perfil" no domínio do plugin precisa de outro nome.
4. Logout/login é necessário pra a sessão recarregar rights (não basta refresh).
## Bônus — Páginas `front/` no GLPI 11 NÃO usam `include('inc/includes.php')`
No GLPI 9/10, todo plugin começava com:
```php
include('../../../inc/includes.php');
```
No **GLPI 11 (Symfony)**, isso quebra porque:
1. O marketplace pode estar em `/var/glpi/marketplace/<plugin>/` (fora do tree do GLPI core que está em `/var/www/glpi/`)
2. `dirname(__DIR__, 3)` ou `../../../` resolve para path errado
3. O `LegacyFileLoadController` já bootou GLPI/autoload antes de invocar o arquivo
### Padrão correto GLPI 11
Começar direto sem include:
```php
<?php
Session::checkLoginUser();
$class = \GlpiPlugin\Meuplugin\Item::class;
Html::header(
\GlpiPlugin\Meuplugin\Item::getTypeName(2),
$_SERVER['PHP_SELF'],
'management',
$class
);
Search::show($class);
Html::footer();
```
Plugins de referência: `webapplications`, `splititil` (ambos no marketplace do GLPI 11).
### Sintoma quando inclui errado
```
include(): Failed opening '../../../inc/includes.php' for inclusion
(include_path='.:/usr/local/lib/php')
at <plugin>/front/<page>.php line 7
```
### Gotcha — Funções SQL agregadas no query builder do GLPI
`$DB->request()` é um query builder que **escapa tudo como nome de coluna** por padrão. Passar `'COUNT(*) AS cnt'` como string em `SELECT` gera SQL inválido:
```
MySQL query error: Unknown column 'COUNT(*)' in 'SELECT'
```
Porque a query final fica:
```sql
SELECT `plugin_estimate_states_id`, `COUNT(*)` AS `cnt` FROM ...
^^^^^^^^^^^^ escapado como coluna
```
**Solução:** usar `Glpi\DBAL\QueryExpression` pra funções SQL:
```php
use Glpi\DBAL\QueryExpression;
$DB->request([
'SELECT' => [
'plugin_estimate_states_id',
new QueryExpression('COUNT(*) AS ' . $DB->quoteName('cnt')),
],
'FROM' => self::getTable(),
'WHERE' => ['is_deleted' => 0],
'GROUPBY' => 'plugin_estimate_states_id',
]);
```
Vale pra `COUNT(*)`, `SUM()`, `AVG()`, `MAX()`, `MIN()`, `IF()`, `CASE WHEN`, etc. Sempre quotar nomes de coluna referenciados com `$DB->quoteName(...)`.
### Gotcha — `name` duplicado em templates Twig customizados
Quando você cria um template Twig pro form do itemtype e estende `generic_show_form.html.twig`, o GLPI **já renderiza automaticamente** os campos padrão (`name`, `entities_id`, datas). Adicionar `fields.textField('name', ...)` no bloco `more_fields` causa **duplicação**.
```twig
{# ERRADO — duplica o campo Nome #}
{% block more_fields %}
{{ fields.textField('name', item.fields['name'], __('Name')) }}
...
{% endblock %}
{# CERTO — só campos adicionais #}
{% block more_fields %}
{# 'name' renderizado pelo generic_show_form #}
{{ fields.dropdownField('Client', 'plugin_client_id', ...) }}
...
{% endblock %}
```
Outros campos auto-renderizados (não duplicar): `id`, `name`, `entities_id`, `is_recursive`, `date_creation`, `date_mod`.
### Internacionalização de plugin (gettext .po/.mo)
Plugins GLPI usam **gettext** com domínio por plugin. Cada `__('String', 'estimate')` busca em `<plugin_dir>/locales/<lang>.mo`.
#### Estrutura
```
estimate/
└── locales/
├── pt_BR.po ← fonte editável (UTF-8)
└── pt_BR.mo ← binário compilado (consumido pelo GLPI)
```
Sem prefixo de domínio no filename — convenção é só o código de idioma (`<lang>_<COUNTRY>.po/.mo`).
#### Workflow
```bash
# 1. Instalar gettext (pacote completo, não só -base)
apt-get install -y gettext # provê msgfmt
# 2. Editar locales/pt_BR.po (formato gettext padrão)
# 3. Compilar
cd marketplace/<plugin>/locales
msgfmt pt_BR.po -o pt_BR.mo
# 4. Limpar cache do GLPI
docker exec <container> find /var/glpi/files/_cache -name "locales" -type d -exec rm -rf {} +
```
#### Formato `.po`
```
msgid ""
msgstr ""
"Language: pt_BR\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
# Termo singular
msgid "Estimate"
msgstr "Estimativa"
# Termo plural (suporta nplurals)
msgid "Item"
msgid_plural "Items"
msgstr[0] "Item"
msgstr[1] "Itens"
# Termo com placeholder
msgid "Add %s"
msgstr "Adicionar %s"
```
#### Carregamento automático
O GLPI 11 chama `Plugin::loadLang('<plugin>')` no boot — não precisa código adicional. Basta o `.mo` estar em `locales/<lang>.mo` e o usuário ter `glpilanguage` setado.
#### Validar tradução via console
```bash
docker exec <container> php -r "
chdir('/var/www/glpi');
require 'vendor/autoload.php';
\$k = new Glpi\Kernel\Kernel('production', false);
\$k->boot();
\$_SESSION['glpilanguage'] = 'pt_BR';
\Session::loadLanguage();
\Plugin::loadLang('<plugin>');
echo __('Total hours', '<plugin>').PHP_EOL;
"
```
#### Termos que GLPI core já traduz (não precisa repetir no plugin)
Estes são traduzidos pelo `.mo` do GLPI core. Use `__('Name')` (sem 2º arg) e herda:
`Name`, `Description`, `Status`, `Category`, `Color`, `Date`, `Hours`, `Quantity`,
`Currency`, `Document`, `Documents`, `Notes`, `Historical`, `Profile`, `User`,
`Group`, `Entity`, `Add`, `Save`, `Delete`, `Cancel`, e a maioria dos verbos/labels comuns.
### Constantes úteis disponíveis no front (já definidas)
| Constante | Valor típico no container | Uso |
|---|---|---|
| `GLPI_ROOT` | `/var/www/glpi` | Path do GLPI core |
| `GLPI_MARKETPLACE_DIR` | `/var/glpi/marketplace` | Onde plugins de marketplace ficam (pode diferir de `GLPI_ROOT`) |
| `GLPI_CONFIG_DIR` | `/var/glpi/config` | Config + chaves OAuth |
| `GLPI_PLUGIN_DOC_DIR` | `/var/glpi/files/_plugins` | Storage de arquivos por plugin |
## Bônus 2 — Sidebar de tabs no form do itemtype
GLPI exibe um menu lateral de tabs em cada itemtype (Documento, Itens associados, Notas, Histórico, etc.). Para um plugin replicar isso:
### Pai (Estimate.php — itemtype principal) — implementa `defineTabs()`
```php
public function defineTabs($options = [])
{
$ong = [];
$this->addDefaultFormTab($ong); // form principal (campos do item)
$this->addStandardTab(EstimateItem::class, $ong, $options); // tab "Itens" (filho custom)
$this->addStandardTab('Document_Item', $ong, $options); // anexos nativos
$this->addStandardTab('Notepad', $ong, $options); // notas nativas
$this->addStandardTab('Log', $ong, $options); // histórico nativo
return $ong;
}
```
### Filho (EstimateItem.php — entidade que aparece como tab no pai)
```php
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
{
if ($item instanceof Estimate) {
$count = (new DbUtils())->countElementsInTable(self::getTable(), [
self::$items_id => $item->getID(),
]);
return self::createTabEntry(self::getTypeName(2), $count);
}
return '';
}
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
{
if ($item instanceof Estimate) {
self::showForEstimate($item); // sua função de renderização
}
return true;
}
```
### Registro em `setup.php`
```php
Plugin::registerClass('GlpiPlugin\\Estimate\\EstimateItem', [
'addtabon' => ['GlpiPlugin\\Estimate\\Estimate']
]);
```
`addtabon` informa ao GLPI que esse itemtype deve aparecer como tab nos itemtypes listados.
### Tabs nativos que "saem de graça"
| Tab nativo | Class GLPI | O que faz |
|---|---|---|
| Documentos | `Document_Item` | Anexar arquivos ao itemtype |
| Notas | `Notepad` | Notas privadas do usuário |
| Histórico | `Log` | Audit log automático de mudanças |
| Reservas | `Reservation` | Reservar item por período (CIs) |
| Itens associados | `KnowbaseItem_Item` ou `Item_Devices` | Itens relacionados |
### Gotcha — `count_on_tabs`
Mostrar contador na label da tab depende de `$_SESSION['glpishow_count_on_tabs']` (config GLPI). Sempre testar com `?? true`:
```php
if ($_SESSION['glpishow_count_on_tabs'] ?? true) {
$count = ...;
}
```
### Gotcha — Tabs nativos exigem rights
`Document_Item`, `Notepad`, `Log` etc. fazem check de `canView()` no usuário corrente. No CLI (`bin/console`) a sessão não existe → `addStandardTab` retorna sem adicionar. Validar tabs sempre no **browser logado**, não no console.
### Linkar itemtype a Ticket / Project (rastreabilidade bidirecional)
Para que seu plugin apareça nos dropdowns "tipo de item" ao **adicionar associação** num Ticket ou Project, precisa adicionar ao `$CFG_GLPI` em `plugin_init_<plugin>()`:
```php
global $CFG_GLPI;
$CFG_GLPI['ticket_types'][] = 'GlpiPlugin\\Meuplugin\\MeuItemtype';
$CFG_GLPI['project_asset_types'][] = 'GlpiPlugin\\Meuplugin\\MeuItemtype';
```
E pra o tab **inverso** (Meu Itemtype aparecer como tab em Ticket/Project), usar `addtabon`:
```php
Plugin::registerClass('GlpiPlugin\\Meuplugin\\MeuItemtype', [
'addtabon' => ['Ticket', 'Project']
]);
```
E no `defineTabs()` do seu itemtype, adicionar os tabs nativos:
```php
$this->addStandardTab('Item_Ticket', $ong, $options);
$this->addStandardTab('Item_Project', $ong, $options);
```
Arrays úteis em `$CFG_GLPI`:
| Array | Para que serve |
|---|---|
| `ticket_types` | Item pode ser associado a Ticket via `Item_Ticket` |
| `project_asset_types` | Item pode ser associado a Project via `Item_Project` |
| `asset_types` | Item aparece como "ativo" em listagens genéricas |
| `link_types` | Item pode ser link de relacionamento via `Link_Itemtype` |
| `document_types` | Item pode receber Documents anexados via `Document_Item` |
| `state_types` | Item pode ter `states_id` (estado do ativo) |

View file

@ -0,0 +1,213 @@
---
id: KB-PLUGIN-029
title: GLPI 11 — Itemtypes administrativos vs. ativos (semântica de vínculo com Ticket)
domain: plugin-dev
tags:
- glpi11
- plugin
- design
- architecture
- ticket
- contract
- taxonomy
status: active
severity: medium
created_at: 2026-06-01
updated_at: 2026-06-01
applies_to:
- glpi-11
- estimate-plugin
related_records:
- KB-PLUGIN-028
---
# GLPI 11 — Itemtypes administrativos vs. ativos (semântica de vínculo com Ticket)
## Contexto
Ao construir um itemtype customizado em plugin, surge a pergunta: "ele deve aparecer como tab em Ticket?". A resposta depende da **natureza semântica** do itemtype, não de praticidade técnica.
## Taxonomia GLPI
GLPI separa itemtypes em duas grandes categorias com fluxos de vínculo distintos:
### Ativos (Assets / CIs)
Coisas físicas/virtuais que recebem suporte: Computer, Monitor, NetworkEquipment, Software, Phone, etc.
| Aspecto | Detalhe |
|---|---|
| Tab em Ticket | "Itens" (genérica) |
| Tabela de vínculo | `glpi_items_tickets` (polimórfica via `itemtype`/`items_id`) |
| Registro | `$CFG_GLPI['ticket_types'][] = MyClass::class` |
| Tab inverso (Ticket em MyClass) | `addtabon: ['Ticket']` + `addStandardTab('Item_Ticket', ...)` |
| Padrão UI | Listagem em "Itens" do Ticket junto com Computer/Monitor/etc. |
### Administrativos
Documentos/registros de gestão que descrevem **regras, acordos, custos**: Contract, Supplier, Document, License, Budget, etc.
| Aspecto | Detalhe |
|---|---|
| Tab em Ticket | **Aba própria, separada** ("Contratos", "Fornecedores", "Documentos") |
| Tabela de vínculo | Dedicada (`glpi_contracts_tickets`, `glpi_documents_tickets`) |
| Registro | Específico do itemtype (nada genérico) |
| Padrão UI | Aba dedicada com semântica própria |
## Por que essa separação importa
**Misturar administrativo com ativo polui semanticamente o Ticket.** Quando um técnico abre "Itens" pra ver os CIs afetados pelo problema, ele espera Computer/Monitor — não Estimate, Contract ou Document.
Sintoma observado no plugin Estimate:
- Tentamos registrar Estimate em `$CFG_GLPI['ticket_types']`
- O tab "Itens" do Ticket aceitou o itemtype no dropdown
- Mas conceitualmente é errado — Estimate é proposta comercial, não ativo afetado
## Decisão para o plugin Estimate
Estimate é **administrativo** — comparável a Contract:
| Estimate | Contract |
|---|---|
| Define escopo + valor | Define escopo + valor |
| Tem cliente/fornecedor | Tem fornecedor |
| Tem prazo de validade | Tem prazo de vigência |
| Estados (rascunho/contratada/etc.) | Estados (ativo/suspenso/encerrado) |
Logo, em v1.0 **Estimate NÃO se vincula a Ticket via `Item_Ticket`**. Vínculo bidirecional Estimate ↔ Ticket fica pra v1.1+ com tabela dedicada:
```sql
CREATE TABLE glpi_plugin_estimate_estimates_tickets (
id int unsigned NOT NULL AUTO_INCREMENT,
plugin_estimate_estimates_id int unsigned NOT NULL,
tickets_id int unsigned NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unicity (plugin_estimate_estimates_id, tickets_id)
);
```
Mais classe `Estimate_Ticket` (CommonDBRelation) + tab dedicado.
## Decisão para Project
Estimate ↔ Project **funciona bem** via `Item_Project`:
- Project é conceitualmente "execução da Estimate aprovada" — a relação semântica é direta
- `glpi_items_projects` é polimórfica e aceita qualquer itemtype administrativo ou ativo
- A UI do Project não tem essa convenção rígida do Ticket (Itens = CIs)
## Checklist de decisão para outros plugins
Antes de adicionar `$CFG_GLPI['ticket_types'][]` no seu plugin, pergunte:
1. **O itemtype recebe suporte?** (alguém abre ticket sobre ele) → SIM, é ativo, registre.
2. **O itemtype define escopo/custo/acordo?** → NÃO registre como ativo; considere tabela dedicada (padrão Contract).
3. **O itemtype é resultado de execução?** (Project, Change) → Use as APIs específicas do GLPI (Project, Change, Problem).
## Tabelas/arrays equivalentes (referência rápida)
| Vínculo desejado | Array `$CFG_GLPI` | Quando usar |
|---|---|---|
| Item ↔ Ticket (asset) | `ticket_types` | Computer, Monitor, etc. — recebem suporte |
| Item ↔ Project (qualquer) | `project_asset_types` | Estimate, Computer, qualquer coisa relacionada à execução |
| Item ↔ Document (anexo) | `document_types` | Item pode receber arquivos anexados |
| Item ↔ Asset (geral) | `asset_types` | Item aparece como ativo em listagens |
| Item ↔ State (de ativo) | `state_types` | Item tem `states_id` (estado do CI) |
| Item ↔ Link genérico | `link_types` | Relações soltas via `Link_Itemtype` |
Para administrativos (Contract-like) não há array genérico — sempre tabela dedicada.
## Lições
1. **`Item_Ticket` polimórfico não significa "qualquer item linkado"** — significa "qualquer CI afetado". Misturar administrativo polui a UX.
2. Antes de registrar em `ticket_types`, classificar semanticamente o itemtype (ativo vs administrativo).
3. Para administrativos com vínculo a Ticket, padrão é **tabela dedicada de relação + UI dedicada**, igual Contract faz.
4. Project é mais permissivo — `Item_Project` aceita administrativos sem problema, porque Project é "container de trabalho" e não "registro de incidente".
## Backlog rastreado
- ~~**Estimate v1.1+**: implementar `glpi_plugin_estimate_estimates_tickets` + `Estimate_Ticket`~~
**Implementado em v1.0** com abordagem **polimórfica** (mais idiomática e flexível). Ver seção abaixo.
## Padrão polimórfico: uma tabela serve vários itemtypes
Após implementar Estimate, percebemos que o padrão **`CommonDBRelation` polimórfico** é mais limpo do que criar tabela dedicada por relacionamento. Uma única classe `EstimateLink` + uma única tabela `glpi_plugin_estimate_estimates_items` cobre Estimate ↔ Ticket, Estimate ↔ Project, e qualquer itemtype futuro.
### Schema da tabela polimórfica
```sql
CREATE TABLE `glpi_plugin_<plugin>_<entity>_items` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`plugin_<plugin>_<entity>_id` int unsigned NOT NULL, -- lado fixo
`itemtype` varchar(255) NOT NULL, -- lado polimórfico
`items_id` int unsigned NOT NULL, -- lado polimórfico
`date_creation` timestamp NULL DEFAULT NULL,
`date_mod` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unicity` (`plugin_<plugin>_<entity>_id`, `itemtype`, `items_id`),
KEY `item` (`itemtype`, `items_id`)
);
```
### Classe relação polimórfica
```php
class EntityLink extends \CommonDBRelation
{
public static $itemtype_1 = Entity::class;
public static $items_id_1 = 'plugin_<plugin>_<entity>_id';
public static $itemtype_2 = 'itemtype'; // polimórfico
public static $items_id_2 = 'items_id';
public static $checkItem_2_Rights = self::HAVE_VIEW_RIGHT_ON_ITEM;
public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0)
{
if ($item instanceof Ticket || $item instanceof Project) {
return self::createTabEntry(Entity::getTypeName(2), $count);
}
if ($item instanceof Entity) {
return self::createTabEntry(__('Linked items'), $count);
}
return '';
}
public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0)
{
// Renderização específica conforme $item
}
}
```
### Registro
```php
Plugin::registerClass('GlpiPlugin\\MyPlugin\\EntityLink', [
'addtabon' => [
'Ticket',
'Project',
'GlpiPlugin\\MyPlugin\\Entity',
]
]);
```
### Vantagens da abordagem polimórfica
| Aspecto | Tabela dedicada (Contract_Ticket) | Polimórfica (EstimateLink) |
|---|---|---|
| Schema | 1 tabela por relação | 1 tabela pra todas |
| Classes PHP | 1 classe por relação | 1 classe pra todas |
| Adicionar novo itemtype suportado | Nova tabela + classe + migration | Apenas `addtabon` |
| Padrão GLPI nativo | Contract usa | Item_Devices, Item_Project usam |
| Idiomaticidade GLPI 11 | Tradicional | Mais moderno |
Para a maioria dos casos, **polimórfico é preferível**. Tabela dedicada faz sentido quando há colunas específicas da relação (ex: Contract_Ticket pode ter `start_date_override`).
### Renderização do tab
Em vez de usar `CommonDBRelation::showListForItem` (genérico), customizamos `displayTabContentForItem` com tabela Bootstrap nativa do GLPI:
- Form de adicionar (dropdown da entidade fixa + itemtype/items_id no lado polimórfico)
- Lista com colunas relevantes do domínio
- Botão de remover por linha (delete via `?delete_link=1&id=N`)
Tudo sem CSS customizado — só classes Bootstrap do GLPI (`table table-hover`, `btn btn-primary`, `text-end`, etc.).

View file

@ -0,0 +1,69 @@
---
id: KB-PLUGIN-030
title: "GLPI 11 — Criação de tabelas exige classe Migration (Executing direct queries is not allowed!)"
domain: plugin-dev
tags:
- glpi11
- plugin
- install
- database
- ddl
- migration
- gotcha
status: active
severity: high
created_at: 2026-06-02
updated_at: 2026-06-02
applies_to:
- plugins locais GLPI 11.x
- hook.php
---
# GLPI 11 — Criação de tabelas exige classe Migration
## Sintoma
Ao tentar instalar um plugin via interface web do GLPI (ou CLI), o processo entra em loop (loading infinito na UI) ou falha silenciosamente.
Nos logs (`files/_log/php-errors.log`), o seguinte Fatal Error (Uncaught Exception) aparece:
`glpi.CRITICAL: *** Uncaught PHP Exception Exception: "Executing direct queries is not allowed!" at DBmysql.php`
## Causa raiz
No GLPI 11, o método `$DB->query()` (e os seus derivados como `queryOrDie()`) bloqueia ativamente a execução direta de comandos DDL (Data Definition Language) como `CREATE TABLE`, `ALTER TABLE` ou `DROP TABLE`. Essa é uma medida de segurança e padronização.
Qualquer plugin que tente criar suas tabelas usando `$DB->queryOrDie("CREATE TABLE ...")` dentro de `hook.php` sofrerá quebra imediata na instalação.
## Solução padrão GLPI 11
A criação ou alteração de estrutura de banco de dados deve ser envelopada dentro da classe nativa `\Migration`.
### Errado (Legacy - GLPI 9.x/10.x)
```php
function plugin_meuplugin_install() {
global $DB;
$query = "CREATE TABLE `glpi_plugin_meuplugin_table` (...)";
$DB->queryOrDie($query, $DB->error());
}
```
### Correto (GLPI 11)
```php
function plugin_meuplugin_install() {
global $DB;
$migration = new \Migration(MEUPLUGIN_VERSION);
if (!$DB->tableExists('glpi_plugin_meuplugin_table')) {
$query = "CREATE TABLE `glpi_plugin_meuplugin_table` (...)";
$migration->addPostQuery($query);
}
// Essencial: executa a fila de queries da migration
$migration->executeMigration();
return true;
}
```
## Checklist de validação
Sempre procure nos arquivos de instalação (`hook.php` ou `setup.php`) se o plugin chama diretamente `$DB->query` para criar tabelas. Se encontrar, reescreva para usar `$migration->addPostQuery()`.

View file

@ -0,0 +1,179 @@
---
id: KB-PLUGIN-031
title: "Runbook — Workflow de desenvolvimento e deploy DEV de plugins GLPI"
domain: plugin-dev
tags:
- runbook
- workflow
- dev
- deploy
- forgejo
- scaffold
- validation
- console
status: active
severity: high
created_at: 2026-06-11
updated_at: 2026-06-11
applies_to:
- GLPI 11.x no ambiente dev (CT 100 docker, stack GLPI11)
- qualquer plugin novo desenvolvido internamente
related_records:
- KB-INFRA-001
- KB-INFRA-002
- KB-PLUGIN-001
- KB-PLUGIN-004
- KB-PLUGIN-013
- KB-PLUGIN-018
- KB-PLUGIN-027
---
# Runbook — Workflow de desenvolvimento e deploy DEV de plugins GLPI
Procedimento padrão do nascimento de um plugin até sua validação no GLPI de
desenvolvimento. A **publicação em produção** (Mindplace/licenciamento) é outro
processo — ver [KB-PLUGIN-013](KB-PLUGIN-013-mindplace-plugin-publication-runbook.md).
Primeiro plugin a seguir este fluxo de ponta a ponta: `assetinherit` (2026-06-11).
## Mapa do ambiente
| Componente | Onde |
|---|---|
| Servidor dev (docker) | CT 100 — `root@192.168.100.49` (SSH key do Mac já cadastrada) |
| Diretório de plugins | `/opt/projects/GLPI11/docker/glpi/plugins/` (bind → `/var/www/glpi/plugins`, ver [KB-INFRA-001]) |
| Container GLPI | `glpi11-app` (GLPI dev em `http://192.168.100.49:8081`) |
| Git de dev | Forgejo local — `git@192.168.100.101:administrador/<plugin>.git` (remote `origin`) |
| Git de produção | Forgejo Mindtek — remote `production`, usado **só** na publicação |
## Fase 1 — Scaffold do plugin
Estrutura mínima obrigatória (a chave/diretório do plugin define TUDO — funções,
hooks, constantes):
```
<key>/
├── setup.php # plugin_init_<key>, plugin_version_<key> + 4 callbacks
├── hook.php # install/uninstall e demais hooks
├── logo.png # PNG 128x128 na raiz — obrigatório [KB-INFRA-002]
├── README.md # dor que resolve, como funciona, instalação, configuração
├── CHANGELOG.md # Keep a Changelog
├── LICENSE # GPL-3.0-or-later (compatível com o GLPI)
└── .gitignore # padrão de plugins [KB-PLUGIN-013]
```
Checklist de armadilhas conhecidas:
- [ ] `setup.php` com os **4 callbacks**: `check_prerequisites`, `check_config`,
`install`, `uninstall` — sem eles a instalação falha genericamente
([KB-PLUGIN-001]).
- [ ] Toda chave em `$PLUGIN_HOOKS[...]['<key>']` é o **plugin key exato**
(= nome do diretório). Variações falham em silêncio ([KB-PLUGIN-004]).
- [ ] `logo.png` é PNG real (SVG renomeado não funciona) ([KB-INFRA-002]).
- [ ] `version` no `setup.php` em SemVer — o release de produção lê de lá.
## Fase 2 — Repositório no Forgejo local
Criar o repo via API (ou UI em `http://192.168.100.101:3000`):
```bash
curl -s -u 'administrador:<senha>' -X POST \
http://192.168.100.101:3000/api/v1/user/repos \
-H 'Content-Type: application/json' \
-d '{"name":"<key>","private":true,"default_branch":"main"}'
```
No diretório do plugin (na máquina de dev):
```bash
git init && git branch -M main
git add -A && git commit -m "<key> 0.1.0: scaffold"
git remote add origin git@192.168.100.101:administrador/<key>.git
git push -u origin main
```
Convenção de remotes: `origin` = Forgejo local (push contínuo de dev);
`production` = Forgejo Mindtek (adicionado apenas na publicação, [KB-PLUGIN-013]).
## Fase 3 — Deploy no servidor dev
```bash
ssh root@192.168.100.49
cd /opt/projects/GLPI11/docker/glpi/plugins
git clone git@192.168.100.101:administrador/<key>.git
git config --global --add safe.directory \
/opt/projects/GLPI11/docker/glpi/plugins/<key>
chown -R www-data:www-data <key>
```
O bind mount entrega o plugin no container instantaneamente — **não** precisa
recriar o container.
## Fase 4 — Instalação e ativação (sempre via console)
```bash
docker exec glpi11-app sh -c '
php -l /var/www/glpi/plugins/<key>/setup.php &&
php -l /var/www/glpi/plugins/<key>/hook.php &&
php /var/www/glpi/bin/console plugin:install <key> -n --username=glpi &&
php /var/www/glpi/bin/console plugin:activate <key> -n &&
php /var/www/glpi/bin/console plugin:list | grep <key>'
```
Esperado: status **Habilitado**. O console dá erros legíveis; a UI só mostra
falha genérica.
## Fase 5 — Validação funcional (teste E2E em CLI)
⚠️ **Gotcha do GLPI 11:** em CLI, `include 'inc/includes.php'` **não** carrega
mais o core (classes como `RuleAsset` ficam indisponíveis). O bootstrap correto
para scripts de teste é o do `bin/console`:
```php
require '/var/www/glpi/vendor/autoload.php';
$kernel = new \Glpi\Kernel\Kernel();
$kernel->boot(); // carrega core + plugins ativos (plugin_init roda aqui)
// sessão CLI mínima para CommonDBTM::add()/update()
$_SESSION['glpiactiveentities'] = [0];
$_SESSION['glpiactive_entity'] = 0;
$_SESSION['glpiactiveprofile']['interface'] = 'central';
```
Padrão do teste: criar massa de dados de teste com prefixo `TEST`
exercitar os cenários → **deletar tudo com purge no final**. Executar:
```bash
scp test_<key>.php root@192.168.100.49:/tmp/
ssh root@192.168.100.49 'docker cp /tmp/test_<key>.php glpi11-app:/tmp/ &&
docker exec glpi11-app php /tmp/test_<key>.php'
```
Exemplo real completo: teste E2E do `assetinherit` (6 cenários, incluindo
descoberta de comportamento que virou documentação — critério `PATTERN_EXISTS`
não casa com `users_id = 0`).
## Fase 6 — Ciclo de iteração
1. IDE no Mac → Remote SSH em `root@192.168.100.49`, pasta do plugin.
2. Editar in-place — GLPI é PHP, refresh no browser e a mudança aparece.
3. `git add . && git commit && git push` → Forgejo local (`origin`).
4. Mudanças feitas no Mac: `git push origin` no Mac + `git pull` no CT 100
(lembrar `chown -R www-data:www-data` se criar arquivos novos como root).
## Fase 7 — Promoção para produção (fora deste runbook)
Quando validado no dev: seguir [KB-PLUGIN-013] (kill switch de licença,
release ZIP com wrapper directory [KB-PLUGIN-018], catálogo Mindplace,
serial do cliente).
## Regra para agentes
Ao criar/depurar plugin novo no ambiente dev, seguir este runbook na ordem.
Antes de diagnosticar erro funcional, validar Fases 3-4 (mount, ownership,
instalação via console). Para testes E2E em CLI, usar SEMPRE o bootstrap do
Kernel (Fase 5), nunca `inc/includes.php`.
## Classificação
- Tipo: Runbook operacional de desenvolvimento.
- Reutilização: obrigatória para todo plugin novo.