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).
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:
render_adapter_notice()— mostra se o tema activo é directamente suportado pelo mapa de adapters (§5.3); se não for, explica as duas alternativas (tagemcp_themer_location()ou o toggle de full-page-takeover).render_type_mismatch_notice()— heurística de detecção de erro humano: percorre todos os templates e sinaliza (a) templates semtypedefinido (nunca renderizam), (b) um templateheader/footercujo conteúdo contém elementos body-only (detecta por substringemcp/post-title,emcp/archive-loop, etc nopost_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 dotypegravado (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):
- Delegação PHP primeiro — se o sub-módulo Themer PHP está activo e há um
_emcp_themer_php_templateanexado, chamaThemer_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. - 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). - 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 aget_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()— testaREST_REQUEST,is_admin(), modo editor/preview do Elementor) para usar a$wp_queryreal num arquivo a sério, ou umaWP_Queryde amostra (respeitandoquery_post_type/query_orderby/query_tax/query_termopcionais) 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 deThemer_Blocks::blocks()[$key]['controls'](mapeados para tipos de controlo Elementor viacontrol_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 widgetarchive-loopadicionalmente 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 aDynamic::render()(§8) — o output já vem escapado do provider partilhado, por isso oechoaqui 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:
- Manifest-only lookup —
manifest_entry()procura noread_manifest(); nenhum ficheiro nunca é localizado por varrimento de directório. - 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. - 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.phpno 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):
- 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.
- 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". - 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 mesmoPHP_Snippet_Validator/PHP_Snippet_Storesubjacentes). - 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". - A ausência deliberada de uma tool
attachno 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_permissiondeThemer_Abilitiesaceitapublish_pages || edit_pagespara CRIAÇÃO (semtemplate_idainda) mas exigeedit_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 chamadopostpor acidente activaria funcionalidade Pro sem querer) mas simples; numa reescrita própria, preferir uma função de capability explícita (is_premium(), já usada emThemer_CPT) em vez de inferir por presença de string.render_type_mismatch_notice()efind_conflicts()(heurísticas de UX) fazemWP_Queryde 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).