# 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 `` opcional de template PHP anexado, e um `
` + `` 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).