docs: mapeamento completo do EMCP Tools (11 docs, ~6070 linhas)
Especificação funcional do plugin emcp-tools v3.12.1 (msrbuilds/elementor-mcp, GPL-2.0-or-later) via leitura directa do código-fonte em emanuelalmeida.pt. - 00: arquitectura (bootstrap, ability registrar, dispatcher, MCP adapter) - 01: Elementor classico (paginas, layout, widgets, templates, globals) - 02: Elementor Atomic v4 + Gutenberg - 03: WordPress core (conteudo, media, settings, temas) - 04: Themer (CPT, condicoes, render, PHP templates) - 05: Redirects + change ledger unificado (rollback) - 06: Sandbox PHP snippets + custom widgets - 07: Filesystem/DB/WP-CLI/Security/Performance (maior risco) - 08: Integracoes terceiros (ACF, Meta Box, forms, SEO) - 09: Stock images + Cloud + OAuth - 10: Sistema de modulos + inventario Pro-only (30 classes) - INDEX: sintese, sequencia de construcao, tabela de risco Produzido por 10 subagentes code-explorer em paralelo + revisao cruzada de consistencia. Cada doc inclui blueprint de replica (copiar/simplificar/omitir).
This commit is contained in:
@@ -0,0 +1,687 @@
|
||||
# 04 — Themer (header/footer/single/archive/search/404, condições, render, blocos/widgets dinâmicos, Themer PHP)
|
||||
|
||||
Fonte: leitura directa do código-fonte `emcp-tools` v3.12.1 (build Free), instalado em
|
||||
`emanuelalmeida.pt` (`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`),
|
||||
19-08-2026. Módulo confirmado como o mais substancial do bundle Free (`skill://emcp-tools` §6-§7):
|
||||
CPT + condições + render controller + blocos + widgets + templates PHP — só a categoria "Themer
|
||||
(free)" já soma 8 tools sempre activas, mais 5 tools do sub-toggle "Themer PHP" quando ligado.
|
||||
|
||||
## 0. Visão geral e ciclo de vida do módulo
|
||||
|
||||
`EMCP_Tools_Themer_Module` (`includes/modules/class-themer-module.php`) estende
|
||||
`EMCP_Tools_Module`: `id()='themer'`, `tier()='free'`, `default_active()=true` — activo por
|
||||
omissão em qualquer instalação. `register()` é chamado pelo `EMCP_Tools_Modules_Registry` em
|
||||
`init:5`, só quando o módulo está activo, e é o único ponto de entrada que fia tudo:
|
||||
|
||||
```php
|
||||
public function register(): void {
|
||||
( new EMCP_Tools_Themer_CPT() )->register();
|
||||
if ( class_exists( 'EMCP_Tools_Themer_HFE_Conflict' ) ) { EMCP_Tools_Themer_HFE_Conflict::init(); }
|
||||
EMCP_Tools_Themer_Index::register_hooks();
|
||||
// one-time heal do índice (ver §6 — bug histórico de ordem de save)
|
||||
if ( ! is_admin() ) { ( new EMCP_Tools_Themer_Render_Controller() )->init(); }
|
||||
if ( is_admin() && class_exists( 'EMCP_Tools_Themer_Metabox' ) ) { ( new EMCP_Tools_Themer_Metabox() )->init(); }
|
||||
if ( class_exists( 'EMCP_Tools_Themer_Blocks' ) ) { ( new EMCP_Tools_Themer_Blocks() )->init(); }
|
||||
if ( class_exists( 'EMCP_Tools_Themer_Widgets' ) ) { ( new EMCP_Tools_Themer_Widgets() )->init(); }
|
||||
if ( class_exists( 'EMCP_Tools_Themer_PHP' ) ) { ( new EMCP_Tools_Themer_PHP() )->init(); }
|
||||
}
|
||||
```
|
||||
|
||||
**Gate de kill-switch verdadeiro:** desligar o módulo (`emcp_tools_active_modules` sem `themer`)
|
||||
pára o CPT, o take-over de front-end, a tab de admin — **e** o registrador de abilities
|
||||
(`class-ability-registrar.php`, linhas 202-211) omite as 8 tools do grupo Themer, porque o
|
||||
registo de abilities corre em `wp_abilities_api_init` (ANTES de `init:5`), pelo que a condição
|
||||
`EMCP_Tools_Themer_Module::is_enabled()` tem de ler a option `emcp_tools_active_modules`
|
||||
directamente em vez de depender de qualquer estado que só existiria depois do boot do módulo:
|
||||
|
||||
```php
|
||||
public static function is_enabled(): bool {
|
||||
$active = (array) get_option( EMCP_Tools_Module::OPTION_ACTIVE, array() );
|
||||
return in_array( 'themer', $active, true );
|
||||
}
|
||||
```
|
||||
|
||||
**Sub-toggle independente — Themer PHP:** as 5 tools `*-theme-php-template` só se registam
|
||||
quando, ADICIONALMENTE ao módulo Themer estar activo, a option própria
|
||||
`emcp_tools_themer_php_enabled` estiver a `'1'` (`EMCP_Tools_Themer_PHP::enabled()`,
|
||||
`includes/abilities/class-ability-registrar.php` linhas 213-221). É desligado por omissão em
|
||||
qualquer instalação nova.
|
||||
|
||||
**Estruturas de dados (todas nativas WordPress, zero tabelas SQL próprias):**
|
||||
|
||||
| Estrutura | Tipo | Papel |
|
||||
|---|---|---|
|
||||
| CPT `emcp_theme_template` | post type, `show_ui=true`, menu próprio | O template em si (título + conteúdo Elementor/Gutenberg/clássico) |
|
||||
| meta `_emcp_themer_type` | string | `header\|footer\|single\|archive\|search\|404` |
|
||||
| meta `_emcp_themer_conditions` | array `{include:Rule[], exclude:Rule[], priority:int}` | Condições de exibição |
|
||||
| meta `_emcp_themer_php_template` | int (post id) | Template PHP anexado a este slot (0/ausente = usa o conteúdo do builder) |
|
||||
| option `emcp_tools_themer_index` (autoloaded) | `{type => rows[]}` | Índice pré-computado, ver §6 — o "fast path" de zero queries no front-end |
|
||||
| option `emcp_tools_themer_index_healed` | `'1'` | Marcador do heal único (bug histórico, ver §6) |
|
||||
| option `emcp_tools_module_themer_force_render` | `'0'\|'1'` | Full-page takeover para temas não suportados |
|
||||
| option `emcp_tools_themer_php_enabled` | `'0'\|'1'` | Sub-toggle Themer PHP |
|
||||
| option `emcp_tools_hfe_conflict_dismissed` | `'1'` | Dispensa da notice de conflito com Ultimate Addons for Elementor |
|
||||
| CPT `emcp_theme_php` (privado, `show_ui=false`) | post type | Templates PHP em bruto (§11) |
|
||||
| meta `_emcp_theme_php_code`/`_type`/`_validation`/`_hash`/`_error` | — | Estado do template PHP |
|
||||
| ficheiro `{sandbox}/theme-php/{id}.php` + `theme-php-manifest.json` | filesystem | Função PHP compilada + manifesto hash-verificado (§11.2-11.3) |
|
||||
|
||||
**Filtros de extensão (o seam onde a versão Pro se encaixa sem qualquer código Pro na árvore
|
||||
free):**
|
||||
|
||||
| Filtro | Free devolve | O que o Pro acrescentaria |
|
||||
|---|---|---|
|
||||
| `emcp_themer_selectors` | 7 chaves largas (`entire-site`, `all-singular`, `all-archives`, `front-page`, `post-type`, `post-type-archive`, `tax-archive`) | Selectores granulares (`post`, `term`, `author`, `date`) — a mera PRESENÇA de `'post'` neste array é usada em vários sítios (`pro_conditions_available()`, `is_pro()`) como o teste "é Pro?" |
|
||||
| `emcp_themer_matchers` | 7 matchers correspondentes | Matchers para os selectores granulares acima |
|
||||
| `emcp_themer_condition_schema` | Só a relação Include + folhas largas | Relação Exclude + pesquisa de objecto específico + nós Author/Date/In-term |
|
||||
| `emcp_themer_rank` | `fn($row) => 0` (sem prioridade real) | Um ranker que lê `$row['priority']` de facto |
|
||||
| `emcp_themer_quota` | `1` (por tipo) | `PHP_INT_MAX` |
|
||||
| `emcp_themer_theme_adapters` | 7 temas mapeados (Astra/GeneratePress/Kadence/OceanWP/Blocksy/Neve/Hello Elementor) | Mais temas, ou pode ser estendido por qualquer terceiro |
|
||||
|
||||
---
|
||||
|
||||
## 1. Abilities MCP — `EMCP_Tools_Themer_Abilities` (`includes/abilities/class-themer-abilities.php`)
|
||||
|
||||
Regista sempre as 8 tools quando o módulo Themer está activo (gate em §0). Duas permissões
|
||||
partilhadas: `check_read_permission` (`edit_posts`) e `check_write_permission`
|
||||
(`edit_post($template_id)` se o id já existir; senão `publish_pages || edit_pages` para criação).
|
||||
|
||||
| Tool | Input schema (resumo) | O que faz | Permissão | readonly / destructive |
|
||||
|---|---|---|---|---|
|
||||
| `list-theme-templates` | `{ type?: string }` | `WP_Query` sobre `emcp_theme_template` (publish+draft, até 200), filtrável por tipo via `meta_key`; devolve `{templates: summary[]}` com `template_id, title, type, status, conditions, edit_url`. | `check_read_permission` | readonly, idempotent |
|
||||
| `get-theme-template` | `{ template_id: int }` **obrigatório** | Devolve `template_id, title, type, conditions, builder (elementor/gutenberg/classic via `Content_Renderer::detect_builder`), content` (post_content bruto). | `check_read_permission` | readonly, idempotent |
|
||||
| `list-condition-targets` | `{}` | Discovery para `set-template-conditions`: `selectors` (o set válido actual via `valid_selectors()`), `post_types` (todos os públicos), `taxonomies` (todas as públicas + `object_types`). | `check_read_permission` | readonly, idempotent |
|
||||
| `create-theme-template` | `{ type: enum(6 tipos)*, title?, content?, scope? }` | Cria o post CPT; aplica a quota 1-por-tipo (`Themer_CPT::can_create`) — devolve `{error}` se excedida; semeia um `scope` largo automático por omissão (header/footer→`entire-site`, single→`all-singular`, archive→`all-archives`); valida o `scope` contra `valid_selectors()` (selector inválido = template criado mas SEM condição, não falha); chama `Themer_Index::rebuild()` no fim. | `check_write_permission` | write, não destructive, não idempotent |
|
||||
| `update-theme-template` | `{ template_id: int*, title?, content? }` | `wp_update_post` parcial (só os campos passados). | `check_write_permission` | write, não idempotent |
|
||||
| `set-template-conditions` | `{ template_id: int*, include: object[]*, exclude?: object[], priority?: int }` | Valida cada regra (`include`+`exclude`) contra `valid_selectors()`; rejeita com erro se `exclude` não vazio e sem camada Pro (`pro_conditions_available()`); ignora silenciosamente `priority` não-zero em free (é um Pro tie-break); grava a meta `_emcp_themer_conditions`; `Themer_Index::rebuild()`. | `check_write_permission` | write, idempotent |
|
||||
| `delete-theme-template` | `{ template_id: int*, force?: bool }` | `wp_delete_post($id, $force)` — trash por omissão, `force=true` apaga definitivo; `Themer_Index::rebuild()`. | `check_write_permission` | **destructive** |
|
||||
| `resolve-template` | `{ post_id?: int, context?: enum(front-page,search,404) }` | Constrói um contexto (`Themer_Context::from_parts`) a partir do `post_id`/`context` dado, corre `Themer_Resolver::resolve()` com um registry fresco e o ranker `emcp_themer_rank` (free = 0), devolve `{slots:{header,body,footer}, context}` — verificação directa de "que template ganha aqui". | `check_read_permission` | readonly, idempotent |
|
||||
|
||||
**Nota de design em `execute_set_conditions`:** o comentário no código é explícito — regras
|
||||
`exclude` **não são silenciosamente ignoradas** em free, são **rejeitadas com erro**
|
||||
("Exclude rules require EMCP Pro"), porque um `exclude` que nunca é avaliado (porque não há
|
||||
matcher registado para o selector) equivaleria a um no-op silencioso — falhar alto evita que o
|
||||
agente pense que configurou uma exclusão que na verdade nunca aplica.
|
||||
|
||||
---
|
||||
|
||||
## 2. Abilities MCP — `EMCP_Tools_Themer_PHP_Abilities` (`includes/abilities/class-themer-php-abilities.php`)
|
||||
|
||||
5 tools, só registadas quando `EMCP_Tools_Themer_PHP::enabled()` (módulo Themer activo **E**
|
||||
`emcp_tools_themer_php_enabled='1'`). Duas permissões: escrita exige
|
||||
`EMCP_Tools_Themer_PHP_Store::can_edit()` (`manage_options` **E** `unfiltered_html` — a dupla
|
||||
capability é deliberada, ver §11.2); leitura exige `can_read()` (`manage_options`).
|
||||
|
||||
| Tool | Input schema (resumo) | O que faz | Permissão | readonly / destructive |
|
||||
|---|---|---|---|---|
|
||||
| `create-theme-php-template` | `{ code: string*, type: enum(header,footer,single,archive,any)* , title? }` | Cria um `emcp_theme_php` em DRAFT via `Themer_PHP_Store::create_draft()`; valida com `EMCP_Tools_PHP_Snippet_Validator` (parse PHP + heurísticas de segurança do sandbox partilhado, ver doc 06); rejeita se inválido/inseguro. **Não existe tool `attach`** — deliberado (ver §11 e blueprint). | `check_write_permission` | write, não destructive |
|
||||
| `list-theme-php-templates` | `{ type?: string }` | Lista drafts (`id, title, type, compiled, last_error`), filtro opcional por tipo. | `check_read_permission` | readonly, idempotent |
|
||||
| `get-theme-php-template` | `{ template_id: int* }` | Registo completo: código, tipo, estado compilado, relatório de validação. | `check_read_permission` | readonly, idempotent |
|
||||
| `update-theme-php-template` | `{ template_id: int*, title?, code?, type? }` | Actualização parcial; re-valida sempre; se já estava compilado (referenciado por um post Themer), recompila a partir do novo código. | `check_write_permission` | write, não idempotent |
|
||||
| `delete-theme-php-template` | `{ template_id: int* }` | Apaga o registo CPT **e** o ficheiro compilado no sandbox (`decompile()` + `wp_delete_post(force=true)`). | `check_write_permission` | **destructive**, idempotent |
|
||||
|
||||
**Blueprint-relevante:** o comentário de topo do ficheiro é a especificação do modelo de
|
||||
segurança inteiro numa frase — "*AI authors + validates DRAFT PHP templates; there is
|
||||
intentionally no attach tool — a human selects a template in the Themer metabox (the execution
|
||||
gate)*". Isto é o padrão mais reutilizável de todo este documento — ver blueprint final.
|
||||
|
||||
---
|
||||
|
||||
## 3. CPT + quota — `EMCP_Tools_Themer_CPT` (`includes/themer/class-themer-cpt.php`)
|
||||
|
||||
Regista `emcp_theme_template`: `public=false` mas `publicly_queryable=true` (deliberado — o
|
||||
comentário explica: permite ao iframe de preview do editor Elementor renderizar a própria vista
|
||||
singular do template; fica fora de menus/pesquisa/arquivos via `exclude_from_search=true`,
|
||||
`has_archive=false`, `rewrite=false`). `menu_position=21` (logo a seguir a Páginas). Suporta
|
||||
`title, editor, author, custom-fields`.
|
||||
|
||||
**Gotcha WordPress genérico, útil para qualquer CPT editável com Elementor:**
|
||||
`add_post_type_support( self::POST_TYPE, 'elementor' )` é OBRIGATÓRIO — o Elementor faz gate do
|
||||
seu editor em `post_type_supports($type, 'elementor')`; sem isto, "Edit with Elementor" não faz
|
||||
absolutamente nada (falha silenciosa, sem erro visível). Também precisa dos dois filtros
|
||||
`elementor/cpt_support/get_public_post_types` e `elementor/utils/get_public_post_types` para o
|
||||
Elementor listar o CPT nos sítios certos da UI.
|
||||
|
||||
**Quota (o mecanismo de "free = 1 por tipo"):**
|
||||
|
||||
```php
|
||||
public static function quota( string $type ): int {
|
||||
return (int) apply_filters( 'emcp_themer_quota', 1, $type ); // Pro sobe para PHP_INT_MAX
|
||||
}
|
||||
public static function can_create( string $type, int $existing_count ): bool {
|
||||
return $existing_count < self::quota( $type );
|
||||
}
|
||||
```
|
||||
|
||||
`count_of_type()` conta ao vivo via `WP_Query` (`found_posts`, sem cache) — chamado tanto pela
|
||||
ability `create-theme-template` (§1) como pela UI (`render_free_limits_notice()`, que desenha
|
||||
"chips" por tipo `used/cap` na lista do CPT).
|
||||
|
||||
**Duas heurísticas de UX no ecrã de listagem, dignas de nota como padrão de qualidade:**
|
||||
|
||||
1. **`render_adapter_notice()`** — mostra se o tema activo é directamente suportado pelo mapa de
|
||||
adapters (§5.3); se não for, explica as duas alternativas (tag `emcp_themer_location()` ou o
|
||||
toggle de full-page-takeover).
|
||||
2. **`render_type_mismatch_notice()`** — heurística de detecção de erro humano: percorre todos os
|
||||
templates e sinaliza (a) templates sem `type` definido (nunca renderizam), (b) um template
|
||||
`header`/`footer` cujo conteúdo contém elementos body-only (detecta por substring
|
||||
`emcp/post-title`, `emcp/archive-loop`, etc no `post_content`/`_elementor_data` — sinal de que
|
||||
o utilizador construiu conteúdo de página dentro de um template de header por engano), (c) um
|
||||
template cujo TÍTULO sugere um tipo diferente do `type` gravado (`type_hint_from_title()` —
|
||||
conservador, só palavras-chave inequívocas como "header"/"404"/"single"). Isto é puro código
|
||||
de qualidade-de-vida sem qualquer dependência de licença — vale a pena copiar tal-e-qual.
|
||||
|
||||
---
|
||||
|
||||
## 4. Sistema de condições — o núcleo mais reutilizável do módulo
|
||||
|
||||
Arquitectura em 5 peças puras + 1 fio de ligação WordPress, desenhada para nunca tocar a BD no
|
||||
caminho crítico do front-end (ver §6 para o índice que torna isto possível).
|
||||
|
||||
### 4.1 Schema da UI — `EMCP_Tools_Themer_Condition_Schema` (`class-themer-condition-schema.php`)
|
||||
|
||||
`for_type(string $type): {relations, groups}` — constrói a árvore de opções em cascata que o
|
||||
metabox (JS `themer-conditions.js`) consome: Relação (`include`, +`exclude` via filtro Pro) →
|
||||
Grupo (`Entire site`/`Archives`/`Singular`, condicionados por tipo — header/footer vêem os 3,
|
||||
single só vê Singular, archive só vê Archives) → Sub-tipo (folha concreta, ex.
|
||||
`post-type-archive:{slug}` para cada post type com arquivo, `tax-archive:{slug}` para cada
|
||||
taxonomia pública). Free = só folhas largas; o filtro `emcp_themer_condition_schema` é o único
|
||||
ponto onde o Pro injecta pesquisa de objecto específico e os nós granulares.
|
||||
|
||||
### 4.2 Matcher registry — `EMCP_Tools_Themer_Matcher_Registry` (`class-themer-matcher-registry.php`)
|
||||
|
||||
Mapa `selector-key => {specificity: int, callback: fn(rule, ctx): bool}`. `fresh()` monta o
|
||||
registry free e aplica `apply_filters('emcp_themer_matchers', ...)`. `key()` extrai a chave antes
|
||||
do primeiro `:` do `object` da regra (`post-type:page` → chave `post-type`, parâmetro `page` via
|
||||
`param()`). `matches()`/`specificity()` são os dois métodos públicos que o resto do sistema usa —
|
||||
uma regra desconhecida NUNCA faz match (fail-closed).
|
||||
|
||||
Especificidades free: `entire-site=0` < `all-singular`/`all-archives=10` <
|
||||
`front-page`/`post-type`/`post-type-archive`/`tax-archive=20`. A escala é o que garante que "toda
|
||||
a categoria" nunca ganha sobre "categoria X" quando ambos aplicam (ver §4.5).
|
||||
|
||||
### 4.3 Avaliação pura — `EMCP_Tools_Themer_Conditions` (`class-themer-conditions.php`)
|
||||
|
||||
`evaluate({include, exclude}, ctx, registry): ?int` — função pura, sem I/O. Percorre `include`,
|
||||
guarda a MAIOR especificidade entre as regras que fazem match (`$best`); se nenhuma fizer match
|
||||
devolve `null` (não aplica). Senão, percorre `exclude`: qualquer match aí devolve `null`
|
||||
imediatamente (exclude ganha sempre a include). Caso contrário devolve `$best`. É este inteiro
|
||||
(ou `null`) que o resolver usa para desempatar entre templates concorrentes do mesmo tipo.
|
||||
|
||||
### 4.4 Contexto de pedido — `EMCP_Tools_Themer_Context` (`class-themer-context.php`)
|
||||
|
||||
`from_parts(array $parts): array` — normalizador puro, aplica defaults a TODAS as chaves
|
||||
(`is_singular, is_archive, is_search, is_404, is_front_page, is_home, is_post_type_archive,
|
||||
is_author, is_date, post_id, post_type, author_id, queried_post_type, queried_taxonomy,
|
||||
queried_term_id, term_ids`) para que matchers/testes nunca tenham de tratar chaves em falta.
|
||||
`from_query()` é o único ponto de contacto com WordPress: lê os condicionais da main query
|
||||
(`is_singular()`, etc) + `get_queried_object()`, incluindo `collect_terms()` (todos os term ids
|
||||
do post, por taxonomia) para suporte a `in-term` no Pro.
|
||||
|
||||
### 4.5 Resolução de slots — `EMCP_Tools_Themer_Resolver` (`class-themer-resolver.php`)
|
||||
|
||||
Função pura central: `resolve(index, ctx, registry, ranker): {header:?int, body:?int, footer:?int}`.
|
||||
|
||||
```php
|
||||
public static function body_type( array $ctx ): ?string {
|
||||
if ( $ctx['is_404'] ) return '404';
|
||||
if ( $ctx['is_search'] ) return 'search';
|
||||
if ( $ctx['is_singular'] ) return 'single';
|
||||
if ( $ctx['is_archive'] || is_post_type_archive || is_author || is_date || is_home )
|
||||
return 'archive';
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
Para cada slot (`header`, `body` — com o tipo dinâmico de `body_type()`, `footer`), `winner()`
|
||||
percorre as linhas candidatas do índice desse tipo, chama `Conditions::evaluate()` por linha, e
|
||||
escolhe segundo um critério de desempate em 3 níveis, por esta ordem: **(1) maior especificidade**
|
||||
(`$spec`), **(2) maior prioridade** (`$prio`, via `$ranker($row)` — free devolve sempre 0, logo
|
||||
este nível nunca decide nada em free), **(3) maior id** (o template mais recente ganha em caso de
|
||||
empate total). Este algoritmo — puro, testável isoladamente, zero acoplamento a WordPress — é o
|
||||
activo de engenharia mais valioso de todo o módulo.
|
||||
|
||||
### 4.6 `resolve-template` — como a ability expõe isto
|
||||
|
||||
`execute_resolve()` (§1) reconstrói um contexto a partir do input (`post_id` → singular; ou
|
||||
`context: front-page/search/404`) e chama exactamente o mesmo `Resolver::resolve()` que o
|
||||
front-end usa (via `Themer_Matcher_Registry::fresh()` + `Themer_Index::get()`), garantindo que a
|
||||
resposta da tool é sempre um espelho fiel do que realmente vai renderizar — não uma simulação
|
||||
paralela que possa divergir.
|
||||
|
||||
---
|
||||
|
||||
## 5. Pipeline de render — o "motor híbrido"
|
||||
|
||||
### 5.1 `EMCP_Tools_Themer_Render_Controller` (`class-themer-render-controller.php`)
|
||||
|
||||
`init()` liga dois hooks: `template_include` (prioridade **99**, deliberadamente tardia — "*so we
|
||||
can defer to Elementor Pro's own theme builder when it wins*", ver `elementor_theme_builder_owns_body()`)
|
||||
e `template_redirect` (para injectar header/footer standalone).
|
||||
|
||||
`slots()` é **memoizado por pedido** (`private static $slots`) — resolve uma única vez por
|
||||
request, reutilizado por `render_mode()`, `maybe_take_over()`, `maybe_inject_parts()`, e pela
|
||||
função global `emcp_themer_location()`.
|
||||
|
||||
**`render_mode()` é a decisão de design mais importante do módulo — 3 modos:**
|
||||
|
||||
| Modo | Condição | Comportamento |
|
||||
|---|---|---|
|
||||
| `none` | Nenhum template `body` ganhou | Não mexe em nada — deixa o tema tratar tudo (um header/footer standalone ainda pode injectar via adapter) |
|
||||
| `body` | Há `body` mas NÃO (header E footer) | **Preserva o chrome do tema**: troca só a área de conteúdo (`template-body.php`) — chama `get_header()`/`get_footer()` do tema activo |
|
||||
| `full` | Há `body` **E** header **E** footer (ou a option `force_render='1'`) | **Takeover total**: documento standalone completo (`template-canvas.php`), zero chrome do tema |
|
||||
|
||||
Isto evita o erro clássico de plugins "theme builder": um utilizador que só quer substituir o
|
||||
`single.php` do tema NÃO perde acidentalmente o header/footer do tema só porque criou UM
|
||||
template body — o full takeover só acontece quando o admin conscientemente criou os 3 slots (ou
|
||||
forçou via option).
|
||||
|
||||
`maybe_take_over()` tem uma excepção crítica antes de qualquer resolução: ao editar/pré-visualizar
|
||||
o próprio CPT `emcp_theme_template`, serve sempre um canvas em branco
|
||||
(`template-edit-canvas.php`) — **nunca aplica a resolução Themer à própria vista singular do CPT**
|
||||
(evitaria um paradoxo: um template a tentar resolver-se a si próprio).
|
||||
|
||||
`maybe_inject_parts()` (em `template_redirect`) só corre quando o modo NÃO é `full` (evita
|
||||
duplicar header/footer). Chama `Themer_Theme_Adapters::current()`; se o tema for suportado,
|
||||
`wire_adapter()` faz `remove_all_actions($hook)` seguido de `add_action($hook, ...)` — **remove
|
||||
TODAS as callbacks existentes no hook do tema antes de adicionar a própria**, para o header do
|
||||
tema não renderizar ao lado/atrás do header Themer. Se o tema não for suportado e
|
||||
`force_render='1'`, cai para full-page takeover mesmo sem um template body (outro filtro em
|
||||
`template_include`, prioridade 100). Se nada disto aplicar, só a tag manual
|
||||
`emcp_themer_location('header'|'footer')` (que o próprio tema teria de chamar) funciona.
|
||||
|
||||
### 5.2 `EMCP_Tools_Themer_Content_Renderer` (`class-themer-content-renderer.php`)
|
||||
|
||||
`detect_builder(post_id): 'elementor'|'gutenberg'|'classic'` — inspecciona
|
||||
`_elementor_edit_mode='builder'` primeiro, senão `has_blocks($content)`. `render(post_id)`:
|
||||
|
||||
1. **Delegação PHP primeiro** — se o sub-módulo Themer PHP está activo e há um
|
||||
`_emcp_themer_php_template` anexado, chama `Themer_PHP_Renderer::render()`; se devolver algo
|
||||
não-vazio, usa isso e **pára aí** (o PHP template substitui o conteúdo do builder para essa
|
||||
região). Saída vazia cai de volta para o builder — nunca deixa a região em branco por um
|
||||
template PHP falhado.
|
||||
2. **Elementor** — `\Elementor\Core\Files\CSS\Post::create($id)->enqueue()` (garante o CSS gerado
|
||||
do template, que normalmente só é enfileirado no contexto da própria página, é injectado fora
|
||||
de contexto) + `Plugin::$instance->frontend->get_builder_content_for_display($id)`.
|
||||
3. **Gutenberg/clássico** — ambos passam por `apply_filters('the_content', $post->post_content)`
|
||||
(resolve blocos + shortcodes num único caminho).
|
||||
|
||||
Garantia de "nunca fatal num builder desconhecido": o caminho por omissão é sempre `the_content`.
|
||||
|
||||
### 5.3 `EMCP_Tools_Themer_Theme_Adapters` (`class-themer-theme-adapters.php`)
|
||||
|
||||
Mapa estático `template-slug => {header: hook, footer: hook}` para 7 temas populares (Astra,
|
||||
GeneratePress, Kadence, OceanWP, Blocksy, Neve, Hello Elementor), extensível via
|
||||
`emcp_themer_theme_adapters`. `current()` usa `get_template()` (slug do tema PAI, não do filho) —
|
||||
correcto para temas filhos.
|
||||
|
||||
### 5.4 `EMCP_Tools_Themer_HFE_Conflict` (`class-themer-hfe-conflict.php`)
|
||||
|
||||
Trata a colisão com "Ultimate Addons for Elementor" (UAE, antigo "Header Footer Elementor" — os
|
||||
hooks/nomes de classe internos ainda usam `HFE`). Ambos os sistemas constroem header/footer e
|
||||
injectam nos mesmos slots; sem mediação, dá dois headers ou uma vitória aleatória "quem se
|
||||
registou por último".
|
||||
|
||||
**Resolução determinística (não é só um aviso):** `filter_header()`/`filter_footer()`
|
||||
ligam-se a `enable_hfe_render_header`/`enable_hfe_render_footer`/`enable_hfe_render_before_footer`
|
||||
com prioridade 20 e **desligam o gate de render do HFE** (`return false`) para o slot que o Themer
|
||||
já resolveu para este pedido (`Render_Controller::slots()`) — Themer ganha sempre que tem
|
||||
template para o slot; o HFE continua a renderizar qualquer slot que o Themer não reclame. A
|
||||
verificação é deliberadamente conservadora: qualquer falha ao resolver devolve `false`
|
||||
(o Themer "não reclama"), nunca arrisca perder o header/footer do site por um bug de integração.
|
||||
|
||||
Adicionalmente mostra uma admin notice explicando o conflito, com um link para gerir módulos e um
|
||||
"Dismiss" persistido em option — UX de reconhecer um conflito real de ecossistema em vez de
|
||||
fingir que não existe.
|
||||
|
||||
---
|
||||
|
||||
## 6. Índice de condições (cache) — `EMCP_Tools_Themer_Index` (`class-themer-index.php`)
|
||||
|
||||
Uma ÚNICA option autoloaded (`emcp_tools_themer_index`) guarda `{type => rows[{id, include,
|
||||
exclude, priority}]}` — o resolver (§4.5) lê isto directamente, **zero queries à BD por pedido**
|
||||
no caminho de render (a option autoloaded já está em memória desde o boot do WordPress).
|
||||
|
||||
`build(records): index` é puro (registos planos → agrupados por tipo). `rebuild()` é o fio WP:
|
||||
`WP_Query` sobre `emcp_theme_template` **só `post_status='publish'`** (draft nunca aplica ao
|
||||
front-end — comentário explícito: "*a draft is work-in-progress and must not render for
|
||||
visitors*"), lê a meta de cada post, chama `build()`, grava a option.
|
||||
|
||||
**Bug histórico documentado + o mecanismo de "heal" que ficou no código como cicatriz
|
||||
permanente**, ordem dos hooks em `register_hooks()`:
|
||||
|
||||
```php
|
||||
// Priority 99: the metabox and the MCP abilities write the type/conditions
|
||||
// meta on save_post_{type} at priority 10, so the rebuild must run AFTER
|
||||
// them or it reads stale/absent meta and produces an empty index (which
|
||||
// makes the front end fall back to the theme's own templates).
|
||||
add_action( 'save_post_' . self::POST_TYPE, array( __CLASS__, 'rebuild' ), 99 );
|
||||
```
|
||||
|
||||
Se o rebuild corresse à prioridade 10 (ou sem prioridade explícita, ligando-se antes da metabox),
|
||||
lia a meta ANTES dela ser escrita — índice ficava vazio, templates paravam de aplicar
|
||||
silenciosamente. A correcção não foi só mudar a prioridade: o módulo carrega um marcador
|
||||
`OPTION_INDEX_HEALED` e, uma única vez por instalação afectada, força um `rebuild()` no boot para
|
||||
sites que já tinham este bug gravado no seu índice (`class-themer-module.php`, comentário
|
||||
"*One-time heal: a prior build could leave the condition index empty*").
|
||||
|
||||
**Lição de engenharia para a réplica:** qualquer sistema com um índice/cache derivado de meta
|
||||
escrita por MÚLTIPLAS fontes (aqui: metabox humana + 2 abilities MCP diferentes) precisa de uma
|
||||
ordem de prioridade EXPLÍCITA e testada, não implícita. Um bug deste tipo é invisível em testes
|
||||
manuais normais (a metabox humana normalmente já grava e o `save_post` global corre depois de
|
||||
qualquer forma) mas manifesta-se de forma imprevisível consoante QUEM escreveu por último.
|
||||
|
||||
`on_deleted_post()` liga-se a `deleted_post`/`trashed_post`/`untrashed_post`, filtra por tipo de
|
||||
post, e chama `rebuild()` — garante que apagar/mover-para-trash/restaurar um template também
|
||||
actualiza o índice.
|
||||
|
||||
---
|
||||
|
||||
## 7. Metabox de admin — `EMCP_Tools_Themer_Metabox` (`class-themer-metabox.php`)
|
||||
|
||||
UI server-driven: o PHP monta o `<select>` de tipo, o `<select>` opcional de template PHP
|
||||
anexado, e um `<div id="emcp-themer-conditions-app">` + `<input type="hidden">` com o JSON
|
||||
serializado das condições. Todo o construtor em cascata (Relação→Grupo→Sub-tipo) é montado
|
||||
client-side por `assets/js/themer-conditions.js` a partir do schema localizado
|
||||
(`emcpThemerCond.schemasByType`), com `emcpThemerCond.isPro` a controlar visibilidade de UI Pro.
|
||||
|
||||
**Decisão deliberada anti-erro-silencioso:** um template NOVO nunca herda um `type` por omissão —
|
||||
fica vazio até o utilizador escolher conscientemente (comentário: "*Do NOT default to a real type
|
||||
('header') — that silently mistyped templates*"). Só é pré-preenchido a partir de
|
||||
`?emcp_themer_type=` na URL (ex. um botão "Add New Header" que já passa o tipo pretendido).
|
||||
|
||||
**Detecção de conflito no próprio ecrã de edição:** `find_conflicts()` procura outros templates DO
|
||||
MESMO TIPO cujas condições `include` partilhem pelo menos um `object` (`entire-site`,
|
||||
`post-type:post`, etc — comparação por string exacta, não por especificidade) com o template
|
||||
actual, e mostra uma notice inline com links directos — porque só um template pode ganhar um dado
|
||||
slot, dois templates a apontar ao mesmo selector é quase sempre um erro do utilizador e o
|
||||
resolver (§4.5) resolve isto de forma silenciosa e não óbvia (especificidade→prioridade→id mais
|
||||
recente) sem esta notice.
|
||||
|
||||
`save()`: valida nonce, ignora autosave, verifica `edit_post`; grava `_emcp_themer_type` (só se
|
||||
válido); se Themer PHP activo, valida e aplica o attach do template PHP
|
||||
(`Themer_PHP_Admin::validate_attachment` + `apply_attachment` — este É o único ponto do sistema
|
||||
inteiro onde um template PHP passa de "draft nunca executa" a "compilado e a renderizar", e requer
|
||||
uma submissão de formulário humana com nonce, não uma chamada MCP); por fim
|
||||
`sanitize_conditions()` — descodifica o JSON, valida cada `object` contra `valid_selectors()`
|
||||
(regras com selector desconhecido são silenciosamente DESCARTADAS aqui, ao contrário da ability
|
||||
que REJEITA — inconsistência aceitável: a UI já só oferece selectores válidos, então uma regra
|
||||
"desconhecida" só chegaria por manipulação directa do campo hidden), e em free força sempre
|
||||
`exclude=[]`/`priority=0` mesmo que o payload tente enviar algo (dupla proteção, já para lá da
|
||||
rejeição da ability).
|
||||
|
||||
---
|
||||
|
||||
## 8. Conteúdo dinâmico partilhado — `EMCP_Tools_Themer_Dynamic` (`class-themer-dynamic.php`)
|
||||
|
||||
**A peça de design mais elegante do módulo**: um único catálogo estático de 10 elementos
|
||||
(`post-title, archive-title, breadcrumbs, post-meta, site-logo, site-title, nav-menu, description,
|
||||
post-content, archive-loop`), cada um com um método `public static function` que devolve HTML já
|
||||
escapado. Gutenberg (§9) e Elementor (§10) chamam exactamente os MESMOS métodos — a lógica de
|
||||
"o que é o título do post/arquivo agora" existe UMA VEZ, nunca duplicada por builder.
|
||||
|
||||
`args_from(key, rawAttrs): array` traduz os atributos de QUALQUER builder (Gutenberg `camelCase`
|
||||
booleanos JS `true/false`, ou Elementor `snake_case`/`'yes'/''`) para o formato de argumentos
|
||||
interno partilhado — normaliza truthy de ambos os mundos automaticamente. `render(key, args)` é o
|
||||
dispatcher final, usado directamente pelos dois builders.
|
||||
|
||||
Todos os elementos resolvem contra a **main query actual**, não o template — é assim que um
|
||||
template Themer "single" mostra o post realmente visitado. `queried_id()` cobre o caso `is_home()`
|
||||
(blog page separada). Detalhes de qualidade notáveis:
|
||||
|
||||
- **`breadcrumbs()`** prefere a função de breadcrumb de um plugin SEO já activo (Yoast/Rank
|
||||
Math/SEOPress, por esta ordem) antes de cair no trail simples próprio — evita reinventar
|
||||
algo que o site já pode ter bem configurado (schema, hierarquia custom, etc).
|
||||
- **`custom_field()`** é ACF-aware (`function_exists('get_field')`) com fallback a
|
||||
`get_post_meta()` — preenche o gap de "campo dinâmico" que nem Gutenberg nem Elementor free têm
|
||||
nativamente.
|
||||
- **`archive_loop()`** distingue explicitamente contexto real de preview
|
||||
(`is_preview_context()` — testa `REST_REQUEST`, `is_admin()`, modo editor/preview do Elementor)
|
||||
para usar a `$wp_query` real num arquivo a sério, ou uma `WP_Query` de amostra (respeitando
|
||||
`query_post_type`/`query_orderby`/`query_tax`/`query_term` opcionais) quando está só a ser
|
||||
desenhado — evita que o widget pareça "sem posts" enquanto o utilizador o configura.
|
||||
- **`is_preview_context()`** é reutilizado por vários pontos do módulo como o teste canónico
|
||||
"estou num contexto de edição, não num pedido real de visitante".
|
||||
|
||||
---
|
||||
|
||||
## 9. Blocos Gutenberg dinâmicos — `EMCP_Tools_Themer_Blocks` (`blocks/class-themer-blocks.php`)
|
||||
|
||||
Regista uma categoria própria (`emcp-themer`) e os 10 blocos `emcp/{key}` via
|
||||
`register_block_type()` API v2, com `render_callback` no servidor (nunca `save` client-side —
|
||||
todo o output é dinâmico). `blocks()` é a fonte única de verdade: título/ícone/`attributes`
|
||||
(tipos+defaults)/`supports` (align/color/spacing/typography/border nativos do editor)/`controls`
|
||||
(descritores partilhados de UI, ex. `{key:'tag', type:'select', options:[...]}`) — o MESMO array
|
||||
serve para registar o bloco em PHP **e** é `wp_localize_script()`'d para
|
||||
`assets/js/themer-blocks.js` construir os controlos `InspectorControls` no editor sem qualquer
|
||||
passo de build (JS vanilla, sem JSX/Webpack).
|
||||
|
||||
`render_block()` chama `Dynamic::args_from()` + `Dynamic::render()` (§8); se a saída for vazia
|
||||
E estivermos num pedido REST (preview do editor), mostra um placeholder com o título do bloco em
|
||||
vez de nada — o editor nunca parece "partido" mesmo quando não há dados de amostra.
|
||||
|
||||
---
|
||||
|
||||
## 10. Widgets Elementor dinâmicos — `EMCP_Tools_Themer_Widgets` + `Widget_Base` (`widgets/`)
|
||||
|
||||
`class-themer-widgets.php` é só o loader: liga `elementor/elements/categories_registered`
|
||||
(categoria "EMCP Themer"), `elementor/widgets/register` (`require_once` tardio de
|
||||
`class-themer-widget-classes.php` — só dentro deste hook, quando `\Elementor\Widget_Base` está
|
||||
garantidamente carregado), e reutiliza a MESMA folha de estilos dos blocos Gutenberg
|
||||
(`themer-blocks.css`) para o layout partilhado.
|
||||
|
||||
`class-themer-widget-classes.php` define `EMCP_Tools_Themer_Widget_Base` (abstract, estende
|
||||
`\Elementor\Widget_Base`) + **10 subclasses triviais** (uma linha cada: `emcp_key(): string`). A
|
||||
base faz TODO o trabalho:
|
||||
|
||||
- `register_controls()` — constrói os controlos de conteúdo a partir dos MESMOS descritores
|
||||
partilhados de `Themer_Blocks::blocks()[$key]['controls']` (mapeados para tipos de controlo
|
||||
Elementor via `control_args()`: select/toggle/text/number/menu) — de novo, zero duplicação entre
|
||||
a definição do bloco Gutenberg e do widget Elementor.
|
||||
- Tab de Estilo partilhada (alinhamento, cor de texto, cor de link, grupo de tipografia via
|
||||
`Group_Control_Typography`) igual em todos os 10 widgets; o widget `archive-loop`
|
||||
adicionalmente ganha uma secção "Cards" completa (gap, largura de imagem, fundo, borda, raio,
|
||||
padding, sombra, cores de título/meta/excerpt/read-more).
|
||||
- `render()` delega directamente a `Dynamic::render()` (§8) — o output já vem escapado do
|
||||
provider partilhado, por isso o `echo` aqui não escapa de novo (comentário explícito no código).
|
||||
|
||||
---
|
||||
|
||||
## 11. Subsistema Themer PHP — templates PHP em bruto autorados por IA
|
||||
|
||||
Feature mais sensível de todo o módulo (execução de código), desenhada com um modelo de segurança
|
||||
em camadas — a peça mais valiosa para copiar tal-e-qual numa réplica.
|
||||
|
||||
### 11.1 Coordenador — `EMCP_Tools_Themer_PHP` (`php/class-themer-php.php`)
|
||||
|
||||
Classe fina. `enabled()` é o único ponto de decisão: módulo Themer activo **E**
|
||||
`emcp_tools_themer_php_enabled='1'`. `init()` regista o CPT **incondicionalmente** (mesmo que o
|
||||
toggle esteja desligado — drafts existentes continuam consultáveis/apagáveis se a feature for
|
||||
depois desligada), mas só arranca o admin quando `enabled()`.
|
||||
|
||||
### 11.2 Store — `EMCP_Tools_Themer_PHP_Store` (`php/class-themer-php-store.php`)
|
||||
|
||||
CPT privado `emcp_theme_php` (`show_ui=false`, `show_in_rest=false`, `public=false` — invisível
|
||||
fora deste subsistema). Meta: `_emcp_theme_php_code` (raw PHP), `_type`, `_validation` (JSON do
|
||||
relatório do validador partilhado), `_hash` (sha256 — **a presença desta meta É a definição de
|
||||
"compilado"**), `_error` (última mensagem de fatal capturada).
|
||||
|
||||
**Permissões deliberadamente duplas:** `can_edit()` exige `manage_options` **E**
|
||||
`unfiltered_html` — a segunda capability é a que o WordPress core usa para gate de código PHP
|
||||
arbitrário (ex. editor de temas/plugins); herdar exactamente essa capability em vez de inventar
|
||||
uma nova é a escolha correcta (multisite normalmente REMOVE `unfiltered_html` de admins de site
|
||||
por omissão, fechando esta feature automaticamente nesses contextos).
|
||||
|
||||
**`create_draft()`/`update()`** correm sempre a validação partilhada
|
||||
(`EMCP_Tools_PHP_Snippet_Validator::validate()`, ver doc 06 — parse PHP + heurísticas de
|
||||
segurança) e recusam com `WP_Error` se `!valid` (erro de parse) ou `!safe` (finding crítico:
|
||||
execução de código, shell, carregamento de ficheiros, rede, escrita em ficheiros — listado na
|
||||
description da ability, §2).
|
||||
|
||||
**Compila-se apenas quando referenciado — o coração do modelo de segurança:**
|
||||
|
||||
```php
|
||||
public static function sync_reference( int $id ) {
|
||||
if ( self::reference_count( $id ) > 0 ) { return self::ensure_compiled( $id ); }
|
||||
self::decompile( $id );
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
`reference_count()` conta posts `emcp_theme_template` cuja meta `_emcp_themer_php_template`
|
||||
aponta para este id. Um draft criado/editado pela IA **não tem ficheiro `.php` em disco até um
|
||||
humano o anexar via metabox** (`Metabox::apply_attachment()`, §7, chama `sync_reference()` depois
|
||||
da meta gravada). Desanexar (ou apagar o post Themer que o referenciava) desfaz a compilação
|
||||
automaticamente. Isto reduz drasticamente a superfície de "código PHP a correr no site" ao
|
||||
subconjunto que um humano explicitamente ligou — a IA nunca consegue tornar um template
|
||||
executável por si só.
|
||||
|
||||
**`ensure_compiled()`** — re-valida, envolve o corpo (já com as tags PHP removidas por
|
||||
`Validator::strip_tags()`) numa função nomeada `emcp_theme_php_{id}` guardada por
|
||||
`function_exists()`, faz `token_get_all($php, TOKEN_PARSE)` como verificação extra de sintaxe
|
||||
antes de escrever, grava o ficheiro em `{sandbox}/theme-php/{id}.php`, e grava o sha256 do
|
||||
conteúdo final como `_hash`. `opcache_invalidate()` é chamado explicitamente após cada
|
||||
escrita/remoção de ficheiro (evita servir bytecode obsoleto em produção com OPcache).
|
||||
|
||||
**Manifesto** (`theme-php-manifest.json`): `rebuild_manifest()` percorre TODOS os drafts, inclui
|
||||
só os que têm `_hash` presente (i.e., compilados), grava `{post_id, func, php_path, hash, type}`
|
||||
por entrada — este ficheiro é a ÚNICA fonte que o renderer (§11.3) consulta, nunca faz scan de
|
||||
directório.
|
||||
|
||||
### 11.3 Renderer — `EMCP_Tools_Themer_PHP_Renderer` (`php/class-themer-php-renderer.php`)
|
||||
|
||||
`render(id): string` — devolve `''` em QUALQUER falha (o content-renderer, §5.2, cai de volta ao
|
||||
conteúdo do builder nesse caso). Três camadas de defesa antes de sequer chamar a função:
|
||||
|
||||
1. **Manifest-only lookup** — `manifest_entry()` procura no `read_manifest()`; nenhum ficheiro
|
||||
nunca é localizado por varrimento de directório.
|
||||
2. **Path containment guard** — `0 !== strpos(normalize($path), normalize($sandbox))` — o caminho
|
||||
resolvido tem de viver dentro do directório sandbox; defende um manifesto envenenado por
|
||||
qualquer via.
|
||||
3. **Tamper guard** — `hash('sha256', file_get_contents($path)) !== $entry['hash']` → recusa. O
|
||||
ficheiro em disco tem de bater exactamente com o hash gravado no manifesto no momento da
|
||||
compilação — qualquer edição directa do `.php` no disco (fora do fluxo Store) invalida-o
|
||||
silenciosamente.
|
||||
|
||||
Só depois de passar as 3 camadas é que `include_once` carrega o ficheiro (isto define a função —
|
||||
"*running no user code*" ainda, o corpo só corre quando chamada). A chamada em si acontece dentro
|
||||
de `ob_start()` + `try/catch(\Throwable)` — uma excepção marca o erro (`mark_error()`, que
|
||||
**também decompila** o template) e devolve `''`.
|
||||
|
||||
**Fatal-recovery via shutdown handler — a rede de segurança final:**
|
||||
|
||||
```php
|
||||
public static function on_shutdown(): void {
|
||||
if ( null === self::$active ) { return; }
|
||||
$err = error_get_last();
|
||||
$fatal = array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR );
|
||||
if ( is_array($err) && in_array($err['type'], $fatal, true) ) {
|
||||
EMCP_Tools_Themer_PHP_Store::mark_error( self::$active, $err['message'] ?? '...' );
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`self::$active` marca qual template está a incluir/executar; se o PROCESSO INTEIRO morrer com
|
||||
fatal error (algo que nem `try/catch` apanha — um erro de PHP verdadeiramente fatal, não uma
|
||||
`\Throwable`), o shutdown handler regista-o e chama `mark_error()`, que **decompila o template**
|
||||
(remove `_hash`, apaga o ficheiro). **Consequência prática:** um template com um bug que provoque
|
||||
fatal auto-recupera para "desligado" logo após o PRIMEIRO pedido que o parte — não continua a
|
||||
derrubar cada pedido subsequente do site. É o mecanismo mais importante para tornar "deixar uma IA
|
||||
escrever PHP que corre no site" aceitável em produção.
|
||||
|
||||
### 11.4 Admin UI — `EMCP_Tools_Themer_PHP_Admin` (`php/class-themer-php-admin.php`)
|
||||
|
||||
Submenu sob o CPT Themer (`edit.php?post_type=emcp_theme_template&page=emcp-themer-php`). Reusa
|
||||
`wp_enqueue_code_editor(['type'=>'text/x-php'])` — o MESMO CodeMirror que o editor nativo de
|
||||
temas/plugins do WordPress usa, com linting — em vez de reinventar um editor de código. Lista
|
||||
templates, e ao ver um (`?view={id}`) mostra o editor completo com título/tipo/código, guardar
|
||||
(re-valida, recompila se já anexado) e apagar. É código de admin puro, sem qualquer registo MCP —
|
||||
existe só para o humano que precisa de intervir manualmente num template gerado pela IA.
|
||||
|
||||
---
|
||||
|
||||
## Blueprint para réplica
|
||||
|
||||
**Copiar quase 1:1 (o valor está no design, não na implementação de detalhe):**
|
||||
|
||||
1. **O sistema de condições completo (§4)** — Schema/Matcher-Registry/Conditions/Context/Resolver
|
||||
é uma máquina de resolução de "qual template para este pedido" desenhada com disciplina pura +
|
||||
glue. É genérico o suficiente para servir QUALQUER sistema de "theme builder"/"template
|
||||
assignment" (não só Themer) — vale a pena extrair como um pacote isolado logo de início.
|
||||
2. **O padrão "catálogo partilhado + dispatcher" de `Themer_Dynamic` (§8)** — um único ponto de
|
||||
verdade para "o que é X neste contexto", consumido por N builders/superfícies diferentes (aqui
|
||||
Gutenberg e Elementor; podia ser qualquer par). Evita a divergência clássica "o título mostra
|
||||
uma coisa no bloco e outra no widget".
|
||||
3. **O modelo de segurança do Themer PHP inteiro (§11)** — compila-só-quando-referenciado +
|
||||
manifesto hash-verificado + `path containment guard` + shutdown fatal-recovery é a resposta
|
||||
correcta a "deixar um agente de IA escrever PHP executável" e generaliza-se a qualquer feature
|
||||
futura do género (snippets, sandbox de widgets/blocos custom — a doc 06 provavelmente reutiliza
|
||||
o mesmo `PHP_Snippet_Validator`/`PHP_Snippet_Store` subjacentes).
|
||||
4. **O `render_mode()` de 3 estados (none/body/full, §5.1)** — a decisão de nunca fazer takeover
|
||||
total do documento a menos que o admin tenha deliberadamente os 3 slots (ou tenha forçado) é a
|
||||
diferença entre "plugin de theme builder que não assusta ninguém" e "plugin que às vezes come o
|
||||
header do tema sem aviso".
|
||||
5. **A ausência deliberada de uma tool `attach` no grupo Themer PHP** — replicar o princípio
|
||||
directamente: **qualquer feature que gere código executável via IA deve ter o "ligar à
|
||||
execução" como um passo humano fora do protocolo MCP**, nunca uma ability chamável.
|
||||
|
||||
**Simplificar numa reescrita própria:**
|
||||
|
||||
- **Os 3 níveis do índice de condições (§6)** são bom design mas exigem disciplina de ordenação de
|
||||
hooks nada óbvia (o bug histórico de §6 prova isto). Numa reescrita, considerar calcular o
|
||||
resolve directamente a partir da CPT em cada pedido com `WP_Object_Cache`/transient de curto TTL
|
||||
em vez de uma option autoloaded mantida manualmente — mais simples de raciocinar, ao custo de
|
||||
uma query extra em cache-miss (aceitável face ao ganho de robustez).
|
||||
- **Os 7 theme adapters fixos (§5.3)** são um mapa estático de hooks específicos por tema —
|
||||
correcto para os temas mais populares mas frágil a longo prazo (nomes de hooks mudam entre
|
||||
versões major de tema). Considerar documentar isto como convenção pública (`emcp_themer_location()`
|
||||
já existe para esse fim) em vez de tentar manter uma lista de adapters actualizada
|
||||
indefinidamente.
|
||||
- **A UI de admin nativa completa (metabox + condition-builder JS + PHP-editor CodeMirror)** é
|
||||
~1500+ linhas de PHP mais JS não lido nesta tarefa (`assets/js/themer-conditions.js`,
|
||||
`assets/js/themer-blocks.js`) — para uma réplica focada em "agente MCP + template engine",
|
||||
considerar reduzir a UI humana ao mínimo (edição via qualquer builder já suportado + um ecrã de
|
||||
condições simples) e investir o esforço poupado na cobertura de testes do resolver puro (§4.5),
|
||||
que é o componente que realmente importa estar correcto.
|
||||
|
||||
**Riscos/gotchas não óbvios a não repetir sem pensar:**
|
||||
|
||||
- A verificação `check_write_permission` de `Themer_Abilities` aceita `publish_pages ||
|
||||
edit_pages` para CRIAÇÃO (sem `template_id` ainda) mas exige `edit_post($id)` específico para
|
||||
edição — replicar esta assimetria correctamente é fácil de errar (a tentação óbvia é usar a
|
||||
mesma capability para os dois casos, o que ou é permissivo demais na criação ou impossível na
|
||||
edição de um post ainda inexistente).
|
||||
- `is_pro()`/`pro_conditions_available()` testam a presença de `'post'` no array de selectores
|
||||
filtrado como proxy de "há licença Pro" — um padrão frágil (qualquer terceiro que registe um
|
||||
selector chamado `post` por acidente activaria funcionalidade Pro sem querer) mas simples;
|
||||
numa reescrita própria, preferir uma função de capability explícita (`is_premium()`, já usada em
|
||||
`Themer_CPT`) em vez de inferir por presença de string.
|
||||
- `render_type_mismatch_notice()` e `find_conflicts()` (heurísticas de UX) fazem `WP_Query` de até
|
||||
100-200 posts em CADA carregamento do ecrã de admin relevante — aceitável à escala de "poucos
|
||||
templates de tema por site" mas não escalaria a um cenário de centenas de templates; não é um
|
||||
problema real neste domínio (o próprio quota de 1-por-tipo em free e o uso normal em Pro nunca
|
||||
chega a esses números), mas vale registar se a réplica reutilizar este padrão noutro contexto de
|
||||
volume maior.
|
||||
|
||||
---
|
||||
|
||||
## Fonte
|
||||
|
||||
Leitura directa (19-08-2026) de todos os 22 ficheiros do módulo Themer em
|
||||
`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`:
|
||||
|
||||
`includes/abilities/class-themer-abilities.php`, `includes/abilities/class-themer-php-abilities.php`,
|
||||
`includes/themer/class-themer-cpt.php`, `includes/themer/class-themer-resolver.php`,
|
||||
`includes/themer/class-themer-conditions.php`, `includes/themer/class-themer-condition-schema.php`,
|
||||
`includes/themer/class-themer-dynamic.php`, `includes/themer/class-themer-metabox.php`,
|
||||
`includes/themer/class-themer-matcher-registry.php`, `includes/themer/class-themer-context.php`,
|
||||
`includes/themer/class-themer-render-controller.php`, `includes/themer/class-themer-content-renderer.php`,
|
||||
`includes/themer/class-themer-hfe-conflict.php`, `includes/themer/class-themer-theme-adapters.php`,
|
||||
`includes/themer/class-themer-index.php`, `includes/themer/blocks/class-themer-blocks.php`,
|
||||
`includes/themer/widgets/class-themer-widgets.php`, `includes/themer/widgets/class-themer-widget-classes.php`,
|
||||
`includes/themer/php/class-themer-php.php`, `includes/themer/php/class-themer-php-store.php`,
|
||||
`includes/themer/php/class-themer-php-renderer.php`, `includes/themer/php/class-themer-php-admin.php`.
|
||||
|
||||
Mais, para contexto do ciclo de vida (não listado no pedido original mas necessário para
|
||||
compreender o gate de activação em §0): `includes/modules/class-themer-module.php`. Verificação
|
||||
cruzada do gating exacto (linhas 202-221) contra `includes/abilities/class-ability-registrar.php`.
|
||||
Cruzado com `skill://emcp-tools` (auditoria de postura de segurança, 16-08-2026) e
|
||||
`docs/00-ARQUITECTURA.md` (arquitectura geral do plugin, já escrito nesta série).
|
||||
Reference in New Issue
Block a user