Oracle PL/Web em versao Open Source com PHP e PostgreSQL

Essa é uma idéia realmente pouco ortodoxa e, por isso, merece uma explicação de seus "por ques"…

Uma vez, na empresa em que trabalho, estava avaliando qual a tecnologia mais adequada para um determinado projeto.
Por uma série de razões, chegamos a conclusão que a tecnologia mais adequada (entre as disponíveis na empresa) era utilizar o bom e velho PL/SQL para WEB (ou PlWeb ou WebToolkit).

Como a Oracle fez uma tremenda salada com o nome dessa tecnologia, vale uma explicação:
Inicialmente, o PL Web era formado apenas por algumas packages (utl_http, owa, etc…) que eram capazes de gerar saídas para um webserver, chamado OAS (Oracle application server). Desta maneira, pode-se gerar páginas web diretamente de uma procedure Oracle.

Um exemplo básico de PLWEB e:

create or replace procedure teste is
begin
   htp.p('<html><body>Hello World</body></html>');
end;

Toda a saída gerada através de htp.p é enviada ao OAS, que nada mais é que um servidor Apache, acrescido de uma extensão chamada mod_plsql.

A primeira vista, essa tecnologia pode parecer um pouco estranha, mas na prática se revelou robusta, estável e de boa performance, uma vez que o grosso do processamento se faz no database server (que é supostamente o maior server entre as camadas).

O único problema é: misturar HTML com lógica de negócio e um pecado capital. Para isso, a Oracle criou o HTMLDB, que é um framework desenvolvido em PL/SQL / PLWEB.

Essa tecnologia cresceu e a Oracle decidiu rebatizar o OAS para iAS (Internet Application Server) e o HTMLDB para APEX.

Voltando ao meu projeto, resolvemos utilizar PLWEB e, ao invés de HTMLDB, utilizamos um framework PL/SQL criado internamente. Esse framework foi baseado na idéia de um outro framework (este em PHP) chamado Phormation.
A Phormation é um framework já bastante antigo e desatualizado técnicamente, mas imbatível em seu conceito.

O projeto começou a ser desenvolvido e, após alguns meses, foi implantado com sucesso.

Foi então que me veio a cabeça uma idéia: Por que não criar uma versão open source desta tecnologia toda?

Mas por que alguém pensaria em fazer algo assim, sendo que existem tantas linguagens e frameworks por ai?
Ai vão os motivos:

  • Performance. Com praticamente uma única camada (a do database), evita-se uma série de IOs, connects, etc.. tornando a performance geral muito satisfatória.
  • Facilidade para o programador. Utilizando esta tecnologia, o programador só precisa conhecer o framework, ao invés de HTML, JS, PHP, linguagens de stored-procedure, etc…
  • Facilidade de administração. Por ter menos camadas, a administração passa a ser centralizada no DB.
  • Vontade de criar… Fiquei atraído pela idéia de recriar algo que foi um sucesso no mundo do software comercial e que não tem um correspondente no mundo open source.

Softwares utilizados

Banco de dados

Uma das dificuldade era adotar um banco de dados que fosse robusto e, ao mesmo tempo tivesse uma linguagem de stored procedure que um programador mediano pudesse utilizar. Eu nunca tinha utilizado PostgreSQL antes, mas como ouvi falar muito bem dele, resolvi instalar para ver como funcionava. Que grata surpresa quando descobri que ele possuia uma extensão chamada PL/PGSQL que implementa PL/SQL no PostgreSQL. Bingo! Este era o database que eu procurava.

Middle-Tier

Assim como o PLWEB tem o mod_plsql, que faz o papel de uma camada intermediária entre o banco e o Apache, nós também teríamos que ter um aqui. A essa camada eu dei o nome de "middle-tier".
O mod_plsql é feito em C e roda como mod do Apache. Isso faz com que a performance seja muito boa, mas ao mesmo tempo, implementa uma das maiores deficiências do PLWEB: qualquer alteração de configuração requer um stop/start do apllication server. E eu não queria repetir este erro em nosso middle-tier.
Como sou um grande fã de PHP e o trabalho da camada intermediária era relativamente simples, resolvi utilizar esta linguagem para seu desenvolvimento.

