TigerZF
🌐Español

65.3. Uso avanzado

Aunque los ejemplos de uso básico son una forma perfectamente aceptable de utilizar las sesiones de Zend Framework, existen algunas buenas prácticas a tener en cuenta. Esta sección analiza los detalles más finos del manejo de sesiones e ilustra un uso más avanzado del componente Zend_Session.

65.3.1. Iniciar una sesión

Si quiere que todas las peticiones tengan una sesión facilitada por Zend_Session, entonces inicie la sesión en el archivo de arranque (bootstrap):

Ejemplo 65.6. Iniciar la sesión global

Zend_Session::start();

Al iniciar la sesión en el archivo de arranque, evita la posibilidad de que su sesión se inicie después de que se hayan enviado cabeceras al navegador, lo que resulta en una excepción, y posiblemente en una página rota para los visitantes del sitio web. Varias funcionalidades avanzadas requieren llamar antes a Zend_Session::start(). (Más sobre funcionalidades avanzadas más adelante.)

Hay cuatro formas de iniciar una sesión, cuando se usa Zend_Session. Dos son incorrectas.

  1. Incorrecto: No active la opción session.auto_start de PHP. Si no tiene la capacidad de desactivar esta opción en php.ini, está usando mod_php (o equivalente), y la opción ya está activada en php.ini, entonces añada lo siguiente a su archivo .htaccess (normalmente en el directorio raíz de documentos HTML):

    php_value session.auto_start 0
    
  2. Incorrecto: No use la función session_start() de PHP directamente. Si usa session_start() directamente, y luego empieza a usar Zend_Session_Namespace, se lanzará una excepción por Zend_Session::start() ("session has already been started"). Si llama a session_start() después de usar Zend_Session_Namespace o de llamar a Zend_Session::start(), se generará un error de nivel E_NOTICE, y la llamada será ignorada.

  3. Correcto: Use Zend_Session::start(). Si quiere que todas las peticiones tengan y usen sesiones, entonces coloque esta llamada de función de forma temprana e incondicional en su código de arranque. Las sesiones tienen cierta sobrecarga. Si algunas peticiones necesitan sesiones, pero otras no las necesitarán, entonces:

    • Establezca incondicionalmente la opción strict a TRUE usando Zend_Session::setOptions() en su código de arranque.

    • Llame a Zend_Session::start() solo para las peticiones que necesiten usar sesiones y antes de que se instancie ningún objeto Zend_Session_Namespace.

    • Use "new Zend_Session_Namespace()" normalmente, donde sea necesario, pero asegúrese de que Zend_Session::start() haya sido llamado previamente.

    La opción strict impide que new Zend_Session_Namespace() inicie automáticamente la sesión usando Zend_Session::start(). Así, esta opción ayuda a los desarrolladores de aplicaciones a hacer cumplir una decisión de diseño para evitar el uso de sesiones en ciertas peticiones, ya que provoca que se lance una excepción cuando Zend_Session_Namespace se instancia antes de que Zend_Session::start() sea llamado. Los desarrolladores deben considerar cuidadosamente el impacto de usar Zend_Session::setOptions(), ya que estas opciones tienen efecto global, debido a su correspondencia con las opciones subyacentes de ext/session.

  4. Correcto: Simplemente instancie Zend_Session_Namespace cuando sea necesario, y la sesión PHP subyacente se iniciará automáticamente. Esto ofrece un uso extremadamente simple que funciona bien en la mayoría de las situaciones. Sin embargo, entonces usted se hace responsable de asegurarse de que la primera new Zend_Session_Namespace() ocurra antes de que se haya enviado cualquier salida (por ejemplo, cabeceras HTTP) por parte de PHP al cliente, si está usando las sesiones predeterminadas basadas en cookies (fuertemente recomendado). Vea esta sección para más información.

65.3.2. Bloqueo de espacios de nombres de sesión

