14/07/10

Referencias en PHP y funciones útiles auxiliares

Uno de los aspectos interesantes de PHP es la gestión de referencias.
Como es bien sabido en PHP, intentar acceder a una variable o una clave en un array que no existe produce un notice y generalmente hay que estar tratando continuamente con isset y con bastantes código redundante.
El caso es que PHP permite crear una referencia a una variable inexistente para crearla a posteriori. Es como crear un slot de la variable en potencia y asignarlo si así se decide. También permite comprobar la existencia de la variable referenciada usando la variable de referencia.

Es bastante típico en PHP querer obtener el valor de una variable si existe, o un valor por defecto en el caso de que no exista. Ejemplo:

$page = isset($_GET['page']) ? $_GET['page'] : 1;

En PHP 5.3 se añadió un shortcut del operador ternario. A priori parecía bastante interesante, porque teóricamente hubiese podido permitir hacer esto: $page = $_GET['page'] ?: 1;
Pero resulta que la cagaron estrepitosamente. Resulta que lo que hace el shortcutdel ternario es evaluar la parte de la izquierda y si al castear implícitamente a bool, el resultado es false, se devuelve el resultado de la expresión de la derecha. Esto para empezar no evita el tema de los notices al intentar acceder a una clave inexistente en el array. Además hay un pequeño matiz que difiere con el isset. Y es que si la variable existe pero tiene un valor evaluado como false (como una cadena vacía o el valor 0), ya se estaría devolviendo lo de la derecha. Y generalmente no es lo deseado.
Así que veo completamente inútil esta adición al PHP 5.3.

Sin embargo PHP y sus referencias permiten crear funciones auxiliares que permiten resolver estos problemas con facilidad y evitando repetir la variable dos veces:

$page = isset_default($_GET['page'], 1);

Ésto es posible sin notices porque en las últimas versiones de PHP especificas los valores que se pasan por referencia en la propia descripción de la función. Así pues:


function isset_default(&$var, $default) {
    return isset($var) ? $var : $default;
}

Esto vendría a ser como una macro bastante útil.

También es bastante común comprobar la existencia de varias claves en un array.

if (isset($_GET['a'], $_GET['b'], $_GET['c'])) { }
->
if (isset_array($_GET, array('a', 'b', 'c'))) { }

Aquí la referencia evita que PHP duplique un array potencialmente grande.


function isset_array(array &$array, array $keys) {
    foreach ($keys as $key) if (!isset($array[$key])) return false;
    return true;
}

Otro patrón común que se puede resolver con macros de referencias es hacer un cambio a posteriori de una variable.

function method() {
    if ($this->alreadyPerformed) return;
    $this->alreadyPerformed = true;
    ...
}
->

function method() {
    if (post_assign($this->alreadyPerformed, true)) return;
    ...
}

function post_assign(&$var, $value) {
    $last_value = $var;
    $var = $value;
    return $last_value;
}


En este caso concreto también se podría usar el pequeño truco de usar enteros para evitar tener la variable dos veces referenciada. Y usar el operador unario del postincremento:

function method() {
    if ($this->alreadyPerformed++) return;
    ...
}

Hay muchas formas útiles de usar las referencias en PHP. Por ejemplo para tener una caché temporal en memoria muy ligera:

static function cached_method($key) {
    static $cache = array();
    $result = &$cache[$key];
    if (!isset($result)) {
        // ...
    }
    return $result;
}

Con referencias y PHP 5.3:

function &cache_result(&$var, $func) {
    if (!isset($var)) $var = $func();
    return $var;
}

static function cached_method($key) {
    static $cache = array();
    return cache_result($cache[$key], function() use ($key) {
         // ...
    });
}

Otras funciones que considero útiles aunque no tengan que ver directamente con referencias:

Por ejemplo si queremos obtener una array a partir de otro sin las claves que empiezan por _:

$array = array_filter_keys($_GET, function($key) { return substr($key, 0, 1) != '_'; } );

function array_filter_keys(&$array, $callback) {
    $result = array();
    foreach ($array as $key => $value) {
        if ($callback($key, $value)) $result[$key] = $value;
    }
    return $result;
}

