<?php
/**
 * A class for handling sessions.
 *
 * This class provides a way to open, manipulate and close a session. It also
 * features a data expiring mechanism. It is also adaptable to various types of
 * storage (file, database, etc.).
 */
class session
{
    /**
     * @var integer The expiration options.
     */
    const SECOND = 0;
    const HOP    = 1;

    /**
     * @var boolean Whether the session is currently opened or not.
     */
    protected $opened;

    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param session_handler (optional) $handler The storage handler.
     */
    public function __construct(session_handler $handler = null)
    {
        $this->opened = false;
        
        if ($handler)
        {
            $this->set_handler($handler);
        }
    }

    public function __destruct()
    {
        $this->close();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Overload functions.

    /**
     * A shortcut for get().
     * 
     * @param string $name The variable name.
     * @return mixed The value.
     */
    public function __get($name)
    {
        return $this->get($name);
    }

    /**
     * A shortcute for set().
     * 
     * @param string $name The variable name.
     * @param mixed $value The value.
     */
    public function __set($name, $value)
    {
        $this->set($name, $value);
    }

    /**
     * A shortcut for is_set().
     * 
     * @param string $name The variable name.
     * @return boolean True if the variable exists, else false.
     */
    public function __isset($name)
    {
        return $this->is_set($name);
    }

    /**
     * A shortcut for clear().
     * 
     * @param string $name The variable name.
     */
    public function __unset($name)
    {
        $this->clear($name);
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.

    /**
     * Opens the session.
     *
     * @param string (optional) $id The session id.
     * @throws logicexception If the headers have already been sent.
     * @throws logicexception If the SID has already been defined.
     * @return session A fluent interface.
     */
    public function open($id = null)
    {
        if ($this->opened)
        {
            return $this;
        }
        
        if (headers_sent())
        {
            throw new logicexception('Headers have already been sent.');
        }
        
        if (defined('SID'))
        {
            throw new logicexception('SID has already been defined.');
        }
        
        if (ini_get('session.save_handler') !== 'user')
        {
            $this->set_handler(new session_handler_file);
        }
        
        if ($id !== null)
        {
            $this->set_id($id);
        }
        
        session_start();
        $this->opened = true;
        return $this;
    }
    
    /**
     * Saves and closes the session.
     * 
     * @return session A fluent interface.
     */
    public function close()
    {
        if ($this->opened)
        {
            $this->clear_expired();
            session_commit();
        }
        
        $this->opened = false;
        return $this;
    }

    /**
     * Gets one or all variable.
     * 
     * @param string (optional) $name The variable name.
     * @throws outofrangeexception If the variable does not exist.
     * @return mixed The value.
     */
    public function get($name = null)
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        if ($name === null)
        {
            return $_SESSION;
        }
        
        if (!array_key_exists($name, $_SESSION))
        {
            throw new outofrangeexception('Variable ' . $name . ' does not exist.');
        }
        
        return $_SESSION[$name];
    }
    
    /**
     * Sets one or more variable.
     * 
     * @param string $name The variable name.
     * @param mixed $value The value.
     * -
     * @param array $name The variables.
     * @return session A fluent interface.
     */
    public function set($name, $value = null)
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        if (is_array($name))
        {
            foreach ($name as $k => $v)
            {
                $_SESSION[$k] = $v;
            }
        }
        else
        {
            $_SESSION[$name] = $value;
        }
        
        return $this;
    }
    
