Gerenciamento de dependências Composer em plugins profissionais WordPress

Domine o Composer em plugins WordPress: autoload PSR-4, isolamento de dependências, versionamento semântico e estratégias para evitar conflitos em produção.

Mais de 60% dos plugins profissionais ativos no repositório oficial já utilizam Composer para gerenciar dependências, segundo dados de telemetria de ferramentas de análise de código aberto. Ainda assim, a maioria dos conflitos fatais reportados em fóruns de suporte origina-se de colisão de namespaces entre bibliotecas compartilhadas — problema que o gerenciamento correto resolve de forma definitiva.

Este artigo apresenta o fluxo completo: da configuração inicial do composer.json à geração de um pacote distribuível isolado, passando por versionamento semântico, autoload otimizado e integração com pipelines de integração contínua.

Fundamentos: por que Composer em plugins WordPress

O ecossistema WordPress não possui um gerenciador de pacotes nativo para código PHP reutilizável entre plugins. Temas e plugins historicamente copiavam bibliotecas para pastas includes/ ou libs/, gerando duplicação de código, impossibilidade de atualização coordenada e colisões de classe quando dois plugins incluíam versões diferentes da mesma dependência — como Guzzle ou League\Flysystem.

O Composer resolve três problemas simultâneos: resolução de grafo de dependências com restrições de versão, autoload PSR-4 otimizado via classmap e geração de arquivo de trava (composer.lock) para builds determinísticos. Diferente do npm no JavaScript, o PHP não carrega módulos em tempo de execução; o autoload gerado pelo Composer mapeia namespaces para caminhos de arquivo uma única vez, sem overhead de filesystem em cada request.

Para plugins distribuídos via repositório oficial ou canais comerciais, o vendor/ não deve ser versionado no git do plugin. Em vez disso, o pipeline de build consome o composer.lock, roda ferramentas de prefixação e empacota apenas o necessário na pasta dist/ que será zipada para o usuário final.

Estrutura do composer.json para plugins profissionais

Um composer.json bem desenhado separa claramente dependências de runtime (produção) de dependências de desenvolvimento. Veja o modelo base:

{
  "name": "vendor/plugin-slug",
  "type": "wordpress-plugin",
  "description": "Descrição curta do plugin",
  "license": "GPL-2.0-or-later",
  "require": {
    "php": "^7.4 || ^8.0",
    "guzzlehttp/guzzle": "^7.5",
    "league/csv": "^9.10"
  },
  "require-dev": {
    "phpunit/phpunit": "^9.6",
    "phpstan/phpstan": "^1.10",
    "squizlabs/php_codesniffer": "^3.7",
    "humbug/php-scoper": "^0.16"
  },
  "config": {
    "platform": {
      "php": "7.4.33"
    },
    "optimize-autoloader": true,
    "sort-packages": true
  },
  "autoload": {
    "psr-4": {
      "Vendor\\PluginName\\": "src/"
    }
  },
  "scripts": {
    "test": "phpunit",
    "analyse": "phpstan analyse src",
    "cs-check": "phpcs src --standard=WordPress",
    "cs-fix": "phpcbf src --standard=WordPress",
    "build": "php vendor/bin/php-scoper add-prefix --config=scoper.inc.php --working-dir=build --output-dir=dist"
  },
  "extra": {
    "branch-alias": {
      "dev-main": "1.x-dev"
    }
  }
}

Pontos críticos:

  • platform.php: força o Composer a resolver pacotes compatíveis com PHP 7.4 mesmo se o ambiente de build roda PHP 8.2.
  • optimize-autoloader: gera classmap autoritativo, evitando stat() em filesystem no production.
  • autoload.psr-4: mapeia seu namespace raiz para pasta src/. Mantenha uma classe por arquivo, nome do arquivo = nome da classe.
  • scripts.build: exemplo de comando que usa PHP-Scoper; o arquivo scoper.inc.php define regras de prefixação e arquivos a excluir.

Isolamento de dependências com PHP-Scoper

O maior inimigo da estabilidade em plugins WordPress é a colisão de classes globais. Se seu plugin usa Guzzle 7.5 e outro plugin usa Guzzle 6.5, quem carregar primeiro vence — e o segundo falha com erro fatal “Cannot declare class GuzzleHttp\Client”.

A solução padrão da indústria é prefixar todas as dependências de vendor com um namespace exclusivo do seu plugin. O PHP-Scoper automatiza isso: ele reescreve namespaces, use statements, strings de classe em anotações e até chamadas dinamicas como new $className().

Exemplo de scoper.inc.php:

<?php
return [
    'prefix' => 'Vendor\\PluginName',
    'whitelist' => [
        // Classes que NÃO devem ser prefixadas (interfaces WP, SPL, etc.)
        'WordPress',
        'WP_*',
        'Iterator',
        'ArrayAccess',
        'Countable',
        'JsonSerializable',
    ],
    'patchers' => [
        // Patchers customizados para bibliotecas problemáticas
        function ($code, $file) {
            // Exemplo: corrigir caminhos de arquivos embutidos em libs
            return $code;
        },
    ],
    'finders' => [
        // Incluir apenas o necessário no build final
        (new Symfony\Component\Finder\Finder())
            ->files()
            ->name('*.php')
            ->in(__DIR__ . '/build/src')
            ->in(__DIR__ . '/build/vendor'),
    ],
];

O fluxo de build típico:

  1. composer install --no-dev --optimize-autoloader em pasta temporária build/
  2. Copiar src/ para build/src/
  3. Rodar php-scoper add-prefix --config=scoper.inc.php --working-dir=build --output-dir=dist
  4. Ajustar o autoload.php gerado para carregar seu namespace original (Vendor\PluginName\) a partir de dist/src/ e as dependências prefixadas a partir de dist/vendor/
  5. Zipar dist/ + arquivo principal do plugin + assets para distribuição

Esse processo garante que Vendor\PluginName\GuzzleHttp\Client coexista pacificamente com OutroPlugin\GuzzleHttp\Client no mesmo site.

Versionamento semântico e estratégia de branches

Plugins profissionais adotam SemVer (MAJOR.MINOR.PATCH) alinhado ao ciclo de vida do WordPress:

Versão Quando incrementar Exemplo impacto
PATCH Correções de bug, segurança, compatibilidade WP 2.1.3 → 2.1.4
MINOR Novas funcionalidades retrocompatíveis, novas hooks/filters 2.1 → 2.2
MAJOR Breaking changes: remoção de hooks, mudança de assinatura de métodos públicos, requisito mínimo de PHP/WP 2.x → 3.0

No composer.json, declare dependências com restrições flexíveis mas seguras:

  • ^7.5 para bibliotecas que seguem SemVer (permite 7.5.0 até < 8.0)
  • ~9.10 se a biblioteca usa versionamento ponto-a-ponto (permite 9.10.0 até < 9.11)
  • Evite * ou dev-main em require de produção

Use extra.branch-alias para mapear dev-main → 1.x-dev, facilitando quem depende do seu plugin como biblioteca (raro, mas possível em suites de plugins).

Mantenha branches:

  • main / trunk: versão estável mais recente (tagged releases)
  • develop: integração de features para próximo MINOR
  • hotfix/x.x.x: correções urgentes para PATCH em versão já lançada

Tags no git (v2.1.4) devem coincidir com versão no cabeçalho do plugin e no composer.json. Ferramentas como git-changelog ou release-please automatizam notas de versão a partir de conventional commits.

Integração com CI/CD e qualidade de código

Um pipeline mínimo para plugin profissional roda em todo push e pull request:

  1. Lint & Static Analysis: composer cs-check && composer analyse
  2. Testes unitários: composer test -- --coverage-clover=coverage.xml
  3. Build de verificação: composer build (gera dist/ prefixado)
  4. Teste de ativação: Instala WordPress limpo + plugin zipado + roda smoke tests via WP-CLI

Exemplo de .github/workflows/ci.yml:

name: CI
on: [push, pull_request]
jobs:
  quality:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['7.4', '8.0', '8.1', '8.2']
        wp: ['5.8', '6.0', '6.3', 'latest']
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, xml, curl, zip
          tools: composer:v2
      - name: Install deps
        run: composer install --prefer-dist --no-progress
      - name: Code style
        run: composer cs-check
      - name: Static analysis
        run: composer analyse
      - name: Unit tests
        run: composer test
      - name: Build distributable
        run: composer build
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: plugin-dist-${{ matrix.php }}
          path: dist/

Essa matriz testa compatibilidade cruzada PHP × WordPress. Para plugins no repositório oficial, o teste mínimo recomendado cobre as duas versões LTS do WordPress e as duas versões ativas de PHP.

Ferramentas como PHPStan nível 5+ detectam bugs latentes (tipos errados, chamadas a métodos inexistentes, branches inalcançáveis). Combine com PHP_CodeSniffer padrão WordPress ou WordPress-VIP para impor convenções de código do core.

Boas práticas de distribuição e manutenção

Ao preparar o zip final para usuários (seja via WordPress.org, Freemius, EDD ou site próprio):

  • Não inclua pasta .git/, tests/, phpunit.xml, phpstan.neon, .github/, composer.json, composer.lock, scoper.inc.php, vendor/ original (não prefixado).
  • Inclua apenas: arquivo principal do plugin (plugin-slug.php), pasta src/ (seu código), pasta vendor/ prefixada, assets (CSS, JS, imagens), readme.txt válido.
  • O arquivo principal deve fazer require_once __DIR__ . '/vendor/autoload.php'; antes de qualquer add_action, register_activation_hook ou instância de classe.
  • Defina cabeçalhos obrigatórios:
    /**
     * Plugin Name: Meu Plugin Profissional
     * Requires at least: 5.8
     * Requires PHP: 7.4
     * Version: 2.1.4
     */
    
  • Use plugin_dir_path(__FILE__) para caminhos absolutos; evite ABSPATH hardcoded.

