Desenvolvendo plugins para WordPress
- Pessoal/Off-topic
- March 10, 2008
Uma peça chave do sucesso do WordPress é justamente sua capacidade de aceitar plugins e widgets, além , é claro, dos milhares de programadores que desenvolvem os mais diversos plugins que fazem de tudo um pouco.
Algum tempo atrás fui abordado pelo Manoel Lemos do BlogBlogs para discutirmos uma missão, criar um plugin para o WordPress, baseado na API do site. Comecei entao minha saga atrás de como desenvolver um plugin. O material estava lá, espalhado em vários sites, mas estava lá.
Então agora com o sucesso do BBUinfo e na véspera de novos projetos, decidi tentar consolidar este conhecimento adquirido e focar ele no público brasileiro. Vou procurar mostrar o caminho das pedras de como começar e onde buscar os dados necessário para integrar seu código ao do WP.
O WP, como eu disse, foi desenhado com plugins em mente. Sua estrutura possui diversos componentes desenhados para que seu plugin se “encaixe” no ponto certo (attach points ou hooks) e na hora certa. Mas para isso é importante observar alguns passos, para melhor desenhar seu plugin.
Os passos a seguir são a minha recomendação de como desenvolver um plugin, e evitar maiores problemas e dificuldades. Para poder acompanhar melhor o código produzido, vou estar criando com vocês o meuPrimeiroPlugin, que será descrito passo a passo abaixo.
Passo 1: Planejamento
No desenvolvimento de qualquer sistema o primeiro passo deve sempre ser uma análise e planejamento do que será feito, com plugins, embora pequenos, isto se mantém.Como em sistemas é importante identificar neste momento o que se deseja fazer, qual o objetivo do plugin. Levantar entradas e saídas, dados externos e suas fontes, enfim, definir de forma geral o que o plugin faz, com que dados ele faz isso e como obter os dados necessários.Faça perguntas como:
- Preciso de um banco de dados?
- Preciso acessar dados externos por webservice ou uma API?
- Vou mostrar algo visual para o usuário?
- O que pretendo fazer?
- Qual o nome darei ao plugin?(se você consegue definir o nome, sabe o que quer)
Um bom exercício para este momento é utilizar o padrão de arquivo readme do WP e já iniciar a escrita da descrição resumida e detalhada do plugin, pois este será seu guia. (você pode validar o arquivo aqui )
meuPrimeiroPlugin
Este plugin, desenhado para o aprendizado de como desenvolver plugins, executa ações básicas e didáticas, mas sem muito uso. Para mostrar todos os limites, o plugin fará as seguintes ações:
- Acrescentará ao final de cada comentário o texto: “O conteúdo deste comentário é de responsabilidade de: {AUTOR}”
- Criará uma tabela nova que irá armazenar uma contagem de visitas a cada post, e mostrará esta contagem logo ao final do POST.
- Substituir a palavra XXX por um link
Exemplos
- Arquivo readme.txt
- Meu brainstorm (idéias para o plugin):
Produtos desta etapa Arquivo readme.txt, arquivo com “brainstorm” (tente um mindmap, como acima)
Passo 2: Projeto
Esta etapa segue os padrões de projetos de sistema, mas com algumas adições específicas do WordPress. Como base sugiro que use a linguagem UML para definir seu projeto e faça um diagrama de MER (base de dados).
Embora não seja obrigatório, a criação do plugin como um objeto, é recomendável e mais fácil de manter do que código estruturado, além claro de ser uma “prática recomendada” para o PHP5. Outra grande vantagem nesta orientação é que como os métodos são chamados de dentro do objeto a preocupação com “nomes coincidentes” é nula, sendo reduzida apenas ao nome do objeto que deve ser único.
Então defina o seu objeto com métodos que se identifiquem com as diversas ações que o plugin vai executar, por exemplo: adicionarAssinatura(), pegarDadosExternos(), salvarDados()…
Agora entra o ponto onde o WordPress traz toda sua mágica com os seus “hooks”, ou ganchos.
Ganchos do WordPress
Para facilitar que se ligue o plugin às ações doWP, são definidos ganchos em sua programação, ou seja, em pontos específicos do funcionamento do plugin, são disponibilizados pontos onde as funções podem se “pendurar”. Desta forma caso deseje criar um plugin, que deve executar determinada função no momento em que um comentário é salvo, você usaria o gancho “comment_post”.
Estes ganchos são divididos em 2, ações e filtros.
- Ações são eventos disparados em momentos exatos durante a execução do WordPress.
- Filtros são lançados no momento de gravação ou apresentação de um texto para que seja possível modificá-lo.
Outros Links: Todos Hooks de todas versões do WP
Para definir os ganchos você pode se perguntar “Em que momento devo fazer isso?” para cada ação e então verificar a lista de ganchos de ações e filtros para achar qual lhe atende.
meuPrimeiroPlugin Neste caso podemos usar os seguintes pontos para anexar nossas funções:
- Instalar: ligada ao “register_activation_hook” que é executado na ativação de um plugin no menu
- Desinstalar: Estas funções foram bastante discutidas , mas ainda não existe um hook fixo.
- Inicializar: Esta deve ser executada logo no início, para isso temos o filtro “init”
- Aba de opções: para fazer a aba precisamos de dois pontos, a ação “admin_menu” e o “add_options_page”.
- Contar Visita: poderia ser melhor,mas o filtro “the_content” garante a contagem na hora mais certa.
- Inserir texto no comentário: Para isso o filtro “get_comment_text” é perfeito
- Substituir palavras: este é bem direto também, vamos usar o filtro “the_content”
Juntamente com este raciocínio, fiz o mapeamento da classe e do que seria necessário na base de dados:
Diagrama de classe do plugin
MER do Banco
Produtos desta etapa: diagrama de classe do seu plugin, um MER do banco de dados e uma lista de possíveis pontos de inserção (ganchos).
Passo 3: Desenvolvendo
Com o caminho a sua frente devidamente mapeado, agora podemos nos aprofundar no código, além de conhecer algumas possibilidades que o WP traz embutidas em si mesmo.
O ideal é que seu plugin caiba dentro de um arquivo só, mas caso isso não seja possível, recomenda-se que todos arquivos sejam jogados dentro de uma pasta com o mesmo nome do plugin. O nome do plugin também pode (recomenda-se) ser o nome de seu arquivo principal, isso irá lhe facilitar usar funções “on activation” disparadas na ativação do plugin. Portanto teremos a seguinte estrutura para nosso exemplo:
A primeira coisa que deve aparecer em nosso arquivo meuPrimeiroPlugin.php é o cabeçalho. Este cabeçalho é usado pelo WP para mostrar os dados de seu plugin na página de administração de plugins, então é muito importante manter estes dados corretos, o cabeçalho segue o padrão abaixo:
/\* Plugin Name: Nome do Plugin Plugin URI: http://Endereço\_da\_pagina Description: Descrição simples e curta do plugin Version: A versão atual do plugin, ex.: 1.0 Author: Nome do Autor do Plugin Author URI: http://Endereço\_do\_site\_do\_autor \*/
Após isso podemos começar a definir a classe. A definição segue o padrão normal de um objeto em PHP (4 ou 5). Aqui vamos seguir uma orientação PHP5 por sua enorme vantagem e pelo “fim” do PHP
O site do WP possui muito material sobre padrões de codificação , e eu recomendo uma leitura, mas são padrões bem estabelecidos que muitos programadores já devem usar normalmente.
Como vimos no passo anterior montamos um diagrama de classe e nesse diagrama já identificamos as funções que precisamos desenvolver em nosso plugin, portanto a seguir vou seguir passo a passo deste arquivos criando e explicando cada uma destas funções, e no final comentarei sobre os ganchos que serão colocados fora do escopo do objeto e porque.
Função Inicializar
Esta função serve para centralizar todas as ações que devem ser sempre tomadas para integrar o plugin com o WP, sejam elas ações de adição/remoção de filtros/ações ou instanciação de objetos, cópias de variáveis globais, enfim, todas ações que serão executadas sempre que o WP carregar.
public static function inicializar(){ global $wpdb;
//Definir ganchos add\_filter("get\_comment\_text", array("meuPrimeiroPlugin","exonerarComentarios")); add\_filter("the\_content", array("meuPrimeiroPlugin","contarVisita")); add\_filter("the\_content", array("meuPrimeiroPlugin","processarSubstituicao")); add\_action('admin\_menu', array('meuPrimeiroPlugin','adicionarMenu'));
//Mapear objetos WP meuPrimeiroPlugin::$wpdb = $wpdb;
//Outros mapeamentos meuPrimeiroPlugin::$info\['plugin\_fpath'\] = dirname(\_\_FILE\_\_);
}
Como podemos ver, no caso do meuPrimeiroPlugin, estamos cadastrando as outras funções com seus devidos filtros, ou seja, os ganchos que irão executá-las e copiando o objeto $wpdb para dentro do nosso script, podendo agora ser referenciado como: meuPrimeiroPlugin::$wpdb que é melhor que ficar sempre usando “global” no início da função. Além disso aproveita-se para obter a localização do plugin, usando a constante __FILE__ do PHP que define o caminho até o arquivo onde esta.
$wpdb Este objeto sempre presente no WordPress é responsável pela sua conexão com a base de dados, ele pode executar queries, buscar dados, e realizar diversas funções relacionadas a base de dados. Veja aqui a lista de métodos ```aqui
Função Instalar
Esta função é mapeada para ser executada no momento que o plugin é ativado na aba de plugins do WordPress. Como nosso plugin utiliza dados que serão salvos e lidos de bases de dados é necessário que elas sejam criadas antes de seu uso, o momento de ativação é excelente para isso, porém o plugin pode ser ativado e desativado diversas vezes (quando se faz upgrade do WordPress por exemplo) e não podemos simplesmente criar a tabela sempre, por isso observe o comando para criar apenas caso não exista.
[php] public static function instalar(){
if ( is_null(meuPrimeiroPlugin::$wpdb) ) meuPrimeiroPlugin::inicializar();
//Criar base de dados $sqlContador = “CREATE TABLE IF NOT EXISTS `".meuPrimeiroPlugin::$wpdb->prefix.“mpp_post_visitas` ( `id_post` INT NOT NULL , `visitas` INT NULL , PRIMARY KEY (`id_post`) )”;
$sqlPalavras = “CREATE TABLE IF NOT EXISTS `".meuPrimeiroPlugin::$wpdb->prefix.“mpp_substituicao` ( `idmpp_substituicao` INT NOT NULL AUTO_INCREMENT , `orig_palavra` VARCHAR(255) NULL , `subst_palavra` VARCHAR(255) NULL , PRIMARY KEY (`idmpp_substituicao`) )”; meuPrimeiroPlugin::$wpdb->query($sqlContador); meuPrimeiroPlugin::$wpdb->query($sqlPalavras);
//Criar opções add_option(“mpp_texto_exonerar”,“O conteúdo deste comentário é de responsabilidade de: “); }
Neste caso, de forma puramente didática, criei uma opção para armazenar o texto que será inserido nos comentários. Esta opção foi criada apenas para mostrar como elas podem ser utilizadas. Estas **opções são gravadas na tabela de opções do próprio WordPress** e acessadas com estes métodos: _add\_option, update\_option, delete\_option, get\_option_.
Note que no ínicio da função chamo a função de inicialização, isso é importante pois no ato em que a função de instalação é chamada o gancho "init" ainda não foi chamado e por isso não temos nossa conexão com o banco por exemplo. Isto causaria erros na ativação do plugin.
> **Dica:** Se você receber um aviso "cannot redeclare class" na ativação do plugin, 9 em 10 vezes o erro não esta na declaração do plugin e sim omitido pelo WordPress, em sua função de captura de erros, que infelizmente possui algumas falhas, como essa.
>
> Para ver o erro verdadeiro vá até o arquivo _wp-admin/plugin.php_ e comente as linhas 14 e 15, mostradas abaixo:
>
> ```php
wp\_redirect(add\_query\_arg('\_error\_nonce', wp\_create\_nonce('plugin-activation-error\_' . $plugin), 'plugins.php?error=true&plugin=' . $plugin)); // we'll override this later if the plugin can be included without fatal error ob\_start();
Função Desinstalar
Esta função não possui um hook para ser executada de forma automática, mas é uma função que recebeu muita atenção nos últimos tempos em diversas discussões pela internet. Nossa aba de opções deve implementar uma chamada a esta função, mas em breve o WordPress deve implementar um hook para ela.
Sua função é bem direta e simples, remover tudo que foi criado pela função de instalação. Por isso não é possível ligar ela ao ato de “desativar” o plugin, pois o usuário perderia suas preferências a cada vez que fizesse upgrade do WordPress (é recomendado que se desative os plugins para tal).
public static function desinstalar(){
//Remover bases de dados $sqlContador = "DROP TABLE \`".meuPrimeiroPlugin::$wpdb->prefix."mpp\_post\_visitas\`"; $sqlPalavras = "DROP TABLE \`".meuPrimeiroPlugin::$wpdb->prefix."mpp\_substituicao\`";
meuPrimeiroPlugin::$wpdb->query($sqlContador); meuPrimeiroPlugin::$wpdb->query($sqlPalavras);
//Remover opções delete\_option("mpp\_texto\_exonerar"); }
Como podemos ver apenas é feito um DROP nas tabelas e a remoção da opção que foi criada na própria tabela do WordPress.
Função adicionarMenu
Esta função indica ao WordPress que ao montar o menu da administração ele deve inserir uma nova aba que aponta para uma função de nosso plugin.
public static function adicionarMenu(){ add\_options\_page('meuPrimeiroPlugin - Gerenciamento','meuPrimeiroPlugin',10,\_\_FILE\_\_,array("meuPrimeiroPlugin","abaOpcoes")); }
A sintaxe deste comando é a seguinte:
add_menu_page(titulo_da_pagina, titulo_do_menu, nivel_de_acesso, arquivo, ```função );
O nível de acesso diz respeito as permissões do usuário, o arquivo é o arquivo de nosso plugin (__FILE__) e a função é a função de nossa classe em formato de call_user_func
Função abaOpcoes
Esta função é a que setamos no exemplo anterior para ser executada quando o usuário clicar na nossa aba do menu. Neste caso fiz uma implementação simplificada, onde uma mesma função mostra o menu e executa as operações de salvar e atualizar as opções.
Para salvar as opções fazemos inserções usando novamente o $wpdb->query(). Esta operação é executada sempre que o array de POST (dados enviados pelo form) possuir algum conteúdo.
Para mostrar o formulário utilizei uma implementação simplificada de templates, onde tenho um arquivo .tpl externo com o layout do menu e chaves especiais ({PALAVRAS},{STATS}…) que são substituídas pelo conteúdo que quero gerar. Você pode usar outra técnica e até usar código espageti com HTML no meio de seu plugin, depende da sua facilidade e organização. Para gerar o conteúdo dinâmico, faço diversas queries no banco buscando os dados que são necessários.
[php] public static function abaOpcoes(){
//Predefinidos $templateVars[’{UPDATED}’] = “”; $templateVars[’{ERROS}’] = “”;
//Executar operações definidas if (count($_POST) > 0){ $ins = meuPrimeiroPlugin::$wpdb->query(“INSERT INTO “.meuPrimeiroPlugin::$wpdb->prefix.“mpp_substituicao (idmpp_substituicao,orig_palavra,subst_palavra) VALUES (NULL,’”.$_POST[‘orig’].”’,’”.$_POST[‘subst’].”’) “); $templateVars[’{UPDATED}’] = ‘<div id=“message” class=“updated fade”><p><strong>’; if ($ins){ $templateVars[’{UPDATED}’] .= “Dados atualizados!”; }else{ $templateVars[’{UPDATED}’] .= “Erro ao atualizar dados!”; } $templateVars[’{UPDATED}’] .= “</strong></p></div>”; }
//Ler arquivo de template usando funções do WP $admTplObj = new FileReader(meuPrimeiroPlugin::$info[‘plugin_fpath’]."/admin_tpl.htm”); $admTpl = $admTplObj->read($admTplObj->_length);
//pegar palavras $resultados = meuPrimeiroPlugin::$wpdb->get_results( “SELECT orig_palavra,subst_palavra FROM “.meuPrimeiroPlugin::$wpdb->prefix.“mpp_substituicao” ); foreach($resultados as $res){ $palavras .= “<li><b>”.$res->orig_palavra.” -></b> “.$res->subst_palavra."</li>”; } $templateVars[’{PALAVRAS}’] = $palavras;
//pegar estatisticas $resultados = meuPrimeiroPlugin::$wpdb->get_results( “SELECT post_title,visitas FROM “.meuPrimeiroPlugin::$wpdb->prefix.“mpp_post_visitas LEFT JOIN “.meuPrimeiroPlugin::$wpdb->prefix.“posts AS posts ON mpp_post_visitas.id_post = posts.ID” ); foreach ($resultados as $res){ $stats .= “<dt>”.$res->post_title."</dt>”; $stats .= “<dd>”.$res->visitas.” Visitas</dd>”; } $templateVars[’{STATS}’] = $stats;
//Substituir variáveis no template $admTpl = strtr($admTpl,$templateVars);
echo $admTpl;
}
Observe o uso da classe _**FileReader**_ para leitura do arquivo .tpl. Esta classe é nativa do WordPress portanto a preferência de se utilizar ela ao invés de utilizar funções como fopen ou similares. É importante lembrar que o **WordPress possui diversas classes disponíveis em suas pastas**, e é uma boa idéia ver se existe alguma para executar a operação que você procura.
Função contarVisita
Como foi dito esta função é convocada pelo gancho "the\_content" sempre que o conteúdo de um post for resgatado do banco para exibição. Então para fazer uma simples contagem de visualizações de um post, sem entrar em muitos detalhes, filtros por IP nem nada do tipo, apenas fazemos uma inserção no banco, em nossa tabela de contagem.
```php
public static function contarVisita($post\_texto){ global $post;
$sql = "INSERT INTO ".meuPrimeiroPlugin::$wpdb->prefix."mpp\_post\_visitas (id\_post,visitas) VALUES ('".$post->ID."',1) ON DUPLICATE KEY UPDATE visitas=visitas+1"; meuPrimeiroPlugin::$wpdb->query($sql);
return $post\_texto; }
Note neste caso que a função recebe um parâmetro que chamei de $post_texto. Sempre que uma função usa o gancho “the_content” ela receberá o conteúdo do post como parâmetro. Ao final da função este conteúdo, alterado ou não, deve ser retornado com uma chamada return, caso contrário o blog não exibirá o conteúdo do post.
Para o comando de SQL caso interesse usei o ON DUPLICATE KEY UPDATE, que verifica se já existe um registro para o post e faz a atualização do mesmo, caso contrário cria o novo registro. Ela é bem útil para não ser necessário fazer um select para decidir se devemos usar um INSERT ou UPDATE.
Função processarSubstituicao
Esta função, também ligada ao “the_content” efetua a troca das palavras que cadastrarmos, por outras que forem escolhidas. É interessante notar que podemos ter várias funções ligadas a um mesmo gancho e elas serão executadas na ordem que os ganchos são designados, por isso a importância ressaltada na função anterior, onde o parâmetro deve ser retornado no final.
Para fazer a substituição usei um método simples também, busquei no banco os pares (palavra original => nova palavra) e montei um array de “tradução”. Usando a função strtr do PHP eu passo este array e o texto do post e a função se encarrega de fazer a troca e me devolver o texto alterado.
public static function processarSubstituicao($post\_texto){
//Montar array de palavras para substituirmos $resultados = meuPrimeiroPlugin::$wpdb->get\_results("SELECT orig\_palavra,subst\_palavra FROM ".meuPrimeiroPlugin::$wpdb->prefix."mpp\_substituicao");
foreach($resultados as $res){ $aTraducao\[$res->orig\_palavra\] = $res->subst\_palavra; }
$post\_text = strtr($post\_texto,$aTraducao);
return $post\_texto; }
Função exonerarComentarios
Finalmente esta função será a responsável por incluir um texto ao final do comentário para nos exonerar das palavras do seu autor. Para isto usamos o gancho get_comment_text que é similar ao get_content acima, mas se refere ao comentário.
public static function exonerarComentarios($cmt\_texto){ global $comment;
$anexo = "<br><br><b><i>".get\_option('mpp\_texto\_exonerar').$comment->comment\_author."</i></b>";
$cmt\_texto .= $anexo;
return $cmt\_texto; }
Como antes, recebemos o texto como parâmetro e devemos retornar ele ao final para evitar quebrar outros plugins ou ocultar comentários .
Nota: Quando desenvolvi meu primeiro plugin de WordPress, usei o gancho get_comment_ID e não fiz o retorno do meu parâmetro ao final. Resultado, todos plugins executados após este quebravam, pois não recebiam um ID e sim uma string vazia.
Usando o contexto global, a variável $comment é um objeto que representa todos os dados do comentário atual, por isso como pode ser visto acima, posso acessar o nome do autor, para agregá-lo à mensagem, que foi buscada da tabela de opções pela chamada get_option. Ao final tudo isso é concatenado ao texto no comentário que segue em frente para outros plugins, ou é simplesmente mostrado.
Código remanescente
Para inicializar nosso plugin o código abaixo é inserido solto em nosso arquivo, para que seja executado diretamente quando o arquivo for incluso, já que tudo que esta no escopo da classe não é executado sem sua instanciação.
$mppPluginFile = substr(strrchr(dirname(\_\_FILE\_\_),DIRECTORY\_SEPARATOR),1).DIRECTORY\_SEPARATOR.basename(\_\_FILE\_\_); /\*\* Funcao de instalacao \*/ register\_activation\_hook($mppPluginFile,array('meuPrimeiroPlugin','instalar')); /\*\* Funcao de inicializacao \*/ add\_filter('init', array('meuPrimeiroPlugin','inicializar'));
Nestes ganchos determinamos a execução da função instalar na ativação do plugin e da init na inicialização do WordPress. Para o gancho de ativação definimos a variável $mppPluginFile que determina a pasta onde o plugin esta, e para isso o plugin deve estar em uma sub-pasta de plugin e não na raiz. Com esta combinação de funções e as constantes DIRECTORY_SEPARATOR (barra invertida ou não), e o __FILE__, saimos com algo como “mpp/meuprimeiroplugin.php” ou seja, pasta e arquivo necessários para a detecção de ativação.
Divulgando seu plugin
Para divulgar seu plugin o primeiro e mais importante passo é adicionar ele no repositório do WordPress. Isso é uma excelente ferramenta, pois além de fornecer um repositório SVN que pode lehe ajudar a melhor controlar seu desenvolvimento ele permite que o usuário de seu plugin possa ser notificado de novas versões diretamente na página de plugins.
Divulgue o seu plugin no repositório oficial do WordPress: http://wordpress.org/extend/plugins/ usando o link http://wordpress.org/extend/plugins/add/ . A única exigência é que o plugin deve ser licenciado como GPL (detalhes )
Conclusão
Espero que com este artigo (que atrasou mais do que devia) muitos usuários de WordPress e desenvolvedores da comunidade possam embarcar no desenvolvimento de novos plugins e que todos tenham uma noção do poder que o blog oferece permitindo tamanha integração de forma tão simplificada e acessível.
O plugin criado neste exemplo esta 100% funcional e pode até ser usado, mas use-o para aprender e vá muito mais longe. Abaixo deixo os links para download caso lhes interesse.
Com o atraso do artigo estamos na beira do WordPress 2.5, mas tudo indica que além da carinha nova da administração, não haverá impactos no processo descrito acima, e inclusive este exemplo já funciona na nova versão e foi testado aqui com ela.
não esqueçam: se quiserem agradecer, contribuam com uma graninha ou um presente da lista ao lado….