Quando trabalhamos com WebServices REST no Protheus, muitas vezes precisamos converter arrays ou estruturas de dados em objetos organizados, para facilitar a serialização em JSON. Este artigo mostra um exemplo prático e reutilizável de como criar uma classe chamada Objetos para esse propósito, com uso de boas práticas de encapsulamento e construção dinâmica.

Definição da Função Inicial

#include 'protheus.ch'
#include 'totvs.ch'

User Function FBOBJ()
Return

Esta função principal serve como ponto de entrada e é necessária para o registro no dicionário de funções do Protheus.

Criação da Classe Objetos

A classe foi definida com o objetivo de armazenar um array e permitir fácil serialização posteriormente:

/*/{Protheus.doc} Objetos
Classe de Objetos para realizar a serialização do objeto de Objetos
@author Fernando Bueno
@since 14/04/2017
@version 1.0
@example
(examples)
@see (links_or_references)
/*/
Class Objetos
  Data aDados As Array
  Method New(aDados) Constructor
EndClass

Implementando o Método Construtor

O método New recebe um array e utiliza uma função auxiliar para tratar os dados antes de armazená-los. Isso facilita a posterior serialização e evita inconsistências.

/*/{Protheus.doc} new
Metodo construtor
@author Fernando Bueno
@since 14/04/2017 
@version 1.0
@example
(examples)
@see (links_or_references)
/*/
Method New(aDad) Class Objetos
  aDad := u_FBArrAcen(aDad)
  ::aDados := aDad
Return(Self)

A função u_FBArrAcen() aparentemente é uma função utilitária personalizada para ajustar ou sanitizar o conteúdo do array, garantindo compatibilidade com o formato de saída esperado.

Código Completo Fornecido

#include 'protheus.ch'
#include 'totvs.ch'

User Function FBOBJ()
Return

/*/{Protheus.doc} Objetos
Classe de Objetos para realizar a serialização do objeto de Objetos
@author Fernando Bueno
@since 14/04/2017
@version 1.0
@example
(examples)
@see (links_or_references)
/*/
Class Objetos
	Data aDados	As Array
	Method New(aDados) Constructor
EndClass

/*/{Protheus.doc} new
Metodo construtor
@author Fernando Bueno
@since 14/04/2017 
@version 1.0
@example
(examples)
@see (links_or_references)
/*/
Method New(aDad) Class Objetos
aDad := u_FBArrAcen(aDad)
::aDados := aDad
Return(Self)

Precisa de ajuda com integração REST no Protheus? Fale comigo.

Exemplo de Serialização para envia os dados para um endpoint:

#INCLUDE "PROTHEUS.CH"
#INCLUDE "RWMAKE.CH"   
#INCLUDE "TBICONN.CH" 
#INCLUDE "COLORS.CH"
#INCLUDE "RPTDEF.CH"
#INCLUDE "FWPrintSetup.ch"
#Include 'parmtype.ch'
#Include "RestFul.CH"
#Include "TopConn.CH"
#INCLUDE "FILEIO.ch"

#Define BREAKLINE		Chr(13)+Chr(10)

/*/{Protheus.doc} FBTIT01
Ensinando Rest em ADVPL
@author Fernando Bueno
@since 13/04/2017
@version undefined
@type function
/*/
User Function FBTIT01()	
Return

/*/{Protheus.doc} TITULOSFINANCEIROS
Definição da estrutura do webservice
@author Fernando Bueno
@since 13/04/2017
@type class
/*/
WSRESTFUL TITULOSFINANCEIROS DESCRIPTION "Serviço REST para manipulação de notas fiscais"
WSDATA SEGKEY  		As String //String que vamos receber via URL
WSDATA VENDEDOR  	As String //String que vamos receber via URL  // Gabriel Soares 04/03/2019 - Colocado parametro para Vendedor
WSDATA CLIDE  		As String //String que vamos receber via URL  // Gabriel Soares 04/03/2019 - Colocado parametro Cliende De
WSDATA CLIATE  		As String //String que vamos receber via URL  // Gabriel Soares 04/03/2019 - Colocado parametro Cliende Até
WSDATA CLIENTE  	As String //String que vamos receber via URL
WSDATA LOJA			As String //String que vamos receber via URL
WSDATA CLIENTES  	As String //String que vamos receber via URL
WSDATA NUMTITDE  	As String //String que vamos receber via URL
WSDATA NUMTITATE  	As String //String que vamos receber via URL
WSDATA PREFIXODE  	As String //String que vamos receber via URL
WSDATA PREFIXOATE  	As String //String que vamos receber via URL
WSDATA EMISSAODE  	As String //String que vamos receber via URL
WSDATA EMISSAOATE  	As String //String que vamos receber via URL
WSDATA VENCTODE  	As String //String que vamos receber via URL
WSDATA VENCTOATE  	As String //String que vamos receber via URL
WSDATA BAIXADE  	As String //String que vamos receber via URL
WSDATA BAIXAATE  	As String //String que vamos receber via URL
WSDATA BAIXAS  		As String //String que vamos receber via URL
WSDATA ULTIMOS  	As String //String que vamos receber via URL
WSDATA ATRASADOSATE	As String //String que vamos receber via URL
WSDATA EMABERTO		As String //String que vamos receber via URL
WSDATA TIPO			As String //String que vamos receber via URL
WSDATA DISSESSFTR	As String //String que vamos receber via URL
WSDATA PAGINA  		As String //String que vamos receber via URL
WSDATA ORDER  		As String //String que vamos receber via URL
WSDATA DIR  		As String //String que vamos receber via URL
WSDATA NUMLIQ  		As String //String que vamos receber via URL
WSDATA PAGINATION   As String //String que vamos receber via URL
WSDATA LIMIT   As String //String que vamos receber via URL
 
