Observando mudanças de arquivos com PHP

Essa semana estou criando um gerador de site estático consumindo a API do WordPress e após uma dúzia de vezes que precisei abrir o terminal para recompilar o site fui atrás de uma opção mais eficiente.

Quem já trabalhou com WebPack, Sass, Vue.js, etc. já deve ter experimentado a opção watch para automatizar o processo de recompilar assim que os arquivos são modificados. É isso que eu quero no meu projeto!

Sempre gosto de pesquisar essas coisas em inglês devido à quantidade de resultados, e pesquisando por “terminal watch file run command” e “create a file watcher in php” descobri o inotify, um subsistema de kernel no Linux que observa mudanças de arquivos. Há implementações em PHP (através do Pear) e na maioria das linguagens mais populares. Com isso já é meio caminho andado.

Também não encontrei nenhum material sobre isso em português, então deixe-me consertar isso.

Nesse processo encontrei o pacote seregazhuk/php-watcher, deixei ele de lado porque seria muito fácil, não gera aprendizado e achei um exagero pelas funcionalidades que ele fornece (também não gosto de ficar adicionando dependências do composer). Outra opção descartada foi o PHP FAM (file alteration monitor – monitor de alteração de arquivo) devido à documentação insuficiente.

A minha implementação com inotify tem dois aspectos principais: quais eventos de arquivo devem ser observados e se o processo vai ser blocante (blocking) ou não. A documentação dessa extensão é meio confusa mas com um pouco de experimentação todas as informações para resolver essas duas questões estavam disponíveis.

Aviso: a extensão inotify do PHP só está disponível para Linux, provavelmente também funcione com Mac e Docker.

Antes de começar a testar é preciso instalar, nada muito fora do comum então vou passar só os comandos do processo no Ubuntu 20.04:

sudo apt-get install php-pear php-dev
sudo pecl install inotify
echo 'extension=inotify.so' | sudo tee /etc/php/7.4/mods-available/inotify.ini
sudo phpenmod -s cli inotify

Usei o PHP no modo interativo com “php -a” para entender como essa extensão funciona usando o exemplo da documentação de inotify_init como base. O tipo de evento tem 23 constantes que podem ser combinadas com operador binário “ou” e tem muitas possibilidades, comecei experimentando com IN_ALL_EVENTS e depois de algumas mudanças decidi pelo uso do valor IN_CLOSE_WRITE:

php -a
$inotify = inotify_init();
inotify_add_watch($inotify, 'diretorio_exemplo', IN_CLOSE_WRITE);
var_export(inotify_read($inotify));

Criei o arquivo “diretorio_exemplo/teste.txt” e a variável foi exibido no terminal um array de eventos:

array (
  0 => array (
    'wd' => 1,
    'mask' => 8,
    'cookie' => 0,
    'name' => 'teste.txt',
  ),
)
  • “wd” – o ID do observador retornado pela função inotify_add_watch
  • “mask” – identificador do tipo de evento (uma das constantes)
  • “cookie” – ID único relacionando eventos (não é relevante no caso em estudo)
  • “name” – nome do arquivo (apenas se um diretório está sendo observado)

Com isso passei para a questão: o processo blocante ou não. Tá mas o que isso significa?

Por padrão a função inotify_read bloqueia a fluxo do programa até que um evento seja capturado para retornar e prosseguir a execução, por isso é um processo blocante. Quando o processo é configurado sem bloqueio ao chamar a função inotify_read irá retornar os eventos capturados ou falso se não houver nenhum evento, permitindo que o programa execute outros código enquanto observa os eventos de arquivos. A mudança entre uma forma e outra é feita com a função stream_set_blocking.

Sabendo disso fica mais simples entender o funcionamento do laço blocante e não blocante:

<?php
$inotify = inotify_init();
inotify_add_watch($inotify, 'exemplo', IN_CLOSE_WRITE);

// processo blocante
while (false !== $events = inotify_read($inotify)) {
    // executar código para cada captura de eventos
    foreach ($events as $event) {
        // executar código para cada evento
    }
}

// processo não blocante
stream_set_blocking($inotify, false);
while (true) {
    if (false !== $events = inotify_read($inotify)) {
        // executar código para cada captura de eventos
        foreach ($events as $event) {
            // executar código para cada evento
        }
    }
    // executar código "paralelo" à observação de eventos
}

Daí configurei meu projeto para gerar os arquivos HTML quando os modelos fossem alterados, o inotify estava observando o diretório “template” e tudo funcionava como esperado até que modifiquei o arquivo “template/section/home.html” e nada aconteceu. Acontece que a extensão do PHP não tem observadores de diretório recursivos 😒.

Encontrei uma implementação que usa o executável do Linux “inotifywait” aqui (em inglês), entretanto o servidor compartilhado em que pretendo rodar esse código não tem ele instalado, então vamos para o “faça você mesmo”. Usei a função glob com a opção GLOB_ONLYDIR e o código ficou assim:

<?php
function inotify_add_watch_recursive($inotify, $path, $mask)
{
    inotify_add_watch($inotify, $path, $mask);
    if (is_dir($path)) {
        foreach (glob($path . '/*', GLOB_ONLYDIR) as $subdir) {
            inotify_add_watch($inotify, $subdir, $mask);
        }
    }
}

Depois disso adicionei a capacidade de executar funções diferentes dependendo de qual diretório registrou um alteração. O código está em uso no projeto pedrosancao/wordpress-static-generator no GitHub.

Comente aqui embaixo, esse conhecimento for útil? Te ajudou em alguma aplicação prática? Quais conteúdos você gostaria de ler?

Até mais.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s