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