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;
    }
  • Criar a função que você vai chamar pelo seu front-end:

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.

  • Registrar o Controller no controllers.xml

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.

  • Registrar a rota/endpoint/url que o Front vai poder acessar

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.


  • Juntar tudo ……

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:

  • Importar o arquivo Factory:

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;
    }

  • Criar a(s) funções que o Controller vai chamar

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.

  • Enviar os dados processados para o Factory.

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.

  • Registrar o Service no services.xml

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);
  • Retornar o resultado da query:

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 :)

  • Registrar o Factory no services.xml (sim, o mesmo arquivo onde se registra services)

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"
}