30.3. Użycie zaawansowane

Chociaż przykłady podstawowego użycia sesji są całkowicie zadowalającym sposobem wykorzystania sesji Zend Framework, to jest kilka dobrych zwyczajów, ktore powinieneś przeanalizować. Przeanalizuj przykład użycia Zend_Auth, który domyślnie używa Zend_Session_Namespace do przechowywania danych uwierzytelniania. Ten przykład pokazuje jeden sposób szybkiego i łatwego zintegrowania Zend_Session_Namespace oraz Zend_Auth.

30.3.1. Rozpoczynanie sesji

Jeśli chcesz aby wszystkie żądania używały sesji i używały sesji Zend Framework, to rozpocznik sesję w pliku ładującym:

Przykład 30.6. Rozpoczynanie globalnej sesji

<?php
...
require_once 'Zend/Session.php';
Zend_Session::start();
...
?>

Rozpoczynając sesję w pliku ładującym zapobiegasz przypadkowym próbom rozpoczęcia sesji po wysłaniu nagłówków do przeglądarki, co powodowałoby wyrzucenie wyjątku i prawdopodobnie wyświetlenie nieprawidłowo wygenerowanej strony. Wiele zaawansowanych funkcjonalności wymaga wcześniejszego wywołania metody Zend_Session::start(). (więcej informacji o zaawansowanych funkcjonalnościach pojawi się później)