Framework

Eu precisaria de um framework escrito em PL/PGSQL, o que afunilou minha busca por componentes open source já prontos a zero projetos… Resolvi então escrever o meu próprio. Mais uma vez, utilizei o conceito da Phormation como base.

Primeiros Passos

Deixe-me explicar um pouco mais a fundo o funcionamento do PL/WEB:

Para cada aplicação, é preciso criar um identificador chamado DAD (Data Access Descriptor). Este serve para informar ao application server, via URL, como se conectar ao banco de dados. Uma URL típica seria:

http://meuServer/meuDAD/minhaProcedure

Quando você acessa uma URL como essa, o application server executa uma série de ações:

  1. Intercepta a chamada;
  2. Extrai o nome do DAD da URL;
  3. Procura esse DAD em seu arquivo de configuração (para achar usuário/senha/DB a ser conectado);
  4. Conecta-se ao banco de dados;
  5. Roda a procedure minhaProcedure;
  6. Exibe na tela o HTML gerado por esta procedure.

A questão era: Como imitar este processo em nossa solução?

Após alguma pesquisa, descobri uma extensão muito interessante do Apache, chamado mod_rewrite. Esta permite que você aplique uma expressão regular a uma URL antes que o Apache a execute.
Esta expressão regular deverá ser escrita no arquivo especial .htaccess, tornando possível coisas do tipo: "tudo que tiver /x/index.html na URL, na verdade deverá executar /y/index.html".

Desta maneira, alterei um pouco a URL de nossa solução para:

http://meuServer/SHOF/meuDAD/minhaProcedure

Em tempo: SHOF é o nome do projeto!

O identificador SHOF informa ao nosso application server que essa URL deverá ser tratada por ele.
Para isso, escrevi um arquivo .htaccess com as seguintes linhas:

Options +FollowSymlinks
RewriteEngine on
RewriteRule ^shof\/(.*)\/(.*)$ /shof/shof.php

Esse arquivo ativa o mod_rewrite e faz com que todas as URLs que iniciem por SHOF sejam desviadas para /shof/shof.php.

Escrevendo o Middle-Tier

Agora ficou mais fácil… Após o usuário digitar http://meuServer/SHOF/meuDAD/minhaProcedure, a solução redirecionará o fluxo do sistema para "shof.php".
O shof.php ainda consegue descobrir qual foi a URL digitada originalmente (usando $_SERVER["REQUEST_URI"])) e, consequentemente, qual DAD utilizar e qual procedure chamar.

Por exemplo: em uma chamada a

http://meuServer/shof/app2/main

o mod_rewrite ira redirecionar para shof/shof.php e, através de um pequeno parse na URL, eu descubro que o usuário deseja acessar a aplicação (DAD) app2 e a procedure main.

O arquivo de configuração

OK, mas como se conectar ao banco da dados? Sabendo que o DAD escolhido foi o app2, procuramos por este em um arquivo de configuracao XML que, entre outras coisas, nos diz como se conectar ao banco de dados.

Ai vai um exemplo dele:

<?xml version="1.0" encoding="ISO-8859-1"?>
<CONF>
    <APP2>
        <SERVER>localhost</SERVER>
        <PORT>5432</PORT>
        <DATABASE>db_shof</DATABASE>
        <AUTH_TYPE>D</AUTH_TYPE>
    </APP2>
</CONF>

Como se pode ver, além dos dados básicos de configuração, temos um parâmetro chamado AUTH_TYPE, ou tipo de autenticação. Este nos diz como queremos efetuar a autenticação:

  • B - Basic. Neste tipo de autenticação, não iremos perguntar usuário/senha ao usuário. Ao invés, utilizaremos o usuário e senha informado no próprio DAD.
  • D - Database. O sistema ira perguntar por usuário/senha e tentará conectar no banco de dados utilizando estas informações.
  • C - Custom. Quando o tipo for custom, iremos perguntar usuário e senha e direcionaremos para a procedure informada em CUSTOM_AUTH. Esta deve ser escrito pelo programador e retornará um TRUE ou FALSE dizendo se o usuário pode ou não prosseguir.

Desta maneira, podemos autenticar usuários de qualquer maneira que se deseje…