Los espacios de nombres de sesión se pueden bloquear, para evitar más alteraciones a los datos de ese espacio de nombres. Use lock() para hacer que un espacio de nombres específico sea de solo lectura, unLock() para hacer que un espacio de nombres de solo lectura sea de lectura y escritura, y isLocked() para comprobar si un espacio de nombres ha sido bloqueado previamente. Los bloqueos son transitorios y no persisten de una petición a otra. Bloquear el espacio de nombres no tiene efecto sobre los métodos setter de los objetos almacenados en el espacio de nombres, pero sí impide el uso del método setter del espacio de nombres para eliminar o reemplazar objetos almacenados directamente en el espacio de nombres. De manera similar, bloquear instancias de Zend_Session_Namespace no impide el uso de alias de tabla de símbolos hacia los mismos datos (vea referencias de PHP).

Ejemplo 65.7. Bloqueo de espacios de nombres de sesión

$userProfileNamespace = new Zend_Session_Namespace('userProfileNamespace');

// marcando la sesión como bloqueada de solo lectura
$userProfileNamespace->lock();

// desbloqueando el bloqueo de solo lectura
if ($userProfileNamespace->isLocked()) {
    $userProfileNamespace->unLock();
}

65.3.3. Expiración de espacios de nombres

Se pueden colocar límites tanto en la longevidad de los espacios de nombres como en las claves individuales dentro de los espacios de nombres. Los casos de uso comunes incluyen pasar información temporal entre peticiones, y reducir la exposición a ciertos riesgos de seguridad eliminando el acceso a información potencialmente sensible algún tiempo después de que ocurra la autenticación. La expiración puede basarse tanto en segundos transcurridos como en el número de "saltos", donde un salto ocurre en cada petición sucesiva.

Ejemplo 65.8. Ejemplos de expiración

$s = new Zend_Session_Namespace('expireAll');
$s->a = 'apple';
$s->p = 'pear';
$s->o = 'orange';

$s->setExpirationSeconds(5, 'a'); // expire only the key "a" in 5 seconds

// expire entire namespace in 5 "hops"
$s->setExpirationHops(5);

$s->setExpirationSeconds(60);
// The "expireAll" namespace will be marked "expired" on
// the first request received after 60 seconds have elapsed,
// or in 5 hops, whichever happens first.

Cuando se trabaja con datos que expiran de la sesión en la petición actual, se debe tener cuidado al recuperarlos. Aunque los datos se devuelven por referencia, modificar los datos no hará que los datos que expiran persistan más allá de la petición actual. Para "restablecer" el tiempo de expiración, obtenga los datos en variables temporales, use el espacio de nombres para eliminarlos, y luego establezca de nuevo las claves apropiadas.

65.3.4. Encapsulación de sesión y controladores

Los espacios de nombres también se pueden usar para separar el acceso a la sesión por controladores para proteger las variables de la contaminación. Por ejemplo, un controlador de autenticación podría mantener sus datos de estado de sesión separados de todos los demás controladores para cumplir con los requisitos de seguridad.

Ejemplo 65.9. Sesiones con espacio de nombres para controladores con expiración automática

El siguiente código, como parte de un controlador que muestra una pregunta de prueba, inicializa una variable booleana para representar si se debe aceptar o no una respuesta enviada a la pregunta de prueba. En este caso, al usuario de la aplicación se le dan 300 segundos para responder a la pregunta mostrada.

// ...
// in the question view controller
$testSpace = new Zend_Session_Namespace('testSpace');
// expire only this variable
$testSpace->setExpirationSeconds(300, 'accept_answer');
$testSpace->accept_answer = true;
//...

A continuación, el controlador que procesa las respuestas a las preguntas de prueba determina si aceptar o no una respuesta según si el usuario envió la respuesta dentro del tiempo asignado:

// ...
// in the answer processing controller
$testSpace = new Zend_Session_Namespace('testSpace');
if ($testSpace->accept_answer === true) {
    // within time
}
else {
    // not within time
}
// ...

65.3.5. Prevenir múltiples instancias por espacio de nombres

Aunque el bloqueo de sesiones proporciona un buen grado de protección contra el uso no deseado de datos de sesión con espacio de nombres, Zend_Session_Namespace también cuenta con la capacidad de evitar la creación de múltiples instancias correspondientes a un único espacio de nombres.

