INTRODUÇÃO
Toda entidade que faz parte do ORM Accelero (e que portanto extends DefaultDBObjectPublicMethods) "ganha" automaticamente uma série de funcionalidades típicas de Controller:
- CRUD básico;
- geração de
selectlists; - geração de
autocomplete.
Obviamente há uma mistura entre Model e Controller nessa arquitetura. Há inconsistências. Viva com isso.
Existem alguns entrypoints que permitem atingir essas funcionalidades, que serão descritos a seguir. Mas alguns métodos dessas classes podem também ser utilizados em algumas situações para outros fins para simplificação de código. Isso também será descrito a seguir.
Daqui em diante, vamos supor a existência das seguintes classes (respeitando a arquitetura do ORM):
class PessoaBase extends DefaultDBObjectPublicMethods {}
class Pessoa extends PessoaBase {}
ROTAS
Todas as rotas para as funcionalidades básicas devem ser criadas manualmente. Abaixo, um template básico que pode ser utilizado (esse template pode ser gerado através do plugin devtools):
return [
new Route('*', '/pessoas', [Pessoa::class, 'listAll'], middleware: LoggedInMiddleware::class),
new Route('*', '/pessoas/', [Pessoa::class, 'listAll'], middleware: LoggedInMiddleware::class),
new Route('GET', '/pessoas/listar', [Pessoa::class, 'listAll'], middleware: LoggedInMiddleware::class),
new Route('POST', '/pessoas/listar', [Pessoa::class, 'listData'], middleware: LoggedInMiddleware::class),
new Route('POST', '/pessoas/massDelete', [Pessoa::class, 'massDelete'], middleware: LoggedInMiddleware::class),
new Route('POST', '/pessoas/selectlist', [Pessoa::class, 'selectlist'], middleware: LoggedInMiddleware::class),
new Route('*', '/pessoas/{pesID}', [Pessoa::class, 'edit'], middleware: LoggedInMiddleware::class),
new Route('POST', '/pessoas/{pesID}/salvar', [Pessoa::class, 'save'], middleware: LoggedInMiddleware::class),
new Route('POST', '/pessoas/{UID}/saveInline', [Pessoa::class, 'saveInline'], middleware: LoggedInMiddleware::class),
new Route('GET', '/pessoas/autocomplete', [Pessoa::class, 'listModelAutocomplete'], middleware: LoggedInMiddleware::class),
]
Note que algumas rotas são parecidas ou levam ao mesmo lugar. É de propósito, como previsão dos casos "usuário digitou com/sem barra no final", por exemplo.
ENTRYPOINTS
listAll
public static function listAll(ServerRequestInterface $request) : ResponseInterface;
Rota simples para obter a view de "listagem geral" (se não falar nada, o nome da view é <prefix>List).
Checa se o usuário tem permissão para fazer a listagem naquela classe.
A view obtida passa pelo método preListAllFlush, que em geral não faz nada (só retorna a própria view). Esse método pode ser usado pra fazer alguma alteração simples na view (por exemplo, as entidades/classes que têm controle de licença usam isso pra incluir os dados de "quanto da licença está sendo usada").
massDelete
public static function massDelete(ServerRequestInterface $request, array $params, callable $fnCheck = null) : ResponseInterface;
Rota para exclusão de objetos.
Sempre parte do princípio que "o operador está vendo uma tabela, selecionou alguns itens e clicou no botão de excluir", ou seja, é pra excluir vários objetos ao mesmo tempo.
Espera receber um parâmetro POST values que é um array dos IDs dos objetos a serem excluídos.
Checa se o usuário tem permissão para fazer exclusões naquela classe.
A exclusão efetiva acontece chamando o método deleteObjectsByUID().
Cada objeto listado será carregado a partir do seu ID (loadByUID), e será executado um deleteMe(). Ou seja, a exclusão NÃO ACONTECE COM UM SIMPLES DELETE NO BANCO, o que permite controlar de forma adequada e centralizada a rotina de exclusão do objeto (às vezes não é só excluir do DB, é necessário fazer outras coisas... e daí tanto faz se está excluindo através desta rota ou manualmente em outro lugar, o processo fica inteiro centralizado).
No final, o método gera uma resposta com a lista dos IDs que foram excluídos, pra que a tabela que o usuário está vendo possa ser atualizada (remover da tabela os itens excluídos).
Toda a operação está em uma transaction, de modo que se uma exclusão falhar, tudo é revertido (ou todas as exclusões funcionam... ou nenhuma...).
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
callable $fnCheck | fn(DatabaseObjectInterface\\<CLASSE\> $obj) : true; | Callback opcional. Se for fornecido, pra cada objeto será executado esse callback. Se o callback retornar false, a operação é abortada. |
edit
public static function edit(ServerRequestInterface $request, array $params, DatabaseObjectInterface $obj = null, array $context = [], string $view = null) : ResponseInterface;
Rota simples para obter a view de "edição".
Carrega o objeto pelo ID recebido na rota.
Checa se o objeto "pode ser editado" através do método canBeEdited() (hook pra fazer alguma checagem).
Checa se o usuário tem permissão pra editar ou criar objetos desse tipo através do método checkEditingPermissions().
Busca a view de edição do objeto através do método getEditingView().
Gera um array com os "dados do objeto para fins de frontend" através do método prepareFEData().
Gera uma instrução pro browser de "popule a view que estou te mandando com esses dados aqui" (os dados em si vão em um objeto JS na resposta).
Se aquele tipo de objeto está configurado pra "gerar logs de visualização" (Pessoas... LGPD...), gera log de visualização.
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
DatabaseObjectInterface $obj | Força o uso daquele objeto específico ao invés do objeto cujo ID foi fornecido na rota. |
saveInline
public static function saveInline(ServerRequestInterface $request, array $params, DatabaseObjectInterface $obj = null, bool $chkPermissao = true, array $callback = []) : ResponseInterface;
Rota pra salvar objetos "direto da tabela", sem estar numa view de edição.
Por padrão, recebe o UID do objeto através da rota e faz seu carregamento.
Checa se o usuário tem permissão pra editar/criar objetos daquele tipo.
Prepara um callback metaCallback pra que após salvar o browser receba uma instrução de "atualizar a linha da tabela com os novos dados do objeto".
Chama o método save(),
Ou seja, esse método faz tudo que o método save() faz, mais um pouquinho.
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
DatabaseObjectInterface $obj | Força o uso daquele objeto específico ao invés do objeto cujo ID foi fornecido na rota. | |
bool $chkPermissao | Se false, ignora checagens de permissão de operador para a ação de salvar | |
array $callback | Array de callback JS (vide seção CALLBACK JS) |
save
public static function save(ServerRequestInterface $request, array $params, array $callback = [], ?DatabaseObjectInterface $obj = null, bool $chkPermissao = true) : ResponseInterface;
Salva um objeto.
Se não foi fornecido um parâmetro $obj (comportamento padrão), carrega o objeto pelo ID da rota com checagem do hash do objeto.
Checa se o usuário tem permissão pra salvar objetos daquele tipo.
Checa se o objeto "pode ser salvo" através do método validateObjInContextBeforeSave() (hook pra fazer alguma checagem).
Faz um initPOST() no objeto.
Faz um saveRecord() no objeto.
Se o objeto for recém-criado, chama o método formatCallbackAfterNewObjSave(), que por padrão gera um callback JS que orienta o browser a "navegar para a tela de edição do objeto". Pode ser usado como hook pra "gerar callbacks JS ao criar objetos".
Chama o método formatCallbackAfterObjSave(), que por padrão adiciona a assinatura/hash do objeto no callback JS. Pode ser usado como hook pra "gerar callbacks JS ao salvar quaisquer objetos".
Finaliza chamando o método saveAfter(), que é basicamente despacha a resposta (mas pode ser usado como hook de "imediatamente antes de gerar a resposta pro usuário").
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
array $callback | Array de callback JS (vide seção CALLBACK JS) | |
DatabaseObjectInterface $obj | Força o uso daquele objeto específico ao invés do objeto cujo ID foi fornecido na rota. | |
bool $chkPermissao | Se false, ignora checagens de permissão de operador para a ação de salvar |
listData
public static function listData(ServerRequestInterface $request, array $params, bool $chkPermissao = true, array $callback = [], FilteringQueryArray $arrFilters = null) : ResponseInterface;
Gera uma listagem de dados pra popular tabelas. Fornece dados paginados + quantidade de páginas totais disponíveis.
Checa se o usuário tem permissão pra listar objetos daquele tipo.
Checa se o usuário está solicitando "dados customizados/opcionais" através do método parseOptionalFields().
Faz uma query pra buscar o total de registros disponíveis com os filtros do request.
Prepara um callback JS pra dizer, basicamente, aonde os dados retornados deverão ser populados (ID da tabela).
Faz uma query pra buscar os registros em si.
Retorna os dados.
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
bool $chkPermissao | Se false, ignora checagens de permissão de operador para a ação de listar | |
array $callback | Array de callback JS (vide seção CALLBACK JS) | |
FilteringQueryArray $arrFilters | Manipulador de query a ser utilizado. Se fornecido, não é feita checagem de "campos customizados/opcionais" (passa a ser responsabilidade desse objeto) |
selectlist
public static function selectlist(ServerRequestInterface $request, array $params, FilteringQueryArray $arrFilters = null) : ResponseInterface;
Rota pra popular selectlists de forma simples.
Obtém os dados chamando um listModel(), e a regra é simples: o value do selectlist será o campo UID de cada registro; o label do selectlist será o que estiver definido no campo <classe>::$selectListLabel.
IMPORTANTE: É feito um cache de 15 segundos dos resultados pra otimizar performance. Portanto, pode acontecer do usuário mexer em um objeto e a alteração demorar um tempinho pra refletir em um selectlist.
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
FilteringQueryArray $arrFilters | Manipulador de query a ser utilizado. |
listModelAutocomplete
public static function listModelAutocomplete(ServerRequestInterface $request, array $params, FilteringQueryArray $arrFilters = null) : ResponseInterface;
Rota pra gerar sugestões de autocomplete.
Espera receber os seguintes query params:
autocomplete- o campo/coluna aonde o filtro deverá ser aplicadoterm- o termo/busca que o usuário digitoulabel- a coluna que deve aparecer para o usuário como sugestão O retorno sempre será truncado em 10 sugestões no máximo. A sugestão para o usuário irá tervalue=UID-da-linha, elabel=label-solicitado-no-request.
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
FilteringQueryArray $arrFilters | Manipulador de query a ser utilizado. |
MÉTODOS E COISAS AUXILIARES IMPORTANTES
deleteObjectsByUID
Método para excluir objetos pelos seus IDs.
Em resumo, recebe uma lista de IDs ($arrObjUID) e vai um por um executando deleteMe().
O parâmetro $allOrNone define se a operação toda estará dentro de uma transaction ou não. Se sim (true), qualquer falha aborta a operação. Se não (false), ele vai excluindo até onde conseguir.
O parâmetro $fnCheck é um callback opcional que será executado pra cada objeto individual, e deve retornar bool. Geralmente é usado pra fazer alguma checagem de última hora, tipo "esse objeto realmente pode ser deletado considerando que pra isso acontecer, aquele outro objeto já devia estar com a configuração atualizada porque um dependia do outro"?
initPOST
Método pra popular um objeto a partir de um array de chave/valor.
Por padrão, todos os campos do objeto podem ser populados por aqui. Pra alterar isso, faça um override no método getInitializableFields().
Você também pode fazer ao contrário: ao invés de listar quais campos PODEM ser inicializados, você pode criar exceções (campos que NÃO podem ser inicialiados por aqui). Pra isso, faça um override no método postExclusion().
formatCallbackAfterNewObjSave
Método pra gerar um callback JS quando um NOVO objeto é criado.
Se a propriedade estática $autoEditOnNew for true, após salvar um objeto o browser recebe um callback JS pra "navegar para a rota de edição do objeto recém criado".
Se a propriedade estática $autoEditOnNew for false, após salvar um objeto o browser recebe um callback JS pra "atualizar a tabela de listagem de objetos que provavelmente o operador está vendo".
formatCallbackAfterObjSave
Método pra gerar um callback JS quando um objeto é alterado.
Por padrão, apenas adiciona o campo context na resposta pro browser (validação de hash em requests subsequentes).
IMPORTANTE: esse método é chamado toda vez que um objeto é alterado, mas também quando um objeto é criado. Cuidado com conflitos entre esse método e o formatCallbackAfterNewObjSave().
saveRecord
Método que efetivamente salva um objeto. Sempre use esse método pra salvar objetos, em qualquer contexto.
Em resumo (retornos bool abortam a operação... tudo pode estourar alguma Exception):
- executa o hook
preSaveObject() : bool;; - valida os campos do model em relação às regras do DB;
- executa o hook
validateObjBeforeSaving() : bool;; - descobre se é um insert/update, e executa o método correto:
- se for insert:
- descobre se o ID do objeto é sequencial ou aleatório (e gera um válido);
- junta os campos todos pra fazer o insert;
- executa a query;
- atualiza o objeto com o ID correto;
- executa o hook
insertCallback(); - gera
logoperacaoda ação.
- se for update:
- checa se houve alterações no objeto -- se não houve, não precisa fazer nada;
- monta a query de update com os dados que foram alterados;
- gera
logoperacaoda ação.
- se for insert:
- executa o hook
postSaveRecord() : bool;; - se objeto puder estar no cache, atualiza o cache;
listModel
Método responsável por ir no DB buscar uma listagem de objetos. Em resumo:
- recebe como parâmetro ou criar (
getQueryObjForListModel()) um objeto do tipoSelectQueryInterfaceque tem as configurações básicas (tabela, joins, etc...) da query a ser executada; - garante que o objeto acima está minimamente configurado;
- se recebeu um
FilteringQueryArraycomo parâmetro, aplica na query; - descobre se o usuário está pedindo (e se poderia pedir) algum campos customizado;
- seta a paginação da query com base nos dados recebidos no request;
- seta os filtros da query com base nos dados recebidos no request;
- seta a ordenação da query com base nos dados recebidos no request;
- aplica um
orderByna query pós-ordenação-do-request; - se o método foi chamado com o parâmetro
getCount=true, retorna a quantidade total de registros, sem paginação; - se o método foi chamado com o parâmetro
getCount=false, faz a query, passa o resultado pelo hooklistModelPostQueryFilter()e retorna o array.
Parâmetros opcionais
| PARÂMETRO | FORMATO/OBSERVAÇÕES | EXPLICAÇÃO |
|---|---|---|
bool $getItemCount | Se true, faz o método retornar um int com o total de registros. Se false, faz o método retorna um array com as linhas da query executada. | |
bool $allowPagination | Se true, permite paginação dos resultados. Se false, garante que sempre os resultados virão completos, sem paginação. | |
FilteringQueryArray $arrFilters | Manipulador de query a ser utilizado. | |
SelectQueryInterface $query | Objeto que deve ser usado como base pra fazer a query. Normalmente deve ser null, pois será gerado automaticamente, mas em algumas situações pode ser interessante setar uma query customizada/especializada como base e aproveitar o restante da infra-estrutura. |
listModelPostQueryFilter
Hook para "trabalhar dados de um listModel antes de retornar os resultados". Geralmente é utilizado pra fazer parse de alguma coisa antes de popular uma tabela. Por exemplo:
protected static function listModelPostQueryFilter(array &$array) : void {
foreach ($array as &$row) {
$row['campoQueHumanoEntende'] = match($row['colunaIDDoBanco']) {
'1' => 'TIPO 01',
'2' => 'TIPO ESPECIAL 02',
default => 'DESCONHECIDO',
}
}
}
ATENÇÃO: o array é passado by-ref nesse método!
getQueryObjForListModel
Método que retorna um SelectQueryInterface preparado pra fazer uma query de listagem de objetos daquela classe.
Geralmente é aqui que você vai determinar:
- quais campos devem ser apresentados numa tabela de listagem;
- como deve ser montada a query pra trazer os resultados (JOINs, por exemplo). Se você não fizer override desse método, o comportamento padrão do sistema será "trazer todas as colunas da tabela respectiva da classe, sem nenhum JOIN". Exemplo:
protected static function getQueryObjForListModel() : SelectQueryInterface {
$query = Accelero::getInjection(DBConnectorInterface::class)->getSelectQuery();
$query->setTablename('empresas');
$query->addFields(['empresas.empID AS UID', 'empresas.empDescricao', 'empresasdivisoes.emdDescricao']);
$query->addJoin('INNER JOIN', 'empresasdivisoes', 'emdID');
return $query;
}
Considere que esse método (e o listModel()) em geral só devem ser usados pra "popular tabelas". Pra carregar objetos, prefira o loadAllThatMatches(). Pra listagens complexas ou que não devam seguir a lógica de "listagem de tabelas", prepare suas próprias queries de forma mais manual ao invés de tentar reaproveitar essa infra-estrutura.
MUITO IMPORTANTE: A query sempre deve retornar uma coluna com nome 'UID' pra que as tabelas sejam "inteligentes". Essa coluna é tratada de forma especial, e a linha da tabela que está sendo populada ganha o UID respectivo da linha da query. É isso que permite que as interações com tabelas no frontend (editar, salvar, navegar, excluir, etc...) funcionem.
prepareFEData
Gera um array chave/valor com os dados do objeto.
Se não falar nada, são incluídos no array todas as colunas do DB.
Em algumas situações, pode não ser desejável incluir alguns campos nesse array (senhas, por exemplo). Pra alterar isso, mexer no método getFEFields().
Além dos dados do objeto, sempre é gerado um campo chamado context (que é um array). Esse campo funciona como uma espécie de hash/token do objeto editado, e serve pra fazer algumas checagens na hora de salvar esse objeto.
CALLBACK JS
Quando é gerada uma resposta do backend para o frontend, existem várias "instruções" que podem ser enviadas para o browser.
Isso sempre é passado no parâmetro $callback do DataFlusher::flush().
Alguns callbacks estão disponíveis "na hora de salvar", outros "na hora de editar", outros "sempre"... tenha fé, navegue no código pra descobrir.
populate
Geralmente usado nas telas de edição. Instrui o browser a popular dados (um array PHP) dentro de um container (um selector jQuery). O browser vai buscar dentro do container campos com data-name=<CHAVE> e alterar os valores.
$callback = [
'populate' => 1,
'element' => '#PessoaEdit',
'data' => ['pesNome' => 'João', 'pesTelefone' => '(11) 12345-1234'],
];
f
Simplesmente f. Executa uma função JS com algo parecido com um eval. Cuidado!
$callback = ['f' => 'alert("Hello world!")'];
reload
Força uma navegação pra uma outra URL. Geralmente usado pra situações do tipo "depois que fizer isso, manda o usuário pra outra página".
$callback = ['reload' => WWW_ADDRESS . 'pessoas/' . $pes->getEveID()];
download
Força download de algum conteúdo. O filename é o que vai aparecer pro usuário (irrelevante para o backend), e os dados devem ser base64_encoded e enviados no campo filedata.
$callback = [
'download' => 1,
'filename' => 'arquivo.csv',
'filedata' => base64_encode($dadosReais),
'mimetype' => 'text/plain',
];
newWindowContent
Abre uma nova aba no navegador e joga um conteúdo estático nela.
$callback = ['newWindowContent' => 'Aqui está seu QRCode, fulano' . $qrcode];
navigateUpdateHistory
Altera o endereço que aparece na URL pra fins de navegação do navegador (Back/Forward). Útil pra situações em que você criou um objeto novo e quer automaticamente navegar pra tela de edição, por exemplo (o que já é o comportamento padrão do sistema).
$callback['navigateUpdateHistory'] = 'pessoas/' . $pes->getPesID();
code
Usado pra determinar como serão exibidas as mensagens de feedback pro usuário (campo msg do DataFlusher::flush()). Toda resposta tem um code implícito: se não for setado nada, então é o próprio código da resposta HTML (200, 400, etc...).
code"ruins" (0, 400, 401, 500, etc...) mostra uma mensagem em vermelho, no canto;code"bons" (1, 200) mostra uma mensagem em verde, no canto;code99 mostra uma mensagem em verde claro, no canto;code98 não mostra nada;code10 mostra uma mensagem em modal que exige que o usuário clique em "OK" pra continuar.
$callback['code'] = 10;
removed
Específico para situações de massDelete(). Informa os IDs dos objetos que foram deletados e que podem ser removidas da tabela sendo visualizada.
$callback['removed'] = [123897,89473,1782368];
refetch
Específico para situações de saveInline(). Informa ao browser que ele deve recarregar os dados da tabela.
$callback['refetch'] = 1;
OPERAÇÕES COMUNS
Customizar a query de listModel() usando FilteringQueryArray
class Veiculo extends VeiculoBase {
public function cartoes(ServerRequestInterface $request, array $params) : ResponseInterface {
if (null === $veiID = $params['veiID'] ?? null) throw new InvalidRequestException();
if (false === static::checkPermissionStatic('edit')) throw new NoPermissionException();
//VAMOS LISTAR OS CARTÕES ASSOCIADOS A UM VEÍCULO ESPECÍFICO
//PORQUE SOMOS PREGUIÇOSOS, VAMOS APROVEITAR O listModel JÁ EXISTENTE NA CLASSE Cartao,
//E APENAS VAMOS INCLUIR ALGUNS FILTROS ADICIONAIS
$arrFilters = new FilteringQueryArray();
$arrFilters->addFilter('carLinkedObject', '=', Veiculo::class);
$arrFilters->addFilter('carLinkedObjectUID', '=', $veiID);
return Cartao::listData($request, $params, true, [], $arrFilters);
}
public function pessoasComCampoCustomizavel(ServerRequestInterface $request, array $params) : ResponseInterface {
if (null === $veiID = $params['veiID'] ?? null) throw new InvalidRequestException();
if (false === static::checkPermissionStatic('edit')) throw new NoPermissionException();
//VAMOS LISTAR AS PESSOAS ASSOCIADAS A UM VEÍCULO ESPECÍFICO
//PORQUE SOMOS PREGUIÇOSOS, VAMOS APROVEITAR O listModel JÁ EXISTENTE NA CLASSE Pessoa,
//E APENAS VAMOS INCLUIR ALGUNS FILTROS ADICIONAIS
$arrFilters = new FilteringQueryArray();
$arrFilters->addFilter('pesID', 'IN', '(SELECT pesID FROM linkpessoasveiculos WHERE veiID=' . $veiID . ')', null, false);
//GERALMENTE O listModel DE PESSOAS NÃO MOSTRA O TELEFONE, MAS SE O USUÁRIO PEDIU ESSE CAMPO, A GENTE INCLUI
$addTelefone = intval($request->getParsedBody()['addTelefone'] ?? null) === 1;
if ($addTelefone) {
$arrFilters->addCustomFields(['pessoas.pesTelefone']);
}
return Pessoa::listData($request, $params, true, [], $arrFilters);
}
}
DICAS
Hooks
Existem vários hooks que você pode usar pra "pendurar" funcionalidades. Você pode fazer "XYZ" toda vez que salvar um objeto do tipo Pessoa, por exemplo, ou fazer "ABC" toda vez que o usuário tentar editar um objeto qualquer. Em geral, esses hooks são métodos vazios, e só fazem alguma coisa se você implementar a sua funcionalidade. Mas em alguns casos, o hook padrão tem alguma funcionalidade, que provavelmente precisa ser mantida. Por isso, pense bem e considere com carinho a possibilidade de sempre implementar hooks retornando o hook do parent:
class Pessoa extends PessoaBase {
protected function postSaveRecord() : bool {
return parent::postSaveRecord();
}
}
Checagem de permissões
Toda vez que existe uma checagem implícita de permissão para executar uma ação (quanto vai salvar um objeto, por exemplo), acontece uma mágica. O padrão é geralmente assim:
$usu = Iongrade::getCurrentLoggedUserObj(); //PEGA O USUÁRIO LOGADO
$prm = static::getPermissionNeeded('list'); //DESCOBRE QUAL PERMISSÃO O USUÁRIO DEVIA TER PARA EXECUTAR A AÇÃO DO TIPO 'list' NESSA CLASSE
$prmHandler = static::$services->permissionHandler; //PEGA O OBJETO QUE FAZ A CHECAGEM DE PERMISSÕES
if (false === $prmHandler->checkPermissoes($usu, $prm)) throw new NoPermissionException(); //CHECA PERMISSÃO
O que nos interessa aqui é o getPermissionNeeded().
Em resumo, quando esse método é chamado o sistema busca na tabela permissoesbinds uma linha com prbGroup=<CLASSE> + prbAction=<ACAO>, e retorna a coluna prbPermission.
Se não for encontrado nada com esses filtros, o sistema "permite executar a ação", mas gera um log de erro interno (comemos bola e esquecemos de atualizar essa tabela).
Se for encontrada uma permissão, daí é feita uma checagem pra ver se qualquer um dos perfis associados ao usuário logado tem aquela permissão habilitada.
Toda essa busca é memoized, o que significa que ela é feita uma vez na inicialização do request e depois está tudo em cache.
Toda classe tem (deveria ter) permissões configuradas para as actions básicas:
- list (ver telas de listagem)
- edit (entrar na tela de edição - provavelmente vai ver dados mais completos do que na listagem)
- create (criar objetos novos)
- save (fazer update em objetos)
- delete (excluir objetos) Essas ações podem apontar pra qualquer permissão que você quiser:
- se achar que a classe toda é "liga uma chavinha e o operador pode fazer tudo", basta fazer com que todas as actions apontem pra mesma permissão.
- se achar que deve separar de forma bem específica as permissões, aponte cada action pra uma permissão.
Não se esqueça que as permissões devem existir na tabela
permissoes.
Você também pode criar actions customizadas da forma que preferir. Por exemplo, talvez você tenha criado um botão em uma tela pra "enviar dados da pessoa para API externa", e pra isso você quer configurar uma permissão específica. Sem problemas, basta seguir o paradigma descrito acima.
Filtros permitidos + dicionário
Em geral, as views de listagem possuem uma seção de filtros (div.pesquisa_advanced).
Quando o frontend vai fazer um request AJAX pra popular uma tabela (lembrete: as tabelas sempre são "servidas" em branco, e o carregamento SEMPRE é assíncrono), ele checa essa seção de filtros pra ver o que o usuário digitou e envia esses filtros juntos com o request. O backend se vira pra "aplicar os filtros" na pesquisa.
Por padrão, qualquer campo existente no DB da classe em questão pode é um filtro válido com o tipo LIKE: basta incluir um campo de filtro com o nome desejado (copie um de exemplo) na seção de pesquisa da view e pronto. Mas existem algumas situações em que você precisa customizar isso.
- filtros que não devem ser "LIKE" (devem ser do tipo "=", ">", "<", "IN", etc...);
- filtrar por colunas de tabelas JOINed;
- o nome do filtro no frontend não corresponde ao nome da coluna no DB (existem vários motivos pra isso).
Alguns desses problemas você vai resolver fazendo override do método
allowedFilters(). Outros problemas você vai resolver fazendo override do métodofilterDictionary(). Abaixo, um exemplo geral das possibilidades:
class Pessoa extends PessoaBase {
protected static function allowedFilters() : array {
$filters = parent::allowedFilters(); //VAMOS COMEÇAR PERMITINDO FILTROS DE QQ CAMPO DA CLASSE
//VAMOS PERMITIR QUE SEJA APLICADO UM FILTRO NO CAMPO `areDescricao` (QUE NÃO EXISTE NA CLASSE Pessoa).
//TEMOS QUE GARANTIR QUE ESSE CAMPO ESTEJA SEMPRE DISPONÍVEL NA QUERY, ENTÃO TEMOS QUE CONFERIR O MÉTODO `getQueryObjForListModel()`
//SERÁ UM FILTRO DO TIPO "LIKE", E SERÁ SEMPRE APLICADO COM REGRA "AND" NA HORA DE MONTAR A QUERY.
$filters['areDescricao'] = ['operator' => 'LIKE', 'ruleAND' => true];
//VAMOS PERMITIR QUE SEJA APLICADO UM FILTRO NO CAMPO `pesAreaAtual`
//SERÁ UM FILTRO DO TIPO "IN", O QUE É GERALMENTE USADO PARA SELECTLISTS MULTIPLE
$filters['pesAreaAtual'] = ['operator' => 'IN', 'ruleAND' => true];
//VAMOS PERMITIR QUE SEJA APLICADO UM FILTRO NO CAMPO `nome` (QUE NÃO EXISTE EM NENHUM LUGAR NO DB)
//A COLUNA "DE VERDADE" ESTÁ DEFINIDA NO "customField" (pesNome)
//MAIS AINDA, O VALOR DO FILTRO QUE QUEREMOS APLICAR SERÁ ABSOLUTAMENTE CUSTOMIZADO
$filters['nome'] = [
'ruleAND' => true,
'customField' => 'pesNome',
'valueFn' => function(string $val) : array {
//SE O NOSSO FILTRO FOR 'foo', ALTERAMOS PARA 'bar'
//ESSE SISTEMA PERMITE FAZER ALGUNS FILTROS IMPOSSÍVEIS DE APLICAR DIRETAMENTE NO FRONTEND
//O RETORNO É UM ARRAY COM CHAVES 'op' (EQUIVALENTE DO 'operator') E 'val' (O VALOR REAL DO FILTRO A SER APLICADO)
if ($val === 'foo') $val = 'bar';
return ['op' => '=', 'val' => $val];
}
],
return $filters;
}
protected static function filterDictionary() : array {
//É POSSÍVEL "NÃO CUSTOMIZAR" ESSE MÉTODO E USAR APENAS O `allowedFilters()` SETANDO REGRAS DE `customField`
//PROS CAMPOS QUE VOCÊ QUER REMAPEAR, MAS SE FOR APENAS PRA FAZER UM REMAPEAMENTO DE CAMPOS DE FILTRO,
//COSTUMA SER MAIS RÁPIDO FAZER POR AQUI.
return [
'telefone' => 'pessoas.pesTelefone', //O CAMPO telefone RECEBIDO DO FRONTEND SERÁ REMAPEADO PARA pessoas.pesTelefone NA QUERY
];
}
}
autocomplete customizados
Você pode precisar (e vai precisar) criar rotas para autocompletes no frontend cuja origem de dados não é o padrão.
Por exemplo, ao invés de criar um autocomplete que sugira todas os objetos Area do sistema, talvez você queira um autocomplete que sugira apenas os objetos com areHabilitada=1. Pra isso, você vai precisar criar uma rota específica e um método específico pra fazer esse tratamento.
O seu objetivo é:
- gerar um array-de-arrays com os itens que quer mostrar no frontend;
- converter esse array-de-arrays em um formato que o frontend entende. O pattern padrão pra atingir esse objetivo é fazer algo assim:
public function autocompleteAreasHabilitadas(ServerRequestInterface $request) : ResponseInterface {
//DADOS RECEBIDOS DO FRONTEND
$baseFilter = $request->getQueryParams()['autocomplete'] ?? null;
$termRaw = $request->getQueryParams()['term'] ?? null;
//TRAPS DE REQUESTS INVÁLIDOS/INCOMPLETOS
if (!is_string($baseFilter)) return DataFlusher::flushAutocompleteList([]);
if (!is_string($termRaw)) return DataFlusher::flushAutocompleteList([]);
if ($baseFilter !== 'areDescricao') return DataFlusher::flushAutocompleteList([]);
$arrAreas = Area::loadAllThatMatches([
['areHabilitada', '=', '1'],
['areDescricao', 'LIKE', '%' . $termRaw . '%']
], limit: 10); //LIMIT 10 PORQUE QUEREMOS MOSTRAR NO MÁXIMO 10 SUGESTÕES
$results = array_map(fn(Area $are) => ['campoComID' => $are->getAreID, 'campoDoLabel' => $are->getAreDescricao()], $arrAreas);
$output = Helper::convertModelToAutocomplete(
array: $results,
fieldName: 'campoDoLabel',
fieldValue: 'campoComID',
);
return DataFlusher::flushAutocompleteList($request, $output);
}
Nada impede você de usar queries brutas ao invés do helper loadAllThatMatches(). Dependendo da situação, pode ser vantajoso um jeito ou outro.
selectlist customizados
Você pode precisar (e vai precisar) preencher selectlists no frontend cuja origem de dados não é o padrão.
Por exemplo, ao invés de criar um selectlist que mostre todas os objetos Area do sistema, talvez você queira um selectlist que mostre apenas os objetos cujo nome comece com a letra "A". Pra isso, você vai precisar criar uma rota específica e um método específico pra fazer esse tratamento.
O seu objetivo é:
- gerar um array-de-arrays com os itens que quer mostrar no frontend;
- converter esse array-de-arrays em um formato que o frontend entende. O pattern padrão pra atingir esse objetivo é fazer algo assim:
public function selectlistAreasStartWithA(ServerRequestInterface $request) : ResponseInterface {
$arrAreas = Area::loadAllThatMatches([['areDescricao', 'LIKE', 'A%']]);
$output = [];
foreach($arrAreas as $are) {
$output[] = ['campoComID' => $are->getAreID, 'campoDoLabel' => $are->getAreDescricao()];
}
$output = Helper::convertModelToSelect(
array: $output,
fieldName: 'campoDoLabel',
fieldValue: 'campoComID',
);
return DataFlusher::flushSelectList($request, $model);
}
7listModel + listModelPostQueryFilter
Por padrão, as classes default do ORM retornam todas as colunas do DB quando é feito um listModel(). Isso pode não ser desejável tanto por questões de segurança (senhas, por exemplo) quanto por tráfego de dados.
Em geral, o correto é alterar o método getQueryObjForListModel() pra que a query-base tenha apenas os campos que você deseja na listagem.
Mas em algumas situações específicas, é preciso ir ainda mais fundo. Por exemplo, considere uma classe que tem um campo de metadata (que é um JSON), e você quer que um dos campos desse JSON seja uma coluna na tabela). Você pode fazer algo como:
protected static function listModelPostQueryFilter(array &$array) : void {
foreach ($array as &$row) {
$jsonData = json_decode((string)$row['campoMetadata'], true);
unset($row['campoMetadata']); //NÃO QUEREMOS ESSE CAMPO BRUTO DISPONÍVEL PRO FRONTEND
if (!$jsonData) continue;
$row['colunaVirtual'] = $jsonData['configuracaoXYZ'] ?? ''; //CRIAMOS UM CAMPO NOVO PARA O FRONTEND
}
}
FilteringQueryArray
O FilteringQueryArray é uma classe helper extremamente poderosa que pode ser usada para manipular queries de forma indireta. De certa forma, ela é um "container de instruções" que podem ser aplicadas em queries quando você não tem acesso direto ao objeto que vai montar a query. Com um FilteringQueryArray você pode:
- adicionar filtros
WHERE/HAVING; - adicionar cláusulas
GROUP BY; - incluir campos a serem retornados;
- incluir
JOIN; - incluir cláusulas
ORDER BY; - incluir cláusulas
LIMIT; Mas o objeto tem limitações, também. Você NÃO pode excluir/alterar coisas que já fazem parte da query.
Esse objeto é usado principalmente em listagem de dados, quando você precisa dar uma "pequena customizada" nos dados de uma listagem padrão (listModel()).
Mas existem vários métodos no sistema que aceitam objetos desse tipo, por exemplo, o método loadAllThatMatches(). Nesse caso, se você for usar o FilteringQueryArray apenas como forma de incluir alguns filtros adicionais, ele não é realmente necessário (você pode especificar os filtros direto no primeiro parâmetro do método). Mas talvez você queira carregar objetos usando um JOIN que não estava previsto originalmente na classe, ou quer pegar apenas os 5 primeiros objetos ordenados de forma decrescente por uma coluna qualquer... nesses casos, o FilteringQueryArray pode te ajudar.
Pense no FilteringQueryArray como uma forma OOP de gerar uma QUERY PARCIAL: é um objeto reaproveitável e um pouco mais estruturado do que um array (ajuda na legibilidade do código).