A cartada final

Resolvido os problemas iniciais de arquitetura, vamos em frente. Já estamos conectados ao banco e já sabemos que procedure chamar. Mas como essa procedure irá retornar HTML para o middle-tier?

Primeiro criamos um schema no PostgreSQL chamado SHOF. Este irá armazenar todas as funções do framework. Depois construimos uma function chamada p (print). Esta função recebe um parâmetro texto, que é o código HTML que se deseja imprimir. Este parâmetro será inserido em uma tabela temporária chamada SHOF_BUFFER. Tabelas temporárias tem escopo local, ou seja, o usuário só vê os dados de sua própria conexão.

Ao final da procedure, o middle-tier fará um select desta tabela e despejará todos os códigos HTML contidos nela na tela.
A esta altura, é provável que você já esteja perdido, então vamos recapitular:

  1. O usuário acessa http://meuServer/shof/meuDAD/minhaProcedure;
  2. O mod_rewrite identifica o SHOF logo após o nome do servidor e desvia o processamento para shof/shof.php;
  3. O shof.php extrai o meuDAD e procura o mesmo em shof.xml;
  4. O shof.php encontra o meuDAD em shof.xml, faz a autenticação, conecta no banco e chama a procedure PostgreSQL minhaProcedure, que foi escrita em PL/PGSQL;
  5. A procedure minhaProcedure monta a página utilizando a função shof.p('<CODIGO HTML>') que por sua vez insere <CODIGO HTML> na tabela temporária shof_buffer;
  6. Ao final do processamento de minhaProcedure, o controle volta para shof.php que faz um select em shof_buffer e imprime seu conteúdo na tela.

Mas e o framework ?

Bem… você poderia escrever seu programa concatenando código HTML com o resultado de comandos SQL (uma vez que você já esta em uma stored procedure) e imprimir tudo usando shof.p. Mas isso seria um pecado capital, pois mistura código HTML com lógica de negócio.

Então criamos um framework baseado em funções PL/PGSQL que recebe parâmetros, monta o HTML e despeja o resultado utilizando shof.p.

Esta é a única parte do SHOF que não está implementado ainda. E.. a falta de tempo também me acomete… Entretanto existe um pequeno protótipo da viabilidade disto…

Posso ver o que já esta pronto?

Claro!

Posso ver o código fonte de tudo isso?

The world is free… lá vai:

Middle-Tier