WSMETHOD GET DESCRIPTION "Retorna os cabeçalhos dos pedidos de venda informado na URL" WSSYNTAX "/TITULOSFINANCEIROS || /TITULOSFINANCEIROS/{NUMTITDE}{NUMTITATE}{PREFIXODE}{PREFIXOATE}{EMISSAODE}{EMISSAOATE}{BAIXAS}{ULTIMOS}{ATRASADOSATE}{PAGINA}{ORDER}{DIR}{NUMLIQ}" //Disponibilizamos um método do tipo GET
 
END WSRESTFUL

/*/{Protheus.doc} GET
Processa as informações e retorna o json
@author Fernando Bueno
@since 13/04/2017
@version undefined
@param oSelf, object, Objeto contendo dados da requisição efetuada pelo cliente, tais como:
   - Parâmetros querystring (parâmetros informado via URL)
   - Objeto JSON caso o requisição seja efetuada via Request Post
   - Header da requisição
   - entre outras ...
@type Method
/*/
WSMETHOD GET WSSERVICE TITULOSFINANCEIROS
//--> Recuperamos o produto informado via URL 
//--> Podemos fazer dessa forma ou utilizando o atributo ::aUrlParms, que é um array com os parâmetros recebidos via URL (QueryString)
Local cNUMTITDE 	:= Self:NUMTITDE
Local cNUMTITATE 	:= Self:NUMTITATE
Local cPREFIXODE 	:= Self:PREFIXODE
Local cPREFIXOATE 	:= Self:PREFIXOATE
Local cEMISSAODE 	:= Self:EMISSAODE
Local cEMISSAOATE 	:= Self:EMISSAOATE
Local cVENCTODE 	:= Self:VENCTODE
Local cVENCTOATE 	:= Self:VENCTOATE
Local cBAIXADE 		:= Self:BAIXADE
Local cBAIXAATE 	:= Self:BAIXAATE
Local cBAIXAS 		:= Self:BAIXAS
Local cULTIMOS 		:= Self:ULTIMOS
Local cATRASADOSATE	:= Self:ATRASADOSATE
Local cEMABERTO		:= Self:EMABERTO
Local cTIPO			:= Self:TIPO
Local cPAGINA 		:= Self:PAGINA
Local cORDER 		:= Self:ORDER
Local cDIR 			:= Self:DIR
Local cNUMLIQ 		:= Self:NUMLIQ
Local cPag := "10"
Local cPAGINATION 	:= Self:PAGINATION
Local lDisSessFtr   := Self:DISSESSFTR == "N" 

Local aArea		:= GetArea()
Local cJson		:= ""
Local cQuery    := ""
Local aDados    := {}
Local nTot		:= 0
Local cBD := AllTrim(GetMV("MV_FBWEB08"))

