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.
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):
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.
-
Incorrecto: No active la opción
session.auto_startde 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 enphp.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
Incorrecto: No use la función
session_start()de PHP directamente. Si usasession_start()directamente, y luego empieza a usarZend_Session_Namespace, se lanzará una excepción porZend_Session::start()("session has already been started"). Si llama asession_start()después de usarZend_Session_Namespaceo de llamar aZend_Session::start(), se generará un error de nivelE_NOTICE, y la llamada será ignorada.-
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
strictaTRUEusandoZend_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 objetoZend_Session_Namespace.Use "
new Zend_Session_Namespace()" normalmente, donde sea necesario, pero asegúrese de queZend_Session::start()haya sido llamado previamente.
La opción
strictimpide quenew Zend_Session_Namespace()inicie automáticamente la sesión usandoZend_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 cuandoZend_Session_Namespacese instancia antes de queZend_Session::start()sea llamado. Los desarrolladores deben considerar cuidadosamente el impacto de usarZend_Session::setOptions(), ya que estas opciones tienen efecto global, debido a su correspondencia con las opciones subyacentes de ext/session. Correcto: Simplemente instancie
Zend_Session_Namespacecuando 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 primeranew 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.
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();
}
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.
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
}
// ...
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.
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"
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.
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)");