Integração de Sistemas em PHP e Delphi usando WebServices Saulo Arruda (http://sauloarruda.eti.br) Esta semana tive uma experiência nova em se tratando de integração de sistemas. O caso envolvia um sistema de gerenciamento da empresa do cliente, desenvolvido internamente em Delphi com Banco de Dados Microsoft SQL Server e um site desenvolvido em PHP & MySQL. No sistema em Delphi, que a partir de agora vou me referir apenas como ERP, os colaboradores da empresa registram solicitações de serviços. O cliente queria disponibilizar um espaço no site para que o cliente possa acompanhar o andamento da sua solicitação. Arquitetura Esse é um cenário muito comum de vários clientes para os quais já trabalhei. A questão interessante desse caso são as tecnologias envolvidas. As primeiras idéias para a integração entre os sistemas era conectar diretamente no Banco SQL Server e mostrar as informações no site. Essa solução se mostrou inviável devido ao nível de segurança exigido para as informações deste banco, não permitindo acesso externo. Neste caso, os desenvolvedores da equipe do cliente sugeriram fazer uma sincronização de dados do ERP para o site. Muito simples, eu pensei, vamos utilizar WebServices e SOAP. Logo a arquitetura da aplicação segue o esquema do diagrama abaixo: O site fornece um serviço que expõe os métodos necessários para a sincronização dos dados do ERP. A comunicação é feita usando o protocolo HTTPS garantindo a integridade dos dados e antes de qualquer operação. Questões de autenticação serão discutidas mais adiante neste artigo. Usando WebServices e SOAP é possível que o próprio servidor que o site está hospedado, geralmente Apache + PHP, forneça o serviço pela porta 443 eliminando a necessidade de abertura de portas em firewalls tanto do cliente quanto do servidor. Para demonstrar a implementação da solução e o sigilo das informações do cliente, vou usar um exemplo mais simples. Porém, todas as dificuldades encontradas no desenvolvimento serão apresentadas. SOAP Server Desenvolvi a primeira versão do serviço usando a extensão PHP_SOAP que é nativa para o PHP 5 e me pareceu bastante confiável e simples de usar. Criei uma classe chamada SolicitacaoService, o arquivo WSDL e sem nenhuma dificuldade registrei o serviço usando o código abaixo: <?php class SolicitacaoService { // Definição da classe... } $server = new SoapServer("solicitacaoService.wsdl"); $server->setClass("SolicitacaoService"); $server->handle(); ?> Devo admitir que tive vários problemas para escrever o arquivo WSDL da forma correta, já que nunca tinha feito isso “manualmente”. Depois, criei uma aplicação em Delphi usei o "WSDL Importer" para criar uma unit a partir do arquivo WSDL. Esse é um recurso bastante prático, pois as interfaces dos métodos e definição dos tipos já são criados automaticamente. Após isto, basta criar um form e um botão e escrever o código abaixo: procedure TForm1.btnLoginClick(Sender: TObject); var arr : ArrayOfstring; begin solicitacaoService := GetSolicitacaoServicePortType(); // Definição do array com os parâmetros... ShowMessage(solicitacaoService.gravar(arr)); end; Em ambiente de desenvolvimento, o problema da integração já está resolvido. Porém, o servidor de produção usava a versão 4.2.2 do PHP que não suporta a extensão PHP_SOAP. Além disso, essa versão do PHP também não suporta todos os recursos de Orientação a Objetos que eu havia utilizado. Neste momento resolvi usar uma implementação do protocolo SOAP para PHP bastante simples chamada NuSOAP. Agora consegui a compatibilidade com o PHP4 e melhor, sem necessidade de instalação de nenhuma extensão do PHP no servidor de produção. Usando o NuSOAP, o código sofreu várias modificações e ficou assim: <?php function gravar($arrAttr) { return "ACK"; } // Definição do serviço $server = new soap_server(); $server->configureWSDL("SolicitacaoService", "urn:UsuarioService"); // Registra o tipo ArrayOfstring $server->wsdl->addComplexType("ArrayOfstring", "complexType","array", "", "SOAP-ENC:Array", array(), array( array("ref"=>"SOAP-ENC:arrayType", "wsdl:arrayType"=>"string[]") ), "xsd:string"); // Registra o método gravar $server->register("gravar", array("arrAttr" => "tns:ArrayOfstring"), array("return" => "xsd:string"), "urn:SolicitacaoService", "gravar", "rpc", "encoded"); // Executa a requisição $HTTP_RAW_POST_DATA = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : ""; $server->service($HTTP_RAW_POST_DATA); ?> Neste caso, é necessário registrar cada método que será acessado remotamente, porém o NuSOAP gera automaticamente o arquivo WSDL, o que em minha opinião, é mais vantagem do que escrever somente: $server = new SoapServer("solicitacaoService.wsdl"); $server->setClass("SolicitacaoService"); e ter de escrever o WSDL, que não é uma tarefa muito divertida. Acredite. Por outro lado, não foi possível utilizar classes, pois o registro do método gravar usando SolicitacaoService.gravar gerava um WSDL com nomes de métodos não muito agradáveis, então preferi usar funções mesmo. Perceba também que a declaração do tipo complexo ArrayOfstring é bastante confusa, mas nada que não se possa achar pronto na Internet. No cliente em Delphi, bastou gerar novamente a unit correspondente ao serviço usando o WSDL Importer, usando o nome do script PHP com o parâmetro wsdl (Ex.: http://sauloarruda.eti.br/service/solicitacao.php?wsdl). Naturalmente a implementação da função gravar deve abrir uma conexão com o banco de dados MySQL e gravar um registro na tabela de solicitação. Para maior simplicidade do nosso exemplo esse código foi omitido e a função retorna simplesmente o texto ACK que, por convenção, significa que a chamada foi processada com sucesso. Esse cliente em Delphi também deverá ser incorporado no ERP sendo que cada vez que uma solicitação for gravada, uma chamada WebService será feita para sincronizá-la. Além disso, será necessário tratar erros implementando uma fila de chamadas do serviço que falharam para que seja feita uma nova tentativa. Autenticação Todos nós sabemos que um WebService é disponibilizado na Web e por isso, qualquer pessoa pode usar um WSDL Importer da vida e sair fazendo chamadas do seu serviço. Como não é isso que queremos nesse caso, precisamos implementar algum tipo de autenticação. A primeira idéia era usar autenticação HTTP. Mas antes de implementar algo em PHP, resolvi testar os componentes em Delphi para ver se é possível fazer isso. Achei um artigo que pareceu funcionar, mas não fez diferença alguma. A segunda opção era implementar um método login que retorna um ticket usando uma idéia parecida com sistemas de Single Sing On. Na primeira chamada, o cliente chama um método login passando usuário e senha e recebe um ticket que será passado como parâmetro nas próximas chamadas. Desta forma, o método login deve ser definido e registrado conforme o código abaixo: <?php function login($usuario, $senha) { if ($usuario == "service" && $senha == "secret") { $ticket = md5(uniqid()); session_name($ticket); session_start(); $_SESSION["login"] = true; return $ticket; } else { return "401: Usuário ou senha inválidos"; } } // Registra o método login $server->register("login", array("usuario" => "xsd:string", "senha" => "xsd:string"), array("return" => "xsd:string"), "urn:SolicitacaoService", "login", "rpc", "encoded"); ?> O ticket é gerado usando a função uniqid, que garante a geração de um identificador único, e aplicando um hash MD5 ao “id” gerado. Esse ticket é armazenado em uma variável da sessão. Um detalhe importante da implementação da função login é usar a função session_name($ticket) para nomear a sessão com o ticket gerado. Desta forma não existe risco de falhas de segurança, pois cada ticket gerado cria uma nova sessão. Para terminar, a função abaixo valida se o ticket passado por parâmetro é válido: <?php function checkLogin($ticket) { session_name($ticket); session_start(); return isset($_SESSION["login"]); } ?> A implementação da função gravar terá sua assinatura alterada para receber o ticket como parâmetro e validá-lo, conforme o código abaixo: <?php function gravar($arrAttr) { if (!checkLogin($ticket)) { return "401: Autenticação não encontrada"; } return "ACK"; } // Registra o método gravar $server->register("gravar", array("ticket" => "xsd:string", "arrAttr" => "tns:ArrayOfstring"), array("return" => "xsd:string"), "urn:SolicitacaoService", "gravar", "rpc", "encoded"); ?> No cliente, novamente devemos usar o WSDL Importer para gerar a unit do serviço e alterar o código para chamar primeiro o método login, conforme o código abaixo: procedure TForm1.btnLoginClick(Sender: TObject); var arr : ArrayOfstring; ticket: string; begin solicitacaoService := GetSolicitacaoServicePortType(); ticket := solicitacaoService.login('service', 'secret'); // Definição do array com os parâmetros... ShowMessage(solicitacaoService.gravar(ticket, arr)); end; Conclusões A integração entre sistemas em plataformas diferentes nem sempre é uma tarefa trivial. Muitas vezes, por limitação da tecnologia, uso de versões antigas da plataforma (PHP 4 e Delphi 7) e pouca documentação disponível esse trabalho acaba tomando muito mais tempo que o previsto. Nestes casos, eu considero de suma importância trocar idéias com colegas de trabalho ou amigos que já possam ter passado pela mesma situação. Inclusive, agradeço a valiosa ajuda dos colegas Bruno Freitas e Rodrigo Toledo que me ajudaram com ótimas idéias para a resolução deste problema. O código-fonte em Delphi e PHP deste artigo está disponível para download no endereço: http://sauloarruda.eti.br/artigos/webservice.zip. Qualquer dúvida, crítica ou sugestão sobre o tema deste artigo pode ser enviada para o e-mail [email protected] ou via comentários deste artigo publicado no endereço http://sauloarruda.eti.br/. Licença Este artigo está licenciado sob a licença Creative Commons Attribution-ShareAlike 2.5 License.