    /**
     * Checks if a variable is set or not.
     * 
     * @param string $name The variable name.
     * @return boolean True if the variable exists, else false.
     */
    public function is_set($name)
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        return array_key_exists($name, $_SESSION);
    }
    
    /**
     * Clears one or all variables.
     * 
     * @param string (optional) $name The variable name.
     * @return session A fluent interface.
     */
    public function clear($name = null)
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        if ($name === null)
        {
            $_SESSION = array();
        }
        else
        {
            unset($_SESSION[$name], $_SESSION['__'][self::HOP][$name], $_SESSION['__'][self::SECOND][$name]);
        }
        
        return $this;
    }

    /**
     * Gets the session id.
     *
     * @return string The id.
     */
    public function get_id()
    {
        return session_id();
    }

    /**
     * Sets the session id.
     *
     * @param string (optional) $id The id.
     * @return session A fluent interface.
     */
    public function set_id($id)
    {
        session_id($id);
        return $this;
    }

    /**
     * Regenerates the session id.
     *
     * @param boolean (optional) $destroy Whether to destroy the old session or
     * not.
     * @throws unexpectedvalueexception If the session id could not be
     * regenerated.
     * @return session A fluent interface.
     */
    public function regenerate_id($destroy = true)
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        if (!session_regenerate_id($destroy))
        {
            throw new unexpectedvalueexception('Session id could not be regenerated.');
        }
        
        return $this;
    }
    
    /**
     * Destroys the session.
     *
     * @throws unexpectedvalueexception If the session could not be destroyed.
     * @return session A fluent interface.
     */
    public function destroy()
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        if (!session_destroy())
        {
            throw new unexpectedvalueexception('Session could not be destroyed.');
        }
        
        return $this;
    }

    /**
     * Sets an expiration date on one or more variables.
     * 
     * @param integer (optional) $number The number of units.
     * @param integer (optional) $unit The expiration unit.
     * @param string/array (optional) $name The variable name(s).
     * @throws outofrangeexception If the expiration unit does not exist.
     * @return session A fluent interface.
     */
    function expire($number = 0, $unit = self::SECOND, $name = null)
    {
        if (!$this->opened)
        {
            $this->open();
        }
        
        $number = floor($number);
        
        if ($number < 0)
        {
            $number = 0;
        }
        
        if ($unit !== self::SECOND && $unit !== self::HOP)
        {
            throw new outofrangeexception('Expiration unit ' . $unit . ' does not exist.');
        }
        
        if ($unit === self::SECOND)
        {
            $number = time() + $number;
        }
        
        if ($name === null)
        {
            $name = array_keys($_SESSION);
        }
        elseif (!is_array($name))
        {
            $name = array($name);
        }
        
        while (($key = array_search('__', $name)) !== false)
        {
            unset($name[$key]);
        }
        
        if (!array_key_exists('__', $_SESSION))
        {
            $_SESSION['__'] = array(self::SECOND => array(), self::HOP => array());
        }
        
        foreach ($name as $v)
        {
            $_SESSION['__'][$unit][$v] = $number;
        }
        
        return $this;
    }
    
    /**
     * Sets a variable until the next hop.
     * 
     * @param string $name The variable name.
     * @param mixed $value The value.
     * @return session A fluent interface.
     */
    public function flash($name, $value)
    {
        $this->set($name, $value);
        $this->expire(1, self::HOP, $name);
        return $this;
    }

    /**
     * Sets the session storage handler.
     * 
     * @param session_handler $handler The handler.
     * @throws logicexception If the session has already been opened.
     * @throws unexpectedvalueexception If the handler could not be set.
     * @return session A fluent interface.
     */
    public function set_handler(session_handler $handler)
    {
        if ($this->opened)
        {
            throw new logicexception('Session has already been opened.');
        }
        
        $set = session_set_save_handler(
            array($handler, 'open'),
            array($handler, 'close'),
            array($handler, 'read'),
            array($handler, 'write'),
            array($handler, 'destroy'),
            array($handler, 'clear')
        );
        
        if (!$set)
        {
            throw new unexpectedvalueexception('Storage handler could not be set.');
        }
        
        return $this;
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.
    
    /**
     * Clears the expired variables.
     * 
     * @return session A fluent interface.
     */
    protected function clear_expired()
    {
        $time = time();
        
        if (array_key_exists('__', $_SESSION))
        {
            foreach (array_keys($_SESSION['__'][self::HOP]) as $k)
            {
                if (--$_SESSION['__'][self::HOP][$k] < 0)
                {
                    unset($_SESSION[$k], $_SESSION['__'][self::HOP][$k], $_SESSION['__'][self::SECOND][$k]);
                }
            }
            
            foreach (array_keys($_SESSION['__'][self::SECOND]) as $k)
            {
                if ($_SESSION['__'][self::SECOND][$k] < $time)
                {
                    unset($_SESSION[$k], $_SESSION['__'][self::SECOND][$k], $_SESSION['__'][self::HOP][$k]);
                }
            }
            
            if (!$_SESSION['__'][self::HOP] && !$_SESSION['__'][self::SECOND])
            {
                unset($_SESSION['__']);
            }
        }
        
        return $this;
    }
}
