19/11/10

Moviendo blog

Estoy moviendo las entradas de este blog a mi blog profesional: http://blog.cballesterosvelasco.es/

Iré borrando entradas de aquí conforme las vaya pasando, así que actualizad los enlaces y feeds de RSS.

Posts: http://blog.cballesterosvelasco.es/category/php/
RSS: http://blog.cballesterosvelasco.es/category/php/feed/

Cuando termine de pasar todos los posts, borraré este blog.

25/10/10

Grandes novedades en la próxima versión de PHP

Hoy me ha dado por mirar el changelog del trunk de PHP. Y he visto cosas la mar de interesantes.
Por lo pronto he visto bastantes mejoras de rendimiento y de uso de memoria. Algo que siempre se agradece. Y cosas que pensaba que ya debería estar haciendo que no hacía. ¡Bien!
Correcciones típicas y las correspondientes actualizaciones de sqlite y pcre y otras librerías.
Pero lo que me ha llamado la atención, son cosas que PHP estaba pidiendo a gritos desde hace tiempo:

- Added support for Traits. (Stefan)
- Added array dereferencing support. (Felipe)
- Added scalar typehints to the parser and the reflection API. (Ilia, Derick)


Y además las tres de golpe. Voy a explicar lo que hace cada una.


TRAITS en PHP:


Los traits ha sido un RFC que lleva bastante tiempo en el wiki de PHP (http://wiki.php.net/rfc/horizontalreuse).
Es algo que otros lenguajes como D soportan desde hace muchísimo tiempo (mediante templates y mixings) y que otros lenguajes como C++ soportan de forma parcial (mediante herencia múltiple). No sé de qué forma se han acabado implementado, pero ahí están.

La idea es que permitirá definir bloques de código reutilizables en distintas clases sin las limitaciones de la herencia simple y sin los problemas de la herencia múltiple.

El uso que le daré yo nada más empezar será el que hacía ahora con herencia simple a lo cutre y que es extender la funcionalidad del __get y el __set en algunos casos:


class BaseObject {
public $__cache = array();


public function __get($name) {
if (isset($this->__cache[$name])) return $this->__cache[$name];


if (method_exists($this, $method = "__get_cached_{$name}")) {
@$this->__cache[$name] = $v = $this->$method();
return $v;
} else if (method_exists($this, $method = "__get_{$name}")) {
return $this->$method();
} else if (method_exists($this, $method = "$name")) {
return $this->$method();
}
}


public function __sleep() {
$keys = array();
foreach ($this as $k => $v) if ($k != '__cache') $keys[] = $k;
return $keys;
}


public function __isset($name) {
return isset($this->__cache[$name]) || method_exists($this, "__get_{$name}") || method_exists($this, "__get_cached_{$name}");
}


public function __toString() {
return sprintf('%s object', get_called_class());
}
}




Array dereferencing en PHP:

PHP ha tenido de siempre la limitación de únicamente poder acceder a elementos de un array haciendo uso de una variable.
Es decir, se podía hacer:
$a[0]
pero no se podía hacer
a()[0]
Había que hacer:
$b = a(); $b[0]

Esta limitación venía impuesta por la dificultad de implementar la característica en una máquina virtual que no funcionaba con pila (donde es muy muy sencillo hacer esto) sino con variables locales (entiendo que es por eso).

Parece que ahora se ha eliminado dicha limitación. No sé hasta qué punto. Todavía tengo que ver si se podrán hacer este tipo de cosas:

echo (array('a' => 1) + array('a' => 2))['a'];
<=>
echo (($z = (array('a' => 1) + array('a' => 2))) || 1) ? $z['a'] : null;

Por cierto, ¿sabéis qué devolvería eso? :P

Scalar typehints en PHP:

A partir de PHP 5, PHP soporta typehinting en los parámetros de las funciones y métodos. Y solo ahí. No soporta typehinting en los campos de los objetos por ejemplo (Lo que sería de gran ayuda). Tampoco soporta typehinting en el valor de retorno de las funciones.
El typehinting no suele ser muy útil en lenguajes de tipado dinámico, aunque puede ayudar a mejorar el optimizador y a evitar errores. El problema del uso de typehinting en lenguajes de tipado dinámico es que eliminan la flexibilidad de usar una función para diferentes propósitos. En lenguajes compilados se suele soportar el uso de un mismo nombre para una función con diferentes prototipos. En lenguajes no compilados esto no es posible haciéndolo de una forma óptima. Así que o se opta por comprobar el tipo de los parámetros, o por usar diferentes nombres para funciones con diferentes tipos de parámetros.

La limitación actual del typehinting de PHP, además de ser el ámbito de los parámetros de las funciones, es el de los tipos que podemos utilizar. Únicamente se soportaban nombres de clases. Ahora se van a soportar tipos escalares y entiendo que también array.

function fname(int $a, string $b, float $c, array $d)



13/8/10

Desempaquetar una cadena con un listado de subcadenas

Esto puede ser útil por ejemplo para extraer los elementos de un enum en PHP usando un SHOW COLUMNS. En ese caso el decode_list_json podría ser el más apropiado ya que, generalmente, los elementos de un enum o un set no contendrán comillas.




// Lenta pero segura y sin efectos colaterales (en principio).
function decode_list_preg($list_string) {
preg_match_all("@(?:,|^)('|\")((?:\\\\'|\\\\\\\\|[^'])*)\\1@Umsi", $list_string, $matches);
return array_map('stripslashes', $matches[2]);
}


// Rápida pero insegura con posibilidad de inyección de código con una entrada inválida.
function decode_list_eval($list_string) {
eval('$list = array(' . $list_string . ');');
return $list;
}


// Rápida pero con efectos colaterales: cambia comillas simples por dobles para que sea un json válido.
function decode_list_json($list_string) {
return json_decode('[' . str_replace("'", '"', $list_string) . ']');
}


// Lenta y segura usando el tokenizer de php.


function decode_list_php($list_string) {
$tokens = array_slice(token_get_all(' $expect_comma = false;
$list = array();
//print_r($tokens);
foreach ($tokens as $token) {
if (!is_array($token)) $token = array(0, $token, -1);
if ($expect_comma) {
if ($token[1] !== ',') throw(new Exception("Invalid input (" . token_name($token[0]) . ")"));
} else {
if ($token[0] !== T_CONSTANT_ENCAPSED_STRING) throw(new Exception("Invalid input (" . token_name($token[0]) . ")"));
$list[] = stripslashes(substr($token[1], 1, -1));
}
$expect_comma = !$expect_comma;
}
return $list;
}

function util_rand_string($len) {
$str = '';
while ($len-- > 0) $str .= chr(mt_rand(0x20, 0x7e));
return $str;
}

$str_list = array();
for ($n = 0, $len = mt_rand(1000, 2000); $n < $len; $n++) $str_list[] = var_export(util_rand_string(mt_rand(40, 80)), true);
$list_string = implode(',', $str_list);


//echo $list_string; exit;

//$list_string = "'ho\'la',\"esto\",'es','una','prueba'";

//print_r(decode_list_preg($list_string));
//print_r(decode_list_eval($list_string));
//print_r(decode_list_json($list_string));
print_r(decode_list_php($list_string));

14/7/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>';
}

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


18/1/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');


?>

15/1/10

parsekit

class A { const C = 'A'; const D = self::C; }
class B extends A { const C = 'B'; }
echo B::D;

-->

B

Debe hacer un mix sin llegar a ejecutar.