En determinadas ocasiones queremos ver un array visualmente que contiene html. Con xdebug la functión var_dump puede estar coloreada. Pero ni siempre está disponible el xdebug, ni tampoco es siempre lo más cómodo visualmente. Aquí hay una función que permite mostrar un print_r para html. Haciendo uso del tag pre y del escapado de html.

function print_r_pre($var) {
    echo '<pre>';
    echo htmlspecialchars(print_r($var, true));
    echo '</pre>';
}

02/06/10

Trabajando con objetos en base de datos con PDO y Mongo + Demo con Twig


Trabajar con resultados de bases de datos como si fuesen objetos permite trabajar de una forma muy cómoda e intuitiva. PDO tiene soporte nativo para devolver instancias de una clase en vez de arrays. Pero con MongoDB tampoco es mucho más complicado conseguir un iterador de objetos de una clase determinada.
Aquí coloco un ejemplo con PDO, Mongo y Twig. Con twig se ve claramente la comodidad de tener objetos con los que poder llamar métodos que produzcan datos derivados sin tenerlos que generar explícitamente (on demand).
He creado una clase derivada de IteratorIterator y un método estático usando late static binding para poder castear un array/objeto a una nueva instancia de una clase especificada.










class CastModelIteratorIterator extends IteratorIterator {
    public $class;

    function current() {
        return Model::cast(parent::current(), $this->class);
    }
}

class Model {
    static public function cast($array, $class = null) {
        $object = ($class === null) ? (new static) : (new $class);
        foreach ($array as $k => $v) $object->$k = $v;
        return $object;
    }
    
    static public function castIterator($iterator) {
        $iterator = new CastModelIteratorIterator($iterator);
        $iterator->class = get_called_class();
        return $iterator;
    }
}

class TestModel extends Model {
    public $name, $pass;

    function __construct($name = 'test') {
        $this->name = $name;
        $this->pass = self::hashPassword('test');
    }
    static function hashPassword($password) {
        return md5("{$password}*");
    }
    function checkPassword($password) {
        return $this->pass == self::hashPassword($password);
    }
    function url() {
        return sprintf('/user/%s', urlencode($this->name));
    }
}

// Sqlite PDO Test
{
    $db = new PDO('sqlite::memory:');
    $db->query('CREATE TABLE TestModel (name, pass);');
    $t = $db->prepare('INSERT INTO TestModel (name, pass) VALUES (?, ?);');
    $model = new TestModel('Test');
    $t->execute(array($model->name, $model->pass));
    foreach ($db->query('SELECT * FROM TestModel;', PDO::FETCH_CLASS, 'TestModel') as $e) {
        printf("%s\n", $e->url());
    }
}

// Mongo Test
{
    $mongo = new Mongo(); // connect
    $db = $mongo->demo;
    $collection = $db->TestModel;
    $collection->remove();
    $collection->insert(new TestModel('Test'));

    foreach (TestModel::castIterator($collection->find()) as $e) {
        printf("%s\n", $e->url());
    }
}

// Twig
{
    require_once(__DIR__ . '/Twig/Autoloader.php');
    Twig_Autoloader::register();
    $collection->insert(new TestModel('Demo'));

    $twig = new Twig_Environment(new Twig_Loader_String(), array(
        'debug'       => true,
        'auto_reload' => true,
    ));

    $twig->loadTemplate('
        <ul>{% for item in list %}
<li><a href="{{ item.url }}">User {{ item.name }}</a></li>
{% endfor %}</ul>
')->display(array(
        'list' => TestModel::castIterator($collection->find()),
    ));
}

01/06/10

Trabajando con archivos binarios en PHP

Aunque PHP no es un lenguaje de programación muy adecuado para trabajar con archivos binarios, en determinadas circunstancias puede ser de utilidad. Y explicaré aquí algunos detalles a tener en cuenta al trabajar con archivos binarios y técnicas para hacerlo con sencillez.