Private cLIMIT 		:= Self:LIMIT

	If !u_FBSegApp(Self:SEGKEY)
		Return
	EndIf
	
	// define o tipo de retorno do método
	::SetContentType("application/json")
	
	cFiltro := ""

	If !Empty(cNUMTITDE) .AND. !Empty(cNUMTITATE)
		cFiltro += "AND SE1.E1_NUM BETWEEN '" + cNUMTITDE + "' AND '" + cNUMTITATE + "' " + BREAKLINE
	EndIf
	If !Empty(cPREFIXODE) .AND. !Empty(cPREFIXOATE)
		cFiltro += "AND SE1.E1_PREFIXO BETWEEN '" + cPREFIXODE + "' AND '" + cPREFIXOATE + "' " + BREAKLINE
	EndIf
	If !Empty(cBAIXAS)
		If cBAIXAS == '1' //Em aberto
			cFiltro += "AND SE1.E1_BAIXA = ' ' " + BREAKLINE	
		ElseIf cBAIXAS == '2' //Baixado parcialmente
			cFiltro += "AND SE1.E1_BAIXA <> ' ' AND SE1.E1_SALDO > 0 " + BREAKLINE
		ElseIf cBAIXAS == '3' //Baixado totalmente
			cFiltro += "AND SE1.E1_BAIXA <> ' ' AND SE1.E1_SALDO = 0 " + BREAKLINE
		EndIf	 
	EndIf
	If !Empty(cULTIMOS)
		cFiltro += "AND SE1.E1_EMISSAO >= '" + DTOS((DATE() - Val(cULTIMOS))) + "' " + BREAKLINE  
	ElseIf !Empty(cEMISSAODE) .AND. !Empty(cEMISSAOATE)
		cFiltro += "AND SE1.E1_EMISSAO BETWEEN '" + DTOS(CTOD(cEMISSAODE)) + "' AND '" + DTOS(CTOD(cEMISSAOATE)) + "' " + BREAKLINE
	EndIf
	If !Empty(cATRASADOSATE)
		cFiltro += "AND SE1.E1_SALDO > 0  AND SE1.E1_VENCREA >= '" + cATRASADOSATE + "' " + BREAKLINE  
	EndIf
	If !Empty(cEMABERTO)
		cFiltro += "AND SE1.E1_SALDO > 0 " + BREAKLINE  
	EndIf
	If !Empty(cVENCTODE) .AND. !Empty(cVENCTOATE)
		cFiltro += "AND SE1.E1_VENCREA BETWEEN '" + DTOS(CTOD(cVENCTODE)) + "' AND '" + DTOS(CTOD(cVENCTOATE)) + "' " + BREAKLINE
	EndIf
	If !Empty(cBAIXADE) .AND. !Empty(cBAIXAATE)
		If cBAIXADE == 'BLANK' .AND. cBAIXAATE == 'BLANK'
			cFiltro += " AND SE1.E1_BAIXA = ' ' AND SE1.E1_VENCREA < '" + DTOS(DATE()) + "' " + BREAKLINE
		ElseIf cBAIXADE == 'AVENC' .AND. cBAIXAATE == 'AVENC'
			cFiltro += " AND SE1.E1_BAIXA = ' ' AND SE1.E1_VENCREA >= '" + DTOS(DATE()) + "' " + BREAKLINE
		Else
			cFiltro += "AND SE1.E1_BAIXA BETWEEN '" + DTOS(CTOD(cBAIXADE)) + "' AND '" + DTOS(CTOD(cBAIXAATE)) + "' " + BREAKLINE
		EndIf
	EndIf
	If !Empty(cNUMLIQ)
		cFiltro += "AND SE1.E1_NUM+SE1.E1_PREFIXO+SE1.E1_PARCELA IN ( SELECT E5.E5_NUMERO+E5.E5_PREFIXO+E5.E5_PARCELA FROM " + RetSQLName("SE5") + " E5 WHERE D_E_L_E_T_ = ' ' AND E5.E5_DOCUMEN = '" + cNUMLIQ + "' ) " + BREAKLINE
	EndIf

	//conout("873738273827382738273823:" + Self:DISSESSFTR + "zzz")

	If Self:DISSESSFTR == "N"

		//conout("hdsjkadhshadkjshdhkashkdshakdhsakhkdshaskdhkashdksahdkashdkashhdk")

		if ! Empty(Self:VENDEDOR) // Gabriel Soares 04/03/2019 - Colocado teste para Vendedor
			cFiltro += u_FBFILTER("TITULOSFINANCEIROS", Self:VENDEDOR)
		endif

		conout("TITULOSFINANCEIROS:clientes1")
		if ! Empty(Self:CLIENTE) /* .AND. !Empty(Self:LOJA)*/ // Gabriel Soares 04/03/2019 - Colocado teste para Cliente
			conout("TITULOSFINANCEIROS:clientes2")
			cFiltro += u_FBFILTER("TITULOSFINANCEIROS", Self:CLIENTE, "CC", Self:CLIENTES)
		endif

	EndIf	
	
	if ! Empty(Self:CLIDE) // Gabriel Soares 04/03/2019 - Colocado teste para Cliente De
		cFiltro += "AND SE1.E1_CLIENTE >= '" + Self:CLIDE + "' " + BREAKLINE
	endif
	
	if ! Empty(Self:CLIATE) // Gabriel Soares 04/03/2019 - Colocado teste para Cliente Até
		cFiltro += "AND SE1.E1_CLIENTE <= '" + Self:CLIATE + "' " + BREAKLINE
	endif

	If !Empty(cTIPO)
		cFiltro += "AND SE1.E1_TIPO = '" + cTIPO + "' " + BREAKLINE
		If cTIPO == "NCC"
			cFiltro += " AND SE1.E1_SALDO > 0 " + BREAKLINE
		EndIf
	EndIf

	//--TITULOSFINANCEIROS
	//--num_nota, ser_nota, num_carga, cliente, total_peso, total_merc, status, motivo, sequencia
	cQuery := "SELECT COUNT(E1_FILIAL) TOTALREG " + BREAKLINE 
	cQuery += "		FROM " + RetSQLName("SE1") + " SE1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " " + BREAKLINE 
	cQuery += "		LEFT JOIN " + RetSQLName("SA1") + " A1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( E1_CLIENTE = A1_COD AND E1_LOJA = A1_LOJA AND A1.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += "WHERE SE1.D_E_L_E_T_ = ' '  "
	
	cQuery += cFiltro
	
	//cQuery += "GROUP BY E1_FILIAL " + BREAKLINE
	
	u_FBCONS("CONTADOR TITULO = " + cQuery)

	//Executando consulta e setando o total da régua
	TCQuery cQuery New Alias "QRY_CNT"
	
	If !QRY_CNT->(Eof())
		nTot := QRY_CNT->TOTALREG
	EndIf
	
	QRY_CNT->(dbCloseArea())

	If !Empty(cPAGINA)
	
		If cPAGINA == '1'
			cPAGINA := ''
		Else	
			cLim := cValToChar(Val(cPag)*(Val(cPAGINA)-1))
			
			If Val(cLim) > nTot
				cLim := cValToChar(Val(cLim) - Val(cPag)) 
			EndIf
		EndIf	
		
	Else
		cLim := cPag	
	EndIf	

	conout("cPAGINATION:" + cPAGINATION)

	cLimitCond := IIF(Empty(cLIMIT), "", "TOP " + cLIMIT)

	//--TITULOSFINANCEIROS
	//--num_nota, ser_nota, num_carga, cliente, total_peso, total_merc, status, motivo, sequencia
	cQuery := "SELECT " + IIF(cPAGINATION <> 'OFF' .AND. cBD == 'MSSQL', "TOP " + cPag, cLimitCond) + " E1_FILIAL, E1_NUM, E1_PREFIXO, E1_PARCELA, E1_TIPO, E1_EMISSAO, E1_VENCREA, E1_BAIXA, SUM(E1_VALOR) E1_VALOR, SUM(E1_SALDO) E1_SALDO, A1_COD, A1_LOJA, A1_NOME, " + BREAKLINE
	cQuery += "		A1_PESSOA, A1_CGC, A1_END, A1_BAIRRO, A1_MUN, A1_EST, A1_TEL, A1_EMAIL, E1_NUMLIQ, E1_IDCNAB, E1_FILORIG " + BREAKLINE 
	cQuery += "		FROM " + RetSQLName("SE1") + " SE1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " " + BREAKLINE 
	//cQuery += "		LEFT JOIN " + RetSQLName("SC5") + " C5 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( E1_FILIAL = C5_FILIAL AND E1_NUM = C5_NOTA AND E1_PREFIXO = C5_SERIE AND SE1.D_E_L_E_T_ = ' ' ) "
	//cQuery += "		LEFT JOIN " + RetSQLName("SC6") + " C6 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C5_FILIAL = C6_FILIAL AND C5_NUM = C6_NUM AND C6.D_E_L_E_T_ = ' ') "
	//cQuery += "		LEFT JOIN " + RetSQLName("SB1") + " B1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( C6_PRODUTO = B1_COD AND B1.D_E_L_E_T_ = ' ') "				
	cQuery += "		LEFT JOIN " + RetSQLName("SA1") + " A1 " + IIF(cBD == 'MSSQL', "WITH (NOLOCK)", "") + " ON ( E1_CLIENTE = A1_COD AND E1_LOJA = A1_LOJA AND A1.D_E_L_E_T_ = ' ') " + BREAKLINE
	cQuery += "WHERE SE1.D_E_L_E_T_ = ' '  " + BREAKLINE
	If cPAGINATION <> 'OFF' .AND. !Empty(cPAGINA) .AND. cBD == 'MSSQL'
		cQuery += "AND SE1.R_E_C_N_O_ NOT IN (SELECT TOP " + cLim + " E1.R_E_C_N_O_ FROM " + RetSQLName("SE1") + " E1 WHERE E1.D_E_L_E_T_ = ' ' " + cFiltro + " " + u_FBORDERBY("TITULOSFINANCEIROS", cORDER, cDIR) + ") " + BREAKLINE
	EndIf	

	cQuery += cFiltro
	
	cQuery += "GROUP BY E1_FILIAL, E1_NUM, E1_PREFIXO, E1_PARCELA, E1_TIPO, E1_EMISSAO, E1_VENCREA, E1_BAIXA, A1_COD, A1_LOJA, A1_NOME, A1_PESSOA, A1_CGC, A1_END, A1_BAIRRO, A1_MUN, A1_EST, A1_TEL, A1_EMAIL, E1_NUMLIQ, E1_IDCNAB, E1_FILORIG " + BREAKLINE
	
	cOrderBy := u_FBORDERBY("TITULOSFINANCEIROS", cORDER, cDIR)
	
	cQuery += cOrderBy
	
	If cBD == 'ORACLE' .AND. cPAGINATION <> 'OFF'

		cQueryORA := " SELECT ROWNUM + (" + cPag + " * (PAGINA - 1)) AS LINNUM " + BREAKLINE
		cQueryORA += "	      ,B.* " + BREAKLINE
		cQueryORA += "	  FROM (SELECT COUNT(*) OVER() AS TOTALREG " + BREAKLINE
		cQueryORA += "	              ,TRUNC((ROW_NUMBER() OVER(" + cOrderBy + ") - 1) / " + cPag + ") + 1 AS PAGINA " + BREAKLINE
		cQueryORA += "	              ,TMP.* " + BREAKLINE
		cQueryORA += "	          FROM ( " + BREAKLINE
		
		cQueryORA += cQuery
		
		cQueryORA += ") TMP " + BREAKLINE
	    cQueryORA += ") B " + BREAKLINE
	    
	    If !Empty(cPAGINA)
	    	cQueryORA += " WHERE PAGINA = " + cPAGINA + " " + BREAKLINE
	    EndIf	
	    
	    cQuery := cQueryORA
	
	EndIf

	CONOUT("TITULOSFINANCEIROS cQuery principal: " 	+ cQuery)

	If Select("QRY_AUX") > 0
		QRY_AUX->(dbCloseArea())
	EndIf
	
	//Executando consulta e setando o total da régua
	TCQuery cQuery New Alias "QRY_AUX"
	Count to nTotal
	
	QRY_AUX->(DbGoTop())

	While ! QRY_AUX->(Eof())
	
		aAdd(aDados, {	QRY_AUX->E1_FILIAL,; 
						QRY_AUX->E1_NUM,; 
						QRY_AUX->E1_PREFIXO,; 
						QRY_AUX->E1_PARCELA,; 
						QRY_AUX->E1_TIPO,; 
						QRY_AUX->E1_EMISSAO,; 
						QRY_AUX->E1_VENCREA,; 
						QRY_AUX->E1_BAIXA,; 
						QRY_AUX->E1_VALOR,; //PENDENTE-VERIFICAR SE ESTÁ EXIBINDO O VALOR CORRETO DO TITULO
						QRY_AUX->E1_SALDO,; 
						QRY_AUX->A1_COD,; 
						QRY_AUX->A1_LOJA,; 
						QRY_AUX->A1_NOME,; 
						QRY_AUX->E1_NUMLIQ,; 
						cValToChar(nTot),;
						QRY_AUX->E1_IDCNAB,;
						QRY_AUX->E1_FILORIG,;
						QRY_AUX->A1_PESSOA,;
						QRY_AUX->A1_CGC,;
						QRY_AUX->A1_END,;
						QRY_AUX->A1_BAIRRO,;
						QRY_AUX->A1_MUN,;
						QRY_AUX->A1_EST,;
						QRY_AUX->A1_TEL,;
						QRY_AUX->A1_EMAIL;
						})
						
		QRY_AUX->(dbSkip())	
	
	EndDo

	QRY_AUX->(dbCloseArea())
	
	oObjCom := Objetos():New(aDados) //Cria um objeto da classe produtos para fazer a serialização na função FWJSONSerialize
	//::SetContentType("application/json")
	// --> Transforma o objeto de produtos em uma string json
	//cJson := "receive([" + FWJsonSerialize(oObjCom) + "])"
	cJson := FWJsonSerialize(oObjCom)
	//u_FBCONS(cJson)
	// --> Envia o JSON Gerado para a aplicação Client
	::SetResponse(cJson)
	RestArea(aArea)
	
Return(.T.)