Para activar este comportamiento, pase TRUE como segundo argumento del constructor al crear la última instancia permitida de Zend_Session_Namespace. Cualquier intento posterior de instanciar el mismo espacio de nombres resultaría en una excepción lanzada.

Ejemplo 65.10. Limitar el acceso al espacio de nombres de sesión a una única instancia

// create an instance of a namespace
$authSpaceAccessor1 = new Zend_Session_Namespace('Zend_Auth');

// create another instance of the same namespace, but disallow any
// new instances
$authSpaceAccessor2 = new Zend_Session_Namespace('Zend_Auth', true);

// making a reference is still possible
$authSpaceAccessor3 = $authSpaceAccessor2;

$authSpaceAccessor1->foo = 'bar';

assert($authSpaceAccessor2->foo, 'bar');

try {
    $aNamespaceObject = new Zend_Session_Namespace('Zend_Auth');
} catch (Zend_Session_Exception $e) {
    echo 'Cannot instantiate this namespace since ' .
         '$authSpaceAccessor2 was created\n';
}

El segundo parámetro en el constructor anterior le indica a Zend_Session_Namespace que no se permiten futuras instancias con el espacio de nombres "Zend_Auth". Intentar crear una instancia de este tipo provoca que el constructor lance una excepción. El desarrollador, por lo tanto, se hace responsable de almacenar una referencia a un objeto de instancia ($authSpaceAccessor1, $authSpaceAccessor2, o $authSpaceAccessor3 en el ejemplo anterior) en algún lugar, si se necesita acceso al espacio de nombres de sesión más tarde durante la misma petición. Por ejemplo, un desarrollador puede almacenar la referencia en una variable estática, añadir la referencia a un registro (vea Zend_Registry), o hacerla disponible de otra manera a otros métodos que puedan necesitar acceso al espacio de nombres de sesión.

65.3.6. Trabajar con arrays

Debido al historial de implementación de los métodos mágicos de PHP, modificar un array dentro de un espacio de nombres puede no funcionar en versiones de PHP anteriores a la 5.2.1. Si va a trabajar solamente con PHP 5.2.1 o posterior, entonces puede saltar a la siguiente sección.

Ejemplo 65.11. Modificar datos de array con un espacio de nombres de sesión

Lo siguiente ilustra cómo se puede reproducir el problema:

$sessionNamespace = new Zend_Session_Namespace();
$sessionNamespace->array = array();

// may not work as expected before PHP 5.2.1
$sessionNamespace->array['testKey'] = 1;
echo $sessionNamespace->array['testKey'];

Ejemplo 65.12. Construir arrays antes del almacenamiento en sesión

Si es posible, evite el problema por completo almacenando arrays en un espacio de nombres de sesión solo después de que se hayan establecido todos los valores del array deseados.

$sessionNamespace = new Zend_Session_Namespace('Foo');
$sessionNamespace->array = array('a', 'b', 'c');

Si está usando una versión afectada de PHP y necesita modificar el array después de asignarlo a una clave de espacio de nombres de sesión, puede usar una o ambas de las siguientes soluciones alternativas.

Ejemplo 65.13. Solución alternativa: Reasignar un array modificado

En el código que sigue, se crea una copia del array almacenado, se modifica, y se reasigna a la ubicación desde la que se creó la copia, sobrescribiendo el array original.

$sessionNamespace = new Zend_Session_Namespace();

// assign the initial array
$sessionNamespace->array = array('tree' => 'apple');

// make a copy of the array
$tmp = $sessionNamespace->array;

// modfiy the array copy
$tmp['fruit'] = 'peach';

// assign a copy of the array back to the session namespace
$sessionNamespace->array = $tmp;

echo $sessionNamespace->array['fruit']; // prints "peach"

Ejemplo 65.14. Solución alternativa: almacenar un array que contenga una referencia

Alternativamente, almacene un array que contenga una referencia al array deseado, y luego acceda a él indirectamente.