fopen. Flag 'b' para binarios en windows.
A la hora de abrir archivos, existen diversos modos: de escritura (write only), de lectura (read only), de lectura/escritura (read/write), de escritura situando el cursor al final del archivo (append). Además hay un flag que permite especifica si el archivo a abrir se usará como un archivo de texto o como uno binario. En modo texto es posible que un salto de línea (\n) se guarde como dos bytes \r\n dependiendo del sistema operativo. Y esto puede causar archivos corruptos en muchos casos. Nota: El flag opuesto a 'b' es 't'. Puede producir problemas también con ftell. Se recomiendo usar siempre el flag 'b' para que la compatibilidad sea la misma indistintamente de la plataforma.

pack, chr / unpack + list, ord
Estas funciones sirven para empaquetar y desempaquetar valores numéricos y cadenas en datos binarios. Y son el pilar fundamental de una codificación/decodificación fácil y cómoda:

ord (ordinal) obtiene el valor numérico que tiene un carácter. Se le pasa una cadena y lo calcula a partir del primer byte de la cadena
chr (character) obtiene el carácter de un valor numérico (la función inversa a ord).
Ambas funciones trabajan a nivel de byte y únicamente trabajan con el rango de 00-FF.

chr(ord('a')) == 'a'

pack permite empaquetar una serie de valores en un formato binario. La función proviene de perl. Se le pasa una cadena con el formato de empaquetado y luego una sucesión de parámetros con los valores a empaquetar. El formato de empaquetado contempla valores con signo y sin signo de 8, 16 y 32 bits pudiendo especificar el endian: usando el de la máquina actual, o usando little o big endian. También permite empaquetar cadenas terminadas con '\0's o con espacios.

unpack permite desempaquetar una serie de valores a partir de una cadena binaria. Si no se especifican nombres para los datos desempaquetados, se devuelve un array de claves numéricas empezando por la clave 1 (en vez de por la clave 0). Para extraer valores en variables con comodidad se puede usar la estructura del lenguaje "list". Teniendo en cuenta que el casting de array a bool es siempre true, se puede usar una asignación intrínseca y el operador ternario para hacer una extracción en una única expresión.
list(, $valor) = unpack('s', "\xFF\xFF");
list($valor) = array_values(unpack('s', "\xFF\xFF"));
$valor = ($v = unpack('s', "\xFF\xFF")) ? $v[1] : false;
echo $valor;


Tip:
pack('H*', '736f7977697a')
Las funciones pack usando H*, permiten convertir datos en hexadecimal a cadenas binarias y viceversa. (ver funciones hex/unhex de las funcionesde utilidad del final del artículo)

fseek (archivos grandes)


PHP trabaja con dos tipos numéricos: signed int (32 bits) y double (64 bits) de los cuales se pueden conseguir 53 bits para valores enteros (sin decimales) que son unos 16 digitos decimales.
$a = 0x7FFFFFFF; var_dump($a); $a++; var_dump($a); var_dump((int)$a);

int(2147483647)
float(2147483648)
int(-2147483648)
El problema es que fseek trabaja con enteros de 32 bits y el direccionamiento de fseek y ftell de archivos está limitado a eso en PHP. Que son 2GB sin signo y 4GB con signo. Diría que se puede superar este límite haciendo varias llamadas con SEEK_CUR.


substr

La función substr nos permite extraer determinados bytes de una cadena binaria como si fuese una cadena de texto normal. Donde cada caracter es un byte.

Nota:
En la configuración de PHP hay una opción llamada "mbstring.func_overload" que permite susitutir las funciones normales. Esto permite hacer que las funciones normales para trabajar con cadenas se sustituyan por sus equivalentes mb_*. Esto es un problema que puede llevar a verdaderos quebraderos de cabeza cuando se trabaja con cadenas binarias.

Una solución pasa por:
$back_encoding = mb_internal_encoding();

mb_internal_encoding('8bit');
// Hacer cosas.
mb_internal_encoding($back_encoding);

O directamente utilizar las funciones mb_* usando 8bit como el encoding normal.
Así que para extraer los primeros 10 bytes:
substr($cadena, 0, 10);
->
mb_substr($cadena, 0, 10, '8bit');

