KB-PLUGIN-032/033: segunda conexão DBmysql + comandos de console de plugin (GLPI 11)

Aprendizados validados no desenvolvimento do plugin sync:
- KB-PLUGIN-032: subclasse DBmysql read-only para banco externo (gotchas de
  senha rawurldecode, query() guard, flag connected).
- KB-PLUGIN-033: comandos de console de plugin exigem src/ + namespace de nome
  plugins:<key>:.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Rodolpho Lopes 2026-06-16 17:16:46 +00:00
parent 135b621576
commit f1ec0c6405
3 changed files with 226 additions and 1 deletions

View file

@ -1,6 +1,6 @@
{ {
"version": "1.0.0", "version": "1.0.0",
"last_updated": "2026-06-11", "last_updated": "2026-06-16",
"records": [ "records": [
{ {
"id": "KB-INFRA-001", "id": "KB-INFRA-001",
@ -571,6 +571,43 @@
"severity": "high", "severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md", "path": "records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md",
"summary": "id: KB-PLUGIN-031" "summary": "id: KB-PLUGIN-031"
},
{
"id": "KB-PLUGIN-032",
"title": "\"GLPI 11 — Segunda conexão de banco read-only via subclasse de DBmysql\"",
"domain": "plugin-dev",
"tags": [
"glpi11",
"plugin",
"database",
"dbmysql",
"connection",
"read-only",
"external-db",
"sync"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-032-glpi11-second-dbmysql-connection.md",
"summary": "id: KB-PLUGIN-032"
},
{
"id": "KB-PLUGIN-033",
"title": "\"GLPI 11 — Comandos de console de plugin exigem src/ e namespace de nome plugins:<key>:\"",
"domain": "plugin-dev",
"tags": [
"glpi11",
"plugin",
"console",
"symfony",
"command",
"cli",
"gotcha"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-033-glpi11-plugin-console-commands.md",
"summary": "id: KB-PLUGIN-033"
} }
] ]
} }

View file

@ -0,0 +1,98 @@
---
id: KB-PLUGIN-032
title: "GLPI 11 — Segunda conexão de banco read-only via subclasse de DBmysql"
domain: plugin-dev
tags:
- glpi11
- plugin
- database
- dbmysql
- connection
- read-only
- external-db
- sync
status: active
severity: high
created_at: 2026-06-16
updated_at: 2026-06-16
applies_to:
- plugins GLPI 11.x que precisam ler de um banco GLPI externo
- plugin Sync (ingestão PROD legado → QA)
related_records:
- KB-PLUGIN-030
- KB-PLUGIN-014
---
# GLPI 11 — Segunda conexão de banco via subclasse de DBmysql
## Contexto
Um plugin pode precisar ler de um **segundo banco** (ex.: o PROD legado de um
cliente) sem tocar na conexão padrão do GLPI (`global $DB`). O core já oferece
toda a infraestrutura: basta uma subclasse de `\DBmysql` apontando para o outro
host. Reaproveita-se `doQuery()`, `request()`, `fetchAssoc()`, `tableExists()` etc.
## Como fazer
`DBmysql::connect()`**propriedades de instância**: `$dbhost`, `$dbuser`,
`$dbpassword`, `$dbdefault`. O construtor nativo (`__construct($choice)`) chama
`connect()` imediatamente — portanto **não chame `parent::__construct()`**:
defina as propriedades primeiro e só então chame `$this->connect()`.
```php
namespace GlpiPlugin\Meuplugin;
use DBmysql;
class SourceDB extends DBmysql
{
public function __construct(string $host, string $dbname, string $user, string $password, int $port = 3306, bool $use_utf8mb4 = true)
{
$this->dbhost = $port > 0 ? $host . ':' . $port : $host;
$this->dbuser = $user;
$this->dbpassword = rawurlencode($password); // ver gotcha abaixo
$this->dbdefault = $dbname;
$this->use_utf8mb4 = $use_utf8mb4;
$this->connect();
}
public function isConnected(): bool
{
return (bool) $this->connected;
}
}
```
## Gotchas confirmados
1. **Senha é rawurldecoded em `connect()`.** A linha real do core é
`real_connect($host, $this->dbuser, rawurldecode($this->dbpassword), ...)`.
Portanto guarde a senha **`rawurlencode()`-ada** na propriedade (idêntico ao
`config/config_db.php` nativo). Senha com `%`, `+`, etc. quebra se passada crua.
2. **Host com porta:** `connect()` faz `explode(':', $host)`. Passe `"host:porta"`
numa string só; não há parâmetro de porta separado.
3. **`query()` SEMPRE lança exceção** (`Executing direct queries is not allowed!`)
— é um guard, não o executor. Para **SELECT** use **`doQuery()`** (retorna
`mysqli_result`) + `fetchAssoc()`/`fetchArray()`, ou `request()` (iterator).
Para DDL, ver [KB-PLUGIN-030].
4. **Sucesso da conexão:** `connect()` seta `$this->connected = true` só em caso de
sucesso; em falha permanece `false` sem lançar exceção. Cheque `connected`
(a falha de credencial NÃO crasha — retorna conexão não-conectada).
5. **Defaults seguros** em DBmysql: `dbssl=false`, `use_utf8mb4=false`,
`connected=false`. Para ler bancos GLPI 10.0.6+ defina `use_utf8mb4=true`.
## Validação
Comprovado no plugin `sync` (2026-06-16): subclasse `SourceDB` conectou a um
GLPI 11.0.7 externo via `bin/console plugins:sync:test-connection`, leu
`VERSION()`, `glpi_configs` (versão) e `COUNT(*)` de `glpi_tickets`. Senha
inválida retornou `connected=false` com exit code 1, sem fatal.
## Classificação
- Tipo: padrão de implementação reutilizável.
- Reutilização: qualquer plugin com fonte de dados em segundo banco.

View file

@ -0,0 +1,90 @@
---
id: KB-PLUGIN-033
title: "GLPI 11 — Comandos de console de plugin exigem src/ e namespace de nome plugins:<key>:"
domain: plugin-dev
tags:
- glpi11
- plugin
- console
- symfony
- command
- cli
- gotcha
status: active
severity: high
created_at: 2026-06-16
updated_at: 2026-06-16
applies_to:
- plugins GLPI 11.x que expõem comandos de bin/console
related_records:
- KB-PLUGIN-031
- KB-PLUGIN-032
---
# GLPI 11 — Comandos de console de plugin
## Como o core descobre os comandos
`Glpi\Console\CommandLoader::findPluginCommands()` varre, para cada plugin
**ativo**, os diretórios `inc/` e `src/`, instanciando classes que sejam
`Symfony\Component\Console\Command\Command`. Os prefixos de classe tentados são:
- `GlpiPlugin\<Ucfirst(key)>\` (namespaced — recomendado), ou
- `Plugin<Ucfirst(key)>` (legado não-namespaced), ou
- vazio (PSR-4 sem namespace).
O `src/` do plugin é registrado como PSR-4 `GlpiPlugin\<Ucfirst(key)>\ → src/`
(`Plugin.php`, ~linha 406). Então `src/Console/FooCommand.php` deve declarar
`namespace GlpiPlugin\<Ucfirst(key)>\Console;`.
## Regra rígida do nome (a armadilha)
O nome do comando **DEVE** casar com o padrão `plugins:<key>:<algo>` (regex no
core: `^plugins:<key>(:[^:]+)+$`). Se não casar, o core emite
`E_USER_WARNING` ("must be moved in the `plugins:<key>` namespace") e **não
registra** o comando — ele simplesmente não aparece em `bin/console`.
```php
namespace GlpiPlugin\Sync\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class TestConnectionCommand extends Command
{
protected function configure(): void
{
$this->setName('plugins:sync:test-connection'); // <- obrigatório
$this->setDescription('...');
$this->addOption('host', null, InputOption::VALUE_REQUIRED, '...');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// ... lógica ...
return Command::SUCCESS; // SUCCESS / FAILURE / INVALID
}
}
```
## Notas
- Estender o `Command` do Symfony direto é suficiente (não exige
`Glpi\Console\AbstractCommand`). O Kernel já está bootado quando o comando roda,
então classes do core e dos plugins ativos estão disponíveis.
- O plugin precisa estar **ativado** para os comandos aparecerem.
- Arquivos de teste sob `src/`/`inc/` que casem o padrão também são varridos —
manter scripts de teste fora desses dirs (ver `.gitignore` do plugin).
## Validação
Comprovado no plugin `sync` (2026-06-16): `plugins:sync:test-connection`
registrou e executou; ver [KB-PLUGIN-032] para o conteúdo do comando (segunda
conexão DBmysql).
## Classificação
- Tipo: gotcha + padrão de implementação.
- Reutilização: qualquer plugin com comando de CLI.