<?
   session_start();
   $bDebug = false;
 
   // Parse URL
   $shof_url_parms = explode('/', $_SERVER["REQUEST_URI"]);
   $shof_dad     = $shof_url_parms[2];
   if (strpos($shof_url_parms[3], '?') === false)
    {
      $shof_module = $shof_url_parms[3];
    }
  else
    {
      $shof_module = substr($shof_url_parms[3],0, strpos($shof_url_parms[3], '?') );
    }
   if ($shof_dad =="" || $shof_module == "" ) { fProceedTo404(); }
 
   if ($bDebug) {
      echo 'Application : ' . $shof_dad    . '<br>';
      echo 'Module      : ' . $shof_module . '<br>';
   }
   // Check if this DAD has been parsed already. If so, use session stored values
   if ($shof_dad != $_SERVER['DAD']) {
      $shof_xml_parms = fShofProcessXML();
 
      $shof_dad_found = false;
      foreach($shof_xml_parms["SHOWY"] as $shof_key => $shof_values) {
         if (strtoupper($shof_dad) == strtoupper($shof_key)  ) {
            $shof_dad_found = true;
            $shof_dad_parms = $shof_values;
            break;
         }
      }
 
      if (!$shof_dad_found) {
         fProceedTo404(); }
 
      if ($bDebug) {echo 'DAD has been found<br>';}
 
      $_SERVER['AUTH_TYPE'] = $shof_dad_parms[strtoupper('AUTH_TYPE')];
 
      if ($_SERVER['AUTH_TYPE'] == 'D'  or $_SERVER['AUTH_TYPE'] == 'C') // Database or Custom Authentication
      {
         if (!isset($_SERVER['PHP_AUTH_USER'])) 
          {
            header('WWW-Authenticate: Basic realm="SHOF LOGON ' . $_SERVER['AUTH_TYPE'] . '"');
            header('HTTP/1.0 401 Unauthorized');
            echo 'ACCESS DENIED';
            exit;
          } 
         else 
          {
             if ($_SERVER['AUTH_TYPE'] == 'D') {
                   $shof_dad_parms['USER']     = $_SERVER['PHP_AUTH_USER'];
                $shof_dad_parms['PASSWORD'] = $_SERVER['PHP_AUTH_PW'];
             }
          }
      }
 
      $_SERVER['DAD']         = $shof_app;
      $_SERVER['SERVER']      = $shof_dad_parms[strtoupper('server')];
      $_SERVER['PORT']        = $shof_dad_parms[strtoupper('port')];
      $_SERVER['USER']        = $shof_dad_parms[strtoupper('user')];
      $_SERVER['PASSWORD']    = $shof_dad_parms[strtoupper('password')];
      $_SERVER['DATABASE']    = $shof_dad_parms[strtoupper('database')];
      $_SERVER['CUSTOM_AUTH'] = $shof_dad_parms[strtoupper('custom_auth')];
 
   }
 
   // Make database connection
   $shof_conn = @pg_connect(" host=     " . $_SERVER['SERVER']   .
                            " port=     " . $_SERVER['PORT']     .
                            " dbname=   " . $_SERVER['DATABASE'] .
                            " user=     " . $_SERVER['USER']     .
                            " password= " . $_SERVER['PASSWORD'] );
 
   if(!$shof_conn) {
      if ($_SERVER['AUTH_TYPE'] == 'D') // Database type auth with wrong password
      {
         header('WWW-Authenticate: Basic realm="SHOF LOGON D"');
         header('HTTP/1.0 401 Unauthorized');
         echo 'ACCESS DENIED';
         exit;
       } 
 
       die("Could not connect to the database {$_SERVER['DATABASE']}");
   }
 
   // TODO: Should we challenge user/pwd every call or not ?
   if ($_SERVER['AUTH_TYPE'] == 'C') // Custom type auth. We're logged in already. Let's check credentials agains custom procedure
    { 
      $shof_result = @pg_query($shof_conn, "SELECT " . $_SERVER['CUSTOM_AUTH'] . "('" . $_SERVER['PHP_AUTH_USER'] . "','" . $_SERVER['PHP_AUTH_PW'] ."')");
      if ( (!$shof_result) or (pg_fetch_result($shof_result,$i,0) == 0 )){
         header('WWW-Authenticate: Basic realm="SHOF LOGON C"');
         header('HTTP/1.0 401 Unauthorized');
         echo 'ACCESS DENIED';
         exit;
      }
    } 
 
   if ($bDebug) {
      echo 'DB connected';
   }
   // Initialize session
   $shof_result = @pg_query($shof_conn, "SELECT shof.init()");
 
   if(!$shof_result) {
      die("Could not initialize session <br><hr>" . pg_last_error ( $shof_conn ) . "<hr>");
   }
 
   // Send necessary variables to shof
   if ($_SERVER['AUTH_TYPE'] == 'D'  or $_SERVER['AUTH_TYPE'] == 'C') // Database or Custom Authentication
         { $sUser =  $_SERVER['PHP_AUTH_USER']; } 
   else  { $sUser =  $_SERVER['USER'];          }
 
   $sSql = "delete from shof.shof_vars where  var_session = '" . session_id() . "' and  var_name = 'USERNAME';
                 insert into shof.shof_vars (var_session, var_name, var_value) values  ('" . session_id()  . "', 'USERNAME', '" . $sUser . "');
                 insert into shof_session (ses_id) values  ('" . session_id()  . "')";
   // Process GETs
   foreach ($_GET as $key => $vl) 
   {
      $sSql .= ";  insert into shof_parms (parm_name, parm_value) values (upper('$key'), '$vl')";
   }
 
   // Process POSTs
   foreach ($_POST as $key => $vl) 
   {
      $sSql .= ";  insert into shof_parms (parm_name, parm_value) values (upper('$key'), '$vl')";
   }
 
   $shof_result = @pg_query($shof_conn, $sSql);
 
   if(!$shof_result) {
      die("Could not initialize variables <br><hr>" . pg_last_error ( $shof_conn ) . "<hr>");
   }
 
   // Call the desired module
   $shof_result = @pg_query($shof_conn, "SELECT $shof_module()");
 
   if(!$shof_result) {
      die("Could not call module $shof_module <br><hr>" . pg_last_error ( $shof_conn ) . "<hr>");
   }
 
   // Send OK header
   if (!$bDebug) {
      header('HTTP/1.1 200 OK');
      header('Status: 200 OK');
   }
 
   // Retrieve results
   $shof_result   = pg_query($shof_conn, "SELECT buf_text FROM shof_buffer");
   $shof_num_rows = pg_num_rows($shof_result);
   $i = 0;
   while($i <= $shof_num_rows - 1) {
      $shof_line = pg_fetch_result($shof_result,$i,0);
      echo $shof_line;
      $i++;
   }
   $shof_line = '';
 
   die('');
  // End of processing
 
   //
   // Helper Functions
   //
 
   function fProceedTo404() {
      global $bDebug;
      if($bDebug) {  die("ERROR 404 - FILE NOT FOUND"); }
      header("HTTP/1.1 404 Not Found");
      header("Status: 404 Not Found");
      die("<h1>Not Found</h1><br>The requested URL {$_SERVER['REQUEST_URI']} was not found on this server.");
   }
 
   function fShofProcessXML() {
      $file = "shof.xml";
      $xml_parser = xml_parser_create();
 
      if (!($fp = fopen($file, "r"))) {
         fProceedTo404();
      }
 
      $data = fread($fp, filesize($file));
      fclose($fp);
      xml_parse_into_struct($xml_parser, $data, $vals, $index);
      xml_parser_free($xml_parser);
 
      $params = array();
      $level = array();
      foreach ($vals as $xml_elem) {
        if ($xml_elem['type'] == 'open') {
         if (array_key_exists('attributes',$xml_elem)) {
           list($level[$xml_elem['level']],$extra) = array_values($xml_elem['attributes']);
         } else {
           $level[$xml_elem['level']] = $xml_elem['tag'];
         }
        }
        if ($xml_elem['type'] == 'complete') {
         $start_level = 1;
         $php_stmt = '$params';
         while($start_level < $xml_elem['level']) {
           $php_stmt .= '[$level['.$start_level.']]';
           $start_level++;
         }
         $php_stmt .= '[$xml_elem[\'tag\']] = $xml_elem[\'value\'];';
         eval($php_stmt);
        }
      }
      return $params;
   }
