Codeigniter 3 — El desafío de utilizar procedimientos

Después de un buen tiempo de que EllisLab abandonó el soporte sobre CI (CodeIgniter), le perdí pisada, lo dejé de usar, y hace poco de curiosidad busqué en Google… me sorprendió que lo tomó otra empresa, y esta en desarrollo activo, feliz de la vida lo descargué… con este framework yo arranqué a utilizar PHP.

Luego de utilizarlo y ver de que seguía igual me encontré con la idea de utilizar store procedures, porque me parecía que es la manera de hacer el código mas limpio y quizá de optimizar las queries.

Al intentarlo encontré algunos inconvenientes:

Por defecto no se pueden modificar los drivers en CI.

La idea era integrar funciones para los call procedures

CodeIgniter no soporta Multiquery

Cuando se llama a un SP se debe llamar inmediatamente un select para obtener el/los resultados.

Manos a la obra

Primero necesitamos modificar el Loader para que cargue nuestra clase custom driver.

* MY_ es el prefijo por defecto.

Copié básicamente la función actual de CI ( está en versión 3.0.6 en este momento).

<?php
class MY_Loader extends CI_Loader {

/**
* Database Loader
*
*
@param mixed $params Database configuration options
*
@param bool $return Whether to return the database object
*
@param bool $query_builder Whether to enable Query Builder
* (overrides the configuration setting)
*
*
@return object|bool Database object if $return is set to TRUE,
* FALSE on failure, CI_Loader instance in any other case
*/
public function database($params = '', $return = FALSE, $query_builder = NULL)
{
// Grab the super object
$CI =& get_instance();

// Do we even need to load the database class?
if ($return === FALSE && $query_builder === NULL && isset($CI->db) && is_object($CI->db) && ! empty($CI->db->conn_id))
{
return FALSE;
}

require_once(BASEPATH.'database/DB.php');

// Load the DB class
$db =& DB($params, $query_builder);

$my_driver = config_item('subclass_prefix').'DB_'.$db->dbdriver.'_driver';
$my_driver_file = APPPATH.'libraries/'.$my_driver.'php';

if (file_exists($my_driver_file))
{
require_once($my_driver_file);
$db_obj = new $my_driver(get_object_vars($db));
$db=& $db_obj;
}

if ($return === TRUE)
{
return $db;
}

// Initialize the db variable. Needed to prevent
// reference errors with some configurations
$CI->db = '';
$CI->db = $db;

return $this;
}

En esa función reestructuré algunas cosas, pero lo agregado puntualmente es:

    $my_driver = config_item('subclass_prefix').
'DB_'.$db->dbdriver.'_driver';
    $my_driver_file = APPPATH.'libraries/'.$my_driver.'php';

if (file_exists($my_driver_file))
{
require_once($my_driver_file);
$db_obj = new $my_driver(get_object_vars($db));
$db=& $db_obj;
}

Recuerdo que esta clase, debe ser nombrada con el prefijo (default MY_) y el archivo se pone en la carpeta /application/core/.

Ahora podemos ir a crear nuestro custom driver:

<?php
class
MY_DB_mysql_driver extends CI_DB_mysqli_driver {

function __construct($params){
parent::__construct($params);
log_message('debug', 'Extended DB driver class instantiated!');
}

/**
* Execute the query
*
*
@param string $sql an SQL query
*
@return mixed
*/

protected function _execute($sql)
{
// free results from previous query
$this->free_results();

$sql = $this->_prep_query($sql);

// get a result code of query (), can be used for test is the query ok
$retval = @mysqli_multi_query($this->conn_id, $sql);

// get a first resultset
$firstResult = @mysqli_store_result($this->conn_id);

// test is the error occur or not
if (!$firstResult && !@mysqli_errno($this->conn_id)) {
return true;
}

return $firstResult;
}

/**
* Read the next result
*
*
@return null
*/
function next_result()
{
$result = null;
if (is_object($this->conn_id))
{
$result = mysqli_next_result($this->conn_id);
} else {
$result = FALSE;
}
return $result;
}

function free_result(){
// free resultset
$result = null;
if(is_object($this->conn_id)){
$result = @mysqli_free_result(@mysqli_store_result($this->conn_id));
}
return $result;
}

function free_results(){
// free other resultsets
while (is_object($this->conn_id) && @mysqli_next_result($this->conn_id)) {
$this->free_result();
}
}
}

Con el driver listo ya se pueden ejecutar procedures, pero vamos a mejorar un poco esto, me tomé el trabajo de agregar al modelo funciones para los procedures, lo agrego al model porque no sería sencillo modificar otra parte del framework:

/**
* Call procedure
*
@param $procName string procedure name
*
@param $params null|string
*
@return null|CI_DB_result procedure array results
*/
public function callProcedure($procName, $params){

$sql = "CALL " . $procName."(";
$results = null;

$params = $this->processProcedureParams($params);

$sql = $sql.$params['in']. ")";
$this->db->query($sql);

$outSql = null;
foreach ($params['out'] as $param){
if($outSql == null){
$outSql = "SELECT ". $param . " as ".substr($param, 1);
} else {
$outSql = $outSql . ", ". $param . " as ".substr($param, 1);
}
}

if($outSql != null){
$results = $this->db->query($outSql)->result();
}

return $results;
}

/**
*
@param $params
*
@return array|
*/

private function processProcedureParams($params){
$processed = null;
$outParams = array();

if($params != null){
if( is_array($params) ){
foreach ($params as $param){
if($processed == null){
if(startsWith($param, "@")){
$processed = $param;
$outParams[] = $param;
} else {
$processed = "'".$param."'";
}
} else {
if(startsWith($param, "@")){
$processed = $processed.", ".$param;
$outParams[] = $param;
} else {
$processed = $processed.", '".$param."'";
}
}
}
} else {
if(startsWith($params, "@")){
$processed = $params;
$outParams[] = $params;
} else {
$processed = "'".$params."'";
}
}
}

$result['in'] = $processed;
$result['out'] = $outParams;

return $result;
}

En la función anterior se utiliza una función auxiliar que puede agregar en un helper o bien en el propio modelo.

/**
*
@param $text string text
*
@param $searchText string text to search
*
@return bool
*/
function startsWith($text, $searchText) {
return $searchText === "" || strrpos($text, $searchText, -strlen($text)) !== FALSE;
}

NOTA: La función “processProcedureParams” procesa automáticamente los parámetros y asume que todos los que empiezan en “@” son OUTs y los resultados se muestran sin el “@” se lo quito porque me parece conveniente.

De este modo los procedures están funcionando :D

Namasté.

Fuentes:
https://github.com/bcit-ci/CodeIgniter/wiki/extending-database-drivers

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.