| Ultima Edição: 28/Oct/25 |
VSF eu nem lembro se eu sei
Fiz um script que cria tudo, não há mais sofrimento: BAIXAR O SCRIPT BASHHHHHHHHHHHHHHHHHHHHHHH!!
Só colocar ele na raízzzzzzz (/workfolder)
depois dar sudo chmod +x ./nova_tela.sh
Depois rodar ./nova_tela.sh
Escolher nome da tela, código se existe , módulo e dizer se precisa ou não de backend
great sucess.
Só que ainda tem que registrar tudo no aclStruct.json, menu.json, controllers.xml,services.xml e routes.json… zzzzz
O caminho que a maioria dos dados das telas fazem é esse:
graph LR
subgraph Frontend
FE["Tela feita <br>pelo JSON"]
JS["Controller <br>Javascript"]
end
subgraph Backend PHP
PC["👂<br>Controller"]
PS["💪<br>Service"]
PF["🗣️<br>Factory"]
end
subgraph Banco de Dados
DB[(Tabela)]
end
%% Main horizontal flow
FE:::blueBorder ==> JS:::blueBorder
JS ==>|Rota Api| PC:::pinkBorder
PC ==> PS:::pinkBorder
PS ==> PF:::pinkBorder
PF ==>|Query SQL| DB
%% Explanatory nodes below PHP
PC --o PHPC[["Recebe os dados,<br>filtra se necessário."]]
PS --o PHPS[["Aplica regras,<br>normaliza,<br>o que <br>mais precisar."]]
PF --o PHPF[["Envia os dados <br> como query SQL<br>ou executa<br> procedure <br>ou outros."]]
%% Edge class definitions
classDef blueBorder stroke:#02f,stroke-width:5px;
classDef pinkBorder stroke:#f04,stroke-width:5px;
%% Clickable nodes
click PC href "/criar_nova_tela/#1---criar-o-controller-php-da-tela" "Controller PHP"
click PS href "/criar_nova_tela/#2---criar-o-service-php-da-tela" "Service PHP"
click PF href "/criar_nova_tela/#3---criar-o-factory-php-da-tela" "Factory PHP"
click JS href "/framework/#chamar-rota-back-end-pelo-controller-javascript" "Chamar rota backend"
Front
1 - Criar o arquivo .json da tela:
O nome do arquivo geralmente está em snake_case (nome_da_tela.json) e o código da tela no Delphi geralmente está no começo, vou fazer igual sempre.
O arquivo json é criado no caminho:
/erp/modules/nome_do_modulo/mobile/assets/json/containers/abc000_nome_da_tela.json
O conteúdo padrão é:
{
"name": "nome_da_tela",
"label": "Nome da Tela",
"showHeader": true,
"showMenu": true,
"showFooter": true,
"showSearch": true,
"footer": "component/footer.html",
"events": [],
"template": "container/window.html",
"widgets": [
{
"id": "12345678901234567890",
"name": "Nome",
"itemsPerPage": 30,
"label": "Nome",
"template": "widget/master_detail/grid.html",
"isVisible": true,
"actions": [],
"events": []
}
],
"id": "12345678901234567890",
"parentMenuId": "09876543210987654321",
"parentMenuName": "nome_da_tela",
"parentMenuLabel": "Nome da Tela"
}
2 - Criar o controller .js da tela:
O nome do arquivo geralmente está em PascalCase (NomeDaTela_Controller.js) e o código da tela no Delphi geralmente está no começo e o _Controller no final, vou fazer igual sempre.
O arquivo javascript é criado no caminho:
/erp/modules/nome_do_modulo/mobile/assets/js/controllers/Abc000_NomeDaTela_Controller.js
O conteúdo padrão é:
var Abc000_NomeDaTela_Controller = function (ScreenService, General, templateManager){
var self = this;
self.nomeDaFuncao = function(){
}
}
Configuration(function(ContextRegister) {
ContextRegister.register('Abc000_NomeDaTela_Controller', Abc000_NomeDaTela_Controller);
});
Back
1 - Criar o controller .php da tela:
O nome do arquivo geralmente está em PascalCase, sem underscore (NomeDaTelaController.php) e o código da tela no Delphi geralmente está no começo e o Controller no final, vou fazer igual sempre.
O arquivo é criado no caminho relativo do módulo (ou submódulo), assim:
/erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/Controllers/NomeDaTelaController.php
O arquivo consiste dos imports, uma classe, uma função especificamente para o construct (passar os imports para variáveis locais) e qualquer função que você queira declarar.
Exemplo de arquivo comum que recebe dados do front-end (Substitua PerfilCliente pelo nome da sua tela.):
<?php
namespace Teknisa\FIS\Modules\Relatorios\Controllers;
use Zeedhi\Framework\DataSource\DataSet;
use Zeedhi\Framework\DTO;
use Zeedhi\Framework\DTO\Response\Error;
use Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService;
class PerfilClienteController {
protected $perfilClienteService;
public function __construct(PerfilClienteService $perfilClienteService) {
$this->perfilClienteService = $perfilClienteService;
}
public function startQuery(DTO\Request\Row $request, DTO\Response $response){
try{
$row = $request->getRow();
$result = $this->perfilClienteService->buildQuery($row);
$response->addDataset(new DataSet('response', array('response' => $result)));
}catch(\Exception $e){
$response->setError(new Error($e->getMessage(), $e->getCode()));
}
}
}
explicação:
Aqui PerfilClienteController é o nome do meu controller, logo minha classe deve ter o mesmo nome:
class PerfilClienteController {
}
Como os dados vão ser tratados no service, o arquivo deve ser referenciado no controller mesmo, para que ele possa ser acessado. Então temos que:
- Importar o arquivo Service:
use Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService;
- Criar variável que vai receber o contexto do Service dentro da classe:
protected $perfilClienteService;
-
Criar o construct e passar como parâmetro a classe do arquivo Service (veja logo abaixo) e depois atribuindo essa classe à variável criada.
public function __construct(PerfilClienteService $perfilClienteService) {
$this->perfilClienteService = $perfilClienteService;
}
Eu ainda não entendo muito bem o padrão/boas práticas, mas se você precisa fazer qualquer tipo de request, usar DTO\Request\Row $request, DTO\Response $response como parâmetros da função é suficiente, sendo REST padrão mesmo. req e res.
Em $request, você tem acesso ao método getRow(), que te permite acessar os dados que o front te enviar.
Já o que você definir para o valor de response, vai ser retornado ao fim da solicitação. Existem outros tipos de dados, mas DataSet(name, resultado) também É SUFICIENTE. Algum dia eu pergunto alguém se eu devo usar outro tipo de Objeto em alguma situação.
public function startQuery(DTO\Request\Row $request, DTO\Response $response){
try{
$row = $request->getRow();
$result = $this->perfilClienteService->buildQuery($row); // Chamar a função do service
$response->addDataset(new DataSet('response', array('response' => $result)));
}catch(\Exception $e){
$response->setError(new Error($e->getMessage(), $e->getCode()));
}
}
Como a gente já definiu perfilClienteService como a nossa classe Service, nós podemos passar os dados vindos do $request->getRow(), pra serem processados no Service:
$result = $this->perfilClienteService->buildQuery($row);
buildQuery é o nome da função no Service, que ainda não existe.
O arquivo controller.xml de cada módulo registra e observa cada controller no projeto e é usado como referência (muito importante!). Ele pode ser encontrado em:
/erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/config/controllers.xml
Com a seguinte sintaxe:
<container>
<services>
<service id="Teknisa\FIS\Modules\Relatorios\Controllers\NomedoController" class="Teknisa\FIS\Modules\Relatorios\Controllers\NomedoController">
<argument type="service" id="Teknisa\FIS\Modules\Relatorios\Services\NomedoService"/>
<argument type="service" id="OutroParametro"/>
</service>
</services>
</container>
Dentro de <container>, tem <services> e dentro dele, cada controler vai ter seu próprio bloco <service>, com atributos <argument> dentro (meu deus).
Então pra registrar o controller que você criou, Só copiar e colar é suficiente! Colar isso depois do ultimo <service>, no meu exemplo, PerfilClienteController usa PerfilClienteService como argumento, então:
<service id="Teknisa\FIS\Modules\Relatorios\Controllers\PerfilClienteController" class="Teknisa\FIS\Modules\Relatorios\Controllers\PerfilClienteController">
<argument type="service" id="Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService"/>
</service>
Isso vai funcionar com qualquer Controller, contanto que os <arguments> sejam os mesmos e na mesma ordem do arquivo Controller.php.
ESSE É O ÚLTIMO PASSO.
O arquivo /erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/config.json expõe endpoints para o front acessar. Nada mais é do que um arquivo json comum com blocos definindo cada Url.
[
{
"uri": "/queryPerfilCliente",
"controller": "Teknisa\\FIS\\Modules\\Relatorios\\Controllers\\PerfilClienteController",
"controllerMethod": "startQuery",
"methods": [
"POST"
],
"requestType": "Row"
}
]
EXPLICANDO:
uri: A url/endpoint/rota que vai ser acessada pelo front, você escolhe o nome.
controller: caminho do arquivo do controller em PHP (sem .php), usando barra invertida dupla (\) — exemplo: Teknisa\\FIS\\Modules\\ (não use \ simples).
controllerMethod: qual das funções definidas no controller vai ser chamada quando uma solicitação for feita para /queryPerfilCliente? No nosso caso startQuery
methods: Que métodos REST vão ser utilizados? Até hoje só vi POST e GET, não sei se é encorajado usar DELETE ou UPDATE ou algo do tipo em algum momento (imagino que não).
requestType: Que tipo de informação está sendo passada e recebida? No nosso caso uma Row, hence: Row. Mas tem outros tipo ‘FilterData’. Quando se usa eles? NUNCA SABEREMOS. Porém eu só uso Row, se for crime que me prendam.
Agora que tudo está feito dá pra acessar a url definida no routes.json pra enviar uma solicitação pro controller php!! Seguir esse link pra lembrar como faz: Chamar rota back-end pelo Controller Javascript
2 - Criar o service .php da tela:
O nome do arquivo geralmente está em PascalCase, sem underscore (NomeDaTelaService.php) e o código da tela no Delphi geralmente está no começo e o Service no final, vou fazer igual sempre.
O arquivo é criado no caminho relativo do módulo (ou submódulo), assim:
/erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/Services/NomeDaTelaService.php
Esse arquivo serve pra aplicar as regras de negócio, então qualquer heavy-lifting, lembrar de não fazer no controller, nem no factory. Eu vejo da seguinte maneira: Controller serve para polir os dados, Service para processar os dados e o Factory pra acessar o Banco de Dados. (100% de certeza que estou simplificando)
Exemplo de arquivo comum que processa os dados vindos do Controller (Substitua PerfilCliente pelo nome da sua tela.):
<?php
namespace Teknisa\FIS\Modules\Relatorios\Services;
use \DateTime;
use Doctrine\ORM\EntityManager;
use Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory;
use Teknisa\Libs\Util\DateUtil;
class PerfilClienteService {
protected $entityManager;
protected $environment;
protected $perfilClienteFactory;
public function __construct(PerfilClienteFactory $perfilClienteFactory, EntityManager $entityManager){
$this->perfilClienteFactory = $perfilClienteFactory;
$this->entityManager = $entityManager;
}
// função pra montar a query que vai ser executada no arquivo Factory
public function buildQuery($row){
try{
$dtini = DateTime::createFromFormat('d/m/Y', $row['P_DTINICIAL']);
$dtfinal = DateTime::createFromFormat('d/m/Y', $row['P_DTFINAL']);
$meses = [];
$mesAno = "(" . implode(", ", $meses) . ")";
$params = [
'P_CDEMPRESA' => $row['P_CDEMPRESA'] ?? 'T',
'P_CDINSCESTA' => $row['P_CDINSCESTA'] ?? 'T',
'P_DTINICIAL' => $row['P_DTINICIAL'],
'P_DTFINAL' => $row['P_DTFINAL'],
];
return $this->perfilClienteFactory->runQuery($params, $mesAno);
}catch(\Exception $e){
throw new \Exception($e->getMessage(), 400);
}
}
}
explicação:
Aqui PerfilClienteService é o nome do meu service, logo minha classe deve ter o mesmo nome:
class PerfilClienteService {
}
Como os dados tratados vão ser enviados para o Factory, o arquivo deve ser referenciado no service mesmo, para que ele possa ser acessado. Então temos que:
Importe também outros arquivos que deseja usar, no meu service estou usando EntityManager e DateUtil. Se estiver só criando o arquivo pra iniciar, só importar o factory é suficiente.
use Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory;
use Doctrine\ORM\EntityManager;
use Teknisa\Libs\Util\DateUtil;
-
Criar variável que vai receber o contexto do Factory dentro da classe, e de outras variáveis que serão necessárias, pré-definidas no services.xml:
protected $perfilClienteFactory;
protected $entityManager;
-
Criar o construct e passar como parâmetro a classe do arquivo Factory (veja logo abaixo) e depois atribuindo essa classe à variável criada.
Lembrando que, o construct tem que ter AS MESMAS variáveis definidas no services.xml (veja abaixo). E na mesma ordem.
No meu arquivo eu só estou usando o EntityManager, então passe ele como parâmetro.
public function __construct(PerfilClienteFactory $perfilClienteFactory, EntityManager $entityManager){
$this->perfilClienteFactory = $perfilClienteFactory;
$this->entityManager = $entityManager;
}
Lá em cima chamamos ela de buildQuery que recebe row, logo criamos:
public function buildQuery($row){}
Se tudo estiver certo, podemos acessar cada campo enviado pelo front, dentro de row:
$row['P_DTINICIAL']
$row['P_CDEMPRESA']
$row['QUALQUERCAMPOQUEEXISTA']
Daí você pode processar todos esses dados antes de enviar eles pro Factory.
No meu exemplo, eu precisava criar uma array com as datas em formato mm/YYYY pra passar pro PIVOT da query do Factory. Fazendo tudo aqui e só passando os ingredientes organizados pro Factory é o ideal.
Da mesma maneira que fizemos no Controller, como nós já temos a Classe do factory definida, podemos acessar os métodos dela e chamar seus métodos. runQuery (que ainda não existe, vai ser o nosso método do Factory que vai receber $params e $mesAno)
return $this->perfilClienteFactory->runQuery($params, $mesAno);
Chamar ela com return vai continuar o caminho dela, e a resposta vai voltar pro Controller assim que o Factory terminar a rotina dele.
O arquivo services.xml de cada módulo registra e observa cada service no projeto e é usado como referência (muito importante!). Ele pode ser encontrado em:
/erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/config/services.xml
Com a seguinte sintaxe:
<container>
<services>
<service id="Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService" class="Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService">
<argument type="service" id="Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory" />
<argument type="service" id="entityManager" />
</service>
</services>
</container>
Dentro de <container>, tem <services> e dentro dele, cada controler vai ter seu próprio bloco <service>, com atributos <argument> dentro (meu deus).
Então pra registrar o controller que você criou, Só copiar e colar é suficiente! Colar isso depois do ultimo <service>, no meu exemplo, PerfilClienteService usa PerfilClienteFactory e entityManager como argumentos, então:
<service id="Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService" class="Teknisa\FIS\Modules\Relatorios\Services\PerfilClienteService">
<argument type="service" id="Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory" />
<argument type="service" id="entityManager" />
</service>
Isso vai funcionar com qualquer Service ou Factory (veja abaixo), contanto que os <arguments> sejam os mesmos e na mesma ordem do arquivo php.
3 - Criar o factory .php da tela:
O nome do arquivo geralmente está em PascalCase, sem underscore (NomeDaTelaFactory.php) e o código da tela no Delphi geralmente está no começo e o Factory no final, vou fazer igual sempre.
O arquivo é criado no caminho relativo do módulo (ou submódulo), assim:
/erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/Factories/NomeDaTelaController.php
O arquivo consiste dos imports, uma classe, uma função especificamente para o construct (passar os imports para variáveis locais) e qualquer função que você queira declarar.
O Factory é quem vai acessar a base de dados e levar os dados do front-end até o caminho final
Exemplo de arquivo comum que executa uma query SELECT com dados recebidos do frontend (Substitua PerfilCliente pelo nome da sua tela.):
<?php
namespace Teknisa\FIS\Modules\Relatorios\Factories;
use Doctrine\ORM\EntityManager;
class PerfilClienteFactory
{
protected $connection;
public function __construct(EntityManager $entityManager){
$this->connection = $entityManager->getConnection();
}
public function runQuery($params, $mesAno) {
// monta a query com o valor da váriavel mesAno
// o mesAno (feito no service) nada mais é do q uma lista com todos os meses dentro do periodo entre parenteses,
// tipo (01/2020 02/2020 03/2020)
$sql = "
SELECT * FROM (
SELECT N.CDEMPRESA, E.NMEMPRESA, N.CDINSCESTA, N.CDTIPOOPER, T.NMTIPOOPER, C.NRINSJURCLIE, C.NMRAZSOCCLIE, EC.SGESTADO, EC.CDMUNICIPIO, M.NMMUNICIPIO,
CASE WHEN NVL(C.IDOPTANTESNCLIE,'N')= 'S'
[...... RESTO DO CÓDIGO, NAO PRECISA COLOCAR TUDO AQUI, DA PRA ENTENDER.]
PIVOT (
SUM(VALOR_TOTAL)
FOR DTEMISSAO
IN $mesAno
)
";
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
}
$mesAno é a variável que nós geramos no arquivo service e enviamos para o Factory por meio do método return $this->perfilClienteFactory->runQuery($params, $mesAno);
runQuery do Factory recebe então os parametros que serão passados para a query juntamente com a array de mes/Ano;
explicação:
Como os dados vamos exceutar queries, precisamos de início apenas:
- Importar o EntityManager:
use Doctrine\ORM\EntityManager;
- Aqui PerfilClienteFactory é o nome do meu factory, logo minha classe deve ter o mesmo nome:
class PerfilClienteFactory {
}
- Montar o construct, já que estamos usando o entity Manager:
Criamos a variável que vai receber o entityManger
protected $connection;
Criamos o construct com os parâmetros que vamos usar, nesse caso só o entityManager, já que o Factory é a ultima etapa da pipeline.
public function __construct(EntityManager $entityManager){
$this->connection = $entityManager->getConnection();
}
A função que definimos no service se chama runQuery, logo temos que chamá-la do mesmo jeito aqui:
public function runQuery($params, $mesAno) {}
- Dentro da função: criar variável que com a query a ser executada, NESSE CASO, como estamos usando o PIVOT, a variável $mesAno vai dentro da query mesmo;
$sql = "
SELECT * FROM (
SELECT N.CDEMPRESA, E.NMEMPRESA, N.CDINSCESTA, N.CDTIPOOPER, T.NMTIPOOPER, C.NRINSJURCLIE, C.NMRAZSOCCLIE, EC.SGESTADO, EC.CDMUNICIPIO, M.NMMUNICIPIO,
CASE WHEN NVL(C.IDOPTANTESNCLIE,'N')= 'S'
[...... RESTO DO CÓDIGO, NAO PRECISA COLOCAR TUDO AQUI, DA PRA ENTENDER.]
PIVOT (
SUM(VALOR_TOTAL)
FOR DTEMISSAO
IN $mesAno
)
";
-
Criar o construct e passar como parâmetro a classe do arquivo Service (veja logo abaixo) e depois atribuindo essa classe à variável criada.
$stmt = $this->connection->prepare($sql);
$stmt->execute($params);
Mantendo o caminho da solicitação:
return $stmt->fetchAll();
Desse jeito assim que a query for executada, o resultado vai ser retornado pro Service, depois pro Controller, e depois do front-end :)
Não existe um factories.xml por motivos de não sabemos. O arquivo services.xml de cada módulo registra e observa cada factory no projeto e é usado como referência (muito importante!). Ele pode ser encontrado em:
/erp/modules/nome_do_modulo/backend/src/Teknisa/NOME_DO_MODULO/config/services.xml
Com a seguinte sintaxe:
<container>
<services>
<service id="Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory" class="Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory">
<argument type="service" id="entityManager" />
</service>
</services>
</container>
Dentro de <container>, tem <services> e dentro dele, cada factory vai ter seu próprio bloco <service>, com atributos <argument> dentro (meu deus).
Então pra registrar o factory que você criou, Só copiar e colar é suficiente! Colar isso depois do ultimo <service>, no meu exemplo, PerfilClienteFactory usa entityManager com argumento, então:
<service id="Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory" class="Teknisa\FIS\Modules\Relatorios\Factories\PerfilClienteFactory">
<argument type="service" id="entityManager" />
</service>
Isso vai funcionar com qualquer Service ou Factory (veja acima), contanto que os <arguments> sejam os mesmos e na mesma ordem do arquivo php.
Na teoria é isso, se deu certo a merda toda, já dá pra receber dados do front end no controller php, enviá-los para o service para tratamento e fazer algo com eles no factory :)
Adicionar tela no menu
1 - menu.json
Caminho: /erp/modules/modulo/mobile/assets/json/menu.json
Adicione um bloco referente a tela dentro do menu correto:
{
"id": "mesmo_id_do_acl",
"name": "abc000_nome_da_tela",
"label": "Relatórios Oficiais",
"windowName": "abc000_nome_da_tela",
"isVisible": true,
"disabled": false
}
2 - aclStruct.json
Caminho: /erp/modules/modulo/mobile/assets/json/aclStruct.json
Adicione um bloco referente a tela dentro do menu correto:
{
"id": "mesmo_id_do_menu",
"name": "abc000_nome_da_tela",
"label": "Nome da Tela",
"windowName": "abc000_nome_da_tela",
"isVisible": true,
"disabled": false,
"containerId": "id_do_container_da_sua_tela",
"containerName": "abc000_nome_da_tela",
"containerLabel": "Nome da Tela"
}
3 - Gerar ID
Gere todos os ids do módulo (o que vai colocar um no menu.json), e sai copiando. Se não for isso me prendam. (Obviamente, os campos devem estar vazios de inicio para que o comando preencha)
4 - Container da tela:
{
"parentMenuId": "mesmo_id_do_acl_e_menu"
}