Są cztery sposoby rozpoczęcia sesji, gdy używamy Zend_Session. Dwa z nich są złe.

  • 1. Źle: Nie ustawiaj parametru PHP session.auto_start w pliku php.ini lub .htaccess (http://www.php.net/manual/en/ref.session.php#ini.session.auto-start). Jeśli nie masz możliwości zablokowania tego parametru w pliku php.ini, a używasz modułu mod_php (lub jego odpowiednika), a ustawienie to jest obecnie włączone w pliku php.ini, to dodaj php_value session.auto_start 0 w twoim pliku .htaccess (najczęściej w twoim głównym katalogu serwera).

  • 2. Źle: Nie używaj bezpośrednio funkcji PHP session_start() . Jeśli użyjesz funkcji session_start() bezpośrednio, a potem rozpoczniesz sesję używając obiektu Zend_Session_Namespace, metoda Zend_Session::start() wyrzuci wyjątek ("session has already been started"). Jeśli wywołasz funkcję session_start(), po użyciu obiektu Zend_Session_Namespace lub po rozpoczęciu sesji za pomocą metody Zend_Session::start(), zostanie wygenerowany błąd poziomu E_NOTICE, a wywołanie funkcji zostanie zignorowane.

  • 3. Dobrze: Użyj Zend_Session::start(). Jeśli chcesz aby wszystkie żądania używały sesji, bezwarunkowo umieść wywołanie tej funkcji na początku, w twoim pliku ładującym ZF. Sessions have some overhead. If some requests need sessions, but other requests will not need to use sessions, then:

    • Unconditionally, set the strict option to true (zobacz Zend_Session::setOptions() ) in your userland bootstrap.

    • Wywołaj metodę Zend_Session::start(), tylko dla żądań, które potrzebują użyć sesji, zanim pierwszy raz wywołasz metodę new Zend_Session_Namespace().

    • Użyj new Zend_Session_Namespace(), normalnie, wtedy gdy będzie to potrzebne, ale upewnij się, że wcześniej zostanie wywołana metoda Zend_Session::start().

    Opcja strict zapobiega automatycznemu rozpoczęciu sesji za pomocą metody Zend_Session::start() w konstruktorze new Zend_Session_Namespace(). Thus, this option helps developers of userland ZF applications enforce a design decision to avoid using sessions for certain requests, since an error will be thrown when using this option and instantiating Zend_Session_Namespace, before an explicit call to Zend_Session::start(). Do not use this option in ZF core library code, because only userland developers should make this design choice. Similarly, all "library" developers should carefully consider the impact of using Zend_Session::setOptions() on users of their library code, since these options have global side-effects (as do the underlying options for ext/session).

  • 4. Dobrze: Po prostu użyj new Zend_Session_Namespace() kiedy tylko będziesz potrzebował, a sesja zostanie automatycznie rozpoczęta przez Zend_Session. To pozwala na ekstremalnie proste użycie, działające w większości sytuacji. However, you then become responsible for ensuring that the first new Zend_Session_Namespace() happens before any output (i.e. HTTP headers ) has been sent by PHP to the client, if you are using the default, cookie-based sessions (strongly recommended). See Sekcja 30.4.3.1, „Headers already sent” for more information.

30.3.2. Blokowanie przestrzeni nazw sesji

Session namespaces can be locked, to prevent further alterations to the data in that namespace. Use Zend_Session_Namespace's lock() to make a specific namespace read-only, unLock() to make a read-only namespace read-write, and isLocked() to test if a namespace has been previously locked. Locks are transient and do not persist from one request to the next. Locking the namespace has no effect on setter methods of objects stored in the namespace, but does prevent the use of the namespace's setter method to remove or replace objects stored directly in the namespace. Similarly, locking Zend_Session_Namespace namespaces does not prevent the use of symbol table aliases to the same data (see PHP references).

Przykład 30.7. Blokowanie przestrzeni nazw sesji

<?php
    // zakładając:
    $userProfileNamespace = new Zend_Session_Namespace('userProfileNamespace');

    // oznaczanie sesji jako zablokowanej tylko do odczytu
    $userProfileNamespace->lock();

    // odblokowanie sesji
    if ($userProfileNamespace->isLocked()) {
        $userProfileNamespace->unLock();
    }
?>

There are numerous ideas for how to manage models in MVC paradigms for the Web, including creating presentation models for use by views. Sometimes existing data, whether part of your domain model or not, is adequate for the task. To discourage views from applying any processing logic to alter such data, consider locking session namespaces before permitting views to access this subset of your "presentation" model.

Przykład 30.8. Blokowanie danych sesji w widokach

<?php
class FooModule_View extends Zend_View
{
    public function show($name)
    {
        if (!isset($this->mySessionNamespace)) {
            $this->mySessionNamespace = Zend::registry('FooModule');
        }

        if ($this->mySessionNamespace->isLocked()) {
            return parent::render($name);
        }

        $this->mySessionNamespace->lock();
        $return = parent::render($name);
        $this->mySessionNamespace->unLock();

        return $return;
    }
}
?>

30.3.3. Namespace Expiration

Limits can be placed on the longevity of both namespaces and individual keys in namespaces. Common use cases include passing temporary information between requests, and reducing exposure to certain security risks by removing access to potentially sensitive information some time after authentication occurred. Expiration can be based on elapsed seconds, or based on the concept of "hops", where a hop occurs for each successive request that activates the namespace via at least one $space = new Zend_Session_Namespace('myspace');.

Przykład 30.9. Expiration Examples

<?php
$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.
?>

When working with data expiring from the session in the current request, care should be used when retrieving it. Although the data is returned by reference, modifying the data will not make expiring data persist past the current request. In order to "reset" the expiration time, fetch the data into temporary variables, use the namespace to unset it, and then set the appropriate keys again.

30.3.4. Session Encapsulation and Controllers

Namespaces can also be used to separate session access by controllers to protect variables from contamination. For example, the 'Zend_Auth' controller might keep its session state data separate from all other controllers.

Przykład 30.10. Namespaced Sessions for Controllers with Automatic Expiration

<?php
require_once 'Zend/Session.php';
// question view controller
$testSpace = new Zend_Session_Namespace('testSpace');
$testSpace->setExpirationSeconds(300, "accept_answer"); // expire only this variable
$testSpace->accept_answer = true;

-- 

// answer processing controller
$testSpace = new Zend_Session_Namespace('testSpace');

if ($testSpace->accept_answer === true) {
    // within time
}
else {
    // not within time
}
?>

30.3.5. Limiting Instances of Zend_Session_Namespace to One Per Namespace

We recommend using session locking (see above) instead of the feature below, which places extra management burden on the developer to pass any Zend_Session_Namespace instances into whatever functions and objects need access to each namespace.

When constructing the first instance of Zend_Session_Namespace attached to a specific namespace, you can also instruct Zend_Session_Namespace to not make any more instances for that namespace. Thus, any future attempts to construct a Zend_Session_Namespace instance having the same namespace will throw an error. Such behavior is optional, and not the default behavior, but remains available to those who prefer to pass around a single instance object for each namespace. This increases protection from changes by components that should not modify a particular session namespace, because they won't have easy access. However, limiting a namespace to a single instance may lead to more code or more complex code, as it removes access to the convient $aNamespace = new Zend_Session_Namespace('aNamespace');, after the first intance has been created, as follows in the example below:

Przykład 30.11. Limiting to Single Instances

<?php
    require_once 'Zend/Session.php';
    $authSpaceAccessor1 = new Zend_Session_Namespace('Zend_Auth');
    $authSpaceAccessor2 = new Zend_Session_Namespace('Zend_Auth', Zend_Session_Namespace::SINGLE_INSTANCE);
    $authSpaceAccessor1->foo = 'bar';
    assert($authSpaceAccessor2->foo, 'bar'); // passes
    doSomething($options, $authSpaceAccessor2); // pass the accessor to wherever it is needed
    .
    .
    .
    $aNamespaceObject = new Zend_Session_Namespace('Zend_Auth'); // this will throw an error
?>

The second parameter in the constructor above will tell Zend_Session_Namespace that any future Zend_Session's that are instantiated with the 'Zend_Auth' namespace are not allowed, and will thus cause an exception. Since new Zend_Session_Namespace('Zend_Auth') will not be allowed after the code above has been executed, the developer becomes responsible for storing the instance object ($authSpaceAccessor2 in the example above) somewhere, if access to this session namespace is needed at a later time during the same request. For example, a developer may store the instance in a static variable, or pass it to other methods that might need access to this session namespace. Session locking (see above) provides a more convenient, and less burdensome approach to limiting access to namespaces.

30.3.6. Working with Arrays in Namespaces

Modifying an array inside a namespace does not work. The simplest solution is to store arrays after all desired values have been set. ZF-800 documents a known issue affecting many PHP applications using magic methods and arrays.

Przykład 30.12. Known problem with arrays

<?php
    $sessionNamespace = new Zend_Session_Namespace('Foo');
    $sessionNamespace->array = array();
    $sessionNamespace->array['testKey'] = 1; // nie działa przed PHP 5.2.1
?>

If you need to modify the array after assigning it to a session namespace key, fetch the array, then

Przykład 30.13. Workaround: fetch, modify, save

<?php
    $sessionNamespace = new Zend_Session_Namespace('Foo');
    $sessionNamespace->array = array('tree' => 'apple');
    $tmp = $sessionNamespace->array;
    $tmp['fruit'] = 'peach';
    $sessionNamespace->array = $tmp;
?>

Alternatively, store an array containing a reference to the desired array, and then access it indirectly.

Przykład 30.14. Workaround: store array containing reference

<?php
    $myNamespace = new Zend_Session_Namespace('mySpace');

    // works, even for broken versions of PHP
    $a = array(1,2,3);
    $myNamespace->someArray = array( & $a ) ;
    $a['foo'] = 'bar';
?>

30.3.7. Using Sessions with Authentication

If your authentication adapter for Zend_Auth returns a result where the authorization identity is an object (not recommended), instead of an array, then make sure to require your authorization identity class definition before starting the session. Instead, we recommend storing the authorization ids computed within an authentication adapter inside a well-known key in a session namespace. For example, the default behavior of Zend_Auth places this in the 'storage' key of the 'Zend_Auth' namespace.

If you tell Zend_Auth to not persist authentication tokens in sessions, then you can manually store the authorization id in the session namespace, in a well-known location in a session namespace of your choice. Often, applications have specific needs about where to store credentials used (if any) and "authorization" identity. Applications often map authentication identities (e.g. usernames) to authorization identities (e.g. a uniquely assigned integer) during authentication, which would occur in the Zend_Auth authentication adapter's authenticate() method.

Przykład 30.15. Przykład: Simplified access of authorization ids

<?php
    // pre-authentication request
    require_once 'Zend/Auth/Adapter/Digest.php';
    $adapter = new Zend_Auth_Adapter_Digest($filename, $realm, $username, $password);
    $result = $adapter->authenticate();
    require_once 'Zend/Session/Namespace.php';
    $namespace = new Zend_Session_Namespace('Zend_Auth');
    if ($result->isValid()) {
        $namespace->authorizationId = $result->getIdentity();
        $namespace->date = time();
    } else {
        $namespace->attempts++;
    }

    // subsequent requests
    require_once 'Zend/Session.php';
    Zend_Session::start();
    $namespace = new Zend_Session_Namespace('Zend_Auth');

    echo "Valid: ", (empty($namespace->authorizationId) ? 'No' : 'Yes'), "\n"';
    echo "Authorization / user Id: ", (empty($namespace->authorizationId)
        ? 'none' : print_r($namespace->authorizationId, true)), "\n"';
    echo "Authentication attempts: ", (empty($namespace->attempts)
        ? '0' : $namespace->attempts), "\n"';
    echo "Authenticated on: ",
        (empty($namespace->date) ? 'No' : date(DATE_ATOM, $namespace->date), "\n"';
?>

Authorization ids stored client-side are subject to privilege escalation vulnerabilities, if these ids are used and trusted by the server, unless, for example, the id is duplicated on the server-side (e.g. in the session) and then cross-checked with the authorization id claimed by the client for the in-effect session. We are differentiating between "authentication ids" (e.g. usernames) and "authorization ids" (e.g. user id #101 in the users DB table).

The latter is not uncommon for performance reasons, such as helping select from a pool of servers caching session information to help solve chicken-and-egg problems. Often debates ensue about whether to use the real authorization id in the cookie, or some substitute that aids in mapping to the real authorization id (or session or server(s) holding the user's session/profile, etc.), as some system security architects wish to prevent true "DB primary keys" from escaping into the wild. These architects try and obtain some level of protection by obfuscation in the event of a SQL injection vulnerability in their system. Not everyone uses auto-increment strategies for authorization ids.

30.3.8. Użycie sesji z testami jednostkowymi

The Zend Framework relies on PHPUnit to facilitate testing of itself. Many developers extend the existing suite of unit tests to cover the code in their applications. The exception "Zend_Session is currently marked as read-only" is thrown while performing unit tests, if any write-related methods are used after ending the session. However, unit tests using Zend_Session require extra attention, because closing (Zend_Session::writeClose()), or destroying a session (Zend_Session::destroy()) prevents any further setting or unsetting of keys in any Zend_Session_Namespace. This behavior is a direct result of the underlying ext/session mechanism and PHP's session_destroy() and session_write_close(), which has no "undo" mechanism to facilitate setup/teardown with unit tests.

To work around this, see the unit test testSetExpirationSeconds() in tests/Zend/Session/SessionTest.php and SessionTestHelper.php, which make use of PHP's exec() to launch a separate process. The new process more accurately simulates a second, successive request from a browser. The separate process begins with a "clean" session, just like any PHP script execution for a web request. Also, any changes to $_SESSION[] made in the calling process become available to the child process, provided the parent closed the session before using exec()

Przykład 30.16. Użycie PHPUnit do testowania kodu używającego Zend_Session*

<?php
        // testowanie setExpirationSeconds()
        require 'tests/Zend/Session/SessionTestHelper.php'; // zobacz także plik SessionTest.php w trunk/
        $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)");
?>