2/6/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()),
    ));
}

Obtener acceso

Lazy loading with attributes over php

class BaseClass
{
protected function __get_xml()
{
$str = ''; for ($n = 0; $n < 10; $n++) $str .= 'XML';
return $str;
}
}


class LazyClassAtribute extends BaseClass
{
public function __get( $name )
{
$method_name = "__get_{$name}";
if ( method_exists( $this, $method_name ) ) return $this->$name = $this->$method_name();
}
}


class LazyClassRegistry extends BaseClass
{
public $cachedXml = null;


public function getXml()
{
if ( $this->cachedXml === null ) $this->cachedXml = $this->__get_xml();
return $this->cachedXml;
}
}


class NonLazyClass extends BaseClass
{
public function getXml()
{
return $this->__get_xml();
}
}

1/6/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");
}
}