Plugin novo, do zero
Por que isto importa
Integrar desde o primeiro commit custa uma fração de integrar depois. Um plugin que nasce sem pensar em licenciamento tipicamente acumula decisões incompatíveis — autoload incondicional, capability hardcoded, constantes com nome genérico — que só aparecem como retrabalho quando alguém tenta encaixar o v3r-core mais tarde. Esta página é o que não pode faltar desde o início.
1. PHP mínimo: 8.2, em três lugares
O v3r-core exige php: >=8.2. Isso precisa aparecer em três lugares do seu plugin, não só um:
{
"require": { "php": ">=8.2" },
"config": { "platform": { "php": "8.2" } }
}
E no cabeçalho do arquivo .php principal do plugin:
/**
* Plugin Name: Meu Plugin
* Requires PHP: 8.2
*/
É o cabeçalho
Requires PHP, não ocomposer.json, que faz o WordPress recusar instalação ou atualização num site incompatível. Ocomposer.jsonsó importa no seu ambiente de build (dev, CI) — ele nunca viaja para o site do cliente nem é lido pelo WordPress. Sem o cabeçalho certo, um site em PHP 7.4 instala o plugin normalmente e só descobre a incompatibilidade num fatal error de sintaxe na ativação — ou pior, num ponto qualquer do código que só aquele PHP mais velho não entende.
Um guard de versão de PHP escrito em sintaxe do PHP 8 não roda no PHP antigo que ele deveria barrar. Se você quiser um aviso amigável em vez do fatal error de parse (recomendado — é isso que o cabeçalho
Requires PHPsozinho não garante em toda instalação, dependendo de como o WordPress hospedeiro dispara a checagem), o arquivo que faz essa checagem precisa ser o único do plugin escrito em sintaxe válida desde o PHP 7.0: sem promoção de propriedade no construtor, sem tipos union, semmatch, sem propriedades tipadas, sem operador nullsafe. Umversion_compare()simples, sem classe, carregado porrequire_onceantes de qualquer autoload — é o único jeito de mostrar “seu PHP é antigo demais” em vez de um erro que não diz nada disso.
2. Declare o v3r-core como dependência
{
"require-dev": {
"v3rtech/v3r-core": "^0.5.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/V3RTECH-DF/V3RCore-Code.git"
}
]
}
require-dev, nuncarequire. O build de produção do plugin rodacomposer install --no-devpara gerar o autoloader final;requiresobrevive a esse--no-deve a lib acaba reinstalada sem prefixo dentro do próprio pacote — o zip final sai com as duas cópias, a prefixada e a original, que é exatamente o estado que colide com outro plugin da casa no mesmo WordPress. A biblioteca nunca precisa ser carregada sem prefixo em runtime; ela só existe como matéria-prima para o Strauss (item 3).
Como o repositório V3RCore-Code é público, composer require funciona sem nenhuma credencial — nem no seu ambiente, nem em CI.
3. Configure o Strauss
O Strauss roda como .phar standalone, nunca como dependência do Composer do seu plugin:
mkdir -p .tooling
curl -L -o .tooling/strauss.phar \
https://github.com/BrianHenryIE/strauss/releases/download/0.19.5/strauss.phar
echo '.tooling/' >> .gitignore
E no composer.json:
{
"autoload": {
"classmap": ["vendor-prefixed/"]
},
"scripts": {
"prefix": ["php .tooling/strauss.phar compose", "composer dump-autoload"]
},
"extra": {
"strauss": {
"target_directory": "vendor-prefixed/",
"namespace_prefix": "MeuPlugin\\Vendor\\",
"classmap_prefix": "MeuPlugin_Vendor_",
"constant_prefix": "MEUPLUGIN_VENDOR_",
"packages": ["v3rtech/v3r-core"],
"override_autoload": {
"yahnis-elsts/plugin-update-checker": {
"classmap": ["Puc/"],
"files": ["load-v5p7.php"]
}
},
"delete_vendor_packages": true
}
}
}
Troque MeuPlugin\Vendor\, MeuPlugin_Vendor_ e MEUPLUGIN_VENDOR_ pelo namespace do seu plugin — é isso que evita colisão entre dois plugins da casa com versões diferentes do v3r-core no mesmo site. O resto do bloco é idêntico em qualquer plugin.
override_autoloadparayahnis-elsts/plugin-update-checkeré obrigatório, mesmo você não requerendo esse pacote diretamente — ele chega como dependência transitiva dov3r-core, e se autoloada por um mecanismo próprio que o Strauss não enxerga sozinho. Sem este bloco, a primeira chamada ao updater é fatal error.
Crie vendor-prefixed/ com um .gitkeep versionado, e ignore o resto:
vendor-prefixed/*
!vendor-prefixed/.gitkeep
Sem hooks automáticos —
composer installsozinho não geravendor-prefixed/; rodarcomposer run prefixé passo explícito, à mão em desenvolvimento e automático dentro dobuild-zip.sh(item 7). É por isso que todo código do plugin que toque ov3r-coreprecisa estar protegido porclass_exists()(item 4) — o estado “lib instalada mas ainda não prefixada” é normal logo depois de um clone.
4. O bootstrap
<?php
declare(strict_types=1);
use MeuPlugin\Vendor\V3R\Core\Bootstrap;
$v3rCoreAutoload = __DIR__ . '/vendor-prefixed/autoload.php';
if ( file_exists( $v3rCoreAutoload ) ) {
require_once $v3rCoreAutoload;
}
add_action( 'plugins_loaded', function () {
if ( ! class_exists( Bootstrap::class ) ) {
return; // vendor-prefixed/ ainda não existe — nunca fatal error, o plugin liga sem licenciamento
}
$licenseConfig = meuplugin_resolve_v3r_license_config(); // item 6
if ( null === $licenseConfig ) {
return;
}
$v3rCore = new Bootstrap(
'meuplugin', // product_slug — precisa bater com o cadastro no V3RLicense
__FILE__,
$licenseConfig['api_url'],
$licenseConfig['public_key'],
'1.0.0', // versão instalada do plugin
'meuplugin_settings_view', // capability de leitura da licença
'meuplugin_settings_manage' // capability de gestão (ativar/desativar)
);
$v3rCore->withCapabilityDecider( function ( int $userId, string $capability ): bool {
return MeuPlugin\Rbac::userCan( $userId, $capability );
} );
$v3rCore->withProductName( 'MeuPlugin' );
$v3rCore->boot();
} );
withCapabilityDecider()é obrigatório —boot()lançaLogicExceptionsem ele. O plugin fornece só a função de decisão (function ( int $userId, string $capability ): bool); quem registra o filtrouser_has_capé a biblioteca, com a guarda de saída antecipada embutida. Dentro do$decidervocê pode chamaruser_can()/current_user_can()à vontade — a biblioteca só invoca$deciderquando a capability perguntada já é uma das duas de licença, então não há recursão a evitar do seu lado.Por que isso é rígido a esse ponto: um filtro
user_has_capescrito à mão, sem a guarda de saída antecipada, fecha o ciclouser_has_cap → user_can → user_has_cap— infinito, e derruba toda requisição de usuário logado por memória esgotada, incluindo o própriowp-login.php. Já aconteceu em produção. Testar só com requisição anônima (curlsem cookie de sessão) não revela o problema — a requisição anônima passa normalmente; é a de usuário autenticado que morre. Sempre teste logado.
As duas capabilities (meuplugin_settings_view/meuplugin_settings_manage) costumam ser sintéticas — pontes para um nível de permissão que já existe no RBAC do seu plugin, não capabilities nativas do WordPress. manage_options só é aceitável para plugin sem RBAC próprio nenhum; mesmo assim, é só um dos jeitos de errar — larga demais concede a qualquer administrador do site, estreita demais exclui quem administra o plugin sem ser administrador do WordPress.
withProductName() nomeia o produto no rótulo do menu e no título da tela padrão de licença — sem ele, o rótulo cai para o productSlug, que identifica pior num site com vários plugins da casa. Só tem efeito em quem usa a tela padrão (item 5).
5. A tela de licença não é opcional
Integração sem tela de licença é integração pela metade.
boot()sem nenhuma tela registrada não falha em nada visível — o plugin ganha o updater e as rotas REST internas, os testes passam, a ativação funciona. O que falta só aparece depois: o cliente não tem onde informar a chave de licença, e oUpdateGaterecusa atualização no estado “nunca houve ativação” — sem período de graça (diferente do estado “ativa, mas sem contato recente”, que tem 14 dias). A versão que introduz a auto-atualização é a mesma que a desliga, sem saída pela interface.
Duas opções, e as duas são válidas:
- Tela padrão da biblioteca — para plugin sem SPA própria. Uma linha registra a tela em PHP.
- Aba própria, dentro do painel do plugin — para plugin com SPA React (ou equivalente). Consome os quatro endpoints REST internos que
boot()já expõe (GET .../license,POST .../license/{activate,deactivate,refresh}) — cada operação com a capability que você definiu no item 4.
Se o plugin tem interface própria, prefira a aba própria: a tela padrão da biblioteca não identifica de qual produto ela é a não ser pelo withProductName(), e num site com vários plugins da casa instalados as entradas ficam menos distinguíveis do que uma aba já integrada ao painel.
6. Configuração de produção: URL e chave pública
Um único par de constantes, com os mesmos nomes em todo plugin da casa:
define( 'V3R_LICENSE_API_URL', 'https://v3rtech.com.br/wp-json/v3r-license/v1' );
define( 'V3R_LICENSE_PUBLIC_KEY', 'CHAVE_PUBLICA_ED25519_BASE64_DO_SERVIDOR' );
O plugin nunca define
V3R_LICENSE_API_URLnemV3R_LICENSE_PUBLIC_KEYdiretamente — nem comif ( ! defined(...) ) define(...). Esses dois nomes pertencem aowp-config.phpdo site; a existência deles ali é o único sinal de que o dono do site sobrescreveu o par (tipicamente para apontar a um servidor de licenças local, em desenvolvimento). Umdefine()condicional no topo do plugin — o padrão comum do WordPress — faz as duas constantes existirem sempre, e qualquer guard que compare “as duas vieram, ou nenhuma” deixa de distinguir wp-config de default.O dano maior é entre plugins, não dentro de um só. Num site com dois plugins da casa, o primeiro a carregar (ordem alfabética, que ninguém controla) define os dois nomes compartilhados com o seu default; o segundo encontra as duas já definidas, conclui que o site sobrescreveu o par, e passa a falar com a URL do primeiro e a conferir a chave do primeiro. Em versões diferentes — o normal — a configuração de um plugin vaza para o outro, e ninguém programou isso.
Os defaults de produção do seu plugin vão em constantes de nome próprio (ex.:
MEUPLUGIN_LICENSE_API_URL_PADRAO), nunca sob os nomes compartilhados. A leitura deV3R_LICENSE_API_URL/V3R_LICENSE_PUBLIC_KEYsó serve para checar se o site sobrescreveu.
A decisão de qual par usar é uma função pura, separada da leitura das constantes — o que torna testável o par incoerente (só uma das duas definida) e o estado “chave de produção ainda não existe” (placeholder no build):
function meuplugin_decide_v3r_license_config(
?string $url_do_site, ?string $chave_do_site,
string $url_padrao, string $chave_padrao, string $placeholder
): array {
$veio_url = null !== $url_do_site && '' !== $url_do_site;
$veio_chave = null !== $chave_do_site && '' !== $chave_do_site;
if ( $veio_url !== $veio_chave ) {
return array( 'status' => 'incoerente' );
}
if ( $veio_url ) {
return array( 'status' => 'ok', 'api_url' => $url_do_site, 'public_key' => $chave_do_site );
}
if ( $placeholder === $chave_padrao ) {
return array( 'status' => 'chave_pendente' );
}
return array( 'status' => 'ok', 'api_url' => $url_padrao, 'public_key' => $chave_padrao );
}
A leitura das constantes globais fica num adaptador fino, em volta dessa função — a única parte que toca estado global, e a única que não dá para testar isoladamente.
7. Empacotamento (build-zip.sh)
Depois de gerar o vendor/ de produção e antes de qualquer dump-autoload que o seu script rode dentro do diretório temporário do pacote:
composer install --no-interaction # garante vendor/ fresco antes de prefixar
composer prefix --no-interaction
if [ ! -f "vendor-prefixed/v3rtech/v3r-core/src/Bootstrap.php" ]; then
echo "ERRO: v3r-core não encontrado em vendor-prefixed/." >&2
exit 1
fi
# guard de duas pontas — ver item 8
cp -r vendor-prefixed "$TEMP_DIR/"
( cd "$TEMP_DIR" && composer install --no-dev --no-interaction && composer dump-autoload --no-interaction )
Prefixar a árvore de desenvolvimento não é prefixar o ZIP. Confira o pacote final, não a pasta
vendor-prefixed/do seu checkout — um passo posterior do build pode reescrevercomposer.jsondentro do diretório temporário e rodardump-autoloadali, quebrando a referência avendor-prefixed/se a cópia não tiver acontecido antes. Edelete_vendor_packages: true(item 3) apaga o pacote-fonte depois da primeira prefixação — um segundocomposer prefixsem reinstalar antes “sucede” sem copiar nada, evendor-prefixed/sai vazio em silêncio. É por isso quecomposer installroda de novo, logo antes de cadacomposer prefix, dentro do próprio script — nunca assuma que ovendor/de uma execução anterior ainda tem a lib.
8. O guard de duas pontas — confira as duas, não só uma
php -r '
require "vendor-prefixed/autoload.php";
$prefixo = "MeuPlugin\\Vendor";
$erros = array();
if ( ! class_exists( $prefixo . "\\V3R\\Core\\Bootstrap" ) ) {
$erros[] = "a classe prefixada não resolve";
}
if ( class_exists( "V3R\\Core\\Bootstrap" ) ) {
$erros[] = "o namespace ORIGINAL ainda resolve";
}
if ( $erros ) {
fwrite( STDERR, implode( "\n", $erros ) . "\n" );
exit( 1 );
}
'
Checar só se a classe prefixada existe não distingue “bem configurado” de “mal configurado com as duas presentes”. É esse segundo estado — namespace original e prefixado, os dois resolvendo — que colide com o próximo plugin da casa que embutir a mesma lib no mesmo site. O guard tem que reprovar as duas formas de errar, não só uma.
9. CI
ci.yml, disparado em push e pull request, com matriz de PHP:
strategy:
matrix:
php: ['8.2', '8.3', '8.4']
Onde houver SPA própria, o build dela entra no mesmo workflow.
release.yml, disparado por tag v*.*.* — nunca publicação à mão:
- Roda a suíte de testes.
- Baixa o Strauss
.phare prefixa. - Roda o guard de duas pontas (item 8).
- Gera o zip pelo
build-zip.sh. - Confere que a versão do formulário/tag bate com o cabeçalho
Version:do arquivo principal. - Publica no V3RLicense:
POST /publishcomproduct_slug,versione o arquivo, autenticado porAuthorization: Bearercom o secretV3RLICENSE_PUBLISH_TOKEN— mesmo nome em todo repositório da casa.
O produto precisa estar cadastrado no V3RLicense antes da primeira publicação — ver Registrar um plugin novo, do lado de quem opera o painel. Publicar para um
product_sluginexistente é recusado.O token vale para um produto só. Ao gerar o token no painel, quem opera escolhe o produto que ele autoriza publicar — um token vazado do seu repositório não publica release de outro plugin da casa. Não peça um token “genérico”; peça um escopado ao seu produto.
Checklist antes de abrir o PR
Requires PHP: 8.2no cabeçalho do arquivo principal — não só nocomposer.json.- Se houver guard de versão de PHP próprio, ele está em sintaxe válida desde o PHP 7.0 (sem tipos union,
match, promoção de propriedade, nullsafe). v3rtech/v3r-coreemrequire-dev(nuncarequire), com o repositóriovcsdoV3RCore-Code.- Bloco
extra.strausscom namespace/prefixos únicos do plugin, eoverride_autoloaddoplugin-update-checkercopiado tal como está. vendor-prefixed/.gitkeepversionado; o resto devendor-prefixed/*no.gitignore.- Todo ponto do código que toque o
v3r-coreestá atrás declass_exists(). withCapabilityDecider()chamado antes deboot().withProductName()chamado, se a tela padrão for usada.- Existe caminho de ativação na interface — tela padrão ou aba própria consumindo os endpoints REST.
V3R_LICENSE_API_URL/V3R_LICENSE_PUBLIC_KEYnunca definidas pelo plugin — só lidas; os defaults de produção vão em constantes de nome próprio.build-zip.shreinstala antes de prefixar, copiavendor-prefixed/antes dodump-autoloaddo pacote, e roda o guard de duas pontas.ci.ymlcom matriz['8.2', '8.3', '8.4'];release.ymldisparado por tagv*.*.*.- Secret
V3RLICENSE_PUBLISH_TOKENconfigurado no repositório, com escopo para o produto certo. - Produto cadastrado no painel do V3RLicense, com o slug exatamente igual à pasta raiz do zip.
- Testado com usuário autenticado, não só com requisição anônima.