diff --git a/src/Symfony/Component/HttpFoundation/Session.php b/src/Symfony/Component/HttpFoundation/Session.php index 721a6c7240..e0961c411f 100644 --- a/src/Symfony/Component/HttpFoundation/Session.php +++ b/src/Symfony/Component/HttpFoundation/Session.php @@ -12,62 +12,45 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\HttpFoundation\SessionStorage\SessionStorageInterface; +use Symfony\Component\HttpFoundation\FlashBagInterface; /** * Session. * * @author Fabien Potencier + * @author Drak * * @api */ -class Session implements \Serializable +class Session implements SessionInterface { + /** + * Storage driver. + * + * @var SessionStorageInterface + */ protected $storage; - protected $started; - protected $attributes; - protected $flashes; - protected $oldFlashes; - protected $closed; /** * Constructor. * - * @param SessionStorageInterface $storage A SessionStorageInterface instance + * @param SessionStorageInterface $storage A SessionStorageInterface instance. */ public function __construct(SessionStorageInterface $storage) { $this->storage = $storage; - $this->flashes = array(); - $this->oldFlashes = array(); - $this->attributes = array(); - $this->started = false; - $this->closed = false; } /** * Starts the session storage. * + * @return boolean True if session started. + * * @api */ public function start() { - if (true === $this->started) { - return; - } - - $this->storage->start(); - - $attributes = $this->storage->read('_symfony2'); - - if (isset($attributes['attributes'])) { - $this->attributes = $attributes['attributes']; - $this->flashes = $attributes['flashes']; - - // flag current flash messages to be removed at shutdown - $this->oldFlashes = $this->flashes; - } - - $this->started = true; + return $this->storage->start(); } /** @@ -81,7 +64,7 @@ class Session implements \Serializable */ public function has($name) { - return array_key_exists($name, $this->attributes); + return $this->storage->getAttributes()->has($name); } /** @@ -96,7 +79,7 @@ class Session implements \Serializable */ public function get($name, $default = null) { - return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + return $this->storage->getAttributes()->get($name, $default); } /** @@ -109,11 +92,7 @@ class Session implements \Serializable */ public function set($name, $value) { - if (false === $this->started) { - $this->start(); - } - - $this->attributes[$name] = $value; + $this->storage->getAttributes()->set($name, $value); } /** @@ -125,7 +104,7 @@ class Session implements \Serializable */ public function all() { - return $this->attributes; + return $this->storage->getAttributes()->all(); } /** @@ -137,11 +116,7 @@ class Session implements \Serializable */ public function replace(array $attributes) { - if (false === $this->started) { - $this->start(); - } - - $this->attributes = $attributes; + $this->storage->getAttributes()->replace($attributes); } /** @@ -153,13 +128,7 @@ class Session implements \Serializable */ public function remove($name) { - if (false === $this->started) { - $this->start(); - } - - if (array_key_exists($name, $this->attributes)) { - unset($this->attributes[$name]); - } + return $this->storage->getAttributes()->remove($name); } /** @@ -169,190 +138,193 @@ class Session implements \Serializable */ public function clear() { - if (false === $this->started) { - $this->start(); - } - - $this->attributes = array(); - $this->flashes = array(); + $this->storage->getAttributes()->clear(); } /** * Invalidates the current session. * + * Clears all session attributes and flashes and regenerates the + * session and deletes the old session from persistence. + * + * @return boolean True if session invalidated, false if error. + * * @api */ public function invalidate() { - $this->clear(); - $this->storage->regenerate(true); + $this->storage->clear(); + + return $this->storage->regenerate(true); } /** * Migrates the current session to a new session id while maintaining all * session attributes. * + * @param boolean $destroy Whether to delete the old session or leave it to garbage collection. + * + * @return boolean True if session migrated, false if error + * * @api */ - public function migrate() + public function migrate($destroy = false) { - $this->storage->regenerate(); + return $this->storage->regenerate($destroy); + } + + /** + * {@inheritdoc} + */ + public function save() + { + $this->storage->save(); } /** * Returns the session ID * - * @return mixed The session ID + * @return mixed The session ID * * @api */ public function getId() { - if (false === $this->started) { - $this->start(); - } - return $this->storage->getId(); } /** - * Gets the flash messages. + * Implements the \Serialize interface. * - * @return array + * @return SessionStorageInterface */ - public function getFlashes() - { - return $this->flashes; - } - - /** - * Sets the flash messages. - * - * @param array $values - */ - public function setFlashes($values) - { - if (false === $this->started) { - $this->start(); - } - - $this->flashes = $values; - $this->oldFlashes = array(); - } - - /** - * Gets a flash message. - * - * @param string $name - * @param string|null $default - * - * @return string - */ - public function getFlash($name, $default = null) - { - return array_key_exists($name, $this->flashes) ? $this->flashes[$name] : $default; - } - - /** - * Sets a flash message. - * - * @param string $name - * @param string $value - */ - public function setFlash($name, $value) - { - if (false === $this->started) { - $this->start(); - } - - $this->flashes[$name] = $value; - unset($this->oldFlashes[$name]); - } - - /** - * Checks whether a flash message exists. - * - * @param string $name - * - * @return Boolean - */ - public function hasFlash($name) - { - if (false === $this->started) { - $this->start(); - } - - return array_key_exists($name, $this->flashes); - } - - /** - * Removes a flash message. - * - * @param string $name - */ - public function removeFlash($name) - { - if (false === $this->started) { - $this->start(); - } - - unset($this->flashes[$name]); - } - - /** - * Removes the flash messages. - */ - public function clearFlashes() - { - if (false === $this->started) { - $this->start(); - } - - $this->flashes = array(); - $this->oldFlashes = array(); - } - - public function save() - { - if (false === $this->started) { - $this->start(); - } - - $this->flashes = array_diff_key($this->flashes, $this->oldFlashes); - - $this->storage->write('_symfony2', array( - 'attributes' => $this->attributes, - 'flashes' => $this->flashes, - )); - } - - /** - * This method should be called when you don't want the session to be saved - * when the Session object is garbaged collected (useful for instance when - * you want to simulate the interaction of several users/sessions in a single - * PHP process). - */ - public function close() - { - $this->closed = true; - } - - public function __destruct() - { - if (true === $this->started && !$this->closed) { - $this->save(); - } - } - public function serialize() { return serialize($this->storage); } + /** + * Implements the \Serialize interface. + * + * @throws \InvalidArgumentException If the passed string does not unserialize to an instance of SessionStorageInterface + */ public function unserialize($serialized) { - $this->storage = unserialize($serialized); - $this->attributes = array(); - $this->started = false; + $storage = unserialize($serialized); + if (!$storage instanceof SessionStorageInterface) { + throw new \InvalidArgumentException('Serialized data did not return a valid instance of SessionStorageInterface'); + } + + $this->storage = $storage; + } + + /** + * Adds a flash to the stack for a given type. + * + * @param string $message + * @param string $type + */ + public function addFlash($message, $type = FlashBagInterface::NOTICE) + { + $this->storage->getFlashes()->add($message, $type); + } + + /** + * Gets flash messages for a given type. + * + * @param string $type Message category type. + * + * @return array + */ + public function getFlashes($type = FlashBagInterface::NOTICE) + { + return $this->storage->getFlashes()->get($type); + } + + /** + * Pops flash messages off th stack for a given type. + * + * @param string $type Message category type. + * + * @return array + */ + public function popFlashes($type = FlashBagInterface::NOTICE) + { + return $this->storage->getFlashes()->pop($type); + } + + /** + * Pop all flash messages from the stack. + * + * @return array Empty array or indexed array of arrays. + */ + public function popAllFlashes() + { + return $this->storage->getFlashes()->popAll(); + } + + /** + * Sets an array of flash messages for a given type. + * + * @param string $type + * @param array $array + */ + public function setFlashes($type, array $array) + { + $this->storage->getFlashes()->set($type, $array); + } + + /** + * Has flash messages for a given type? + * + * @param string $type + * + * @return boolean + */ + public function hasFlashes($type) + { + return $this->storage->getFlashes()->has($type); + } + + /** + * Returns a list of all defined types. + * + * @return array + */ + public function getFlashKeys() + { + return $this->storage->getFlashes()->keys(); + } + + /** + * Gets all flash messages. + * + * @return array + */ + public function getAllFlashes() + { + return $this->storage->getFlashes()->all(); + } + + /** + * Clears flash messages for a given type. + * + * @param string $type + * + * @return array Returns an array of what was just cleared. + */ + public function clearFlashes($type) + { + return $this->storage->getFlashes()->clear($type); + } + + /** + * Clears all flash messages. + * + * @return array Empty array or indexed arrays or array if none. + */ + public function clearAllFlashes() + { + return $this->storage->getFlashes()->clearAll(); } } diff --git a/src/Symfony/Component/HttpFoundation/SessionInterface.php b/src/Symfony/Component/HttpFoundation/SessionInterface.php new file mode 100644 index 0000000000..acc53635fc --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/SessionInterface.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\SessionStorage\AttributeInterface; +use Symfony\Component\HttpFoundation\FlashBagInterface; + +/** + * Interface for the session. + * + * @author Drak + */ +interface SessionInterface extends AttributeInterface, \Serializable +{ + /** + * Starts the session storage. + * + * @throws \RuntimeException If session fails to start. + */ + function start(); + + /** + * Invalidates the current session. + * + * @return boolean True if session invalidated, false if error. + */ + function invalidate(); + + /** + * Migrates the current session to a new session id while maintaining all + * session attributes. + * + * @param boolean $destroy Whether to delete the old session or leave it to garbage collection. + * + * @return boolean True if session migrated, false if error. + * + * @api + */ + function migrate($destroy = false); + + /** + * Force the session to be saved and closed. + * + * This method is generally not required for real sessions as + * the session will be automatically saved at the end of + * code execution. + */ + function save(); + + /** + * Adds a flash to the stack for a given type. + * + * @param string $message + * @param string $type + */ + function addFlash($message, $type = FlashBagInterface::NOTICE); + + /** + * Gets flash messages for a given type. + * + * @param string $type Message category type. + * + * @return array + */ + function getFlashes($type = FlashBagInterface::NOTICE); + + /** + * Pops flash messages off th stack for a given type. + * + * @param string $type Message category type. + * + * @return array + */ + function popFlashes($type = FlashBagInterface::NOTICE); + + /** + * Pop all flash messages from the stack. + * + * @return array Empty array or indexed array of arrays. + */ + function popAllFlashes(); + + /** + * Sets an array of flash messages for a given type. + * + * @param string $type + * @param array $array + */ + function setFlashes($type, array $array); + + /** + * Has flash messages for a given type? + * + * @param string $type + * + * @return boolean + */ + function hasFlashes($type); + + /** + * Returns a list of all defined types. + * + * @return array + */ + function getFlashKeys(); + + /** + * Gets all flash messages. + * + * @return array + */ + function getAllFlashes(); + + /** + * Clears flash messages for a given type. + * + * @param string $type + * + * @return array Returns an array of what was just cleared. + */ + function clearFlashes($type); + + /** + * Clears all flash messages. + * + * @return array Array of arrays or array if none. + */ + function clearAllFlashes(); +} diff --git a/src/Symfony/Component/HttpFoundation/SessionStorage/AbstractSessionStorage.php b/src/Symfony/Component/HttpFoundation/SessionStorage/AbstractSessionStorage.php new file mode 100644 index 0000000000..db0ecca327 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/SessionStorage/AbstractSessionStorage.php @@ -0,0 +1,326 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +use Symfony\Component\HttpFoundation\FlashBag; +use Symfony\Component\HttpFoundation\FlashBagInterface; +use Symfony\Component\HttpFoundation\AttributeBag; +use Symfony\Component\HttpFoundation\AttributeBagInterface; + +/** + * This provides a base class for session attribute storage. + * + * @author Drak + */ +abstract class AbstractSessionStorage implements SessionStorageInterface +{ + /** + * @var \Symfony\Component\HttpFoundation\FlashBagInterface + */ + protected $flashBag; + + /** + * @var \Symfony\Component\HttpFoundation\AttributeBagInterface + */ + protected $attributeBag; + + /** + * @var array + */ + protected $options; + + /** + * @var boolean + */ + protected $started = false; + + /** + * @var boolean + */ + protected $closed = false; + + /** + * Constructor. + * + * Depending on how you want the storage driver to behave you probably + * want top override this constructor entirely. + * + * List of options for $options array with their defaults. + * @see http://www.php.net/manual/en/session.configuration.php for options + * but we omit 'session.' from the beginning of the keys. + * + * auto_start, "0" + * cookie_domain, "" + * cookie_httponly, "" + * cookie_lifetime, "0" + * cookie_path, "/" + * cookie_secure, "" + * entropy_file, "" + * entropy_length, "0" + * gc_divisor, "100" + * gc_maxlifetime, "1440" + * gc_probability, "1" + * hash_bits_per_character, "4" + * hash_function, "0" + * name, "PHPSESSID" + * referer_check, "" + * save_path, "" + * serialize_handler, "php" + * use_cookies, "1" + * use_only_cookies, "1" + * use_trans_sid, "0" + * upload_progress.enabled, "1" + * upload_progress.cleanup, "1" + * upload_progress.prefix, "upload_progress_" + * upload_progress.name, "PHP_SESSION_UPLOAD_PROGRESS" + * upload_progress.freq, "1%" + * upload_progress.min-freq, "1" + * url_rewriter.tags, "a=href,area=href,frame=src,form=,fieldset=" + * + * @param AttributeBagInterface $attributes An AttributeBagInterface instance, (defaults null for default AttributeBag) + * @param FlashBagInterface $flashes A FlashBagInterface instance (defaults null for default FlashBag) + * @param array $options Session configuration options. + */ + public function __construct(AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, array $options = array()) + { + $this->attributeBag = $attributes ? $attributes : new AttributeBag(); + $this->flashBag = $flashes ? $flashes : new FlashBag(); + $this->setOptions($options); + $this->registerSaveHandlers(); + $this->registerShutdownFunction(); + } + + /** + * {@inheritdoc} + */ + public function getFlashes() + { + if (!$this->started) { + $this->start(); + } + + return $this->flashBag; + } + + /** + * {@inheritdoc} + */ + public function getAttributes() + { + if (!$this->started) { + $this->start(); + } + + return $this->attributeBag; + } + + /** + * {@inheritdoc} + */ + public function start() + { + if ($this->started && !$this->closed) { + return true; + } + + // start the session + if (!session_start()) { + throw new \RuntimeException('Failed to start the session'); + } + + $this->loadSession(); + + $this->started = true; + $this->closed = false; + + return true; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + if (!$this->started) { + return ''; // returning empty is consistent with session_id() behaviour + } + + return session_id(); + } + + /** + * Regenerates the session. + * + * This method will regenerate the session ID and optionally + * destroy the old ID. Session regeneration should be done + * periodically and for example, should be done when converting + * an anonymous session to a logged in user session. + * + * @param boolean $destroy + * + * @return boolean Returns true on success or false on failure. + */ + public function regenerate($destroy = false) + { + return session_regenerate_id($destroy); + } + + /** + * {@inheritdoc} + */ + public function save() + { + session_write_close(); + $this->closed = true; + } + + /** + * {@inheritdoc} + */ + public function clear() + { + // clear out the bags + $this->attributeBag->clear(); + $this->flashBag->clearAll(); + + // clear out the session + $_SESSION = array(); + + // reconnect the bags to the session + $this->loadSession(); + } + + /** + * Sets session.* ini variables. + * + * For convenience we omit 'session.' from the beginning of the keys. + * Explicitly ignores other ini keys. + * + * session_get_cookie_params() overrides values. + * + * @param array $options + * + * @see http://www.php.net/manual/en/session.configuration.php + */ + protected function setOptions(array $options) + { + $cookieDefaults = session_get_cookie_params(); + $this->options = array_merge(array( + 'cookie_lifetime' => $cookieDefaults['lifetime'], + 'cookie_path' => $cookieDefaults['path'], + 'cookie_domain' => $cookieDefaults['domain'], + 'cookie_secure' => $cookieDefaults['secure'], + 'cookie_httponly' => isset($cookieDefaults['httponly']) ? $cookieDefaults['httponly'] : false, + ), $options); + + // Unless session.cache_limiter has been set explicitly, disable it + // because this is managed by HeaderBag directly (if used). + if (!array_key_exists('cache_limiter', $this->options)) { + $this->options['cache_limiter'] = 0; + } + + foreach ($this->options as $key => $value) { + if (in_array($key, array( + 'auto_start', 'cookie_domain', 'cookie_httponly', + 'cookie_lifetime', 'cookie_path', 'cookie_secure', + 'entropy_file', 'entropy_length', 'gc_divisor', + 'gc_maxlifetime', 'gc_probability', 'hash_bits_per_character', + 'hash_function', 'name', 'referer_check', + 'save_path', 'serialize_handler', 'use_cookies', + 'use_only_cookies', 'use_trans_sid', 'upload_progress.enabled', + 'upload_progress.cleanup', 'upload_progress.prefix', 'upload_progress.name', + 'upload_progress.freq', 'upload_progress.min-freq', 'url_rewriter.tags'))) { + ini_set('session.'.$key, $value); + } + } + } + + /** + * Registers this storage device for PHP session handling. + * + * PHP requires session save handlers to be set, either it's own, or custom ones. + * There are some defaults set automatically when PHP starts, but these can be overriden + * using this command if you need anything other than PHP's default handling. + * + * When the session starts, PHP will call the sessionRead() handler which should return an array + * of any session attributes. PHP will then populate these into $_SESSION. + * + * When PHP shuts down, the sessionWrite() handler is called and will pass the $_SESSION contents + * to be stored. + * + * When a session is specifically destroyed, PHP will call the sessionDestroy() handler with the + * session ID. This happens when the session is regenerated for example and th handler + * MUST delete the session by ID from the persistent storage immediately. + * + * PHP will call sessionGc() from time to time to expire any session records according to the + * set max lifetime of a session. This routine should delete all records from persistent + * storage which were last accessed longer than the $lifetime. + * + * PHP sessionOpen() and sessionClose() are pretty much redundant and can just return true. + * + * NOTE: + * + * To use PHP native save handlers, override this method using ini_set with + * session.save_handlers and session.save_path e.g. + * + * ini_set('session.save_handlers', 'files'); + * ini_set('session.save_path', /tmp'); + * + * @see http://php.net/manual/en/function.session-set-save-handler.php + * @see SessionSaveHandlerInterface + */ + protected function registerSaveHandlers() + { + // note this can be reset to PHP's control using ini_set('session.save_handler', 'files'); + // so long as ini_set() is called before the session is started. + if ($this instanceof SessionSaveHandlerInterface) { + session_set_save_handler( + array($this, 'sessionOpen'), + array($this, 'sessionClose'), + array($this, 'sessionRead'), + array($this, 'sessionWrite'), + array($this, 'sessionDestroy'), + array($this, 'sessionGc') + ); + } + } + + /** + * Registers PHP shutdown function. + * + * This method is required to avoid strange issues when using PHP objects as + * session save handlers. + */ + protected function registerShutdownFunction() + { + register_shutdown_function('session_write_close'); + } + + /** + * Load the session with attributes. + * + * After starting the session, PHP retrieves the session from whatever handlers + * are set to (either PHP's internal, custom set with session_set_save_handler()). + * PHP takes the return value from the sessionRead() handler, unserializes it + * and populates $_SESSION with the result automatically. + */ + protected function loadSession() + { + $key = $this->attributeBag->getStorageKey(); + $_SESSION[$key] = isset($_SESSION[$key]) ? $_SESSION[$key] : array(); + $this->attributeBag->initialize($_SESSION[$key]); + + $key = $this->flashBag->getStorageKey(); + $_SESSION[$key] = isset($_SESSION[$key]) ? $_SESSION[$key] : array(); + $this->flashBag->initialize($_SESSION[$key]); + } +} diff --git a/src/Symfony/Component/HttpFoundation/SessionStorage/SessionSaveHandlerInterface.php b/src/Symfony/Component/HttpFoundation/SessionStorage/SessionSaveHandlerInterface.php new file mode 100644 index 0000000000..c45b9d22dc --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/SessionStorage/SessionSaveHandlerInterface.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\SessionStorage; + +/** + * Session Savehandler Interface. + * + * This interface is for implementing methods required for the + * session_set_save_handler() function. + * + * @see http://php.net/session_set_save_handler + * + * These are methods called by PHP when the session is started + * and closed and for various house-keeping tasks required + * by session management. + * + * PHP requires session save handlers. There are some defaults set + * automatically when PHP starts, but these can be overriden using + * this command if you need anything other than PHP's default handling. + * + * When the session starts, PHP will call the sessionRead() handler + * which should return a string extactly as stored (which will have + * been encoded by PHP using a special session serializer session_decode() + * which is different to the serialize() function. PHP will then populate + * these into $_SESSION. + * + * When PHP shuts down, the sessionWrite() handler is called and will pass + * the $_SESSION contents already serialized (using session_encode()) to + * be stored. + * + * When a session is specifically destroyed, PHP will call the + * sessionDestroy() handler with the session ID. This happens when the + * session is regenerated for example and th handler MUST delete the + * session by ID from the persistent storage immediately. + * + * PHP will call sessionGc() from time to time to expire any session + * records according to the set max lifetime of a session. This routine + * should delete all records from persistent storage which were last + * accessed longer than the $lifetime. + * + * PHP sessionOpen() and sessionClose() are pretty much redundant and + * can return true. + * + * @author Drak + */ +interface SessionSaveHandlerInterface +{ + /** + * Open session. + * + * This method is for internal use by PHP and must not be called manually. + * + * @param string $savePath Save path. + * @param string $sessionName Session Name. + * + * @throws \RuntimeException If something goes wrong starting the session. + * + * @return boolean + */ + function sessionOpen($savePath, $sessionName); + + /** + * Close session. + * + * This method is for internal use by PHP and must not be called manually. + * + * @return boolean + */ + function sessionClose(); + + /** + * Read session. + * + * This method is for internal use by PHP and must not be called manually. + * + * This method is called by PHP itself when the session is started. + * This method should retrieve the session data from storage by the + * ID provided by PHP. Return the string directly as is from storage. + * If the record was not found you must return an empty string. + * + * The returned data will be unserialized automatically by PHP using a + * special unserializer method session_decode() and the result will be used + * to populate the $_SESSION superglobal. This is done automatically and + * is not configurable. + * + * @param string $sessionId Session ID. + * + * @throws \RuntimeException On fatal error but not "record not found". + * + * @return string String as stored in persistent storage or empty string in all other cases. + */ + function sessionRead($sessionId); + + /** + * Commit session to storage. + * + * This method is for internal use by PHP and must not be called manually. + * + * PHP will call this method when the session is closed. It sends + * the session ID and the contents of $_SESSION to be saved in a lightweight + * serialized format (which PHP does automatically using session_encode() + * which should be stored exactly as is given in $data. + * + * Note this method is normally called by PHP after the output buffers + * have been closed. + * + * @param string $sessionId Session ID. + * @param string $data Session serialized data to save. + * + * @throws \RuntimeException On fatal error. + * + * @return boolean + */ + function sessionWrite($sessionId, $data); + + /** + * Destroys this session. + * + * This method is for internal use by PHP and must not be called manually. + * + * PHP will call this method when the session data associated + * with the session ID provided needs to be immediately + * deleted from the permanent storage. + * + * @param string $sessionId Session ID. + * + * @throws \RuntimeException On fatal error. + * + * @return boolean + */ + function sessionDestroy($sessionId); + + /** + * Garbage collection for storage. + * + * This method is for internal use by PHP and must not be called manually. + * + * This method is called by PHP periodically and passes the maximum + * time a session can exist for before being deleted from permanent storage. + * + * @param integer $lifetime Max lifetime in seconds to keep sessions stored. + * + * @throws \RuntimeException On fatal error. + * + * @return boolean + */ + function sessionGc($lifetime); +} diff --git a/src/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php b/src/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php index b61a2557b2..8624e52ec6 100644 --- a/src/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php +++ b/src/Symfony/Component/HttpFoundation/SessionStorage/SessionStorageInterface.php @@ -11,10 +11,14 @@ namespace Symfony\Component\HttpFoundation\SessionStorage; +use Symfony\Component\HttpFoundation\FlashBagInterface; +use Symfony\Component\HttpFoundation\AttributeBagInterface; + /** * SessionStorageInterface. * * @author Fabien Potencier + * @author Drak * * @api */ @@ -23,6 +27,10 @@ interface SessionStorageInterface /** * Starts the session. * + * @throws \RuntimeException If something goes wrong starting the session. + * + * @return boolean True if started. + * * @api */ function start(); @@ -30,61 +38,20 @@ interface SessionStorageInterface /** * Returns the session ID * - * @return mixed The session ID - * - * @throws \RuntimeException If the session was not started yet + * @return mixed The session ID or false if the session has not started. * * @api */ function getId(); - /** - * Reads data from this storage. - * - * The preferred format for a key is directory style so naming conflicts can be avoided. - * - * @param string $key A unique key identifying your data - * - * @return mixed Data associated with the key - * - * @throws \RuntimeException If an error occurs while reading data from this storage - * - * @api - */ - function read($key); - - /** - * Removes data from this storage. - * - * The preferred format for a key is directory style so naming conflicts can be avoided. - * - * @param string $key A unique key identifying your data - * - * @return mixed Data associated with the key - * - * @throws \RuntimeException If an error occurs while removing data from this storage - * - * @api - */ - function remove($key); - - /** - * Writes data to this storage. - * - * The preferred format for a key is directory style so naming conflicts can be avoided. - * - * @param string $key A unique key identifying your data - * @param mixed $data Data associated with your key - * - * @throws \RuntimeException If an error occurs while writing to this storage - * - * @api - */ - function write($key, $data); - /** * Regenerates id that represents this storage. * + * This method must invoke session_regenerate_id($destroy) unless + * this interface is used for a storage object designed for unit + * or functional testing where a real PHP session would interfere + * with testing. + * * @param Boolean $destroy Destroy session when regenerating? * * @return Boolean True if session regenerated, false if error @@ -94,4 +61,33 @@ interface SessionStorageInterface * @api */ function regenerate($destroy = false); + + /** + * Force the session to be saved and closed. + * + * This method must invoke session_write_close() unless this interface is + * used for a storage object design for unit or functional testing where + * a real PHP session would interfere with testing, in which case it + * it should actually persist the session data if required. + */ + function save(); + + /** + * Clear all session data in memory. + */ + function clear(); + + /** + * Gets the FlashBagInterface driver. + * + * @return FlashBagInterface + */ + function getFlashes(); + + /** + * Gets the AttributeBagInterface driver. + * + * @return AttributeBagInterface + */ + function getAttributes(); } diff --git a/tests/Symfony/Tests/Component/HttpFoundation/SessionStorage/AbstractSessionStorageTest.php b/tests/Symfony/Tests/Component/HttpFoundation/SessionStorage/AbstractSessionStorageTest.php new file mode 100644 index 0000000000..c98d03cd87 --- /dev/null +++ b/tests/Symfony/Tests/Component/HttpFoundation/SessionStorage/AbstractSessionStorageTest.php @@ -0,0 +1,118 @@ + + * + * These tests require separate processes. + * + * @runTestsInSeparateProcesses + */ +class AbstractSessionStorageTest extends \PHPUnit_Framework_TestCase +{ + /** + * @return AbstractSessionStorage + */ + protected function getStorage() + { + return new CustomHandlerSessionStorage(new AttributeBag(), new FlashBag()); + } + + public function testGetFlashBag() + { + $storage = $this->getStorage(); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\FlashBagInterface', $storage->getFlashes()); + } + + public function testGetAttributeBag() + { + $storage = $this->getStorage(); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\AttributeBagInterface', $storage->getAttributes()); + } + + public function testGetId() + { + $storage = $this->getStorage(); + $this->assertEquals('', $storage->getId()); + $storage->start(); + $this->assertNotEquals('', $storage->getId()); + } + + public function testRegenerate() + { + $storage = $this->getStorage(); + $storage->start(); + $id = $storage->getId(); + $storage->getAttributes()->set('lucky', 7); + $storage->regenerate(); + $this->assertNotEquals($id, $storage->getId()); + $this->assertEquals(7, $storage->getAttributes()->get('lucky')); + + } + + public function testRegenerateDestroy() + { + $storage = $this->getStorage(); + $storage->start(); + $id = $storage->getId(); + $storage->getAttributes()->set('legs', 11); + $storage->regenerate(true); + $this->assertNotEquals($id, $storage->getId()); + $this->assertEquals(11, $storage->getAttributes()->get('legs')); + } + + public function testCustomSaveHandlers() + { + $storage = new CustomHandlerSessionStorage(new AttributeBag(), new FlashBag()); + $this->assertEquals('user', ini_get('session.save_handler')); + } + + public function testNativeSaveHandlers() + { + $storage = new ConcreteSessionStorage(new AttributeBag(), new FlashBag()); + $this->assertNotEquals('user', ini_get('session.save_handler')); + } +} diff --git a/tests/Symfony/Tests/Component/HttpFoundation/SessionTest.php b/tests/Symfony/Tests/Component/HttpFoundation/SessionTest.php index 8318101e66..892672e9e0 100644 --- a/tests/Symfony/Tests/Component/HttpFoundation/SessionTest.php +++ b/tests/Symfony/Tests/Component/HttpFoundation/SessionTest.php @@ -12,6 +12,10 @@ namespace Symfony\Tests\Component\HttpFoundation; use Symfony\Component\HttpFoundation\Session; +use Symfony\Component\HttpFoundation\FlashBag; +use Symfony\Component\HttpFoundation\FlashBagInterface; +use Symfony\Component\HttpFoundation\AttributeBag; +use Symfony\Component\HttpFoundation\AttributeBagInterface; use Symfony\Component\HttpFoundation\SessionStorage\ArraySessionStorage; /** @@ -19,16 +23,24 @@ use Symfony\Component\HttpFoundation\SessionStorage\ArraySessionStorage; * * @author Fabien Potencier * @author Robert Schönthal + * @author Drak */ class SessionTest extends \PHPUnit_Framework_TestCase { + /** + * @var \Symfony\Component\HttpFoundation\SessionStorage\SessionStorageInterface + */ protected $storage; + + /** + * @var \Symfony\Component\HttpFoundation\SessionInterface + */ protected $session; public function setUp() { - $this->storage = new ArraySessionStorage(); - $this->session = $this->getSession(); + $this->storage = new ArraySessionStorage(new AttributeBag(), new FlashBag()); + $this->session = new Session($this->storage); } protected function tearDown() @@ -37,99 +49,106 @@ class SessionTest extends \PHPUnit_Framework_TestCase $this->session = null; } - public function testFlash() + public function testStart() { - $this->session->clearFlashes(); - - $this->assertSame(array(), $this->session->getFlashes()); - - $this->assertFalse($this->session->hasFlash('foo')); - - $this->session->setFlash('foo', 'bar'); - - $this->assertTrue($this->session->hasFlash('foo')); - $this->assertSame('bar', $this->session->getFlash('foo')); - - $this->session->removeFlash('foo'); - - $this->assertFalse($this->session->hasFlash('foo')); - - $flashes = array('foo' => 'bar', 'bar' => 'foo'); - - $this->session->setFlashes($flashes); - - $this->assertSame($flashes, $this->session->getFlashes()); + $this->assertEquals('', $this->session->getId()); + $this->assertTrue($this->session->start()); + $this->assertNotEquals('', $this->session->getId()); } - public function testFlashesAreFlushedWhenNeeded() + public function testGet() { - $this->session->setFlash('foo', 'bar'); - $this->session->save(); - - $this->session = $this->getSession(); - $this->assertTrue($this->session->hasFlash('foo')); - $this->session->save(); - - $this->session = $this->getSession(); - $this->assertFalse($this->session->hasFlash('foo')); - } - - public function testAll() - { - $this->assertFalse($this->session->has('foo')); + // tests defaults $this->assertNull($this->session->get('foo')); - - $this->session->set('foo', 'bar'); - - $this->assertTrue($this->session->has('foo')); - $this->assertSame('bar', $this->session->get('foo')); - - $this->session = $this->getSession(); - - $this->session->remove('foo'); - $this->session->set('foo', 'bar'); - - $this->session->remove('foo'); - - $this->assertFalse($this->session->has('foo')); - - $attrs = array('foo' => 'bar', 'bar' => 'foo'); - - $this->session = $this->getSession(); - - $this->session->replace($attrs); - - $this->assertSame($attrs, $this->session->all()); - - $this->session->clear(); - - $this->assertSame(array(), $this->session->all()); + $this->assertEquals(1, $this->session->get('foo', 1)); } - public function testMigrateAndInvalidate() + /** + * @dataProvider setProvider + */ + public function testSet($key, $value) { - $this->session->set('foo', 'bar'); - $this->session->setFlash('foo', 'bar'); + $this->session->set($key, $value); + $this->assertEquals($value, $this->session->get($key)); + } - $this->assertSame('bar', $this->session->get('foo')); - $this->assertSame('bar', $this->session->getFlash('foo')); + public function testReplace() + { + $this->session->replace(array('happiness' => 'be good', 'symfony' => 'awesome')); + $this->assertEquals(array('happiness' => 'be good', 'symfony' => 'awesome'), $this->session->all()); + $this->session->replace(array()); + $this->assertEquals(array(), $this->session->all()); + } - $this->session->migrate(); + /** + * @dataProvider setProvider + */ + public function testAll($key, $value, $result) + { + $this->session->set($key, $value); + $this->assertEquals($result, $this->session->all()); + } - $this->assertSame('bar', $this->session->get('foo')); - $this->assertSame('bar', $this->session->getFlash('foo')); + /** + * @dataProvider setProvider + */ + public function testClear($key, $value) + { + $this->session->set('hi', 'fabien'); + $this->session->set($key, $value); + $this->session->clear(); + $this->assertEquals(array(), $this->session->all()); + } - $this->session = $this->getSession(); + public function setProvider() + { + return array( + array('foo', 'bar', array('foo' => 'bar')), + array('foo.bar', 'too much beer', array('foo.bar' => 'too much beer')), + array('great', 'symfony2 is great', array('great' => 'symfony2 is great')), + ); + } + + /** + * @dataProvider setProvider + */ + public function testRemove($key, $value) + { + $this->session->set('hi.world', 'have a nice day'); + $this->session->set($key, $value); + $this->session->remove($key); + $this->assertEquals(array('hi.world' => 'have a nice day'), $this->session->all()); + } + + public function testInvalidate() + { + $this->session->set('invalidate', 123); + $this->session->addFlash('OK'); $this->session->invalidate(); + $this->assertEquals(array(), $this->session->all()); + $this->assertEquals(array(), $this->session->getAllFlashes()); + } - $this->assertSame(array(), $this->session->all()); - $this->assertSame(array(), $this->session->getFlashes()); + public function testMigrate() + { + $this->session->set('migrate', 321); + $this->session->addFlash('HI'); + $this->session->migrate(); + $this->assertEquals(321, $this->session->get('migrate')); + $this->assertEquals(array('HI'), $this->session->getFlashes(FlashBag::NOTICE)); + } + + public function testMigrateDestroy() + { + $this->session->set('migrate', 333); + $this->session->addFlash('Bye'); + $this->session->migrate(true); + $this->assertEquals(333, $this->session->get('migrate')); + $this->assertEquals(array('Bye'), $this->session->getFlashes(FlashBag::NOTICE)); } public function testSerialize() { - $this->session = new Session($this->storage); - $compare = serialize($this->storage); $this->assertSame($compare, $this->session->serialize()); @@ -142,91 +161,182 @@ class SessionTest extends \PHPUnit_Framework_TestCase $this->assertEquals($_storage->getValue($this->session), $this->storage, 'storage match'); } - public function testSave() + /** + * @expectedException \InvalidArgumentException + */ + public function testUnserializeException() { - $this->storage = new ArraySessionStorage(); - $this->session = new Session($this->storage); - $this->session->set('foo', 'bar'); - - $this->session->save(); - $compare = array('_symfony2' => array('attributes' => array('foo' => 'bar'), 'flashes' => array())); - - $r = new \ReflectionObject($this->storage); - $p = $r->getProperty('data'); - $p->setAccessible(true); - - $this->assertSame($p->getValue($this->storage), $compare); + $serialized = serialize(new \ArrayObject()); + $this->session->unserialize($serialized); } public function testGetId() { - $this->assertNull($this->session->getId()); - } - - public function testStart() - { + $this->assertEquals('', $this->session->getId()); $this->session->start(); - - $this->assertSame(array(), $this->session->getFlashes()); - $this->assertSame(array(), $this->session->all()); + $this->assertNotEquals('', $this->session->getId()); } - public function testSavedOnDestruct() + /** + * @dataProvider provideFlashes + */ + public function testAddFlash($type, $flashes) { - $this->session->set('foo', 'bar'); + foreach ($flashes as $message) { + $this->session->addFlash($message, $type); + } - $this->session->__destruct(); + $this->assertEquals($flashes, $this->session->getFlashes($type)); + } + + /** + * @dataProvider provideFlashes + */ + public function testGetFlashes($type, $flashes) + { + $this->session->setFlashes($type, $flashes); + $this->assertEquals($flashes, $this->session->getFlashes($type)); + } + + /** + * @dataProvider provideFlashes + */ + public function testPopFlashes($type, $flashes) + { + $this->session->setFlashes($type, $flashes); + $this->assertEquals($flashes, $this->session->popFlashes($type)); + $this->assertEquals(array(), $this->session->popFlashes($type)); + } + + /** + * @dataProvider provideFlashes + */ + public function testPopAllFlashes($type, $flashes) + { + $this->session->setFlashes(FlashBag::NOTICE, array('First', 'Second')); + $this->session->setFlashes(FlashBag::ERROR, array('Third')); $expected = array( - 'attributes'=>array('foo'=>'bar'), - 'flashes'=>array(), + FlashBag::NOTICE => array('First', 'Second'), + FlashBag::ERROR => array('Third'), ); - $saved = $this->storage->read('_symfony2'); - $this->assertSame($expected, $saved); + + $this->assertEquals($expected, $this->session->popAllFlashes()); + $this->assertEquals(array(), $this->session->popAllFlashes()); } - public function testSavedOnDestructAfterManualSave() + public function testSetFlashes() { - $this->session->set('foo', 'nothing'); - $this->session->save(); - $this->session->set('foo', 'bar'); + $this->session->setFlashes(FlashBag::NOTICE, array('First', 'Second')); + $this->session->setFlashes(FlashBag::ERROR, array('Third')); + $this->assertEquals(array('First', 'Second'), $this->session->getFlashes(FlashBag::NOTICE, false)); + $this->assertEquals(array('Third'), $this->session->getFlashes(FlashBag::ERROR, false)); + } - $this->session->__destruct(); + /** + * @dataProvider provideFlashes + */ + public function testHasFlashes($type, $flashes) + { + $this->assertFalse($this->session->hasFlashes($type)); + $this->session->setFlashes($type, $flashes); + $this->assertTrue($this->session->hasFlashes($type)); + } - $expected = array( - 'attributes'=>array('foo'=>'bar'), - 'flashes'=>array(), + /** + * @dataProvider provideFlashes + */ + public function testGetFlashKeys($type, $flashes) + { + $this->assertEquals(array(), $this->session->getFlashKeys()); + $this->session->setFlashes($type, $flashes); + $this->assertEquals(array($type), $this->session->getFlashKeys()); + } + + public function testGetFlashKeysBulk() + { + $this->loadFlashes(); + $this->assertEquals(array( + FlashBag::NOTICE, FlashBag::ERROR, FlashBag::WARNING, FlashBag::INFO), $this->session->getFlashKeys() ); - $saved = $this->storage->read('_symfony2'); - $this->assertSame($expected, $saved); } - public function testStorageRegenerate() + public function testGetAllFlashes() { - $this->storage->write('foo', 'bar'); + $this->assertEquals(array(), $this->session->getAllFlashes()); - $this->assertTrue($this->storage->regenerate()); + $this->session->addFlash('a', FlashBag::NOTICE); + $this->assertEquals(array( + FlashBag::NOTICE => array('a') + ), $this->session->getAllFlashes() + ); - $this->assertEquals('bar', $this->storage->read('foo')); + $this->session->addFlash('a', FlashBag::ERROR); + $this->assertEquals(array( + FlashBag::NOTICE => array('a'), + FlashBag::ERROR => array('a'), + ), $this->session->getAllFlashes()); - $this->assertTrue($this->storage->regenerate(true)); + $this->session->addFlash('a', FlashBag::WARNING); + $this->assertEquals(array( + FlashBag::NOTICE => array('a'), + FlashBag::ERROR => array('a'), + FlashBag::WARNING => array('a'), + ), $this->session->getAllFlashes() + ); - $this->assertNull($this->storage->read('foo')); + $this->session->addFlash('a', FlashBag::INFO); + $this->assertEquals(array( + FlashBag::NOTICE => array('a'), + FlashBag::ERROR => array('a'), + FlashBag::WARNING => array('a'), + FlashBag::INFO => array('a'), + ), $this->session->getAllFlashes() + ); + + $this->assertEquals(array( + FlashBag::NOTICE => array('a'), + FlashBag::ERROR => array('a'), + FlashBag::WARNING => array('a'), + FlashBag::INFO => array('a'), + ), $this->session->getAllFlashes() + ); } - public function testStorageRemove() + /** + * @dataProvider provideFlashes + */ + public function testClearFlashes($type, $flashes) { - $this->storage->write('foo', 'bar'); - - $this->assertEquals('bar', $this->storage->read('foo')); - - $this->storage->remove('foo'); - - $this->assertNull($this->storage->read('foo')); + $this->session->setFlashes($type, $flashes); + $this->session->clearFlashes($type); + $this->assertEquals(array(), $this->session->getFlashes($type)); } - protected function getSession() + public function testClearAllFlashes() { - return new Session($this->storage); + $this->loadFlashes(); + $this->assertNotEquals(array(), $this->session->getAllFlashes()); + $this->session->clearAllFlashes(); + $this->assertEquals(array(), $this->session->getAllFlashes()); } + + protected function loadFlashes() + { + $flashes = $this->provideFlashes(); + foreach ($flashes as $data) { + $this->session->setFlashes($data[0], $data[1]); + } + } + + public function provideFlashes() + { + return array( + array(FlashBag::NOTICE, array('a', 'b', 'c')), + array(FlashBag::ERROR, array('d', 'e', 'f')), + array(FlashBag::WARNING, array('g', 'h', 'i')), + array(FlashBag::INFO, array('j', 'k', 'l')), + ); + } + }