$myNamespace = new Zend_Session_Namespace('myNamespace');
$a = array(1, 2, 3);
$myNamespace->someArray = array( &$a );
$a['foo'] = 'bar';
echo $myNamespace->someArray['foo']; // prints "bar"

65.3.7. Uso de sesiones con objetos

Si planea persistir objetos en la sesión de PHP, sepa que se serializarán para su almacenamiento. Por lo tanto, cualquier objeto persistido con la sesión de PHP debe ser deserializado al recuperarlo del almacenamiento. La implicación es que el desarrollador debe asegurarse de que las clases de los objetos persistidos hayan sido definidas antes de que el objeto sea deserializado desde el almacenamiento de sesión. Si la clase de un objeto deserializado no está definida, entonces se convierte en una instancia de stdClass.

65.3.8. Uso de sesiones con pruebas unitarias

Zend Framework se apoya en PHPUnit para facilitar las pruebas de sí mismo. Muchos desarrolladores extienden el conjunto existente de pruebas unitarias para cubrir el código de sus aplicaciones. La excepción "Zend_Session is currently marked as read-only" se lanza al realizar pruebas unitarias, si se usan métodos de escritura después de finalizar la sesión. Sin embargo, las pruebas unitarias que usan Zend_Session requieren atención adicional, porque cerrar (Zend_Session::writeClose()), o destruir una sesión (Zend_Session::destroy()) impide cualquier establecimiento o eliminación posterior de claves en cualquier instancia de Zend_Session_Namespace. Este comportamiento es un resultado directo del mecanismo subyacente de ext/session y de las funciones de PHP session_destroy() y session_write_close(), que no tienen ningún mecanismo de "deshacer" para facilitar la configuración/desmontaje con pruebas unitarias.

Para solucionar esto, vea la prueba unitaria testSetExpirationSeconds() en SessionTest.php y SessionTestHelper.php, ambos ubicados en tests/Zend/Session, que hacen uso de exec() de PHP para lanzar un proceso separado. El nuevo proceso simula con más precisión una segunda petición sucesiva desde un navegador. El proceso separado comienza con una sesión "limpia", igual que cualquier ejecución de script PHP para una petición web. Además, cualquier cambio en $_SESSION realizado en el proceso que llama se hace disponible para el proceso hijo, siempre que el padre haya cerrado la sesión antes de usar exec().

Ejemplo 65.15. Código de pruebas PHPUnit dependiente de Zend_Session

// testing setExpirationSeconds()
$script = 'SessionTestHelper.php';
$s = new Zend_Session_Namespace('space');
$s->a = 'apple';
$s->o = 'orange';
$s->setExpirationSeconds(5);

Zend_Session::regenerateId();
$id = Zend_Session::getId();
session_write_close(); // release session so process below can use it
sleep(4); // not long enough for things to expire
exec($script . "expireAll $id expireAll", $result);
$result = $this->sortResult($result);
$expect = ';a === apple;o === orange;p === pear';
$this->assertTrue($result === $expect,
    "iteration over default Zend_Session namespace failed; " .
    "expecting result === '$expect', but got '$result'");

sleep(2); // long enough for things to expire (total of 6 seconds
          // waiting, but expires in 5)
exec($script . "expireAll $id expireAll", $result);
$result = array_pop($result);
$this->assertTrue($result === '',
    "iteration over default Zend_Session namespace failed; " .
    "expecting result === '', but got '$result')");
session_start(); // resume artificially suspended session

// We could split this into a separate test, but actually, if anything
// leftover from above contaminates the tests below, that is also a
// bug that we want to know about.
$s = new Zend_Session_Namespace('expireGuava');
$s->setExpirationSeconds(5, 'g'); // now try to expire only 1 of the
                                  // keys in the namespace
$s->g = 'guava';
$s->p = 'peach';
$s->p = 'plum';

session_write_close(); // release session so process below can use it
sleep(6); // not long enough for things to expire
exec($script . "expireAll $id expireGuava", $result);
$result = $this->sortResult($result);
session_start(); // resume artificially suspended session
$this->assertTrue($result === ';p === plum',
    "iteration over named Zend_Session namespace failed (result=$result)");