Con el resto de funciones de cadena tipo strlen pasa exáctamente lo mismo.

acceso como array [] {}
PHP permite acceder a las cadenas como si fuesen arrays. Tanto para lectura como para escritura.
Así que:
$str = 'abc';
$str[1] == substr($str, 1, 1);

$str[1] = 'a';
$str = substr_replace($str, 'a', 1, 1);

$str == 'aac'

streams: php://memory, php://temp, data://, file_get_contents
En lenguajes que soportan slicing de streams, leer y procesar archivos binarios suele ser bastante mas cómodo. PHP no soporta slicing de streams directamente, y aunque se puede hacer un apaño, no se soporta nativamente. Sin embargo la lectura secuencial de datos y el "consumo" de datos es un patrón básico en el procesado de archivos binarios medianamente complejos. Los streams son muy cómodos para la consumición de datos ya que tiene un cursor y un torrente de datos y cada vez que lees, se actualiza ese cursor. En determinadas ocasiones tendremos los datos que queremos consumir en una cadena binaria, por ejemplo tras obtenerlos directamente con file_get_contents, tras generarlos por otro medio o al leer un subtream en una cadena.
Una función que puede permitir la consumición de datos en una cadena podría ser esta:
function fread_str(&$str, $len) { $data = substr($str, 0, $len); $str = substr($str, $len); return $data; }
Aunque esta forma de procesar datos es muy poco eficiente. Porque estás reconstruyendo una cadena todo el rato, copiando datos contínuamente y en cadenas grandes puede ser un proceso muy costoso.
otra forma es tener un cursor, de forma que evitamos tocar la cadena y únicamente extraemos la parte que nos interesa:

function fread_str_cur(&$cur, &$str, $len) { $data = substr($str, $cur, $len); $cur += $len; return $data; }
En PHP se puede generar un stream a partir de una cadena con relativa facilidad. Hay diversas formas:
$f = fopen('data://text/plain;base64,' . base64_encode($data), 'r+');
$f = fopen('php://memory', 'w+'); fwrite($f, $data); fseek($f, 0);



rtrim+\0
Al trabajar con archivos binarios, suele trabajarse con stringz muy amenudo o con cadenas que tienen un right padding de o bien espacios o bien el carácter 0.
La función rtrim nos puede ayudar a eliminar esos caracteres sobrantes de la cadena. rtrim tiene un segundo parámetro opcional que permite especificar los caracteres a eliminar. En nuestro caso \0. Por defecto elimina espacios, tabuladores, saltos de línea y el carácter \0. Pero en estos casos nos interesa únicamente que elimine el carácter de padding:
rtrim("hola\0\0\0", "\0") == 'hola'

Algunas funciones de utilidad:
function fread1($f) { @list(, $r) = unpack('C', fread($f, 1)); return $r; }
// Little Endian.
function fread2le($f) { @list(, $r) = unpack('v', fread($f, 2)); return $r; }
function fread4le($f) { @list(, $r) = unpack('V', fread($f, 4)); return $r; }
function freadsz($f, $l) { return rtrim(fread($f, $l), "\0"); }

function hex  ($str) { return strtoupper(($v = unpack('H*', $str)) ? $v[1] : ''); }
function unhex($str) { return pack('H*', $str); }
unhex(hex('prueba')) == 'prueba';


function fread_str(&$str, $len) { $data = substr($str, 0, $len); $str = substr($str, $len); return $data; }


Variantes:

function fread1($f) { return ord(fread($f, 1)); }
function fread2le($f) { return ($v = unpack('v', fread($f, 2))) ? $v[1] : false; }
function fread4le($f) { return ($v = unpack('V', fread($f, 4))) ? $v[1] : false; }

function freadsz($f, $l = false) {
if ($l === false) {
$s = '';
while (!feof($f)) {
$c = fread($f, 1);
if ($c == '' || $c == "\0") break;
$s .= $c;
}
return $s;
} else {
return rtrim(fread($f, $l), "\0");
}
}


18/01/10

