From f1ec0c640551cb854093ee996c88715d0c04c5b5 Mon Sep 17 00:00:00 2001 From: Rodolpho Lopes Date: Tue, 16 Jun 2026 17:16:46 +0000 Subject: [PATCH] =?UTF-8?q?KB-PLUGIN-032/033:=20segunda=20conex=C3=A3o=20D?= =?UTF-8?q?Bmysql=20+=20comandos=20de=20console=20de=20plugin=20(GLPI=2011?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::. Co-Authored-By: Claude Fable 5 --- index.json | 39 +++++++- ...IN-032-glpi11-second-dbmysql-connection.md | 98 +++++++++++++++++++ ...UGIN-033-glpi11-plugin-console-commands.md | 90 +++++++++++++++++ 3 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 records/plugin-dev/KB-PLUGIN-032-glpi11-second-dbmysql-connection.md create mode 100644 records/plugin-dev/KB-PLUGIN-033-glpi11-plugin-console-commands.md diff --git a/index.json b/index.json index 549a08f..d3741ad 100755 --- a/index.json +++ b/index.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-06-11", + "last_updated": "2026-06-16", "records": [ { "id": "KB-INFRA-001", @@ -571,6 +571,43 @@ "severity": "high", "path": "records/plugin-dev/KB-PLUGIN-031-plugin-dev-workflow-runbook.md", "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::\"", + "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" } ] } diff --git a/records/plugin-dev/KB-PLUGIN-032-glpi11-second-dbmysql-connection.md b/records/plugin-dev/KB-PLUGIN-032-glpi11-second-dbmysql-connection.md new file mode 100644 index 0000000..899a5eb --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-032-glpi11-second-dbmysql-connection.md @@ -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()` lê **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. diff --git a/records/plugin-dev/KB-PLUGIN-033-glpi11-plugin-console-commands.md b/records/plugin-dev/KB-PLUGIN-033-glpi11-plugin-console-commands.md new file mode 100644 index 0000000..38bb8d8 --- /dev/null +++ b/records/plugin-dev/KB-PLUGIN-033-glpi11-plugin-console-commands.md @@ -0,0 +1,90 @@ +--- +id: KB-PLUGIN-033 +title: "GLPI 11 — Comandos de console de plugin exigem src/ e namespace de nome plugins::" +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\\` (namespaced — recomendado), ou +- `Plugin` (legado não-namespaced), ou +- vazio (PSR-4 sem namespace). + +O `src/` do plugin é registrado como PSR-4 `GlpiPlugin\\ → src/` +(`Plugin.php`, ~linha 406). Então `src/Console/FooCommand.php` deve declarar +`namespace GlpiPlugin\\Console;`. + +## Regra rígida do nome (a armadilha) + +O nome do comando **DEVE** casar com o padrão `plugins::` (regex no +core: `^plugins:(:[^:]+)+$`). Se não casar, o core emite +`E_USER_WARNING` ("must be moved in the `plugins:` 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.