Para atualizações automáticas via repositório oficial, o readme.txt deve ter Stable tag apontando para a tag git correspondente. Para distribuição privada, implemente endpoint de atualização compatível com pre_set_site_transient_update_plugins — veja nosso guia sobre autenticação via Application Passwords na REST API para autenticar chamadas de verificação de versão.

Armadilhas comuns e como evitá-las

Armadilha Sintoma Prevenção
Esquecer composer.lock no git Builds inconsistentes entre máquinas Commit obrigatório no .gitignore não listar composer.lock
Prefixar classes do WordPress core Fatal error “Class WP_Post not found” Whitelist completo no scoper.inc.php: WP_*, Walker, WP_Error, WP_Query, etc.
Usar require-dev em produção Plugin pesado, superfície de ataque --no-dev no install de build; audit regular com composer audit
Não testar PHP mínimo suportado Erros de sintaxe em PHP 7.4 (ex: fn() arrow functions) Matriz CI com versão mínima; platform.php no composer.json
Autoload quebrado após prefixação “Class Vendor\PluginName\Vendor\GuzzleHttp\Client not found” Verificar autoload.php gerado; testar new \Vendor\PluginName\GuzzleHttp\Client() isolado
Dependências com conflitos de licença Incompatibilidade GPL composer licenses --format=json no CI; bloquear LGPL/MPL se plugin é GPL-only

Outro ponto frequente: plugins que registram blocos Gutenberg dinâmicos com React precisam garantir que o build do frontend (webpack/vite) gere assets versionados e que o PHP saiba o caminho correto via wp_register_script com dependências declaradas. O Composer cuida apenas do PHP; o build de assets é pipeline separado, mas ambos devem versionar juntos.

Conclusão prática

Gerenciar dependências com Composer em plugins WordPress não é opcional para código profissional — é a barreira entre manutenibilidade a longo prazo e o ciclo interminável de conflitos fatais e tickets de suporte evitáveis. Configure o composer.json com plataforma travada, adote PSR-4 rigoroso, prefixe vendor no build e integre análise estática no CI. O investimento inicial de algumas horas economiza semanas de depuração em produção e garante que seu plugin conviva harmonicamente em qualquer instalação, por mais complexa que seja a pilha de plugins ativos.

Checklist prático

0 de 9 concluídos

Perguntas frequentes

Por que não posso apenas usar require_once para incluir bibliotecas externas no meu plugin?

O require_once não gerencia versões, atualizações de segurança ou conflitos de namespace entre plugins. O Composer resolve o grafo de dependências, garante autoload otimizado via PSR-4 e permite travar versões exatas no composer.lock para builds reprodutíveis.

Como evitar que meu plugin quebre outro que use a mesma biblioteca em versão diferente?

Use PHP-Scoper ou Strauss para aplicar um prefixo exclusivo (ex: MeuPlugin\Vendor\GuzzleHttp) a todas as dependências. Isso isola o namespace no arquivo vendor compilado, permitindo que múltiplos plugins usem versões diferentes da mesma biblioteca sem colisão fatal.

O arquivo composer.lock deve ir para o controle de versão do plugin?

Sim. O composer.lock garante que qualquer desenvolvedor, servidor de CI ou pipeline de deploy instale exatamente as mesmas versões testadas. Sem ele, uma atualização menor de dependência pode introduzir bugs sutis em produção.

Qual a diferença entre require e require-dev no contexto de plugins WordPress?

Require instala dependências necessárias para o plugin funcionar em produção (ex: Guzzle, League\CSV). Require-dev instala ferramentas de desenvolvimento como PHPUnit, PHPStan, PHP_CodeSniffer e PHP-Scoper, que não devem acompanhar o zip final distribuído aos usuários.

Como configurar o autoload PSR-4 corretamente no composer.json do plugin?

Defina "autoload": {"psr-4": {"MeuPlugin\\": "src/"}} no composer.json. Mantenha classes em src/ seguindo a estrutura de pastas igual ao namespace. Após rodar composer dump-autoload, inclua vendor/autoload.php no arquivo principal do plugin antes de qualquer outra lógica.

Posso usar dependências que exijam PHP 8.2 se o WordPress ainda suporta PHP 7.4?

Não. Seu plugin deve respeitar o requisito mínimo de PHP do WordPress core ou definir um requisito próprio no cabeçalho do plugin (Requires PHP). Use a plataforma config no composer.json ("platform": {"php": "7.4"}) para forçar o Composer a resolver dependências compatíveis com essa versão.