Simulate static binding on PHP < 5.3




class A {
const CONSTANT = 'A';


function __construct() {
echo constant(get_class($this) . '::CONSTANT');
// echo self::CONSTANT; // No static. Would output AA instead of AB.
// echo static::CONSTANT; // PHP >= 5.3
}
}


class B extends A {
const CONSTANT = 'B';
}


ob_start();
$a = new A;
$b = new B;
var_dump(ob_get_clean() == 'AB');


?>

20/10/09

Macro operaciones con las funciones array_*

Quitar todos los espacios de principio y de final de cada elemento en un array (array_map):

$array_trimmed = array_map('trim', $array);
Alternativas:
$array_trimmed = array(); foreach ($array as $v) $array_trimmed[] = trim($v);
foreach ($array_trimmed as &$v) $v = trim($v); // Mala idea. PHP tiene leaks desde hace mucho tiempo con foreach + &.

Obtener una lista segura de enteros (por ejemplo IDs) para insertar en una query dentro de un IN().
$array_str_list = implode(',', array_map('intval', $array));

Filtrar un array obteniendo únicamente valores numéricos (array_filter):
$array_numbers = array_filter($array, 'is_numeric')

Para creación de valores para un insert con pdo (array_map):

$query_insert_values = implode(",", array_map(array($pdo, 'quote'), $values))

Para creación de asignaciones en un update con pdo (array_map con dos arrays):
$query_set = implode(',', array_map(function($k, $v) use ($pdo) {
return '`' . implode('`,`', explode('.', $k)) . '`=' . $pdo->quote($v);
}, array_keys($array), array_values($array)));

Obtener una lista de elementos únicos usando un array que contiene un número indeterminado de arrays (array_reduce):

$array_unique = array_unique( array_reduce( $arrays, 'array_merge', array() ) );

19/07/09

Crear "estructuras" en php5.3

En alguna ocasión me ha interesado crear una clase que contendría simplemente unos cuantos atributos y poco más y al final he acabado haciendo un constructor del tipo:

class mystruct {
    public $a, $b, $c;
    function __construct($a, $b, $c) {
        $this->a = $a;
        $this->b = $b;
        $this->c = $c;
    }
}


class struct {
    static public function create() {
        $obj = new static;
        $keys = array_keys((array)$obj);
        foreach (func_get_args() as $k => $v) $obj->{$keys[$k]} = $v;
        return $obj;
    }
}

Para versiones anteriores de php podemos usar el constructor:

class struct {
    static function __construct() {
        $keys = array_keys((array)$obj);
        foreach (func_get_args() as $k => $v) $this->{$keys[$k]} = $v;
        return $this;
    }
}


Con lo que podríamos hacer lo siguiente:


class mystruct extends struct { public $a, $b, $c; }
mystruct::create(1, 2, 3);
o
new mystruct(1, 2, 3);

17/07/09

Novedades con phpSTE

Ahora que vuelvo a tener Internet en plan bien, y ya vuelvo a estar con mis proyectos, he seguido mejorando phpSTE.

He cambiado bastantes cosas desde que lo empecé, y cambiaré bastantes más posiblemente hasta que esté pulido del todo.

Por ahora ocupa unas 550 líneas y ya es bastante funcional.

Además de soportar herencia, he añadido los siguientes tags:

{extends name=""} - extiende un template.
{include name=""} - incluye un template.
{block name=""}{/block} - define e imprime/modifica un bloque.
{addblock name=""}{/addblock} - modifica un bloque añadiendo el contenido al final.

{blockdef name=""}{/blockdef} - define un bloque sin utilizarlo.
{putblock name=""} - imprime un bloque existente.

{t}{/t} - llama a la función de gettext

{if cond=""}{/if}
{elseif cond=""}{else}

{for var="" from="" to="" step=""}{/for}

{foreach list="" var=""}{/foreach}


He hecho optimizaciones para que los tags puedan saber si el contenido es un literal, de forma que {t}Texto{/t} se convertiría a mientras que {t}{if cond=1}Texto{/if}{/t} se convertiría a Texto