Skip to main content

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