?>

Stored procedure shof.p

Function p ("ptext" character varying) is
begin
   insert into shof_buffer values (pText);
end;

Procedure de teste

begin
   PERFORM shof.p('Hello World !');
end;

.htaccess

Options +FollowSymlinks
RewriteEngine on
RewriteRule ^shof\/(.*)\/(.*)$ /shof/shof.php

SHOF.XML

<?xml version="1.0" encoding="ISO-8859-1"?>
<SHOWY>
    <APP1>
        <SERVER>localhost</SERVER>
        <PORT>5432</PORT>
        <USER>postgres</USER>
        <PASSWORD>temp123</PASSWORD>
        <DATABASE>db_shof</DATABASE>
        <AUTH_TYPE>B</AUTH_TYPE>
    </APP1>
 
    <APP2>
        <SERVER>localhost</SERVER>
        <PORT>5432</PORT>
        <DATABASE>db_shof</DATABASE>
               <AUTH_TYPE>D</AUTH_TYPE>
    </APP2>
 
    <APP3>
        <SERVER>localhost</SERVER>
        <PORT>5432</PORT>
        <USER>postgres</USER>
        <PASSWORD>temp123</PASSWORD>
        <DATABASE>db_shof</DATABASE>
                <AUTH_TYPE>C</AUTH_TYPE>
        <CUSTOM_AUTH>mylogon</CUSTOM_AUTH>        
    </APP3>
 
</SHOWY>

É isso ai… se alguém quiser mais informações, ou mesmo tocar o projeto em frente, e só mandar um email!

Abraços
Emerson Ribeiro

free hit counter

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-Share Alike 2.5 License.