CONCEITO GERAL
O Accelero usa bastante o design pattern "Observer" (ou algo parecido com esse pattern) pra garatir que as "coisas aconteçam" com a maior desacoplação de código possível.
Por exemplo, existem diversas coisas que precisam acontecer quando o cadastro de uma pessoa é salvo, mas não é uma boa idéia que a classe Pessoa tenha conhecimento de todas essas ações. Pra isso, usamos o conceito de eventos.
A ideia geral é bastante simples.
Durante o bootstrap do sistema, é possível registrar trechos de código que queiram "ser notificados quando um evento XYZ acontecer". Quando esse evento XYZ acontecer, o trecho "que está gerando" esse evento "avisa que esse evento está acontecendo", mas não sabe (e nem precisa saber) quem está sendo notificado.
Um pseudocódigo pra ilustrar:
#bootstrap.php
EventHandler::registerListener(event: 'evento_xyz', callback: function() {
echo 'Evento capturado!';
});
#outroarquivo.php
EventHandler::emitEvent(event: 'evento_xyz');
///SERÁ PRINTADO "Evento capturado!"
É possível passar parâmetros (tantos quantos forem necessários) durante a emissão de eventos, que podem ser capturados pelos listeners. É importante que nesse caso as coisas estejam "casadas" como se fosse uma interface:
#bootstrap.php
EventHandler::registerListener(event: 'evento_xyz', callback: function(Pessoa $pessoa, int $idade) {
echo $pessoa->getPesNome() . ' tem '. $idade . 'anos';
});
#outroarquivo.php
$pessoa = new Pessoa();
$pessoa->setPesNome('Joaquim');
EventHandler::emitEvent(event: 'evento_xyz', pessoa: $pes, idade: 25);
///SERÁ PRINTADO "Joaquim tem 25 anos"
Várias coisas podem estar registradas pra receber o mesmo evento, cada uma fazendo uma coisa diferente:
#bootstrap.php
EventHandler::registerListener(event: 'evento_xyz', callback: function(Pessoa $pessoa, int $idade) {
echo $pessoa->getPesNome() . ' tem '. $idade . 'anos';
});
EventHandler::registerListener(event: 'evento_xyz', callback: function(Pessoa $pessoa, int $idade) {
sendMail('asd@man.com', 'Alteração de cadastrado da pessoa ' . $pes->getPesNome());
});
COMPONENTES
Existem dois componentes que podem ser usados pra implementar esse pattern no Accelero, cada um com suas vantagens e desvantagens.
- Um é o
EventHandler, que é ideal para situações em que "quem está gerando o evento não precisa ter nenhum retorno". - Outro é o
Interceptor, que é ideal para situações em que "quem está gerando o evento espera algum retorno".
Na hora de registrar um listener, ambos pedem um identifier. Esse é o "CPF do listener". Se dois listeners forem registrados com o mesmo identifier, o último vai sobrescrever o anterior. Também é possível des-registrar um listener se conhecermos o seu identifier (às vezes é útil durante o desenvolvimento).
Exemplos a seguir.
EventHandler
Ideal para situações do tipo "fire and forget". Por definição, os callbacks devem ter tipo de retorno void.
A única forma de um evento desse tipo interferir no processo é estourando uma Exception, que pode ser capturada/tratada (ou não) por quem está emitindo o evento.
#bootstrap.php
new EventListener(
identifier: 'meu_listener',
event: 'evento_xyz',
callback: function(Pessoa $pessoa) : void {
//DO STUFF
},
priority: 10,
);
#outroarquivo.php
try {
Accelero::getInjection(EventHandler::class)->emitEvent('evento_xyz', $pessoa);
} catch (Exception $e) {
//HANDLE EXCEPTION
}
Eventos com prioridade menor (mais próxima de zero) serão executados ANTES.
Eventos com a mesma prioridade (default = 50) serão executados na ordem em que foram registrados.
Interceptor
Exatamente o mesmo conceito do EventHandler, mas permite que os callbacks retornem algo para quem está gerando o evento. O método InterceptorHandler::emit() retorna um Generator, e por isso precisa ser consumido (foreach ou algo parecido):
#bootstrap.php
new Interceptor(
identifier: 'meu_interceptor',
event: 'evento_xyz',
callback: function(Pessoa $pessoa) : bool {
return $pessoa->getPesNome() === 'Maria';
},
);
#outroarquivo.php
try {
$interceptorReturns = Accelero::getInjection(InterceptorHandler::class)->emit('evento_xyz', $pessoa);
foreach($interceptorReturns as $individualReturn) {
if (false === $individualReturn) die('Alguém retornou false');
}
} catch (Exception $e) {
//HANDLE EXCEPTION
}
APLICAÇÕES BÁSICAS - EXEMPLOS
Integrações
Uma aplicação comum é "enviar fotos de pessoas para dispositivos faciais quando houver uma alteração de cadastro". Nesse caso, é disparado um evento de pessoa_update assim que há alteração em um objeto do tipo Pessoa e esse evento é capturado pelos trechos de código que lidam com a integração com os faciais. Como não precisa retornar nada, usamos um EventHandler.
#boostrap.php
new EventListener(
identifier: 'facialintegration_pessoa_update',
event: 'pessoa_update',
callback: function(Pessoa $pessoa) : bool {
//CUIDA DA INTEGRAÇÃO COM FACIAL
},
);
#Pessoa.php
protected function postSaveRecord() : bool {
...
Accelero::getInjection(EventHandler::class)->emitEvent('pessoa_update', $this);
...
}
Construção dinâmica de views
Às vezes pode ser desejado alterar uma view dependendo de determinadas condições. Por exemplo, se o cliente possui o módulo XYZ ativado, então a view deve incluir uma nova aba de configuração. Uma forma de fazer isso é com Interceptors (às vezes precisamos ser criativos com o retorno):
#boostrap.php
new Interceptor('area_custom_view', 'area_edit_view', fn() : Closure => function(string $view) : string {
$dom = new Document($view);
$customNode = $dom->find('[data-module=custom]')[0] ?? null;
if (!($customNode instanceof Node)) return $view;
$html = Accelero::getTranslatedView('aba_extra_da_view');
$currentData = $customNode->innerHtml();
$currentData .= $html;
if ($customNode instanceof Element) $customNode->setInnerHtml($currentData);
return $dom->html();
});
#Area.php
public function getEditingView(DatabaseObjectInterface $obj = null, array $injections = []) : string {
...
foreach (Accelero::getInjection(InterceptorHandler::class)->emit('area_edit_view') as $closure) {
$view = $closure($view);
}
return $view;
}
Lidar com metadados em telas de edição
Às vezes temos dados que não têm colunas próprias no banco de dados, mas ficam armazenados num campo de metadata (geralmente um JSON que funciona como um document). Nesses casos, é interessante usar EventHandlers/Interceptors tanto pra salvar dados vindos da tela quanto pra enviar dados para a tela de edição:
#boostrap.php
new EventListener('custom_metadata_pessoa', 'pessoa_presave', function(Pessoa $pes) : void {
$request = Accelero::getRequest();
$data = $request->getParsedBody();
if (!is_array($data)) return;
$pes->setMetadata($data['xyzCustomField'] ?? null, 'integracaoXYZ', 'customField');
});
new Interceptor('custom_metadata_pessoa_fedata', 'pessoa_fedata', fn(Pessoa $pes) : array => [
'xyzCustomField' => $pes->getMetadataField('xyzCustomField'),
]);
#Pessoa.php
protected function preSaveObject() : bool {
...
Accelero::getInjection(EventHandler::class)->emitEvent('pessoa_presave', $this);
...
}
public function prepareFEData(DatabaseObjectInterface $obj = null) : array {
...
foreach (Accelero::getInjection(InterceptorHandler::class)->emit('pessoa_fedata', $this) as $return) {
if (!is_array($return)) continue;
$data = array_merge($data, $return);
}
...
}
No limite, sempre que houver algo que "talvez deva acontecer (ou não) baseado no que o cliente está usando", deve-se usar um EventHandler/Interceptor.
Registrar esses handlers é uma boa forma de refatoração.
ONDE E COMO REGISTRAR LISTENERS
Todos devem ser registrados no diretório private/config/event_listeners ou private/config/interceptor.
Todos os arquivos devem retornar arrays de EventHandler/Interceptors:
#private/config/event_listeners/meus_listeners.php
return [
new EventListener('aaa', 'abc', fn() => true),
new EventListener('bbb', 'def', fn() => true),
new EventListener('ccc', 'ghi', fn() => true),
];
Arquivos "na raiz" serão carregados sempre (private/config/event_listeners/meus_listeners.php por exemplo).
Arquivos dentro de sub-diretórios serão carregados se houver um módulo ativo no sistema com o nome daquele subdiretório. Por exemplo:
#private/config/event_listeners/module123/meus_listeners.php
return [
new EventListener('aaa', 'abc', fn() => true),
new EventListener('bbb', 'def', fn() => true),
];
//OS ARQUIVOS DESTE SUBDIRETÓRIO module123 (E OS LISTENERS DEFINIDOS NELES) SÓ SERÃO CARREGADOS SE O MÓDULO module123 ESTIVER ATIVO NO SISTEMA
Dessa forma é possível fazer o registro/carregamento dinâmico de handlers, e não é necessário que o próprio handler cheque se o módulo XYZ está ativo (claro, se houver dependência de 2+ módulos, daí não tem escapatória).
DICAS GERAIS
Quando usar um ou outro
Às vezes você pode ficar na dúvida sobre o que é melhor usar numa determinada situação, um EventHandler ou um Interceptor, especialmente quando você vai passar um objeto como parâmetro para o handler e "mexer" nesse objeto dentro do callback. Por um lado, você não está retornando nada pra função original (então o melhor seria um EventHandler), mas por outro lado está causando alterações no estado do objeto (então talvez um Interceptor seja melhor pra deixar clara a intenção do código). A resposta é: tanto faz... e apenas pra ficar claro, esses dois trecos de código são basicamente equivalentes:
//ESSE TRECHO COM UM LOOP VAZIO
foreach (Accelero::getInjection(InterceptorHandler::class)->emit('myevent') as $return) {}
//É BASICAMENTE IGUAL A ISSO
foreach (Accelero::getInjection(EventHandler::class)->emitEvent('myevent');
Cuidado ao passar não-objetos como argumentos
Tanto o EventHandler quanto o Interceptor passam como argumentos para o callback exatamente aquilo que foi definido no emissor (by-val no caso de não-objetos), E NÃO O RETORNO DO CALLBACK ANTERIOR. Exemplo:
#boostrap.php
new EventListener('aaa', 'evento', function(int $a) { echo 'Valor: '. ($a * 2); });
new EventListener('bbb', 'evento', function(int $a) { echo 'Valor: '. ($a * 2); });
foreach (Accelero::getInjection(EventHandler::class)->emitEvent('myevent', a: 10);
//OUTPUT
//Valor: 20
//Valor: 20
Se você quiser criar um "chain" (o output de um passando pro input do outro), precisa usar "Interceptor + Closures" ou classes anônimas:
#EXEMPLO DE USO DE CLASSE ANÔNIMA PARA CRIAR UM WRAPPER
new EventListener('a', 'teste', function(object $a) {
echo $a->name . PHP_EOL;
$a->name = 'another name';
});
new EventListener('b', 'teste', function(object $a) {
echo $a->name . PHP_EOL;
});
$anonClass = new class('my name') {
public function __construct(public string $name) {}
};
Accelero::getInjection(EventHandler::class)->emitEvent('teste', $anonClass);
//OUTPUT
//my name
//another name
#EXEMPLO DE USO DE CLOSURES INTERMEDIÁRIAS PARA CRIAR UM CHAIN
new Interceptor('a', 'teste', fn() : Closure => function(string $name) {
return $name . '-int01';
});
new Interceptor('b', 'teste', fn() : Closure => function(string $name) {
return $name . '-int02';
});
$string = 'string';
foreach(Accelero::getInjection(InterceptorHandler::class)->emit('teste') as $closure) {
$string = $closure($string);
}
echo $string;
//OUTPUT
//string-int01-int02