Files
Claude Code 8ada367bd0 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).
2026-08-19 06:41:04 +01:00

47 KiB

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:

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:

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"):

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}.

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():

// 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:

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:

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).