2
0
Просмотр исходного кода

Backported EventManager from ZF2 to ZF1 trunk

- Converted namespaces to vendor prefixes
- Translated all closures used in tests to method callbacks
- Converted PHPUnit 3.5+-specific assertions to assertType()
- Implemented userland versions of SplPriorityQueue and SplStack when PHP <
  5.3.0 is detected
- Replaced all static:: references with self::

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@24681 44c647ce-9c0f-0410-b52a-842ac1e357ba
matthew 14 лет назад
Родитель
Сommit
35b58c78fd
39 измененных файлов с 6380 добавлено и 0 удалено
  1. 9 0
      documentation/manual/en/manual.xml.in
  2. 833 0
      documentation/manual/en/module_specs/Zend_EventManager-EventManager.xml
  3. 225 0
      library/Zend/EventManager/Event.php
  4. 109 0
      library/Zend/EventManager/EventCollection.php
  5. 108 0
      library/Zend/EventManager/EventDescription.php
  6. 541 0
      library/Zend/EventManager/EventManager.php
  7. 36 0
      library/Zend/EventManager/Exception.php
  8. 39 0
      library/Zend/EventManager/Exception/InvalidArgumentException.php
  9. 78 0
      library/Zend/EventManager/Filter.php
  10. 113 0
      library/Zend/EventManager/Filter/FilterIterator.php
  11. 138 0
      library/Zend/EventManager/FilterChain.php
  12. 149 0
      library/Zend/EventManager/GlobalEventManager.php
  13. 53 0
      library/Zend/EventManager/ListenerAggregate.php
  14. 424 0
      library/Zend/EventManager/ResponseCollection.php
  15. 32 0
      library/Zend/EventManager/StaticEventCollection.php
  16. 194 0
      library/Zend/EventManager/StaticEventManager.php
  17. 301 0
      library/Zend/Stdlib/CallbackHandler.php
  18. 31 0
      library/Zend/Stdlib/Exception.php
  19. 35 0
      library/Zend/Stdlib/Exception/InvalidCallbackException.php
  20. 319 0
      library/Zend/Stdlib/PriorityQueue.php
  21. 500 0
      library/Zend/Stdlib/SplPriorityQueue.php
  22. 64 0
      tests/Zend/EventManager/AllTests.php
  23. 665 0
      tests/Zend/EventManager/EventManagerTest.php
  24. 168 0
      tests/Zend/EventManager/FilterChainTest.php
  25. 122 0
      tests/Zend/EventManager/GlobalEventManagerTest.php
  26. 268 0
      tests/Zend/EventManager/StaticEventManagerTest.php
  27. 173 0
      tests/Zend/EventManager/StaticIntegrationTest.php
  28. 52 0
      tests/Zend/EventManager/TestAsset/ClassWithEvents.php
  29. 9 0
      tests/Zend/EventManager/TestAsset/Functor.php
  30. 70 0
      tests/Zend/EventManager/TestAsset/MockAggregate.php
  31. 38 0
      tests/Zend/EventManager/TestAsset/StaticEventsMock.php
  32. 60 0
      tests/Zend/Stdlib/AllTests.php
  33. 143 0
      tests/Zend/Stdlib/CallbackHandlerTest.php
  34. 146 0
      tests/Zend/Stdlib/PriorityQueueTest.php
  35. 103 0
      tests/Zend/Stdlib/SplPriorityQueueTest.php
  36. 8 0
      tests/Zend/Stdlib/TestAsset/SignalHandlers/InstanceMethod.php
  37. 8 0
      tests/Zend/Stdlib/TestAsset/SignalHandlers/Invokable.php
  38. 8 0
      tests/Zend/Stdlib/TestAsset/SignalHandlers/ObjectCallback.php
  39. 8 0
      tests/Zend/Stdlib/TestAsset/SignalHandlers/Overloadable.php

+ 9 - 0
documentation/manual/en/manual.xml.in

@@ -839,6 +839,15 @@
             </xi:include>
         </chapter>
 
+        <chapter id="zend.event-manager">
+            <title>Zend_EventManager</title>
+            <xi:include href="module_specs/Zend_EventManager-EventManager.xml">
+                <xi:fallback>
+                    <xi:include href="../en/module_specs/Zend_EventManager-EventManager.xml" />
+                </xi:fallback>
+            </xi:include>
+        </chapter>
+
         <chapter id="zend.exception">
             <title>Zend_Exception</title>
             <xi:include href="module_specs/Zend_Exception.xml">

+ 833 - 0
documentation/manual/en/module_specs/Zend_EventManager-EventManager.xml

@@ -0,0 +1,833 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Reviewed: no -->
+<sect1 id="zend.event-manager.event-manager">
+    <title>The EventManager</title>
+ 
+    <sect2 id="zend.event-manager.event-manager.intro">
+        <title>Overview</title>
+
+        <para>
+            <classname>Zend_EventManager</classname> is a component designed for the following use
+            cases:
+        </para>
+
+        <itemizedlist>
+            <listitem>
+                <para>
+                    Implementing simple subject/observer patterns.
+                </para>
+            </listitem>
+
+            <listitem>
+                <para>
+                    Implementing Aspect-Oriented designs.
+                </para>
+            </listitem>
+
+            <listitem>
+                <para>
+                    Implementing event-driven architectures.
+                </para>
+            </listitem>
+        </itemizedlist>
+
+        <para>
+            The basic architecture allows you to attach and detach listeners to named events, both on
+            a per-instance basis as well as statically; trigger events; and interrupt execution of
+            listeners.
+        </para>
+    </sect2>
+ 
+    <sect2 id="zend.event-manager.event-manager.quick-start">
+        <title>Quick Start</title>
+
+        <para>
+            Typically, you will compose a <classname>Zend_EventManager_EventManager</classname> instance in a class.
+        </para>
+
+        <programlisting language="php"><![CDATA[
+class Foo
+{
+    protected $events;
+
+    public function events(Zend_EventManager_EventCollection $events = null)
+    {
+        if (null !== $events) {
+            $this->events = $events;
+        } elseif (null === $this->events) {
+            $this->events = new Zend_EventManager_EventManager(__CLASS__);
+        }
+        return $this->events;
+    }
+}
+]]></programlisting>
+
+        <para>
+            The above allows users to access the <classname>EventManager</classname> instance, or
+            reset it with a new instance; if one does not exist, it will be lazily instantiated
+            on-demand.
+        </para>
+
+        <para>
+            An <classname>EventManager</classname> is really only interesting if it triggers some
+            events. Basic triggering takes three arguments: the event name, which is usually the
+            current function/method name; the "context", which is usually the current object
+            instance; and the arguments, which are usually the arguments provided to the current
+            function/method.
+        </para>
+
+        <programlisting language="php"><![CDATA[
+class Foo
+{
+    // ... assume events definition from above
+
+    public function bar($baz, $bat = null)
+    {
+        $params = compact('baz', 'bat');
+        $this->events()->trigger(__FUNCTION__, $this, $params);
+    }
+}
+]]></programlisting>
+
+        <para>
+            In turn, triggering events is only interesting if something is listening for the event.
+            Listeners attach to the <classname>EventManager</classname>, specifying a named event
+            and the callback to notify. The callback receives a <classname>Zend_EventManager_Event</classname>
+            object, which has accessors for retrieving the event name, context, and parameters.
+            Let's add a listener, and trigger the event.
+        </para>
+
+        <programlisting language="php"><![CDATA[
+$log = Zend_Log::factory($someConfig);
+$foo = new Foo();
+$foo->events()->attach('bar', function ($e) use ($log) {
+    $event  = $e->getName();
+    $target = get_class($e->getTarget());
+    $params = json_encode($e->getParams());
+
+    $log->info(sprintf(
+        '%s called on %s, using params %s',
+        $event,
+        $target,
+        $params
+    ));
+});
+
+// Results in log message:
+$foo->bar('baz', 'bat');
+// reading: bar called on Foo, using params {"baz" : "baz", "bat" : "bat"}"
+]]></programlisting>
+
+        <para>
+            Note that the second argument to <methodname>attach()</methodname> is any valid callback;
+            an anonymous function is shown in the example in order to keep the example
+            self-contained. However, you could also utilize a valid function name, a functor, a
+            string referencing a static method, or an array callback with a named static method or
+            instance method. Again, any PHP callback is valid.
+        </para>
+
+        <para>
+            Sometimes you may want to specify listeners without yet having an object instance of the
+            class composing an <classname>EventManager</classname>. The
+            <classname>Zend_EventManager_StaticEventManager</classname> allows you to do this. The call to
+            <methodname>attach</methodname> is identical to the <classname>EventManager</classname>,
+            but expects an additional parameter at the beginning: a named instance. Remember the
+            example of composing an <classname>EventManager</classname>, how we passed it
+            <constant>__CLASS__</constant>? That value, or any strings you provide in an array to
+            the constructor, may be used to identify an instance when using the
+            <classname>StaticEventManager</classname>. As an example, we could change the above
+            example to attach statically:
+        </para>
+
+        <programlisting language="php"><![CDATA[
+$log = Zend_Log::factory($someConfig);
+$events = Zend_EventManager_StaticEventManager::getInstance();
+$events->attach('Foo', 'bar', function ($e) use ($log) {
+    $event  = $e->getName();
+    $target = get_class($e->getTarget());
+    $params = json_encode($e->getParams());
+
+    $log->info(sprintf(
+        '%s called on %s, using params %s',
+        $event,
+        $target,
+        $params
+    ));
+});
+
+// Later, instantiate Foo:
+$foo = new Foo();
+
+// And we can still trigger the above event:
+$foo->bar('baz', 'bat');
+// results in log message: 
+// bar called on Foo, using params {"baz" : "baz", "bat" : "bat"}"
+]]></programlisting>
+
+        <para>
+            The <classname>EventManager</classname> also provides the ability to detach listeners,
+            short-circuit execution of an event either from within a listener or by testing return
+            values of listeners, test and loop through the results returned by listeners, prioritize
+            listeners, and more. Many of these features are detailed in the examples.
+        </para>
+        
+        <sect2 id="zend.event-manager.event-manager.quick-start.wildcard">
+            <title>Wildcard Listeners</title>
+
+            <para>
+                Sometimes you'll want to attach the same listener to many events or to all events of
+                a given instance -- or potentially, with the static manager, many contexts, and many
+                events. The <classname>EventManager</classname> component allows for this.
+            </para>
+
+            <example id="zend.event-manager.event-manager.quick-start.wildcard.many">
+                <title>Attaching to many events at once</title>
+
+                <programlisting language="php"><![CDATA[
+$events = new Zend_EventManager_EventManager();
+$events->attach(array('these', 'are', 'event', 'names'), $callback);
+]]></programlisting>
+
+                <para>
+                    Note that if you specify a priority, that priority will be used for all events
+                    specified.
+                </para>
+            </example>
+
+            <example id="zend.event-manager.event-manager.quick-start.wildcard.wildcard">
+                <title>Attaching using the wildcard</title>
+
+                <programlisting language="php"><![CDATA[
+$events = new Zend_EventManager_EventManager();
+$events->attach('*', $callback);
+]]></programlisting>
+
+                <para>
+                    Note that if you specify a priority, that priority will be used for this
+                    listener for any event triggered.
+                </para>
+
+                <para>
+                    What the above specifies is that <emphasis>any</emphasis> event triggered will
+                    result in notification of this particular listener.
+                </para>
+            </example>
+
+            <example id="zend.event-manager.event-manager.quick-start.wildcard.static-many">
+                <title>Attaching to many events at once via the StaticEventManager</title>
+
+                <programlisting language="php"><![CDATA[
+$events = Zend_EventManager_StaticEventManager::getInstance();
+// Attach to many events on the context "foo"
+$events->attach('foo', array('these', 'are', 'event', 'names'), $callback);
+
+// Attach to many events on the contexts "foo" and "bar"
+$events->attach(array('foo', 'bar'), array('these', 'are', 'event', 'names'), $callback);
+]]></programlisting>
+
+                <para>
+                    Note that if you specify a priority, that priority will be used for all events
+                    specified.
+                </para>
+            </example>
+
+            <example id="zend.event-manager.event-manager.quick-start.wildcard.static-wildcard">
+                <title>Attaching to many events at once via the StaticEventManager</title>
+
+                <programlisting language="php"><![CDATA[
+$events = Zend_EventManager_StaticEventManager::getInstance();
+// Attach to all events on the context "foo"
+$events->attach('foo', '*', $callback);
+
+// Attach to all events on the contexts "foo" and "bar"
+$events->attach(array('foo', 'bar'), '*', $callback);
+]]></programlisting>
+
+                <para>
+                    Note that if you specify a priority, that priority will be used for all events
+                    specified.
+                </para>
+
+                <para>
+                    The above is specifying that for the contexts "foo" and "bar", the specified
+                    listener should be notified for any event they trigger.
+                </para>
+            </example>
+        </sect2>
+    </sect2>
+ 
+    <sect2 id="zend.event-manager.event-manager.options">
+        <title>Configuration Options</title>
+ 
+        <variablelist>
+            <title>Zend_EventManager_EventManager Options</title>
+ 
+            <varlistentry>
+                <term>identifier</term>
+ 
+                <listitem>
+                    <para>
+                        A string or array of strings to which the given
+                        <classname>EventManager</classname> instance can answer when accessed via
+                        the <classname>StaticEventManager</classname>.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term>event_class</term>
+ 
+                <listitem>
+                    <para>
+                        The name of an alternate <classname>Zend_EventManager_Event</classname> class to use for
+                        representing events passed to listeners.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry>
+                <term>static_connections</term>
+ 
+                <listitem>
+                    <para>
+                        An instance of a <interfacename>Zend_EventManager_StaticEventCollection</interfacename>
+                        instance to use when triggering events. By default, this will use
+                        the global <classname>Zend_EventManager_StaticEventManager</classname> instance, but that can
+                        be overridden by passing a value to this method. A <constant>null</constant>
+                        value will prevent the instance from triggering any further statically
+                        attached listeners.
+                    </para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </sect2>
+ 
+    <sect2 id="zend.event-manager.event-manager.methods">
+        <title>Available Methods</title>
+ 
+        <variablelist>
+            <varlistentry id="zend.event-manager.event-manager.methods.constructor">
+                <term>__construct</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>__construct</methodname>
+                        <methodparam>
+                            <funcparams>null|string|int $identifier</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Constructs a new <classname>EventManager</classname> instance, using the
+                        given identifier, if provided, for purposes of static attachment.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.set-event-class">
+                <term>setEventClass</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>setEventClass</methodname>
+                        <methodparam>
+                            <funcparams>string $class</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Provide the name of an alternate <classname>Zend_EventManager_Event</classname> class to use
+                        when creating events to pass to triggered listeners.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.set-static-connections">
+                <term>setStaticConnections</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>setStaticConnections</methodname>
+                        <methodparam>
+                            <funcparams>Zend_EventManager_StaticEventCollection $connections = null</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        An instance of a <interfacename>Zend_EventManager_StaticEventCollection</interfacename>
+                        instance to use when triggering events. By default, this will use
+                        the global <classname>Zend_EventManager_StaticEventManager</classname> instance, but that can
+                        be overridden by passing a value to this method. A <constant>null</constant>
+                        value will prevent the instance from triggering any further statically
+                        attached listeners.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.get-static-connections">
+                <term>getStaticConnections</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>getStaticConnections</methodname>
+                        <void/>
+                    </methodsynopsis>
+                    <para>
+                        Returns the currently attached
+                        <interfacename>Zend_EventManager_StaticEventCollection</interfacename> instance, lazily
+                        retrieving the global <classname>Zend_EventManager_StaticEventManager</classname> instance if
+                        none is attached and usage of static listeners hasn't been disabled by
+                        passing a <constant>null</constant> value to <link
+                            linkend="zend.event-manager.event-manager.methods.set-static-connections">setStaticConnections()</link>.
+                        Returns either a boolean <constant>false</constant> if static listeners are
+                        disabled, or a <interfacename>StaticEventCollection</interfacename> instance
+                        otherwise.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.trigger">
+                <term>trigger</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>trigger</methodname>
+
+                        <methodparam>
+                            <funcparams>string $event, mixed $target, mixed $argv, callback $callback</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Triggers all listeners to a named event. The recommendation is to use the
+                        current function/method name for <varname>$event</varname>, appending it
+                        with values such as ".pre", ".post", etc. as needed.
+                        <varname>$context</varname> should be the current object instance, or the
+                        name of the function if not triggering within an object.
+                        <varname>$params</varname> should typically be an associative array or
+                        <classname>ArrayAccess</classname> instance; we recommend using the
+                        parameters passed to the function/method (<function>compact()</function> is
+                        often useful here). This method can also take a callback and behave in the 
+                        same way as <methodname>triggerUntil()</methodname>.
+                    </para>
+
+                    <para>
+                        The method returns an instance of <classname>Zend_EventManager_ResponseCollection</classname>,
+                        which may be used to introspect return values of the various listeners, test
+                        for short-circuiting, and more.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.trigger-until">
+                <term>triggerUntil</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>triggerUntil</methodname>
+
+                        <methodparam>
+                            <funcparams>string $event, mixed $context, mixed $argv, callback
+                                $callback</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Triggers all listeners to a named event, just like <link
+                            linkend="zend.event-manager.event-manager.methods.trigger">trigger()</link>,
+                        with the addition that it passes the return value from each listener to
+                        <varname>$callback</varname>; if <varname>$callback</varname> returns a
+                        boolean <constant>true</constant> value, execution of the listeners is
+                        interrupted. You can test for this using <code>$result-&gt;stopped()</code>.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.attach">
+                <term>attach</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>attach</methodname>
+
+                        <methodparam>
+                            <funcparams>string $event, callback $callback, int $priority</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Attaches <varname>$callback</varname> to the
+                        <classname>Zend_EventManager_EventManager</classname> instance, listening for the event
+                        <varname>$event</varname>. If a <varname>$priority</varname> is provided,
+                        the listener will be inserted into the internal listener stack using that
+                        priority; higher values execute earliest. (Default priority is "1", and
+                        negative priorities are allowed.)
+                    </para>
+
+                    <para>
+                        The method returns an instance of
+                        <classname>Zend_Stdlib_CallbackHandler</classname>; this value can later be
+                        passed to <methodname>detach()</methodname> if desired.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.attach-aggregate">
+                <term>attachAggregate</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>attachAggregate</methodname>
+                        <methodparam>
+                            <funcparams>string|Zend_EventManager_ListenerAggregate $aggregate</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        If a string is passed for <varname>$aggregate</varname>, instantiates that
+                        class. The <varname>$aggregate</varname> is then passed the
+                        <classname>EventManager</classname> instance to its
+                        <methodname>attach()</methodname> method so that it may register listeners.
+                    </para>
+
+                    <para>
+                        The <classname>ListenerAggregate</classname> instance is returned.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.detach">
+                <term>detach</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>detach</methodname>
+                        <methodparam>
+                            <funcparams>Zend_Stdlib_CallbackHandler $listener</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Scans all listeners, and detaches any that match <varname>$listener</varname>
+                        so that they will no longer be triggered.
+                    </para>
+
+                    <para>
+                        Returns a boolean <constant>true</constant> if any listeners have been
+                        identified and unsubscribed, and a boolean <constant>false</constant>
+                        otherwise.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.detach-aggregate">
+                <term>detachAggregate</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>detachAggregate</methodname>
+                        <methodparam>
+                            <funcparams>Zend_EventManager_ListenerAggregate $aggregate</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Loops through all listeners of all events to identify listeners that are
+                        represented by the aggregate; for all matches, the listeners will be removed.
+
+                    </para>
+
+                    <para>
+                        Returns a boolean <constant>true</constant> if any listeners have been
+                        identified and unsubscribed, and a boolean <constant>false</constant>
+                        otherwise.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.get-events">
+                <term>detachAggregate</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>getEvents</methodname>
+                        <void/>
+                    </methodsynopsis>
+                    <para>
+                        Returns an array of all event names that have listeners attached.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.get-listeners">
+                <term>getListeners</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>getListeners</methodname>
+
+                        <methodparam>
+                            <funcparams>string $event</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Returns a <classname>Zend_Stdlib_PriorityQueue</classname> instance of all
+                        listeners attached to <varname>$event</varname>.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.clear-listeners">
+                <term>clearListeners</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>clearListeners</methodname>
+
+                        <methodparam>
+                            <funcparams>string $event</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Removes all listeners attached to <varname>$event</varname>.
+                    </para>
+                </listitem>
+            </varlistentry>
+
+            <varlistentry id="zend.event-manager.event-manager.methods.prepare-args">
+                <term>prepareArgs</term>
+                <listitem>
+                    <methodsynopsis>
+                        <methodname>prepareArgs</methodname>
+
+                        <methodparam>
+                            <funcparams>array $args</funcparams>
+                        </methodparam>
+                    </methodsynopsis>
+                    <para>
+                        Creates an <classname>ArrayObject</classname> from the provided
+                        <varname>$args</varname>. This can be useful if you want yours listeners
+                        to be able to modify arguments such that later listeners or the triggering
+                        method can see the changes.
+                    </para>
+                </listitem>
+            </varlistentry>
+        </variablelist>
+    </sect2>
+ 
+    <sect2 id="zend.event-manager.event-manager.examples">
+        <title>Examples</title>
+ 
+        <example id="zend.event-manager.event-manager.examples.modifying-args">
+            <title>Modifying Arguments</title>
+ 
+            <para>
+                Occasionally it can be useful to allow listeners to modify the arguments they
+                receive so that later listeners or the calling method will receive those changed
+                values.
+            </para>
+
+            <para>
+                As an example, you might want to pre-filter a date that you know will arrive as a
+                string and convert it to a <classname>DateTime</classname> argument.
+            </para>
+
+            <para>
+                To do this, you can pass your arguments to <methodname>prepareArgs()</methodname>,
+                and pass this new object when triggering an event. You will then pull that value
+                back into your method.
+            </para>
+ 
+            <programlisting language="php"><![CDATA[
+class ValueObject
+{
+    // assume a composed event manager
+
+    function inject(array $values)
+    {
+        $argv = compact('values');
+        $argv = $this->events()->prepareArgs($argv);
+        $this->events()->trigger(__FUNCTION__, $this, $argv);
+        $date = isset($argv['values']['date']) ? $argv['values']['date'] : new DateTime('now');
+
+        // ...
+    }
+}
+
+$v = new ValueObject();
+
+$v->events()->attach('inject', function($e) {
+    $values = $e->getParam('values');
+    if (!$values) {
+        return;
+    }
+    if (!isset($values['date'])) {
+        $values['date'] = new DateTime('now');
+        return;
+    }
+    $values['date'] = new Datetime($values['date']);
+});
+
+$v->inject(array(
+    'date' => '2011-08-10 15:30:29',
+));
+]]></programlisting>
+        </example>
+
+        <example id="zend.event-manager.event-manager.examples.short-circuiting">
+            <title>Short Circuiting</title>
+ 
+            <para>
+                One common use case for events is to trigger listeners until either one indicates no
+                further processing should be done, or until a return value meets specific criteria.
+                As examples, if an event creates a Response object, it may want execution to stop.
+            </para>
+
+            <programlisting language="php"><![CDATA[
+$listener = function($e) {
+    // do some work
+    
+    // Stop propagation and return a response
+    $e->stopPropagation(true);
+    return $response;
+};
+]]></programlisting>
+
+            <para>
+                Alternately, we could do the check from the method triggering the event.
+            </para>
+ 
+            <programlisting language="php"><![CDATA[
+class Foo implements Dispatchable
+{
+    // assume composed event manager
+
+    public function dispatch(Request $request, Response $response = null)
+    {
+        $argv = compact('request', 'response');
+        $results = $this->events()->triggerUntil(__FUNCTION__, $this, $argv, function($v) {
+            return ($v instanceof Response);
+        });
+    }
+}
+]]></programlisting>
+
+            <para>
+                Typically, you may want to return a value that stopped execution, or use it some
+                way. Both <methodname>trigger()</methodname> and
+                <methodname>triggerUntil()</methodname> return a
+                <classname>Zend_EventManager_ResponseCollection</classname> instance; call its
+                <methodname>stopped()</methodname> method to test if execution was stopped, and
+                <methodname>last()</methodname> method to retrieve the return value from the last
+                executed listener:
+            </para>
+ 
+            <programlisting language="php"><![CDATA[
+class Foo implements Dispatchable
+{
+    // assume composed event manager
+
+    public function dispatch(Request $request, Response $response = null)
+    {
+        $argv = compact('request', 'response');
+        $results = $this->events()->triggerUntil(__FUNCTION__, $this, $argv, function($v) {
+            return ($v instanceof Response);
+        });
+
+        // Test if execution was halted, and return last result:
+        if ($results->stopped()) {
+            return $results->last();
+        }
+
+        // continue...
+    }
+}
+]]></programlisting>
+        </example>
+
+        <example id="zend.event-manager.event-manager.examples.priority">
+            <title>Assigning Priority to Listeners</title>
+ 
+            <para>
+                One use case for the <classname>EventManager</classname> is for implementing caching
+                systems. As such, you often want to check the cache early, and save to it late. 
+            </para>
+
+            <para>
+                The third argument to <methodname>attach()</methodname> is a priority value. The
+                higher this number, the earlier that listener will execute; the lower it is, the
+                later it executes. The value defaults to 1, and values will trigger in the order
+                registered within a given priority.
+            </para>
+
+            <para>
+                So, to implement a caching system, our method will need to trigger an event at
+                method start as well as at method end. At method start, we want an event that will
+                trigger early; at method end, an event should trigger late. 
+            </para>
+
+            <para>
+                Here is the class in which we want caching:
+            </para>
+
+            <programlisting language="php"><![CDATA[
+class SomeValueObject
+{
+    // assume it composes an event manager
+
+    public function get($id)
+    {
+        $params = compact('id');
+        $results = $this->events()->trigger('get.pre', $this, $params);
+
+        // If an event stopped propagation, return the value
+        if ($results->stopped()) {
+            return $results->last();
+        }
+
+        // do some work...
+
+        $params['__RESULT__'] = $someComputedContent;
+        $this->events()->trigger('get.post', $this, $params);
+    }
+}
+]]></programlisting>
+
+            <para>
+                Now, let's create a <interfacename>ListenerAggregate</interfacename> that can handle
+                caching for us:
+            </para>
+
+            <programlisting language="php"><![CDATA[
+class CacheListener implements Zend_EventManager_ListenerAggregate
+{
+    protected $cache;
+
+    public function __construct(Cache $cache)
+    {
+        $this->cache = $cache;
+    }
+
+    public function attach(Zend_EventManager_EventCollection $events)
+    {
+        $events->attach('get.pre', array($this, 'load'), 100);
+        $events->attach('get.post', array($this, 'save'), -100);
+    }
+
+    public function load($e)
+    {
+        $id = get_class($e->getTarget()) . '-' . json_encode($e->getParams());
+        if (false !== ($content = $this->cache->load($id))) {
+            $e->stopPropagation(true);
+            return $content;
+        }
+    }
+
+    public function save($e)
+    {
+        $params  = $e->getParams();
+        $content = $params['__RESULT__'];
+        unset($params['__RESULT__']);
+
+        $id = get_class($e->getTarget()) . '-' . json_encode($params);
+        $this->cache->save($content, $id);
+    }
+}
+]]></programlisting>
+
+            <para>
+                We can then attach the aggregate to an instance.
+            </para>
+
+            <programlisting language="php"><![CDATA[
+$value         = new SomeValueObject();
+$cacheListener = new CacheListener($cache);
+$value->events()->attachAggregate($cacheListener);
+]]></programlisting>
+
+            <para>
+                Now, as we call <methodname>get()</methodname>, if we have a cached entry, it will
+                be returned immediately; if not, a computed entry will be cached when we complete
+                the method.
+            </para>
+        </example>
+    </sect2>
+</sect1>

+ 225 - 0
library/Zend/EventManager/Event.php

@@ -0,0 +1,225 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/EventDescription.php';
+
+/**
+ * Representation of an event
+ *
+ * Encapsulates the target context and parameters passed, and provides some 
+ * behavior for interacting with the event manager.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_Event implements Zend_EventManager_EventDescription
+{
+    /**
+     * @var string Event name
+     */
+    protected $name;
+
+    /**
+     * @var string|object The event target
+     */
+    protected $target;
+
+    /**
+     * @var array|ArrayAccess|object The event parameters
+     */
+    protected $params = array();
+
+    /**
+     * @var bool Whether or not to stop propagation
+     */
+    protected $stopPropagation = false;
+
+    /**
+     * Constructor
+     *
+     * Accept a target and its parameters.
+     * 
+     * @param  string $name Event name
+     * @param  string|object $target 
+     * @param  array|ArrayAccess $params 
+     * @return void
+     */
+    public function __construct($name = null, $target = null, $params = null)
+    {
+        if (null !== $name) {
+            $this->setName($name);
+        }
+
+        if (null !== $target) {
+            $this->setTarget($target);
+        }
+
+        if (null !== $params) {
+            $this->setParams($params);
+        }
+    }
+
+    /**
+     * Get event name
+     * 
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * Get the event target
+     *
+     * This may be either an object, or the name of a static method.
+     * 
+     * @return string|object
+     */
+    public function getTarget()
+    {
+        return $this->target;
+    }
+
+    /**
+     * Set parameters
+     *
+     * Overwrites parameters
+     * 
+     * @param  array|ArrayAccess|object $params 
+     * @return Event
+     */
+    public function setParams($params)
+    {
+        if (!is_array($params) && !is_object($params)) {
+            require_once 'Zend/EventManager/Exception/InvalidArgumentException.php';
+            throw new Zend_EventManager_Exception_InvalidArgumentException(sprintf(
+                'Event parameters must be an array or object; received "%s"',
+                (is_object($params) ? get_class($params) : gettype($params))
+            ));
+        }
+
+        $this->params = $params;
+        return $this;
+    }
+
+    /**
+     * Get all parameters
+     * 
+     * @return array|object|ArrayAccess
+     */
+    public function getParams()
+    {
+        return $this->params;
+    }
+
+    /**
+     * Get an individual parameter
+     *
+     * If the parameter does not exist, the $default value will be returned.
+     * 
+     * @param  string|int $name 
+     * @param  mixed $default 
+     * @return mixed
+     */
+    public function getParam($name, $default = null)
+    {
+        // Check in params that are arrays or implement array access
+        if (is_array($this->params) || $this->params instanceof ArrayAccess) {
+            if (!isset($this->params[$name])) {
+                return $default;
+            }
+
+            return $this->params[$name];
+        }
+
+        // Check in normal objects
+        if (!isset($this->params->{$name})) {
+            return $default;
+        }
+        return $this->params->{$name};
+    }
+
+    /**
+     * Set the event name
+     * 
+     * @param  string $name 
+     * @return Zend_EventManager_Event
+     */
+    public function setName($name)
+    {
+        $this->name = (string) $name;
+        return $this;
+    }
+
+    /**
+     * Set the event target/context
+     * 
+     * @param  null|string|object $target 
+     * @return Zend_EventManager_Event
+     */
+    public function setTarget($target)
+    {
+        $this->target = $target;
+        return $this;
+    }
+
+    /**
+     * Set an individual parameter to a value
+     * 
+     * @param  string|int $name 
+     * @param  mixed $value 
+     * @return Zend_EventManager_Event
+     */
+    public function setParam($name, $value)
+    {
+        if (is_array($this->params) || $this->params instanceof ArrayAccess) {
+            // Arrays or objects implementing array access
+            $this->params[$name] = $value;
+        } else {
+            // Objects
+            $this->params->{$name} = $value;
+        }
+        return $this;
+    }
+
+    /**
+     * Stop further event propagation
+     * 
+     * @param  bool $flag 
+     * @return void
+     */
+    public function stopPropagation($flag = true)
+    {
+        $this->stopPropagation = (bool) $flag;
+    }
+
+    /**
+     * Is propagation stopped?
+     * 
+     * @return bool
+     */
+    public function propagationIsStopped()
+    {
+        return $this->stopPropagation;
+    }
+}

+ 109 - 0
library/Zend/EventManager/EventCollection.php

@@ -0,0 +1,109 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Stdlib/CallbackHandler.php';
+
+/**
+ * Interface for messengers
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_EventManager_EventCollection
+{
+    /**
+     * Trigger an event
+     *
+     * Should allow handling the following scenarios:
+     * - Passing Event object only
+     * - Passing event name and Event object only
+     * - Passing event name, target, and Event object
+     * - Passing event name, target, and array|ArrayAccess of arguments
+     *
+     * Can emulate triggerUntil() if the last argument provided is a callback.
+     * 
+     * @param  string $event 
+     * @param  object|string $target 
+     * @param  array|object $argv 
+     * @param  null|callback $callback 
+     * @return Zend_EventManager_ResponseCollection
+     */
+    public function trigger($event, $target = null, $argv = array(), $callback = null);
+
+    /**
+     * Trigger an event until the given callback returns a boolean false
+     *
+     * Should allow handling the following scenarios:
+     * - Passing Event object and callback only
+     * - Passing event name, Event object, and callback only
+     * - Passing event name, target, Event object, and callback
+     * - Passing event name, target, array|ArrayAccess of arguments, and callback
+     * 
+     * @param  string $event 
+     * @param  object|string $target 
+     * @param  array|object $argv 
+     * @param  callback $callback 
+     * @return Zend_EventManager_ResponseCollection
+     */
+    public function triggerUntil($event, $target, $argv = null, $callback = null);
+
+    /**
+     * Attach a listener to an event
+     * 
+     * @param  string $event 
+     * @param  callback $callback
+     * @param  int $priority Priority at which to register listener
+     * @return Zend_Stdlib_CallbackHandler
+     */
+    public function attach($event, $callback = null, $priority = 1);
+
+    /**
+     * Detach an event listener
+     * 
+     * @param  Zend_Stdlib_CallbackHandler|Zend_EventManager_ListenerAggregate $listener 
+     * @return void
+     */
+    public function detach($listener);
+
+    /**
+     * Get a list of events for which this collection has listeners
+     * 
+     * @return array
+     */
+    public function getEvents();
+
+    /**
+     * Retrieve a list of listeners registered to a given event
+     * 
+     * @param  string $event 
+     * @return array|object
+     */
+    public function getListeners($event);
+
+    /**
+     * Clear all listeners for a given event
+     * 
+     * @param  string $event 
+     * @return void
+     */
+    public function clearListeners($event);
+}

+ 108 - 0
library/Zend/EventManager/EventDescription.php

@@ -0,0 +1,108 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Representation of an event
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_EventManager_EventDescription
+{
+    /**
+     * Get event name
+     * 
+     * @return string
+     */
+    public function getName();
+
+    /**
+     * Get target/context from which event was triggered
+     * 
+     * @return null|string|object
+     */
+    public function getTarget();
+
+    /**
+     * Get parameters passed to the event
+     * 
+     * @return array|ArrayAccess
+     */
+    public function getParams();
+
+    /**
+     * Get a single parameter by name
+     * 
+     * @param  string $name 
+     * @param  mixed $default Default value to return if parameter does not exist
+     * @return mixed
+     */
+    public function getParam($name, $default = null);
+
+    /**
+     * Set the event name
+     * 
+     * @param  string $name 
+     * @return void
+     */
+    public function setName($name);
+
+    /**
+     * Set the event target/context
+     * 
+     * @param  null|string|object $target 
+     * @return void
+     */
+    public function setTarget($target);
+
+    /**
+     * Set event parameters
+     * 
+     * @param  string $params 
+     * @return void
+     */
+    public function setParams($params);
+
+    /**
+     * Set a single parameter by key
+     * 
+     * @param  string $name 
+     * @param  mixed $value 
+     * @return void
+     */
+    public function setParam($name, $value);
+
+    /**
+     * Indicate whether or not the parent EventCollection should stop propagating events
+     * 
+     * @param  bool $flag 
+     * @return void
+     */
+    public function stopPropagation($flag = true);
+
+    /**
+     * Has this event indicated event propagation should stop?
+     * 
+     * @return bool
+     */
+    public function propagationIsStopped();
+}

+ 541 - 0
library/Zend/EventManager/EventManager.php

@@ -0,0 +1,541 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/Event.php';
+require_once 'Zend/EventManager/EventCollection.php';
+require_once 'Zend/EventManager/ResponseCollection.php';
+require_once 'Zend/EventManager/StaticEventManager.php';
+require_once 'Zend/Stdlib/CallbackHandler.php';
+require_once 'Zend/Stdlib/PriorityQueue.php';
+
+/**
+ * Event manager: notification system
+ *
+ * Use the EventManager when you want to create a per-instance notification
+ * system for your objects.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_EventManager implements Zend_EventManager_EventCollection
+{
+    /**
+     * Subscribed events and their listeners
+     * @var array Array of Zend_Stdlib_PriorityQueue objects
+     */
+    protected $events = array();
+
+    /**
+     * @var string Class representing the event being emitted
+     */
+    protected $eventClass = 'Zend_EventManager_Event';
+
+    /**
+     * Identifiers, used to pull static signals from StaticEventManager
+     * @var array
+     */
+    protected $identifiers = array();
+
+    /**
+     * Static connections
+     * @var false|null|Zend_EventManager_StaticEventCollection
+     */
+    protected $staticConnections = null;
+
+    /**
+     * Constructor
+     *
+     * Allows optionally specifying identifier(s) to use to pull signals from a
+     * StaticEventManager.
+     *
+     * @param  null|string|int|array|Traversable $identifiers
+     * @return void
+     */
+    public function __construct($identifiers = null)
+    {
+        $this->setIdentifiers($identifiers);
+    }
+
+    /**
+     * Set the event class to utilize
+     *
+     * @param  string $class
+     * @return Zend_EventManager_EventManager
+     */
+    public function setEventClass($class)
+    {
+        $this->eventClass = $class;
+        return $this;
+    }
+
+    /**
+     * Set static connections container
+     *
+     * @param  null|Zend_EventManager_StaticEventCollection $connections
+     * @return void
+     */
+    public function setStaticConnections(Zend_EventManager_StaticEventCollection $connections = null)
+    {
+        if (null === $connections) {
+            $this->staticConnections = false;
+        } else {
+            $this->staticConnections = $connections;
+        }
+        return $this;
+    }
+
+    /**
+     * Get static connections container
+     *
+     * @return false|Zend_EventManager_StaticEventCollection
+     */
+    public function getStaticConnections()
+    {
+        if (null === $this->staticConnections) {
+            $this->setStaticConnections(Zend_EventManager_StaticEventManager::getInstance());
+        }
+        return $this->staticConnections;
+    }
+
+    /**
+     * Get the identifier(s) for this Zend_EventManager_EventManager
+     *
+     * @return array
+     */
+    public function getIdentifiers()
+    {
+        return $this->identifiers;
+    }
+
+    /**
+     * Set the identifiers (overrides any currently set identifiers)
+     *
+     * @param string|int|array|Traversable $identifiers
+     * @return Zend_EventManager_EventManager
+     */
+    public function setIdentifiers($identifiers)
+    {
+        if (is_array($identifiers) || $identifiers instanceof Traversable) {
+            $this->identifiers = array_unique((array) $identifiers);
+        } elseif ($identifiers !== null) {
+            $this->identifiers = array($identifiers);
+        }
+        return $this;
+    }
+
+    /**
+     * Add some identifier(s) (appends to any currently set identifiers)
+     *
+     * @param string|int|array|Traversable $identifiers
+     * @return Zend_EventManager_EventManager
+     */
+    public function addIdentifiers($identifiers)
+    {
+        if (is_array($identifiers) || $identifiers instanceof Traversable) {
+            $this->identifiers = array_unique($this->identifiers + (array) $identifiers);
+        } elseif ($identifiers !== null) {
+            $this->identifiers = array_unique($this->identifiers + array($identifiers));
+        }
+        return $this;
+    }
+
+    /**
+     * Trigger all listeners for a given event
+     *
+     * Can emulate triggerUntil() if the last argument provided is a callback.
+     *
+     * @param  string $event
+     * @param  string|object $target Object calling emit, or symbol describing target (such as static method name)
+     * @param  array|ArrayAccess $argv Array of arguments; typically, should be associative
+     * @param  null|callback $callback
+     * @return Zend_EventManager_ResponseCollection All listener return values
+     */
+    public function trigger($event, $target = null, $argv = array(), $callback = null)
+    {
+        if ($event instanceof Zend_EventManager_EventDescription) {
+            $e        = $event;
+            $event    = $e->getName();
+            $callback = $target;
+        } elseif ($target instanceof Zend_EventManager_EventDescription) {
+            $e = $target;
+            $e->setName($event);
+            $callback = $argv;
+        } elseif ($argv instanceof Zend_EventManager_EventDescription) {
+            $e = $argv;
+            $e->setName($event);
+            $e->setTarget($target);
+        } else {
+            $e = new $this->eventClass();
+            $e->setName($event);
+            $e->setTarget($target);
+            $e->setParams($argv);
+        }
+
+        if ($callback && !is_callable($callback)) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException('Invalid callback provided');
+        }
+
+        return $this->triggerListeners($event, $e, $callback);
+    }
+
+    /**
+     * Trigger listeners until return value of one causes a callback to
+     * evaluate to true
+     *
+     * Triggers listeners until the provided callback evaluates the return
+     * value of one as true, or until all listeners have been executed.
+     *
+     * @param  string $event
+     * @param  string|object $target Object calling emit, or symbol describing target (such as static method name)
+     * @param  array|ArrayAccess $argv Array of arguments; typically, should be associative
+     * @param  Callable $callback
+     * @throws Zend_Stdlib_Exception_InvalidCallbackException if invalid callback provided
+     */
+    public function triggerUntil($event, $target, $argv = null, $callback = null)
+    {
+        if ($event instanceof Zend_EventManager_EventDescription) {
+            $e        = $event;
+            $event    = $e->getName();
+            $callback = $target;
+        } elseif ($target instanceof Zend_EventManager_EventDescription) {
+            $e = $target;
+            $e->setName($event);
+            $callback = $argv;
+        } elseif ($argv instanceof Zend_EventManager_EventDescription) {
+            $e = $argv;
+            $e->setName($event);
+            $e->setTarget($target);
+        } else {
+            $e = new $this->eventClass();
+            $e->setName($event);
+            $e->setTarget($target);
+            $e->setParams($argv);
+        }
+
+        if (!is_callable($callback)) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException('Invalid callback provided');
+        }
+
+        return $this->triggerListeners($event, $e, $callback);
+    }
+
+    /**
+     * Attach a listener to an event
+     *
+     * The first argument is the event, and the next argument describes a
+     * callback that will respond to that event. A CallbackHandler instance
+     * describing the event listener combination will be returned.
+     *
+     * The last argument indicates a priority at which the event should be
+     * executed. By default, this value is 1; however, you may set it for any
+     * integer value. Higher values have higher priority (i.e., execute first).
+     *
+     * You can specify "*" for the event name. In such cases, the listener will 
+     * be triggered for every event.
+     *
+     * @param  string|array|Zend_EventManager_ListenerAggregate $event An event or array of event names. If a ListenerAggregate, proxies to {@link attachAggregate()}.
+     * @param  callback|int $callback If string $event provided, expects PHP callback; for a ListenerAggregate $event, this will be the priority
+     * @param  int $priority If provided, the priority at which to register the callback
+     * @return Zend_Stdlib_CallbackHandler|mixed CallbackHandler if attaching callback (to allow later unsubscribe); mixed if attaching aggregate
+     */
+    public function attach($event, $callback = null, $priority = 1)
+    {
+        // Proxy ListenerAggregate arguments to attachAggregate()
+        if ($event instanceof Zend_EventManager_ListenerAggregate) {
+            return $this->attachAggregate($event, $callback);
+        }
+
+        // Null callback is invalid
+        if (null === $callback) {
+            require_once 'Zend/EventManager/Exception/InvalidArgumentException.php';
+            throw new Zend_EventManager_Exception_InvalidArgumentException(sprintf(
+                '%s: expects a callback; none provided',
+                __METHOD__
+            ));
+        }
+
+        // Array of events should be registered individually, and return an array of all listeners
+        if (is_array($event)) {
+            $listeners = array();
+            foreach ($event as $name) {
+                $listeners[] = $this->attach($name, $callback, $priority);
+            }
+            return $listeners;
+        }
+
+        // If we don't have a priority queue for the event yet, create one
+        if (empty($this->events[$event])) {
+            $this->events[$event] = new Zend_Stdlib_PriorityQueue();
+        }
+
+        // Create a callback handler, setting the event and priority in its metadata
+        $listener = new Zend_Stdlib_CallbackHandler($callback, array('event' => $event, 'priority' => $priority));
+
+        // Inject the callback handler into the queue
+        $this->events[$event]->insert($listener, $priority);
+        return $listener;
+    }
+
+    /**
+     * Attach a listener aggregate
+     *
+     * Listener aggregates accept an EventCollection instance, and call attach()
+     * one or more times, typically to attach to multiple events using local
+     * methods.
+     *
+     * @param  Zend_EventManager_ListenerAggregate $aggregate
+     * @param  int $priority If provided, a suggested priority for the aggregate to use
+     * @return mixed return value of {@link Zend_EventManager_ListenerAggregate::attach()}
+     */
+    public function attachAggregate(Zend_EventManager_ListenerAggregate $aggregate, $priority = 1)
+    {
+        return $aggregate->attach($this, $priority);
+    }
+
+    /**
+     * Unsubscribe a listener from an event
+     *
+     * @param  Zend_Stdlib_CallbackHandler|Zend_EventManager_ListenerAggregate $listener
+     * @return bool Returns true if event and listener found, and unsubscribed; returns false if either event or listener not found
+     * @throws Zend_EventManager_Exception_InvalidArgumentException if invalid listener provided
+     */
+    public function detach($listener)
+    {
+        if ($listener instanceof Zend_EventManager_ListenerAggregate) {
+            return $this->detachAggregate($listener);
+        }
+
+        if (!$listener instanceof Zend_Stdlib_CallbackHandler) {
+            require_once 'Zend/EventManager/Exception/InvalidArgumentException.php';
+            throw new Zend_EventManager_Exception_InvalidArgumentException(sprintf(
+                '%s: expected a Zend_EventManager_ListenerAggregate or Zend_Stdlib_CallbackHandler; received "%s"',
+                __METHOD__,
+                (is_object($listener) ? get_class($listener) : gettype($listener))
+            ));
+        }
+
+        $event = $listener->getMetadatum('event');
+        if (!$event || empty($this->events[$event])) {
+            return false;
+        }
+        $return = $this->events[$event]->remove($listener);
+        if (!$return) {
+            return false;
+        }
+        if (!count($this->events[$event])) {
+            unset($this->events[$event]);
+        }
+        return true;
+    }
+
+    /**
+     * Detach a listener aggregate
+     *
+     * Listener aggregates accept an EventCollection instance, and call detach()
+     * of all previously attached listeners.
+     *
+     * @param  Zend_EventManager_ListenerAggregate $aggregate
+     * @return mixed return value of {@link Zend_EventManager_ListenerAggregate::detach()}
+     */
+    public function detachAggregate(Zend_EventManager_ListenerAggregate $aggregate)
+    {
+        return $aggregate->detach($this);
+    }
+
+    /**
+     * Retrieve all registered events
+     *
+     * @return array
+     */
+    public function getEvents()
+    {
+        return array_keys($this->events);
+    }
+
+    /**
+     * Retrieve all listeners for a given event
+     *
+     * @param  string $event
+     * @return Zend_Stdlib_PriorityQueue
+     */
+    public function getListeners($event)
+    {
+        if (!array_key_exists($event, $this->events)) {
+            return new Zend_Stdlib_PriorityQueue();
+        }
+        return $this->events[$event];
+    }
+
+    /**
+     * Clear all listeners for a given event
+     *
+     * @param  string $event
+     * @return void
+     */
+    public function clearListeners($event)
+    {
+        if (!empty($this->events[$event])) {
+            unset($this->events[$event]);
+        }
+    }
+
+    /**
+     * Prepare arguments
+     *
+     * Use this method if you want to be able to modify arguments from within a
+     * listener. It returns an ArrayObject of the arguments, which may then be
+     * passed to trigger() or triggerUntil().
+     *
+     * @param  array $args
+     * @return ArrayObject
+     */
+    public function prepareArgs(array $args)
+    {
+        return new ArrayObject($args);
+    }
+
+    /**
+     * Trigger listeners
+     *
+     * Actual functionality for triggering listeners, to which both trigger() and triggerUntil()
+     * delegate.
+     *
+     * @param  string           $event Event name
+     * @param  EventDescription $e
+     * @param  null|callback    $callback
+     * @return ResponseCollection
+     */
+    protected function triggerListeners($event, Zend_EventManager_EventDescription $e, $callback = null)
+    {
+        $responses = new Zend_EventManager_ResponseCollection;
+        $listeners = $this->getListeners($event);
+
+        // Add static/wildcard listeners to the list of listeners,
+        // but don't modify the listeners object
+        $staticListeners         = $this->getStaticListeners($event);
+        $staticWildcardListeners = $this->getStaticListeners('*');
+        $wildcardListeners       = $this->getListeners('*');
+        if (count($staticListeners) || count($staticWildcardListeners) || count($wildcardListeners)) {
+            $listeners = clone $listeners;
+        }
+
+        // Static listeners on this specific event
+        $this->insertListeners($listeners, $staticListeners);
+
+        // Static wildcard listeners
+        $this->insertListeners($listeners, $staticWildcardListeners);
+
+        // Add wildcard listeners
+        $this->insertListeners($listeners, $wildcardListeners);
+
+        if ($listeners->isEmpty()) {
+            return $responses;
+        }
+
+        foreach ($listeners as $listener) {
+            // Trigger the listener's callback, and push its result onto the
+            // response collection
+            $responses->push(call_user_func($listener->getCallback(), $e));
+
+            // If the event was asked to stop propagating, do so
+            if ($e->propagationIsStopped()) {
+                $responses->setStopped(true);
+                break;
+            }
+
+            // If the result causes our validation callback to return true,
+            // stop propagation
+            if ($callback && call_user_func($callback, $responses->last())) {
+                $responses->setStopped(true);
+                break;
+            }
+        }
+
+        return $responses;
+    }
+
+    /**
+     * Get list of all listeners attached to the static collection for
+     * identifiers registered by this instance
+     *
+     * @param  string $event
+     * @return array
+     */
+    protected function getStaticListeners($event)
+    {
+        if (!$staticConnections = $this->getStaticConnections()) {
+            return array();
+        }
+
+        $identifiers     = $this->getIdentifiers();
+        $staticListeners = array();
+
+        foreach ($identifiers as $id) {
+            if (!$listeners = $staticConnections->getListeners($id, $event)) {
+                continue;
+            }
+
+            if (!is_array($listeners) && !($listeners instanceof Traversable)) {
+                continue;
+            }
+
+            foreach ($listeners as $listener) {
+                if (!$listener instanceof Zend_Stdlib_CallbackHandler) {
+                    continue;
+                }
+                $staticListeners[] = $listener;
+            }
+        }
+
+        return $staticListeners;
+    }
+
+    /**
+     * Add listeners to the master queue of listeners
+     *
+     * Used to inject static listeners and wildcard listeners.
+     * 
+     * @param  Zend_Stdlib_PriorityQueue $masterListeners 
+     * @param  Zend_Stdlib_PriorityQueue $listeners 
+     * @return void
+     */
+    protected function insertListeners($masterListeners, $listeners)
+    {
+        if (!count($listeners)) {
+            return;
+        }
+
+        foreach ($listeners as $listener) {
+            $priority = $listener->getMetadatum('priority');
+            if (null === $priority) {
+                $priority = 1;
+            } elseif (is_array($priority)) {
+                // If we have an array, likely using PriorityQueue. Grab first
+                // element of the array, as that's the actual priority.
+                $priority = array_shift($priority);
+            }
+            $masterListeners->insert($listener, $priority);
+        }
+    }
+}

+ 36 - 0
library/Zend/EventManager/Exception.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @namespace
+ */
+namespace Zend\EventManager;
+
+/**
+ * Base exception interface
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Exception
+{
+}

+ 39 - 0
library/Zend/EventManager/Exception/InvalidArgumentException.php

@@ -0,0 +1,39 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @namespace
+ */
+namespace Zend\EventManager\Exception;
+
+use Zend\EventManager\Exception;
+
+/**
+ * Invalid argument exception
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class InvalidArgumentException 
+    extends \InvalidArgumentException implements Exception
+{
+}

+ 78 - 0
library/Zend/EventManager/Filter.php

@@ -0,0 +1,78 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Stdlib/CallbackHandler.php';
+
+/**
+ * Interface for intercepting filter chains
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_EventManager_Filter
+{
+    /**
+     * Execute the filter chain
+     * 
+     * @param  string|object $context 
+     * @param  array $params 
+     * @return mixed
+     */
+    public function run($context, array $params = array());
+
+    /**
+     * Attach an intercepting filter
+     * 
+     * @param  callback $callback 
+     * @return Zend_Stdlib_CallbackHandler
+     */
+    public function attach($callback);
+
+    /**
+     * Detach an intercepting filter
+     * 
+     * @param  Zend_Stdlib_CallbackHandler $filter 
+     * @return bool
+     */
+    public function detach(Zend_Stdlib_CallbackHandler $filter);
+
+    /**
+     * Get all intercepting filters
+     * 
+     * @return array
+     */
+    public function getFilters();
+
+    /**
+     * Clear all filters
+     * 
+     * @return void
+     */
+    public function clearFilters();
+
+    /**
+     * Get all filter responses
+     * 
+     * @return Zend_EventManager_ResponseCollection
+     */
+    public function getResponses();
+}

+ 113 - 0
library/Zend/EventManager/Filter/FilterIterator.php

@@ -0,0 +1,113 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Stdlib/CallbackHandler.php';
+require_once 'Zend/Stdlib/SplPriorityQueue.php';
+
+/**
+ * Specialized priority queue implementation for use with an intercepting 
+ * filter chain.
+ *
+ * Allows removal
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_Filter_FilterIterator extends Zend_Stdlib_SplPriorityQueue
+{
+    /**
+     * Does the queue contain a given value?
+     * 
+     * @param  mixed $datum 
+     * @return bool
+     */
+    public function contains($datum)
+    {
+        $chain = clone $this;
+        foreach ($chain as $item) {
+            if ($item === $datum) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Remove a value from the queue
+     *
+     * This is an expensive operation. It must first iterate through all values,
+     * and then re-populate itself. Use only if absolutely necessary.
+     * 
+     * @param  mixed $datum 
+     * @return bool
+     */
+    public function remove($datum)
+    {
+        $this->setExtractFlags(self::EXTR_BOTH);
+
+        // Iterate and remove any matches
+        $removed = false;
+        $items   = array();
+        $this->rewind();
+        while (!$this->isEmpty()) {
+            $item = $this->extract();
+            if ($item['data'] === $datum) {
+                $removed = true;
+                continue;
+            }
+            $items[] = $item;
+        }
+
+        // Repopulate
+        foreach ($items as $item) {
+            $this->insert($item['data'], $item['priority']);
+        }
+
+        $this->setExtractFlags(self::EXTR_DATA);
+        return $removed;
+    }
+
+    /**
+     * Iterate the next filter in the chain
+     *
+     * Iterates and calls the next filter in the chain.
+     * 
+     * @param  mixed $context 
+     * @param  array $params 
+     * @param  Zend_EventManager_Filter_FilterIterator $chain 
+     * @return void
+     */
+    public function next($context = null, array $params = array(), $chain = null)
+    {
+        if (empty($context) || $chain->isEmpty()) {
+            return;
+        }
+
+        $next = $this->extract();
+        if (!$next instanceof Zend_Stdlib_CallbackHandler) {
+            return;
+        }
+
+        $return = call_user_func($next->getCallback(), $context, $params, $chain);
+        return $return;
+    }
+}

+ 138 - 0
library/Zend/EventManager/FilterChain.php

@@ -0,0 +1,138 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/Filter.php';
+require_once 'Zend/EventManager/Filter/FilterIterator.php';
+require_once 'Zend/Stdlib/CallbackHandler.php';
+
+/**
+ * FilterChain: intercepting filter manager
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_FilterChain implements Zend_EventManager_Filter
+{
+    /**
+     * @var Zend_EventManager_Filter_FilterIterator All filters
+     */
+    protected $filters;
+
+    /**
+     * Constructor
+     *
+     * Initializes Zend_EventManager_Filter_FilterIterator in which filters will be aggregated
+     * 
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->filters = new Zend_EventManager_Filter_FilterIterator();
+    }
+
+    /**
+     * Apply the filters
+     *
+     * Begins iteration of the filters.
+     * 
+     * @param  mixed $context Object under observation
+     * @param  mixed $argv Associative array of arguments
+     * @return mixed
+     */
+    public function run($context, array $argv = array())
+    {
+        $chain = clone $this->getFilters();
+
+        if ($chain->isEmpty()) {
+            return;
+        }
+
+        $next = $chain->extract();
+        if (!$next instanceof Zend_Stdlib_CallbackHandler) {
+            return;
+        }
+
+        return call_user_func($next->getCallback(), $context, $argv, $chain);
+    }
+
+    /**
+     * Connect a filter to the chain
+     * 
+     * @param  callback $callback PHP Callback
+     * @param  int $priority Priority in the queue at which to execute; defaults to 1 (higher numbers == higher priority)
+     * @return Zend_Stdlib_CallbackHandler (to allow later unsubscribe)
+     */
+    public function attach($callback, $priority = 1)
+    {
+        if (empty($callback)) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException('No callback provided');
+        }
+        $filter = new Zend_Stdlib_CallbackHandler($callback, array('priority' => $priority));
+        $this->filters->insert($filter, $priority);
+        return $filter;
+    }
+
+    /**
+     * Detach a filter from the chain
+     * 
+     * @param  Zend_Stdlib_CallbackHandler $filter 
+     * @return bool Returns true if filter found and unsubscribed; returns false otherwise
+     */
+    public function detach(Zend_Stdlib_CallbackHandler $filter)
+    {
+        return $this->filters->remove($filter);
+    }
+
+    /**
+     * Retrieve all filters
+     * 
+     * @return Zend_EventManager_Filter_FilterIterator
+     */
+    public function getFilters()
+    {
+        return $this->filters;
+    }
+
+    /**
+     * Clear all filters
+     * 
+     * @return void
+     */
+    public function clearFilters()
+    {
+        $this->filters = new Zend_EventManager_Filter_FilterIterator();
+    }
+
+    /**
+     * Return current responses
+     *
+     * Only available while the chain is still being iterated. Returns the 
+     * current ResponseCollection.
+     * 
+     * @return null|Zend_EventManager_ResponseCollection
+     */
+    public function getResponses()
+    {
+        return $this->responses;
+    }
+}

+ 149 - 0
library/Zend/EventManager/GlobalEventManager.php

@@ -0,0 +1,149 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Stdlib/CallbackHandler.php';
+require_once 'Zend/Stdlib/PriorityQueue.php';
+
+/**
+ * Event manager: notification system
+ *
+ * Use the EventManager when you want to create a per-instance notification 
+ * system for your objects.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_GlobalEventManager
+{
+    /**
+     * @var Zend_EventManager_EventCollection
+     */
+    protected static $events;
+
+    /**
+     * Set the event collection on which this will operate
+     * 
+     * @param  null|Zend_EventManager_EventCollection $events 
+     * @return void
+     */
+    public static function setEventCollection(Zend_EventManager_EventCollection $events = null)
+    {
+        self::$events = $events;
+    }
+
+    /**
+     * Get event collection on which this operates
+     * 
+     * @return void
+     */
+    public static function getEventCollection()
+    {
+        if (null === self::$events) {
+            self::setEventCollection(new Zend_EventManager_EventManager());
+        }
+        return self::$events;
+    }
+
+    /**
+     * Trigger an event
+     * 
+     * @param  string $event 
+     * @param  object|string $context 
+     * @param  array|object $argv 
+     * @return Zend_EventManager_ResponseCollection
+     */
+    public static function trigger($event, $context, $argv = array())
+    {
+        return self::getEventCollection()->trigger($event, $context, $argv);
+    }
+
+    /**
+     * Trigger listeenrs until return value of one causes a callback to evaluate 
+     * to true.
+     * 
+     * @param  string $event 
+     * @param  string|object $context 
+     * @param  array|object $argv 
+     * @param  callback $callback 
+     * @return Zend_EventManager_ResponseCollection
+     */
+    public static function triggerUntil($event, $context, $argv, $callback)
+    {
+        return self::getEventCollection()->triggerUntil($event, $context, $argv, $callback);
+    }
+
+    /**
+     * Attach a listener to an event
+     * 
+     * @param  string $event 
+     * @param  callback $callback 
+     * @param  int $priority 
+     * @return Zend_Stdlib_CallbackHandler
+     */
+    public static function attach($event, $callback, $priority = 1)
+    {
+        return self::getEventCollection()->attach($event, $callback, $priority);
+    }
+
+    /**
+     * Detach a callback from a listener
+     * 
+     * @param  Zend_Stdlib_CallbackHandler $listener 
+     * @return bool
+     */
+    public static function detach(Zend_Stdlib_CallbackHandler $listener)
+    {
+        return self::getEventCollection()->detach($listener);
+    }
+
+    /**
+     * Retrieve list of events this object manages
+     * 
+     * @return array
+     */
+    public static function getEvents()
+    {
+        return self::getEventCollection()->getEvents();
+    }
+
+    /**
+     * Retrieve all listeners for a given event
+     * 
+     * @param  string $event 
+     * @return Zend_Stdlib_PriorityQueue|array
+     */
+    public static function getListeners($event)
+    {
+        return self::getEventCollection()->getListeners($event);
+    }
+
+    /**
+     * Clear all listeners for a given event
+     * 
+     * @param  string $event 
+     * @return void
+     */
+    public static function clearListeners($event)
+    {
+        return self::getEventCollection()->clearListeners($event);
+    }
+}

+ 53 - 0
library/Zend/EventManager/ListenerAggregate.php

@@ -0,0 +1,53 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Interface for self-registering event listeners.
+ *
+ * Classes implementing this interface may be registered by name or instance
+ * with an EventManager, without an event name. The {@link attach()} method will
+ * then be called with the current EventManager instance, allowing the class to
+ * wire up one or more listeners.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_EventManager_ListenerAggregate
+{
+    /**
+     * Attach one or more listeners
+     *
+     * Implementors may add an optional $priority argument; the EventManager
+     * implementation will pass this to the aggregate.
+     *
+     * @param Zend_EventManager_EventCollection $events
+     * @param null|int $priority Optional priority "hint" to use when attaching listeners
+     */
+    public function attach(Zend_EventManager_EventCollection $events);
+
+    /**
+     * Detach all previously attached listeners
+     *
+     * @param Zend_EventManager_EventCollection $events
+     */
+    public function detach(Zend_EventManager_EventCollection $events);
+}

+ 424 - 0
library/Zend/EventManager/ResponseCollection.php

@@ -0,0 +1,424 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (version_compare(PHP_VERSION, '5.3.0', '<')) {
+    class SplStack implements Iterator, ArrayAccess, Countable
+    {
+        /**
+         * Delete items during iteration
+         */
+        const IT_MODE_DELETE = 1;
+
+        /**
+         * Keep items during iteration
+         */
+        const IT_MODE_KEEP = 0;
+
+        /**
+         * Mode used when iterating
+         * @var int
+         */
+        protected $mode = self::IT_MODE_KEEP;
+
+        /**
+         * Count of elements in the stack 
+         * 
+         * @var int
+         */
+        protected $count = 0;
+
+        /**
+         * Data represented by this stack
+         * 
+         * @var array
+         */
+        protected $data = array();
+
+        /**
+         * Sorted stack of values
+         * 
+         * @var false|array
+         */
+        protected $stack = false;
+
+        /**
+         * Set the iterator mode
+         *
+         * Must be set to one of IT_MODE_DELETE or IT_MODE_KEEP
+         * 
+         * @todo   Currently, IteratorMode is ignored, as we use the default (keep); should this be implemented?
+         * @param  int $mode 
+         * @return void
+         * @throws InvalidArgumentException
+         */
+        public function setIteratorMode($mode)
+        {
+            $expected = array(
+                self::IT_MODE_DELETE => true,
+                self::IT_MODE_KEEP => true,
+            );
+
+            if (!isset($expected[$mode])) {
+                throw new InvalidArgumentException(sprintf('Invalid iterator mode specified ("%s")', $mode));
+            }
+
+            $this->mode = $mode;
+        }
+
+        /**
+         * Return last element in the stack
+         * 
+         * @return mixed
+         */
+        public function bottom()
+        {
+            $this->rewind();
+            $value = array_pop($this->stack);
+            array_push($this->stack, $value);
+            return $value;
+        }
+
+        /**
+         * Countable: return count of items in the stack
+         * 
+         * @return int
+         */
+        public function count()
+        {
+            return $this->count;
+        }
+
+        /**
+         * Iterator: return current item in the stack
+         * 
+         * @return mixed
+         */
+        public function current()
+        {
+            if (!$this->stack) {
+                $this->rewind();
+            }
+            return current($this->stack);
+        }
+
+        /**
+         * Get iteration mode
+         * 
+         * @return int
+         */
+        public function getIteratorMode()
+        {
+            return $this->mode;
+        }
+
+        /**
+         * Is the stack empty?
+         *
+         * @return bool
+         */
+        public function isEmpty()
+        {
+            return ($this->count === 0);
+        }
+
+        /**
+         * Iterator: return key of current item in the stack
+         *
+         * @return mixed
+         */
+        public function key()
+        {
+            if (!$this->stack) {
+                $this->rewind();
+            }
+            return key($this->stack);
+        }
+
+        /**
+         * Iterator: advance pointer to next item in the stack
+         * 
+         * @return void
+         */
+        public function next()
+        {
+            if (!$this->stack) {
+                $this->rewind();
+            }
+            return next($this->stack);
+        }
+
+        /**
+         * ArrayAccess: does an item exist at the specified offset?
+         * 
+         * @param  mixed $index 
+         * @return bool
+         */
+        public function offsetExists($index)
+        {
+            return array_key_exists($index, $this->data);
+        }
+
+        /**
+         * ArrayAccess: get the item at the specified offset
+         * 
+         * @param  mixed $index 
+         * @return mixed
+         * @throws OutOfRangeException
+         */
+        public function offsetGet($index)
+        {
+            if (!$this->offsetExists($index)) {
+                throw OutOfRangeException(sprintf('Invalid index ("%s") specified', $index));
+            }
+            return $this->data[$index];
+        }
+
+        /**
+         * ArrayAccess: add an item at the specified offset
+         * 
+         * @param  mixed $index 
+         * @param  mixed $newval 
+         * @return void
+         */
+        public function offsetSet($index, $newval)
+        {
+            $this->data[$index] = $newval;
+            $this->stack = false;
+            $this->count++;
+        }
+
+        /**
+         * ArrayAccess: unset the item at the specified offset
+         * 
+         * @param  mixed $index 
+         * @return void
+         * @throws OutOfRangeException
+         */
+        public function offsetUnset($index)
+        {
+            if (!$this->offsetExists($index)) {
+                throw OutOfRangeException(sprintf('Invalid index ("%s") specified', $index));
+            }
+            unset($this->data[$index]);
+            $this->stack = false;
+            $this->count--;
+        }
+
+        /**
+         * Pop a node from the end of the stack
+         *
+         * @return mixed
+         * @throws RuntimeException
+         */
+        public function pop()
+        {
+            $val         = array_pop($this->data);
+            $this->stack = false;
+            $this->count--;
+            return $val;
+        }
+
+        /**
+         * Move the iterator to the previous node
+         *
+         * @todo   Does this need to be implemented?
+         * @return void
+         */
+        public function prev()
+        {
+        }
+
+        /**
+         * Push an element to the list
+         * 
+         * @param  mixed $value 
+         * @return void
+         */
+        public function push($value)
+        {
+            array_push($this->data, $value);
+            $this->count++;
+            $this->stack  = false;
+        }
+
+        /**
+         * Iterator: rewind to beginning of stack
+         * 
+         * @return void
+         */
+        public function rewind()
+        {
+            if (is_array($this->stack)) {
+                return reset($this->stack);
+            }
+            $this->stack = array_reverse($this->data, true);
+        }
+
+        /**
+         * Serialize the storage
+         *
+         * @return string
+         */
+        public function serialize()
+        {
+            return serialize($this->data);
+        }
+
+        /**
+         * Shifts a node from the beginning of the list
+         *
+         * @return mixed
+         * @throws RuntimeException
+         */
+        public function shift()
+        {
+            $val         = array_shift($this->data);
+            $this->stack = false;
+            $this->count--;
+            return $val;
+        }
+
+        /**
+         * Peek at the top node of the stack
+         * 
+         * @return mixed
+         */
+        public function top()
+        {
+            $this->rewind();
+            $value = array_shift($this->stack);
+            array_unshift($this->stack, $value);
+            return $value;
+        }
+
+        /**
+         * Unserialize the storage
+         *
+         * @param  string
+         * @return void
+         */
+        public function unserialize($serialized)
+        {
+            $this->data  = unserialize($serialized);
+            $this->stack = false;
+        }
+
+        /**
+         * Unshift a node onto the beginning of the list
+         *
+         * @param  mixed $value
+         * @return void
+         */
+        public function unshift($value)
+        {
+            array_unshift($this->data, $value);
+            $this->count++;
+            $this->stack  = false;
+        }
+        
+        /**
+         * Iterator: is the current pointer valid?
+         *
+         * @return bool
+         */
+        public function valid()
+        {
+            $key = key($this->stack);
+            $var = ($key !== null && $key !== false);
+            return $var;
+        }
+    }
+}
+
+/**
+ * Collection of signal handler return values
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_ResponseCollection extends SplStack 
+{
+    protected $stopped = false;
+
+    /**
+     * Did the last response provided trigger a short circuit of the stack?
+     * 
+     * @return bool
+     */
+    public function stopped()
+    {
+        return $this->stopped;
+    }
+
+    /**
+     * Mark the collection as stopped (or its opposite)
+     * 
+     * @param  bool $flag 
+     * @return Zend_EventManager_ResponseCollection
+     */
+    public function setStopped($flag)
+    {
+        $this->stopped = (bool) $flag;
+        return $this;
+    }
+
+    /**
+     * Convenient access to the first handler return value.
+     *
+     * @return mixed The first handler return value
+     */
+    public function first()
+    {
+        return parent::bottom();
+    }
+
+    /**
+     * Convenient access to the last handler return value.
+     *
+     * If the collection is empty, returns null. Otherwise, returns value
+     * returned by last handler.
+     *
+     * @return mixed The last handler return value
+     */
+    public function last()
+    {
+        if (count($this) === 0) {
+            return null;
+        }
+        return parent::top();
+    }
+
+    /**
+     * Check if any of the responses match the given value.
+     *
+     * @param  mixed $value The value to look for among responses
+     */
+    public function contains($value)
+    {
+        foreach ($this as $response) {
+            if ($response === $value) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

+ 32 - 0
library/Zend/EventManager/StaticEventCollection.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Interface for global (static) event listener collections
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_EventManager_StaticEventCollection
+{
+    public function getListeners($id, $event);
+}

+ 194 - 0
library/Zend/EventManager/StaticEventManager.php

@@ -0,0 +1,194 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/EventManager.php';
+require_once 'Zend/EventManager/StaticEventCollection.php';
+require_once 'Zend/Stdlib/CallbackHandler.php';
+
+/**
+ * Static version of EventManager
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_StaticEventManager implements Zend_EventManager_StaticEventCollection
+{
+    /**
+     * @var Zend_EventManager_StaticEventManager
+     */
+    protected static $instance;
+
+    /**
+     * Identifiers with event connections
+     * @var array
+     */
+    protected $identifiers = array();
+
+    /**
+     * Singleton
+     * 
+     * @return void
+     */
+    protected function __construct()
+    {
+    }
+
+    /**
+     * Singleton
+     *
+     * @return void
+     */
+    private function __clone()
+    {
+    }
+
+    /**
+     * Retrieve instance
+     * 
+     * @return Zend_EventManager_StaticEventManager
+     */
+    public static function getInstance()
+    {
+        if (null === self::$instance) {
+            self::$instance = new self();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Reset the singleton instance
+     * 
+     * @return void
+     */
+    public static function resetInstance()
+    {
+        self::$instance = null;
+    }
+
+    /**
+     * Attach a listener to an event
+     *
+     * Allows attaching a callback to an event offerred by one or more 
+     * identifying components. As an example, the following connects to the 
+     * "getAll" event of both an AbstractResource and EntityResource:
+     *
+     * <code>
+     * Zend_EventManager_StaticEventManager::getInstance()->connect(
+     *     array('My_Resource_AbstractResource', 'My_Resource_EntityResource'),
+     *     'getOne',
+     *     function ($e) use ($cache) {
+     *         if (!$id = $e->getParam('id', false)) {
+     *             return;
+     *         }
+     *         if (!$data = $cache->load(get_class($resource) . '::getOne::' . $id )) {
+     *             return;
+     *         }
+     *         return $data;
+     *     }
+     * );
+     * </code>
+     *
+     * Note: a PHP 5.3 closure is used in this example only for brevity; you 
+     * may pass any valid PHP callback as a listener.
+     * 
+     * @param  string|array $id Identifier(s) for event emitting component(s)
+     * @param  string $event 
+     * @param  callback $callback PHP Callback
+     * @param  int $priority Priority at which listener should execute
+     * @return void
+     */
+    public function attach($id, $event, $callback, $priority = 1)
+    {
+        $ids = (array) $id;
+        foreach ($ids as $id) {
+            if (!array_key_exists($id, $this->identifiers)) {
+                $this->identifiers[$id] = new Zend_EventManager_EventManager();
+            }
+            $this->identifiers[$id]->attach($event, $callback, $priority);
+        }
+    }
+
+    /**
+     * Detach a listener from an event offered by a given resource
+     * 
+     * @param  string|int $id
+     * @param  Zend_Stdlib_CallbackHandler $listener 
+     * @return bool Returns true if event and listener found, and unsubscribed; returns false if either event or listener not found
+     */
+    public function detach($id, Zend_Stdlib_CallbackHandler $listener)
+    {
+        if (!array_key_exists($id, $this->identifiers)) {
+            return false;
+        }
+        return $this->identifiers[$id]->detach($listener);
+    }
+
+    /**
+     * Retrieve all registered events for a given resource
+     * 
+     * @param  string|int $id
+     * @return array
+     */
+    public function getEvents($id)
+    {
+        if (!array_key_exists($id, $this->identifiers)) {
+            return false;
+        }
+        return $this->identifiers[$id]->getEvents();
+    }
+
+    /**
+     * Retrieve all listeners for a given identifier and event
+     * 
+     * @param  string|int $id
+     * @param  string|int $event 
+     * @return false|Zend_Stdlib_PriorityQueue
+     */
+    public function getListeners($id, $event)
+    {
+        if (!array_key_exists($id, $this->identifiers)) {
+            return false;
+        }
+        return $this->identifiers[$id]->getListeners($event);
+    }
+
+    /**
+     * Clear all listeners for a given identifier, optionally for a specific event
+     * 
+     * @param  string|int $id 
+     * @param  null|string $event 
+     * @return bool
+     */
+    public function clearListeners($id, $event = null)
+    {
+        if (!array_key_exists($id, $this->identifiers)) {
+            return false;
+        }
+
+        if (null === $event) {
+            unset($this->identifiers[$id]);
+            return true;
+        }
+
+        return $this->identifiers[$id]->clearListeners($event);
+    }
+}

+ 301 - 0
library/Zend/Stdlib/CallbackHandler.php

@@ -0,0 +1,301 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * CallbackHandler
+ *
+ * A handler for a event, event, filterchain, etc. Abstracts PHP callbacks,
+ * primarily to allow for lazy-loading and ensuring availability of default
+ * arguments (currying).
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_CallbackHandler
+{
+    /**
+     * @var string|array PHP callback to invoke
+     */
+    protected $callback;
+
+    /**
+     * Did an error occur when testing the validity of the callback?
+     * @var bool
+     */
+    protected $error = false;
+
+    /**
+     * Callback metadata, if any
+     * @var array
+     */
+    protected $metadata;
+
+    /**
+     * Constructor
+     * 
+     * @param  string $event Event to which slot is subscribed
+     * @param  string|array|object $callback PHP callback 
+     * @param  array $options Options used by the callback handler (e.g., priority)
+     * @return void
+     */
+    public function __construct($callback, array $metadata = array())
+    {
+        $this->metadata  = $metadata;
+        $this->registerCallback($callback);
+    }
+
+    /**
+     * Error handler
+     *
+     * Used by registerCallback() when calling is_callable() to capture engine warnings.
+     * 
+     * @param  int $errno 
+     * @param  string $errstr 
+     * @return void
+     */
+    public function errorHandler($errno, $errstr)
+    {
+        $this->error = true;
+    }
+
+    /**
+     * Registers the callback provided in the constructor
+     *
+     * If you have pecl/weakref {@see http://pecl.php.net/weakref} installed, 
+     * this method provides additional behavior.
+     *
+     * If a callback is a functor, or an array callback composing an object 
+     * instance, this method will pass the object to a WeakRef instance prior
+     * to registering the callback.
+     * 
+     * @param  Callable $callback 
+     * @return void
+     */
+    protected function registerCallback($callback)
+    {
+        set_error_handler(array($this, 'errorHandler'), E_STRICT);
+        $callable = is_callable($callback);
+        restore_error_handler();
+        if (!$callable || $this->error) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException('Invalid callback provided; not callable');
+        }
+
+        // If pecl/weakref is not installed, simply store the callback and return
+        if (!class_exists('WeakRef')) {
+            $this->callback = $callback;
+            return;
+        }
+
+        // If WeakRef exists, we want to use it.
+
+        // If we have a non-closure object, pass it to WeakRef, and then
+        // register it.
+        if (is_object($callback) && !$callback instanceof Closure) {
+            $this->callback = new WeakRef($callback);
+            return;
+        }
+
+        // If we have a string or closure, register as-is
+        if (!is_array($callback)) {
+            $this->callback = $callback;
+            return;
+        }
+
+        list($target, $method) = $callback;
+
+        // If we have an array callback, and the first argument is not an 
+        // object, register as-is
+        if (!is_object($target)) {
+            $this->callback = $callback;
+            return;
+        }
+
+        // We have an array callback with an object as the first argument;
+        // pass it to WeakRef, and then register the new callback
+        $target = new WeakRef($target);
+        $this->callback = array($target, $method);
+    }
+
+    /**
+     * Retrieve registered callback
+     * 
+     * @return Callable
+     */
+    public function getCallback()
+    {
+        $callback = $this->callback;
+
+        // String callbacks -- simply return
+        if (is_string($callback)) {
+            return $callback;
+        }
+
+        // WeakRef callbacks -- pull it out of the object and return it
+        if ($callback instanceof WeakRef) {
+            return $callback->get();
+        }
+
+        // Non-WeakRef object callback -- return it
+        if (is_object($callback)) {
+            return $callback;
+        }
+
+        // Array callback with WeakRef object -- retrieve the object first, and 
+        // then return
+        list($target, $method) = $callback;
+        if ($target instanceof WeakRef) {
+            return array($target->get(), $method);
+        }
+
+        // Otherwise, return it
+        return $callback;
+    }
+
+    /**
+     * Invoke handler
+     * 
+     * @param  array $args Arguments to pass to callback
+     * @return mixed
+     */
+    public function call(array $args = array())
+    {
+        $callback = $this->getCallback();
+
+        $isPhp54 = version_compare(PHP_VERSION, '5.4.0rc1', '>=');
+
+        if ($isPhp54 && is_string($callback)) {
+            $this->validateStringCallbackFor54($callback);
+        }
+
+        // Minor performance tweak; use call_user_func() until > 3 arguments 
+        // reached
+        switch (count($args)) {
+            case 0:
+                if ($isPhp54) {
+                    return $callback();
+                }
+                return call_user_func($callback);
+            case 1:
+                if ($isPhp54) {
+                    return $callback(array_shift($args));
+                }
+                return call_user_func($callback, array_shift($args));
+            case 2:
+                $arg1 = array_shift($args);
+                $arg2 = array_shift($args);
+                if ($isPhp54) {
+                    return $callback($arg1, $arg2);
+                }
+                return call_user_func($callback, $arg1, $arg2);
+            case 3:
+                $arg1 = array_shift($args);
+                $arg2 = array_shift($args);
+                $arg3 = array_shift($args);
+                if ($isPhp54) {
+                    return $callback($arg1, $arg2, $arg3);
+                }
+                return call_user_func($callback, $arg1, $arg2, $arg3);
+            default:
+                return call_user_func_array($callback, $args);
+        }
+    }
+
+    /**
+     * Invoke as functor
+     * 
+     * @return mixed
+     */
+    public function __invoke()
+    {
+        return $this->call(func_get_args());
+    }
+
+    /**
+     * Get all callback metadata
+     * 
+     * @return array
+     */
+    public function getMetadata()
+    {
+        return $this->metadata;
+    }
+
+    /**
+     * Retrieve a single metadatum
+     * 
+     * @param  string $name 
+     * @return mixed
+     */
+    public function getMetadatum($name)
+    {
+        if (array_key_exists($name, $this->metadata)) {
+            return $this->metadata[$name];
+        }
+        return null;
+    }
+
+    /**
+     * Validate a static method call
+     *
+     * Validates that a static method call in PHP 5.4 will actually work
+     * 
+     * @param  string $callback 
+     * @return true
+     * @throws Zend_Stdlib_Exception_InvalidCallbackException if invalid
+     */
+    protected function validateStringCallbackFor54($callback)
+    {
+        if (!strstr($callback, '::')) {
+            return true;
+        }
+
+        list($class, $method) = explode('::', $callback, 2);
+
+        if (!class_exists($class)) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException(sprintf(
+                'Static method call "%s" refers to a class that does not exist',
+                $callback
+            ));
+        }
+
+        $r = new ReflectionClass($class);
+        if (!$r->hasMethod($method)) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException(sprintf(
+                'Static method call "%s" refers to a method that does not exist',
+                $callback
+            ));
+        }
+        $m = $r->getMethod($method);
+        if (!$m->isStatic()) {
+            require_once 'Zend/Stdlib/Exception/InvalidCallbackException.php';
+            throw new Zend_Stdlib_Exception_InvalidCallbackException(sprintf(
+                'Static method call "%s" refers to a method that is not static',
+                $callback
+            ));
+        }
+
+        return true;
+    }
+}

+ 31 - 0
library/Zend/Stdlib/Exception.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Marker interface for Stdlib exceptions
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Stdlib_Exception
+{
+}

+ 35 - 0
library/Zend/Stdlib/Exception/InvalidCallbackException.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Stdlib/Exception.php';
+
+/**
+ * Invalid callback exception
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_Exception_InvalidCallbackException
+    extends DomainException
+    implements Zend_Stdlib_Exception
+{
+}

+ 319 - 0
library/Zend/Stdlib/PriorityQueue.php

@@ -0,0 +1,319 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Stdlib/SplPriorityQueue.php';
+
+/**
+ * Re-usable, serializable priority queue implementation
+ *
+ * SplPriorityQueue acts as a heap; on iteration, each item is removed from the
+ * queue. If you wish to re-use such a queue, you need to clone it first. This 
+ * makes for some interesting issues if you wish to delete items from the queue,
+ * or, as already stated, iterate over it multiple times.
+ *
+ * This class aggregates items for the queue itself, but also composes an 
+ * "inner" iterator in the form of an SplPriorityQueue object for performing
+ * the actual iteration.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_PriorityQueue implements Countable, IteratorAggregate, Serializable
+{
+    const EXTR_DATA     = 0x00000001;
+    const EXTR_PRIORITY = 0x00000002;
+    const EXTR_BOTH     = 0x00000003;
+
+    /**
+     * Inner queue class to use for iteration
+     * @var string
+     */
+    protected $queueClass = 'Zend_Stdlib_SplPriorityQueue';
+
+    /**
+     * Actual items aggregated in the priority queue. Each item is an array
+     * with keys "data" and "priority".
+     * @var array
+     */
+    protected $items      = array();
+
+    /**
+     * Inner queue object
+     * @var SplPriorityQueue
+     */
+    protected $queue;
+
+    /**
+     * Insert an item into the queue
+     *
+     * Priority defaults to 1 (low priority) if none provided.
+     * 
+     * @param  mixed $data 
+     * @param  int $priority 
+     * @return Zend_Stdlib_PriorityQueue
+     */
+    public function insert($data, $priority = 1)
+    {
+        $priority = (int) $priority;
+        $this->items[] = array(
+            'data'     => $data,
+            'priority' => $priority,
+        );
+        $this->getQueue()->insert($data, $priority);
+        return $this;
+    }
+
+    /**
+     * Remove an item from the queue
+     *
+     * This is different than {@link extract()}; its purpose is to dequeue an
+     * item.
+     *
+     * This operation is potentially expensive, as it requires 
+     * re-initialization and re-population of the inner queue.
+     * 
+     * Note: this removes the first item matching the provided item found. If
+     * the same item has been added multiple times, it will not remove other 
+     * instances.
+     *
+     * @param  mixed $datum
+     * @return boolean False if the item was not found, true otherwise.
+     */
+    public function remove($datum)
+    {
+        $found = false;
+        foreach ($this->items as $key => $item) {
+            if ($item['data'] === $datum) {
+                $found = true;
+                break;
+            }
+        }
+        if ($found) {
+            unset($this->items[$key]);
+            $this->queue = null;
+            $queue = $this->getQueue();
+            foreach ($this->items as $item) {
+                $queue->insert($item['data'], $item['priority']);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Is the queue empty?
+     * 
+     * @return bool
+     */
+    public function isEmpty()
+    {
+        return (0 === $this->count());
+    }
+
+    /**
+     * How many items are in the queue?
+     * 
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->items);
+    }
+
+    /**
+     * Peek at the top node in the queue, based on priority.
+     * 
+     * @return mixed
+     */
+    public function top()
+    {
+        return $this->getIterator()->top();
+    }
+
+    /**
+     * Extract a node from the inner queue and sift up 
+     * 
+     * @return mixed
+     */
+    public function extract()
+    {
+        return $this->getQueue()->extract();
+    }
+
+    /**
+     * Retrieve the inner iterator
+     *
+     * SplPriorityQueue acts as a heap, which typically implies that as items
+     * are iterated, they are also removed. This does not work for situations
+     * where the queue may be iterated multiple times. As such, this class 
+     * aggregates the values, and also injects an SplPriorityQueue. This method 
+     * retrieves the inner queue object, and clones it for purposes of 
+     * iteration.
+     * 
+     * @return SplPriorityQueue
+     */
+    public function getIterator()
+    {
+        $queue = $this->getQueue();
+        return clone $queue;
+    }
+
+    /**
+     * Serialize the data structure
+     * 
+     * @return string
+     */
+    public function serialize()
+    {
+        return serialize($this->items);
+    }
+
+    /**
+     * Unserialize a string into a Zend_Stdlib_PriorityQueue object
+     *
+     * Serialization format is compatible with {@link Zend_Stdlib_SplPriorityQueue}
+     * 
+     * @param  string $data 
+     * @return void
+     */
+    public function unserialize($data)
+    {
+        foreach (unserialize($data) as $item) {
+            $this->insert($item['data'], $item['priority']);
+        }
+    }
+
+    /**
+     * Serialize to an array
+     *
+     * By default, returns only the item data, and in the order registered (not
+     * sorted). You may provide one of the EXTR_* flags as an argument, allowing
+     * the ability to return priorities or both data and priority.
+     * 
+     * @param  int $flag 
+     * @return array
+     */
+    public function toArray($flag = self::EXTR_DATA)
+    {
+        switch ($flag) {
+            case self::EXTR_BOTH:
+                return $this->items;
+            case self::EXTR_PRIORITY:
+                return array_map(array($this, 'returnPriority'), $this->items);
+            case self::EXTR_DATA:
+            default:
+                return array_map(array($this, 'returnData'), $this->items);
+        }
+    }
+
+    /**
+     * Specify the internal queue class
+     *
+     * Please see {@link getIterator()} for details on the necessity of an
+     * internal queue class. The class provided should extend SplPriorityQueue.
+     * 
+     * @param  string $class 
+     * @return Zend_Stdlib_PriorityQueue
+     */
+    public function setInternalQueueClass($class)
+    {
+        $this->queueClass = (string) $class;
+        return $this;
+    }
+
+    /**
+     * Does the queue contain the given datum?
+     * 
+     * @param  mixed $datum 
+     * @return bool
+     */
+    public function contains($datum)
+    {
+        foreach ($this->items as $item) {
+            if ($item['data'] === $datum) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Does the queue have an item with the given priority?
+     * 
+     * @param  int $priority 
+     * @return bool
+     */
+    public function hasPriority($priority)
+    {
+        foreach ($this->items as $item) {
+            if ($item['priority'] === $priority) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get the inner priority queue instance
+     * 
+     * @return Zend_Stdlib_SplPriorityQueue
+     */
+    protected function getQueue()
+    {
+        if (null === $this->queue) {
+            $this->queue = new $this->queueClass();
+            if (!$this->queue instanceof SplPriorityQueue) {
+                throw new DomainException(sprintf(
+                    'Zend_Stdlib_PriorityQueue expects an internal queue of type SplPriorityQueue; received "%s"',
+                    get_class($this->queue)
+                ));
+            }
+        }
+        return $this->queue;
+    }
+
+    /**
+     * Return priority from an internal item
+     *
+     * Used as a lambda in toArray().
+     * 
+     * @param  array $item 
+     * @return mixed
+     */
+    public function returnPriority($item)
+    {
+        return $item['priority'];
+    }
+
+    /**
+     * Return data from an internal item
+     *
+     * Used as a lambda in toArray().
+     * 
+     * @param  array $item 
+     * @return mixed
+     */
+    public function returnData($item)
+    {
+        return $item['data'];
+    }
+}

+ 500 - 0
library/Zend/Stdlib/SplPriorityQueue.php

@@ -0,0 +1,500 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (version_compare(PHP_VERSION, '5.3.0', '<')) {
+    /**
+     * SplPriorityQueue 
+     *
+     * PHP 5.2.X userland implementation of PHP's SplPriorityQueue
+     */
+    class SplPriorityQueue implements Iterator , Countable 
+    {
+        /**
+         * Extract data only
+         */
+        const EXTR_DATA = 0x00000001;
+
+        /**
+         * Extract priority only
+         */
+        const EXTR_PRIORITY = 0x00000002;
+
+        /**
+         * Extract an array of ('data' => $value, 'priority' => $priority)
+         */
+        const EXTR_BOTH = 0x00000003;
+
+        /**
+         * Count of items in the queue
+         * @var int
+         */
+        protected $count = 0;
+
+        /**
+         * Flag indicating what should be returned when iterating or extracting
+         * @var int
+         */
+        protected $extractFlags = self::EXTR_DATA;
+
+        /**
+         * @var bool|array
+         */
+        protected $preparedQueue = false;
+
+        /**
+         * All items in the queue
+         * @var array
+         */
+        protected $queue = array();
+
+        /**
+         * Constructor
+         * 
+         * Creates a new, empty queue
+         * 
+         * @return void
+         */
+        public function __construct()
+        {
+        }
+
+        /**
+         * Compare two priorities
+         *
+         * Returns positive integer if $priority1 is greater than $priority2, 0 
+         * if equal, negative otherwise.
+         *
+         * Unused internally, and only included in order to retain the same 
+         * interface as PHP's SplPriorityQueue.
+         *
+         * @param  mixed $priority1
+         * @param  mixed $priority2
+         * @return int
+         */
+        public function compare($priority1, $priority2)
+        {
+            if ($priority1 > $priority2) {
+                return 1;
+            }
+            if ($priority1 == $priority2) {
+                return 0;
+            }
+
+            return -1;
+        }
+
+        /**
+         * Countable: return number of items composed in the queue
+         * 
+         * @return int
+         */
+        public function count()
+        {
+            return $this->count;
+        }
+
+        /**
+         * Iterator: return current item
+         *
+         * @return mixed
+         */
+        public function current()
+        {
+            if (!$this->preparedQueue) {
+                $this->rewind();
+            }
+            if (!$this->count) {
+                throw new OutOfBoundsException('Cannot iterate SplPriorityQueue; no elements present');
+            }
+
+if (!is_array($this->preparedQueue)) {
+    throw new DomainException(sprintf(
+        "Queue was prepared, but is empty?\n    PreparedQueue: %s\n    Internal Queue: %s\n",
+        var_export($this->preparedQueue, 1),
+        var_export($this->queue, 1)
+    ));
+}
+
+            $return      = array_shift($this->preparedQueue);
+            $priority    = $return['priority'];
+            $priorityKey = $return['priorityKey'];
+            $key         = $return['key'];
+            unset($return['key']);
+            unset($return['priorityKey']);
+            unset($this->queue[$priorityKey][$key]);
+
+            switch ($this->extractFlags) {
+                case self::EXTR_DATA:
+                    return $return['data'];
+                case self::EXTR_PRIORITY:
+                    return $return['priority'];
+                case self::EXTR_BOTH:
+                default:
+                    return $return;
+            };
+        }
+
+        /**
+         * Extract a node from top of the heap and sift up
+         *
+         * Returns either the value, the priority, or both, depending on the extract flag.
+         *
+         * @return mixed;
+         */
+        public function extract()
+        {
+            if (!$this->count) {
+                return null;
+            }
+
+            if (!$this->preparedQueue) {
+                $this->prepareQueue();
+            }
+
+            if (empty($this->preparedQueue)) {
+                return null;
+            }
+
+            $return      = array_shift($this->preparedQueue);
+            $priority    = $return['priority'];
+            $priorityKey = $return['priorityKey'];
+            $key         = $return['key'];
+            unset($return['key']);
+            unset($return['priorityKey']);
+            unset($this->queue[$priorityKey][$key]);
+            $this->count--;
+
+            switch ($this->extractFlags) {
+                case self::EXTR_DATA:
+                    return $return['data'];
+                case self::EXTR_PRIORITY:
+                    return $return['priority'];
+                case self::EXTR_BOTH:
+                default:
+                    return $return;
+            };
+        }
+
+        /**
+         * Insert a value into the heap, at the specified priority
+         *
+         * @param  mixed $value
+         * @param  mixed $priority
+         * @return void
+         */
+        public function insert($value, $priority)
+        {
+            if (!is_scalar($priority)) {
+                $priority = serialize($priority);
+            }
+            if (!isset($this->queue[$priority])) {
+                $this->queue[$priority] = array();
+            }
+            $this->queue[$priority][] = $value;
+            $this->count++;
+            $this->preparedQueue = false;
+        }
+
+        /**
+         * Is the queue currently empty?
+         *
+         * @return bool
+         */
+        public function isEmpty()
+        {
+            return (0 == $this->count);
+        }
+
+        /**
+         * Iterator: return current key
+         *
+         * @return mixed Usually an int or string
+         */
+        public function key()
+        {
+            return $this->count;
+        }
+
+        /**
+         * Iterator: Move pointer forward
+         *
+         * @return void
+         */
+        public function next()
+        {
+            $this->count--;
+        }
+
+        /**
+         * Recover from corrupted state and allow further actions on the queue
+         *
+         * Unimplemented, and only included in order to retain the same interface as PHP's 
+         * SplPriorityQueue.
+         *
+         * @return void
+         */
+        public function recoverFromCorruption()
+        {
+        }
+
+        /**
+         * Iterator: Move pointer to first item
+         *
+         * @return void
+         */
+        public function rewind()
+        {
+            if (!$this->preparedQueue) {
+                $this->prepareQueue();
+            }
+        }
+
+        /**
+         * Set the extract flags
+         * 
+         * Defines what is extracted by SplPriorityQueue::current(), 
+         * SplPriorityQueue::top() and SplPriorityQueue::extract().
+         * 
+         * - SplPriorityQueue::EXTR_DATA (0x00000001): Extract the data
+         * - SplPriorityQueue::EXTR_PRIORITY (0x00000002): Extract the priority
+         * - SplPriorityQueue::EXTR_BOTH (0x00000003): Extract an array containing both
+         *
+         * The default mode is SplPriorityQueue::EXTR_DATA.
+         *
+         * @param  int $flags
+         * @return void
+         */
+        public function setExtractFlags($flags)
+        {
+            $expected = array(
+                self::EXTR_DATA => true,
+                self::EXTR_PRIORITY => true,
+                self::EXTR_BOTH => true,
+            );
+            if (!isset($expected[$flags])) {
+                throw new InvalidArgumentException(sprintf('Expected an EXTR_* flag; received %s', $flags));
+            }
+            $this->extractFlags = $flags;
+        }
+
+        /**
+         * Return the value or priority (or both) of the top node, depending on 
+         * the extract flag
+         *
+         * @return mixed
+         */
+        public function top()
+        {
+            $this->sort();
+            $keys = array_keys($this->queue);
+            $key  = array_shift($keys);
+            if (preg_match('/^(a|O):/', $key)) {
+                $key = unserialize($key);
+            }
+
+            if ($this->extractFlags == self::EXTR_PRIORITY) {
+                return $key;
+            }
+
+            if ($this->extractFlags == self::EXTR_DATA) {
+                return $this->queue[$key][0];
+            }
+
+            return array(
+                'data'     => $this->queue[$key][0],
+                'priority' => $key,
+            );
+        }
+
+        /**
+         * Iterator: is the current position valid for the queue
+         *
+         * @return bool
+         */
+        public function valid()
+        {
+            return (bool) $this->count;
+        }
+
+        /**
+         * Sort the queue
+         * 
+         * @return void
+         */
+        protected function sort()
+        {
+            krsort($this->queue);
+        }
+
+        /**
+         * Prepare the queue for iteration and/or extraction
+         * 
+         * @return void
+         */
+        protected function prepareQueue()
+        {
+            $this->sort();
+            $count = $this->count;
+            $queue = array();
+            foreach ($this->queue as $priority => $values) {
+                $priorityKey = $priority;
+                if (preg_match('/^(a|O):/', $priority)) {
+                    $priority = unserialize($priority);
+                }
+                foreach ($values as $key => $value) {
+                    $queue[$count] = array(
+                        'data'        => $value,
+                        'priority'    => $priority,
+                        'priorityKey' => $priorityKey,
+                        'key'         => $key,
+                    );
+                    $count--;
+                }
+            }
+            $this->preparedQueue = $queue;
+        }
+    }
+}
+
+/**
+ * Serializable version of SplPriorityQueue
+ *
+ * Also, provides predictable heap order for datums added with the same priority
+ * (i.e., they will be emitted in the same order they are enqueued).
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_SplPriorityQueue extends SplPriorityQueue implements Serializable
+{
+    /**
+     * @var int Seed used to ensure queue order for items of the same priority
+     */
+    protected $serial = PHP_INT_MAX;
+
+    /**
+     * @var bool
+     */
+    protected $isPhp53;
+
+    /**
+     * Constructor
+     * 
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->isPhp53 = version_compare(PHP_VERSION, '5.3', '>=');
+        parent::__construct();
+    }
+
+    /**
+     * Insert a value with a given priority
+     *
+     * Utilizes {@var $serial} to ensure that values of equal priority are 
+     * emitted in the same order in which they are inserted.
+     * 
+     * @param  mixed $datum 
+     * @param  mixed $priority 
+     * @return void
+     */
+    public function insert($datum, $priority)
+    {
+        // If using the native PHP SplPriorityQueue implementation, we need to
+        // hack around it to ensure that items registered at the same priority
+        // return in the order registered. In the userland version, this is not
+        // necessary.
+        if ($this->isPhp53) {
+            if (!is_array($priority)) {
+                $priority = array($priority, $this->serial--);
+            }
+        }
+        parent::insert($datum, $priority);
+    }
+
+    /**
+     * Serialize to an array
+     *
+     * Array will be priority => data pairs
+     * 
+     * @return array
+     */
+    public function toArray()
+    {
+        $this->setExtractFlags(self::EXTR_BOTH);
+        $array = array();
+        while ($this->valid()) {
+            $array[] = $this->current();
+            $this->next();
+        }
+        $this->setExtractFlags(self::EXTR_DATA);
+
+        // Iterating through a priority queue removes items
+        foreach ($array as $item) {
+            $this->insert($item['data'], $item['priority']);
+        }
+
+        // Return only the data
+        $return = array();
+        foreach ($array as $item) {
+            $return[] = $item['data'];
+        }
+
+        return $return;
+    }
+
+    /**
+     * Serialize
+     * 
+     * @return string
+     */
+    public function serialize()
+    {
+        $data = array();
+        $this->setExtractFlags(self::EXTR_BOTH);
+        while ($this->valid()) {
+            $data[] = $this->current();
+            $this->next();
+        }
+        $this->setExtractFlags(self::EXTR_DATA);
+
+        // Iterating through a priority queue removes items
+        foreach ($data as $item) {
+            $this->insert($item['data'], $item['priority']);
+        }
+
+        return serialize($data);
+    }
+
+    /**
+     * Deserialize
+     * 
+     * @param  string $data
+     * @return void
+     */
+    public function unserialize($data)
+    {
+        foreach (unserialize($data) as $item) {
+            $this->insert($item['data'], $item['priority']);
+        }
+    }
+}

+ 64 - 0
tests/Zend/EventManager/AllTests.php

@@ -0,0 +1,64 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_EventManager_AllTests::main');
+}
+
+require_once 'Zend/EventManager/EventManagerTest.php';
+require_once 'Zend/EventManager/FilterChainTest.php';
+require_once 'Zend/EventManager/GlobalEventManagerTest.php';
+require_once 'Zend/EventManager/StaticEventManagerTest.php';
+require_once 'Zend/EventManager/StaticIntegrationTest.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @group      Zend_EventManager
+ */
+class Zend_EventManager_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_EventManager');
+
+        $suite->addTestSuite('Zend_EventManager_EventManagerTest');
+        $suite->addTestSuite('Zend_EventManager_FilterChainTest');
+        $suite->addTestSuite('Zend_EventManager_GlobalEventManagerTest');
+        $suite->addTestSuite('Zend_EventManager_StaticEventManagerTest');
+        $suite->addTestSuite('Zend_EventManager_StaticIntegrationTest');
+
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_EventManager_AllTests::main') {
+    Zend_EventManager_AllTests::main();
+}

+ 665 - 0
tests/Zend/EventManager/EventManagerTest.php

@@ -0,0 +1,665 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_EventManager_EventManagerTest::main');
+}
+
+require_once 'Zend/EventManager/Event.php';
+require_once 'Zend/EventManager/EventDescription.php';
+require_once 'Zend/EventManager/EventManager.php';
+require_once 'Zend/EventManager/ResponseCollection.php';
+require_once 'Zend/EventManager/TestAsset/Functor.php';
+require_once 'Zend/EventManager/TestAsset/MockAggregate.php';
+require_once 'Zend/Stdlib/CallbackHandler.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_EventManagerTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        if (isset($this->message)) {
+            unset($this->message);
+        }
+        $this->default = '';
+        $this->events  = new Zend_EventManager_EventManager;
+    }
+
+    public function testAttachShouldReturnCallbackHandler()
+    {
+        $listener = $this->events->attach('test', array($this, __METHOD__));
+        $this->assertTrue($listener instanceof Zend_Stdlib_CallbackHandler);
+    }
+
+    public function testAttachShouldAddListenerToEvent()
+    {
+        $listener  = $this->events->attach('test', array($this, __METHOD__));
+        $listeners = $this->events->getListeners('test');
+        $this->assertEquals(1, count($listeners));
+        $this->assertContains($listener, $listeners);
+    }
+
+    public function testAttachShouldAddEventIfItDoesNotExist()
+    {
+        $events = $this->events->getEvents();
+        $this->assertTrue(empty($events), var_export($events, 1));
+        $listener = $this->events->attach('test', array($this, __METHOD__));
+        $events = $this->events->getEvents();
+        $this->assertFalse(empty($events));
+        $this->assertContains('test', $events);
+    }
+
+    public function testAllowsPassingArrayOfEventNamesWhenAttaching()
+    {
+        $callback = array($this, 'returnName');
+        $this->events->attach(array('foo', 'bar'), $callback);
+
+        foreach (array('foo', 'bar') as $event) {
+            $listeners = $this->events->getListeners($event);
+            $this->assertTrue(count($listeners) > 0);
+            foreach ($listeners as $listener) {
+                $this->assertSame($callback, $listener->getCallback());
+            }
+        }
+    }
+
+    public function testPassingArrayOfEventNamesWhenAttachingReturnsArrayOfCallbackHandlers()
+    {
+        $callback = array($this, 'returnName');
+        $listeners = $this->events->attach(array('foo', 'bar'), $callback);
+
+        $this->assertType('array', $listeners);
+
+        foreach ($listeners as $listener) {
+            $this->assertType('Zend_Stdlib_CallbackHandler', $listener);
+            $this->assertSame($callback, $listener->getCallback());
+        }
+    }
+
+    public function testDetachShouldRemoveListenerFromEvent()
+    {
+        $listener  = $this->events->attach('test', array($this, __METHOD__));
+        $listeners = $this->events->getListeners('test');
+        $this->assertContains($listener, $listeners);
+        $this->events->detach($listener);
+        $listeners = $this->events->getListeners('test');
+        $this->assertNotContains($listener, $listeners);
+    }
+
+    public function testDetachShouldReturnFalseIfEventDoesNotExist()
+    {
+        $listener = $this->events->attach('test', array($this, __METHOD__));
+        $this->events->clearListeners('test');
+        $this->assertFalse($this->events->detach($listener));
+    }
+
+    public function testDetachShouldReturnFalseIfListenerDoesNotExist()
+    {
+        $listener1 = $this->events->attach('test', array($this, __METHOD__));
+        $this->events->clearListeners('test');
+        $listener2 = $this->events->attach('test', array($this, 'handleTestEvent'));
+        $this->assertFalse($this->events->detach($listener1));
+    }
+
+    public function testRetrievingAttachedListenersShouldReturnEmptyArrayWhenEventDoesNotExist()
+    {
+        $listeners = $this->events->getListeners('test');
+        $this->assertEquals(0, count($listeners));
+    }
+
+    public function testTriggerShouldTriggerAttachedListeners()
+    {
+        $listener = $this->events->attach('test', array($this, 'handleTestEvent'));
+        $this->events->trigger('test', $this, array('message' => 'test message'));
+        $this->assertEquals('test message', $this->message);
+    }
+
+    public function testTriggerShouldReturnAllListenerReturnValues()
+    {
+        $this->default = '__NOT_FOUND__';
+        $this->events->attach('string.transform', array($this, 'trimString'));
+        $this->events->attach('string.transform', array($this, 'stringRot13'));
+        $responses = $this->events->trigger('string.transform', $this, array('string' => ' foo '));
+        $this->assertTrue($responses instanceof Zend_EventManager_ResponseCollection);
+        $this->assertEquals(2, $responses->count());
+        $this->assertEquals('foo', $responses->first());
+        $this->assertEquals(str_rot13(' foo '), $responses->last());
+    }
+
+    public function testTriggerUntilShouldReturnAsSoonAsCallbackReturnsTrue()
+    {
+        $this->events->attach('foo.bar', array($this, 'stringPosition'));
+        $this->events->attach('foo.bar', array($this, 'stringInString'));
+        $responses = $this->events->triggerUntil(
+            'foo.bar',
+            $this,
+            array('string' => 'foo', 'search' => 'f'),
+            array($this, 'evaluateStringCallback')
+        );
+        $this->assertTrue($responses instanceof Zend_EventManager_ResponseCollection);
+        $this->assertSame(0, $responses->last());
+    }
+
+    public function testTriggerResponseCollectionContains()
+    {
+        $this->events->attach('string.transform', array($this, 'trimString'));
+        $this->events->attach('string.transform', array($this, 'stringRot13'));
+        $responses = $this->events->trigger('string.transform', $this, array('string' => ' foo '));
+        $this->assertEquals(2, count($responses));
+        $this->assertTrue($responses->contains('foo'));
+        $this->assertTrue($responses->contains(str_rot13(' foo ')));
+        $this->assertFalse($responses->contains(' foo '));
+    }
+
+    public function handleTestEvent($e)
+    {
+        $message = $e->getParam('message', '__NOT_FOUND__');
+        $this->message = $message;
+    }
+
+    public function evaluateStringCallback($value)
+    {
+        return (!$value);
+    }
+
+    public function testTriggerUntilShouldMarkResponseCollectionStoppedWhenConditionMet()
+    {
+        $this->events->attach('foo.bar', array($this, 'returnBogus'), 4);
+        $this->events->attach('foo.bar', array($this, 'returnNada'), 3);
+        $this->events->attach('foo.bar', array($this, 'returnFound'), 2);
+        $this->events->attach('foo.bar', array($this, 'returnZero'), 1);
+        $responses = $this->events->triggerUntil('foo.bar', $this, array(), array($this, 'returnOnFound'));
+        $this->assertTrue($responses instanceof Zend_EventManager_ResponseCollection);
+        $this->assertTrue($responses->stopped());
+        $result = $responses->last();
+        $this->assertEquals('found', $result);
+        $this->assertFalse($responses->contains('zero'));
+    }
+
+    public function testTriggerUntilShouldMarkResponseCollectionStoppedWhenConditionMetByLastListener()
+    {
+        $this->events->attach('foo.bar', array($this, 'returnBogus'));
+        $this->events->attach('foo.bar', array($this, 'returnNada'));
+        $this->events->attach('foo.bar', array($this, 'returnZero'));
+        $this->events->attach('foo.bar', array($this, 'returnFound'));
+        $responses = $this->events->triggerUntil('foo.bar', $this, array(), array($this, 'returnOnFound'));
+        $this->assertTrue($responses instanceof Zend_EventManager_ResponseCollection);
+        $this->assertTrue($responses->stopped());
+        $this->assertEquals('found', $responses->last());
+    }
+
+    public function testResponseCollectionIsNotStoppedWhenNoCallbackMatchedByTriggerUntil()
+    {
+        $this->events->attach('foo.bar', array($this, 'returnBogus'), 4);
+        $this->events->attach('foo.bar', array($this, 'returnNada'), 3);
+        $this->events->attach('foo.bar', array($this, 'returnZero'), 1);
+        $this->events->attach('foo.bar', array($this, 'returnFound'), 2);
+        $responses = $this->events->triggerUntil('foo.bar', $this, array(), array($this, 'returnOnNeverFound'));
+        $this->assertTrue($responses instanceof Zend_EventManager_ResponseCollection);
+        $this->assertFalse($responses->stopped());
+        $this->assertEquals('zero', $responses->last());
+    }
+
+    public function testCanAttachListenerAggregate()
+    {
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attachAggregate($aggregate);
+        $events = $this->events->getEvents();
+        foreach (array('foo.bar', 'foo.baz') as $event) {
+            $this->assertContains($event, $events);
+        }
+    }
+
+    public function testCanAttachListenerAggregateViaAttach()
+    {
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attach($aggregate);
+        $events = $this->events->getEvents();
+        foreach (array('foo.bar', 'foo.baz') as $event) {
+            $this->assertContains($event, $events);
+        }
+    }
+
+    public function testAttachAggregateReturnsAttachOfListenerAggregate()
+    {
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $method    = $this->events->attachAggregate($aggregate);
+        $this->assertSame('Zend_EventManager_TestAsset_MockAggregate::attach', $method);
+    }
+
+    public function testCanDetachListenerAggregates()
+    {
+        // setup some other event listeners, to ensure appropriate items are detached
+        $listenerFooBar1 = $this->events->attach('foo.bar', array($this, 'returnTrue'));
+        $listenerFooBar2 = $this->events->attach('foo.bar', array($this, 'returnTrue'));
+        $listenerFooBaz1 = $this->events->attach('foo.baz', array($this, 'returnTrue'));
+        $listenerOther   = $this->events->attach('other', array($this, 'returnTrue'));
+
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attachAggregate($aggregate);
+        $this->events->detachAggregate($aggregate);
+        $events = $this->events->getEvents();
+        foreach (array('foo.bar', 'foo.baz', 'other') as $event) {
+            $this->assertContains($event, $events);
+        }
+
+        $listeners = $this->events->getListeners('foo.bar');
+        $this->assertEquals(2, count($listeners));
+        $this->assertContains($listenerFooBar1, $listeners);
+        $this->assertContains($listenerFooBar2, $listeners);
+
+        $listeners = $this->events->getListeners('foo.baz');
+        $this->assertEquals(1, count($listeners));
+        $this->assertContains($listenerFooBaz1, $listeners);
+
+        $listeners = $this->events->getListeners('other');
+        $this->assertEquals(1, count($listeners));
+        $this->assertContains($listenerOther, $listeners);
+    }
+
+    public function testCanDetachListenerAggregatesViaDetach()
+    {
+        // setup some other event listeners, to ensure appropriate items are detached
+        $listenerFooBar1 = $this->events->attach('foo.bar', array($this, 'returnTrue'));
+        $listenerFooBar2 = $this->events->attach('foo.bar', array($this, 'returnTrue'));
+        $listenerFooBaz1 = $this->events->attach('foo.baz', array($this, 'returnTrue'));
+        $listenerOther   = $this->events->attach('other',   array($this, 'returnTrue'));
+
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attach($aggregate);
+        $this->events->detach($aggregate);
+        $events = $this->events->getEvents();
+        foreach (array('foo.bar', 'foo.baz', 'other') as $event) {
+            $this->assertContains($event, $events);
+        }
+
+        $listeners = $this->events->getListeners('foo.bar');
+        $this->assertEquals(2, count($listeners));
+        $this->assertContains($listenerFooBar1, $listeners);
+        $this->assertContains($listenerFooBar2, $listeners);
+
+        $listeners = $this->events->getListeners('foo.baz');
+        $this->assertEquals(1, count($listeners));
+        $this->assertContains($listenerFooBaz1, $listeners);
+
+        $listeners = $this->events->getListeners('other');
+        $this->assertEquals(1, count($listeners));
+        $this->assertContains($listenerOther, $listeners);
+    }
+
+    public function testDetachAggregateReturnsDetachOfListenerAggregate()
+    {
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attachAggregate($aggregate);
+        $method = $this->events->detachAggregate($aggregate);
+        $this->assertSame('Zend_EventManager_TestAsset_MockAggregate::detach', $method);
+    }
+
+    public function testAttachAggregateAcceptsOptionalPriorityValue()
+    {
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attachAggregate($aggregate, 1);
+        $this->assertEquals(1, $aggregate->priority);
+    }
+
+    public function testAttachAggregateAcceptsOptionalPriorityValueViaAttachCallbackArgument()
+    {
+        $aggregate = new Zend_EventManager_TestAsset_MockAggregate();
+        $this->events->attach($aggregate, 1);
+        $this->assertEquals(1, $aggregate->priority);
+    }
+
+    public function testCallingEventsStopPropagationMethodHaltsEventEmission()
+    {
+        $this->events->attach('foo.bar', array($this, 'returnBogus'), 4);
+        $this->events->attach('foo.bar', array($this, 'returnNadaAndStopPropagation'), 3);
+        $this->events->attach('foo.bar', array($this, 'returnFound'), 2);
+        $this->events->attach('foo.bar', array($this, 'returnZero'), 1);
+        $responses = $this->events->trigger('foo.bar', $this, array());
+        $this->assertTrue($responses instanceof Zend_EventManager_ResponseCollection);
+        $this->assertTrue($responses->stopped());
+        $this->assertEquals('nada', $responses->last());
+        $this->assertTrue($responses->contains('bogus'));
+        $this->assertFalse($responses->contains('found'));
+        $this->assertFalse($responses->contains('zero'));
+    }
+
+    public function testCanAlterParametersWithinAEvent()
+    {
+        $this->foo = 'bar';
+        $this->bar = 'baz';
+        $this->events->attach('foo.bar', array($this, 'setParamFoo'));
+        $this->events->attach('foo.bar', array($this, 'setParamBar'));
+        $this->events->attach('foo.bar', array($this, 'returnParamsFooAndBar'));
+        $responses = $this->events->trigger('foo.bar', $this, array());
+        $this->assertEquals('bar:baz', $responses->last());
+    }
+
+    public function testParametersArePassedToEventByReference()
+    {
+        $this->foo = 'FOO';
+        $this->bar = 'BAR';
+        $params = array( 'foo' => 'bar', 'bar' => 'baz');
+        $args   = $this->events->prepareArgs($params);
+        $this->events->attach('foo.bar', array($this, 'setParamFoo'));
+        $this->events->attach('foo.bar', array($this, 'setParamBar'));
+        $responses = $this->events->trigger('foo.bar', $this, $args);
+        $this->assertEquals('FOO', $args['foo']);
+        $this->assertEquals('BAR', $args['bar']);
+    }
+
+    public function testCanPassObjectForEventParameters()
+    {
+        $this->foo = 'FOO';
+        $this->bar = 'BAR';
+        $params = (object) array( 'foo' => 'bar', 'bar' => 'baz');
+        $this->events->attach('foo.bar', array($this, 'setParamFoo'));
+        $this->events->attach('foo.bar', array($this, 'setParamBar'));
+        $responses = $this->events->trigger('foo.bar', $this, $params);
+        $this->assertEquals('FOO', $params->foo);
+        $this->assertEquals('BAR', $params->bar);
+    }
+
+    public function testCanPassEventObjectAsSoleArgumentToTrigger()
+    {
+        $event = new Zend_EventManager_Event();
+        $event->setName(__FUNCTION__);
+        $event->setTarget($this);
+        $event->setParams(array('foo' => 'bar'));
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+        $responses = $this->events->trigger($event);
+        $this->assertSame($event, $responses->last());
+    }
+
+    public function testCanPassEventNameAndEventObjectAsSoleArgumentsToTrigger()
+    {
+        $event = new Zend_EventManager_Event();
+        $event->setTarget($this);
+        $event->setParams(array('foo' => 'bar'));
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+        $responses = $this->events->trigger(__FUNCTION__, $event);
+        $this->assertSame($event, $responses->last());
+        $this->assertEquals(__FUNCTION__, $event->getName());
+    }
+
+    public function testCanPassEventObjectAsArgvToTrigger()
+    {
+        $event = new Zend_EventManager_Event();
+        $event->setParams(array('foo' => 'bar'));
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+        $responses = $this->events->trigger(__FUNCTION__, $this, $event);
+        $this->assertSame($event, $responses->last());
+        $this->assertEquals(__FUNCTION__, $event->getName());
+        $this->assertSame($this, $event->getTarget());
+    }
+
+    public function testCanPassEventObjectAndCallbackAsSoleArgumentsToTriggerUntil()
+    {
+        $event = new Zend_EventManager_Event();
+        $event->setName(__FUNCTION__);
+        $event->setTarget($this);
+        $event->setParams(array('foo' => 'bar'));
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+        $responses = $this->events->triggerUntil($event, array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+        $this->assertSame($event, $responses->last());
+    }
+
+    public function testCanPassEventNameAndEventObjectAndCallbackAsSoleArgumentsToTriggerUntil()
+    {
+        $event = new Zend_EventManager_Event();
+        $event->setTarget($this);
+        $event->setParams(array('foo' => 'bar'));
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+        $responses = $this->events->triggerUntil(__FUNCTION__, $event, array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+        $this->assertSame($event, $responses->last());
+        $this->assertEquals(__FUNCTION__, $event->getName());
+    }
+
+    public function testCanPassEventObjectAsArgvToTriggerUntil()
+    {
+        $event = new Zend_EventManager_Event();
+        $event->setParams(array('foo' => 'bar'));
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+        $responses = $this->events->triggerUntil(__FUNCTION__, $this, $event, array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+        $this->assertSame($event, $responses->last());
+        $this->assertEquals(__FUNCTION__, $event->getName());
+        $this->assertSame($this, $event->getTarget());
+    }
+
+    public function testTriggerCanTakeAnOptionalCallbackArgumentToEmulateTriggerUntil()
+    {
+        $this->events->attach(__FUNCTION__, array($this, 'returnEvent'));
+
+        // Four scenarios:
+        // First: normal signature:
+        $responses = $this->events->trigger(__FUNCTION__, $this, array(), array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+
+        // Second: Event as $argv parameter:
+        $event = new Zend_EventManager_Event();
+        $responses = $this->events->trigger(__FUNCTION__, $this, $event, array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+
+        // Third: Event as $target parameter:
+        $event = new Zend_EventManager_Event();
+        $event->setTarget($this);
+        $responses = $this->events->trigger(__FUNCTION__, $event, array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+
+        // Fourth: Event as $event parameter:
+        $event = new Zend_EventManager_Event();
+        $event->setTarget($this);
+        $event->setName(__FUNCTION__);
+        $responses = $this->events->trigger($event, array($this, 'returnOnEvent'));
+        $this->assertTrue($responses->stopped());
+    }
+
+    public function testWeakRefsAreHonoredWhenTriggering()
+    {
+        if (version_compare(PHP_VERSION, '5.3.0', '<')) {
+            $this->markTestSkipped('Requires PHP >= 5.3.0 as it tests functors');
+        }
+        if (!class_exists('WeakRef', false)) {
+            $this->markTestSkipped('Requires pecl/weakref');
+        }
+
+        $functor = new Zend_EventManager_TestAsset_Functor;
+        $this->events->attach('test', $functor);
+
+        unset($functor);
+
+        $result = $this->events->trigger('test', $this, array());
+        $message = $result->last();
+        $this->assertNull($message);
+    }
+
+    public function testDuplicateIdentifiersAreNotRegistered()
+    {
+        $events = new Zend_EventManager_EventManager(array(__CLASS__, get_class($this)));
+        $identifiers = $events->getIdentifiers();
+        $this->assertSame(count($identifiers), 1);
+        $this->assertSame($identifiers[0], __CLASS__);
+        $events->addIdentifiers(__CLASS__);
+        $this->assertSame(count($identifiers), 1);
+        $this->assertSame($identifiers[0], __CLASS__);
+    }
+
+    public function testIdentifierGetterSettersWorkWithArrays()
+    {
+        $identifiers = array('foo', 'bar');
+        $this->assertType('Zend_EventManager_EventManager', $this->events->setIdentifiers($identifiers));
+        $this->assertSame($this->events->getIdentifiers(), $identifiers);
+        $identifiers[] = 'baz';
+        $this->assertType('Zend_EventManager_EventManager', $this->events->addIdentifiers($identifiers));
+        $this->assertSame($this->events->getIdentifiers(), $identifiers);
+    }
+
+    public function testIdentifierGetterSettersWorkWithTraversables()
+    {
+        $identifiers = new ArrayIterator(array('foo', 'bar'));
+        $this->assertType('Zend_EventManager_EventManager', $this->events->setIdentifiers($identifiers));
+        $this->assertSame($this->events->getIdentifiers(), (array) $identifiers);
+        $identifiers = new ArrayIterator(array('foo', 'bar', 'baz'));
+        $this->assertType('Zend_EventManager_EventManager', $this->events->addIdentifiers($identifiers));
+        $this->assertSame($this->events->getIdentifiers(), (array) $identifiers);
+    }
+
+    public function testListenersAttachedWithWildcardAreTriggeredForAllEvents()
+    {
+        $this->test         = new stdClass;
+        $this->test->events = array();
+        $callback           = array($this, 'setEventName');
+
+        $this->events->attach('*', $callback);
+        foreach (array('foo', 'bar', 'baz') as $event) {
+            $this->events->trigger($event);
+            $this->assertContains($event, $this->test->events);
+        }
+    }
+
+    /*
+     * Listeners used in tests
+     */
+
+    public function returnName($e)
+    {
+        return $e->getName();
+    }
+
+    public function trimString($e)
+    {
+        $string = $e->getParam('string', $this->default);
+        return trim($string);
+    }
+
+    public function stringRot13($e)
+    {
+        $string = $e->getParam('string', $this->default);
+        return str_rot13($string);
+    }
+
+    public function stringPosition($e)
+    {
+        $string = $e->getParam('string', '');
+        $search = $e->getParam('search', '?');
+        return strpos($string, $search);
+    }
+
+    public function stringInString($e)
+    {
+        $string = $e->getParam('string', '');
+        $search = $e->getParam('search', '?');
+        return strstr($string, $search);
+    }
+
+    public function returnBogus()
+    {
+        return 'bogus';
+    }
+
+    public function returnNada()
+    {
+        return 'nada';
+    }
+
+    public function returnFound()
+    {
+        return 'found';
+    }
+
+    public function returnZero()
+    {
+        return 'zero';
+    }
+
+    public function returnTrue()
+    {
+        return true;
+    }
+
+    public function returnNadaAndStopPropagation($e)
+    {
+        $e->stopPropagation(true);
+        return 'nada';
+    }
+
+    public function setParamFoo($e)
+    {
+        $e->setParam('foo', $this->foo);
+    }
+
+    public function setParamBar($e)
+    {
+        $e->setParam('bar', $this->bar);
+    }
+
+    public function returnParamsFooAndBar($e)
+    {
+        $foo = $e->getParam('foo', '__NO_FOO__');
+        $bar = $e->getParam('bar', '__NO_BAR__');
+        return $foo . ":" . $bar;
+    }
+
+    public function returnEvent($e)
+    {
+        return $e;
+    }
+
+    public function setEventName($e)
+    {
+        $this->test->events[] = $e->getName();
+    }
+
+    public function returnOnFound($result)
+    {
+        return ($result === 'found');
+    }
+
+    public function returnOnNeverFound($result)
+    {
+        return ($result === 'never found');
+    }
+
+    public function returnOnEvent($result)
+    {
+        return ($result instanceof Zend_EventManager_EventDescription);
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_EventManager_EventManagerTest::main') {
+    Zend_EventManager_EventManagerTest::main();
+}

+ 168 - 0
tests/Zend/EventManager/FilterChainTest.php

@@ -0,0 +1,168 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id:$
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_EventManager_FilterChainTest::main');
+}
+
+require_once 'Zend/EventManager/FilterChain.php';
+require_once 'Zend/Stdlib/CallbackHandler.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_FilterChainTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        if (isset($this->message)) {
+            unset($this->message);
+        }
+        $this->filterchain = new Zend_EventManager_FilterChain;
+    }
+
+    public function testSubscribeShouldReturnCallbackHandler()
+    {
+        $handle = $this->filterchain->attach(array( $this, __METHOD__ ));
+        $this->assertTrue($handle instanceof Zend_Stdlib_CallbackHandler);
+    }
+
+    public function testSubscribeShouldAddCallbackHandlerToFilters()
+    {
+        $handler  = $this->filterchain->attach(array($this, __METHOD__));
+        $handlers = $this->filterchain->getFilters();
+        $this->assertEquals(1, count($handlers));
+        $this->assertTrue($handlers->contains($handler));
+    }
+
+    public function testDetachShouldRemoveCallbackHandlerFromFilters()
+    {
+        $handle = $this->filterchain->attach(array( $this, __METHOD__ ));
+        $handles = $this->filterchain->getFilters();
+        $this->assertTrue($handles->contains($handle));
+        $this->filterchain->detach($handle);
+        $handles = $this->filterchain->getFilters();
+        $this->assertFalse($handles->contains($handle));
+    }
+
+    public function testDetachShouldReturnFalseIfCallbackHandlerDoesNotExist()
+    {
+        $handle1 = $this->filterchain->attach(array( $this, __METHOD__ ));
+        $this->filterchain->clearFilters();
+        $handle2 = $this->filterchain->attach(array( $this, 'handleTestTopic' ));
+        $this->assertFalse($this->filterchain->detach($handle1));
+    }
+
+    public function testRetrievingAttachedFiltersShouldReturnEmptyArrayWhenNoFiltersExist()
+    {
+        $handles = $this->filterchain->getFilters();
+        $this->assertEquals(0, count($handles));
+    }
+
+    public function testFilterChainShouldReturnLastResponse()
+    {
+        $this->filterchain->attach(array($this, 'filterTrim'));
+        $this->filterchain->attach(array($this, 'filterStrRot13'));
+        $value = $this->filterchain->run($this, array('string' => ' foo '));
+        $this->assertEquals(str_rot13(trim(' foo ')), $value);
+    }
+
+    public function testFilterIsPassedContextAndArguments()
+    {
+        $this->filterchain->attach(array( $this, 'filterTestCallback1' ));
+        $obj = (object) array('foo' => 'bar', 'bar' => 'baz');
+        $value = $this->filterchain->run($this, array('object' => $obj));
+        $this->assertEquals('filtered', $value);
+        $this->assertEquals('filterTestCallback1', $this->message);
+        $this->assertEquals('foobarbaz', $obj->foo);
+    }
+
+    public function testInterceptingFilterShouldReceiveChain()
+    {
+        $this->filterchain->attach(array($this, 'filterReceivalCallback'));
+        $this->filterchain->run($this);
+    }
+
+    public function testFilteringStopsAsSoonAsAFilterFailsToCallNext()
+    {
+        $this->filterchain->attach(array($this, 'filterTrim'), 10000);
+        $this->filterchain->attach(array($this, 'filterStrRot13'), 1000);
+        $this->filterchain->attach(array($this, 'filterHashMd5'), 100);
+        $value = $this->filterchain->run($this, array('string' => ' foo '));
+        $this->assertEquals(str_rot13(trim(' foo ')), $value);
+    }
+
+    public function handleTestTopic($message)
+    {
+        $this->message = $message;
+    }
+
+    public function filterTestCallback1($context, array $params)
+    {
+        $context->message = __FUNCTION__;
+        if (isset($params['object']) && is_object($params['object'])) {
+            $params['object']->foo = 'foobarbaz';
+        }
+        return 'filtered';
+    }
+
+    public function filterReceivalCallback($context, array $params, $chain)
+    {
+        $this->assertType('Zend_EventManager_Filter_FilterIterator', $chain);
+    }
+
+    public function filterTrim($context, $params, $chain)
+    {
+        if (isset($params['string'])) {
+            $params['string'] = trim($params['string']);
+        }
+        $return =  $chain->next($context, $params, $chain);
+        return $return;
+    }
+
+    public function filterStrRot13($context, array $params)
+    {
+        $string = isset($params['string']) ? $params['string'] : '';
+        return str_rot13($string);
+    }
+
+    public function filterHashMd5($context, $params, $chain)
+    {
+        $string = isset($params['string']) ? $params['string'] : '';
+        return hash('md5', $string);
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_EventManager_FilterChainTest::main') {
+    Zend_EventManager_FilterChainTest::main();
+}

+ 122 - 0
tests/Zend/EventManager/GlobalEventManagerTest.php

@@ -0,0 +1,122 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_EventManager_GlobalEventManagerTest::main');
+}
+
+require_once 'Zend/EventManager/GlobalEventManager.php';
+require_once 'Zend/EventManager/EventManager.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_GlobalEventManagerTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        Zend_EventManager_GlobalEventManager::setEventCollection(null);
+    }
+
+    public function testStoresAnEventManagerInstanceByDefault()
+    {
+        $events = Zend_EventManager_GlobalEventManager::getEventCollection();
+        $this->assertType('Zend_EventManager_EventManager', $events);
+    }
+
+    public function testPassingNullValueForEventCollectionResetsInstance()
+    {
+        $events = Zend_EventManager_GlobalEventManager::getEventCollection();
+        $this->assertType('Zend_EventManager_EventManager', $events);
+        Zend_EventManager_GlobalEventManager::setEventCollection(null);
+        $events2 = Zend_EventManager_GlobalEventManager::getEventCollection();
+        $this->assertType('Zend_EventManager_EventManager', $events2);
+        $this->assertNotSame($events, $events2);
+    }
+
+    public function testProxiesAllStaticOperationsToEventCollectionInstance()
+    {
+        $this->test = new stdClass();
+        $listener = Zend_EventManager_GlobalEventManager::attach('foo.bar', array($this, 'aggregateEventMetadata'));
+        $this->assertType('Zend_Stdlib_CallbackHandler', $listener);
+
+        Zend_EventManager_GlobalEventManager::trigger('foo.bar', $this, array('foo' => 'bar'));
+        $this->assertSame($this, $this->test->target);
+        $this->assertEquals('foo.bar', $this->test->event);
+        $this->assertEquals(array('foo' => 'bar'), $this->test->params);
+
+        $results = Zend_EventManager_GlobalEventManager::triggerUntil('foo.bar', $this, array('baz' => 'bat'), array($this, 'returnOnArray'));
+        $this->assertTrue($results->stopped());
+        $this->assertEquals(array('baz' => 'bat'), $this->test->params);
+        $this->assertEquals(array('baz' => 'bat'), $results->last());
+
+        $events = Zend_EventManager_GlobalEventManager::getEvents();
+        $this->assertEquals(array('foo.bar'), $events);
+
+        $listeners = Zend_EventManager_GlobalEventManager::getListeners('foo.bar');
+        $this->assertEquals(1, count($listeners));
+        $this->assertTrue($listeners->contains($listener));
+
+        Zend_EventManager_GlobalEventManager::detach($listener);
+        $events = Zend_EventManager_GlobalEventManager::getEvents();
+        $this->assertEquals(array(), $events);
+
+        $this->test = new stdClass;
+        $listener = Zend_EventManager_GlobalEventManager::attach('foo.bar', array($this, 'aggregateEventMetadata'));
+        $events = Zend_EventManager_GlobalEventManager::getEvents();
+        $this->assertEquals(array('foo.bar'), $events);
+        Zend_EventManager_GlobalEventManager::clearListeners('foo.bar');
+        $events = Zend_EventManager_GlobalEventManager::getEvents();
+        $this->assertEquals(array(), $events);
+    }
+
+    /*
+     * Listeners used in tests
+     */
+
+    public function aggregateEventMetadata($e)
+    {
+        $this->test->event  = $e->getName();
+        $this->test->target = $e->getTarget();
+        $this->test->params = $e->getParams();
+        return $this->test->params;
+    }
+
+    public function returnOnArray($result)
+    {
+        return is_array($result);
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_EventManager_GlobalEventManagerTest::main') {
+    Zend_EventManager_GlobalEventManagerTest::main();
+}

+ 268 - 0
tests/Zend/EventManager/StaticEventManagerTest.php

@@ -0,0 +1,268 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_EventManager_StaticEventManagerTest::main');
+}
+
+require_once 'Zend/EventManager/EventManager.php';
+require_once 'Zend/EventManager/StaticEventManager.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_StaticEventManagerTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        Zend_EventManager_StaticEventManager::resetInstance();
+    }
+
+    public function tearDown()
+    {
+        Zend_EventManager_StaticEventManager::resetInstance();
+    }
+
+    public function testOperatesAsASingleton()
+    {
+        $expected = Zend_EventManager_StaticEventManager::getInstance();
+        $test     = Zend_EventManager_StaticEventManager::getInstance();
+        $this->assertSame($expected, $test);
+    }
+
+    public function testCanResetInstance()
+    {
+        $original = Zend_EventManager_StaticEventManager::getInstance();
+        Zend_EventManager_StaticEventManager::resetInstance();
+        $test = Zend_EventManager_StaticEventManager::getInstance();
+        $this->assertNotSame($original, $test);
+    }
+
+    public function testSingletonInstanceIsInstanceOfClass()
+    {
+        $this->assertType('Zend_EventManager_StaticEventManager', Zend_EventManager_StaticEventManager::getInstance());
+    }
+
+    public function testCanAttachCallbackToEvent()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('foo', 'bar', array($this, __FUNCTION__));
+        $this->assertContains('bar', $events->getEvents('foo'));
+        $expected  = array($this, __FUNCTION__);
+        $found     = false;
+        $listeners = $events->getListeners('foo', 'bar');
+        $this->assertType('Zend_Stdlib_PriorityQueue', $listeners);
+        $this->assertTrue(0 < count($listeners), 'Empty listeners!');
+        foreach ($listeners as $listener) {
+            if ($expected === $listener->getCallback()) {
+                $found = true;
+                break;
+            }
+        }
+        $this->assertTrue($found, 'Did not find listener!');
+    }
+
+    public function testCanAttachCallbackToMultipleEventsAtOnce()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('bar', array('foo', 'test'), array($this, __FUNCTION__));
+        $this->assertContains('foo', $events->getEvents('bar'));
+        $this->assertContains('test', $events->getEvents('bar'));
+        $expected = array($this, __FUNCTION__);
+        foreach (array('foo', 'test') as $event) {
+            $found     = false;
+            $listeners = $events->getListeners('bar', $event);
+            $this->assertType('Zend_Stdlib_PriorityQueue', $listeners);
+            $this->assertTrue(0 < count($listeners), 'Empty listeners!');
+            foreach ($listeners as $listener) {
+                if ($expected === $listener->getCallback()) {
+                    $found = true;
+                    break;
+                }
+            }
+            $this->assertTrue($found, 'Did not find listener!');
+        }
+    }
+
+    public function testCanAttachSameEventToMultipleResourcesAtOnce()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach(array('foo', 'test'), 'bar', array($this, __FUNCTION__));
+        $this->assertContains('bar', $events->getEvents('foo'));
+        $this->assertContains('bar', $events->getEvents('test'));
+        $expected = array($this, __FUNCTION__);
+        foreach (array('foo', 'test') as $id) {
+            $found     = false;
+            $listeners = $events->getListeners($id, 'bar');
+            $this->assertType('Zend_Stdlib_PriorityQueue', $listeners);
+            $this->assertTrue(0 < count($listeners), 'Empty listeners!');
+            foreach ($listeners as $listener) {
+                if ($expected === $listener->getCallback()) {
+                    $found = true;
+                    break;
+                }
+            }
+            $this->assertTrue($found, 'Did not find listener!');
+        }
+    }
+
+    public function testCanAttachCallbackToMultipleEventsOnMultipleResourcesAtOnce()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach(array('bar', 'baz'), array('foo', 'test'), array($this, __FUNCTION__));
+        $this->assertContains('foo', $events->getEvents('bar'));
+        $this->assertContains('test', $events->getEvents('bar'));
+        $expected = array($this, __FUNCTION__);
+        foreach (array('bar', 'baz') as $resource) {
+            foreach (array('foo', 'test') as $event) {
+                $found     = false;
+                $listeners = $events->getListeners($resource, $event);
+                $this->assertType('Zend_Stdlib_PriorityQueue', $listeners);
+                $this->assertTrue(0 < count($listeners), 'Empty listeners!');
+                foreach ($listeners as $listener) {
+                    if ($expected === $listener->getCallback()) {
+                        $found = true;
+                        break;
+                    }
+                }
+                $this->assertTrue($found, 'Did not find listener!');
+            }
+        }
+    }
+
+    public function testListenersAttachedUsingWildcardEventWillBeTriggeredByResource()
+    {
+        $this->test         = new stdClass;
+        $this->test->events = array();
+        $callback           = array($this, 'setEventName');
+
+        $staticEvents = Zend_EventManager_StaticEventManager::getInstance();
+        $staticEvents->attach('bar', '*', $callback);
+
+        $events = new Zend_EventManager_EventManager('bar');
+
+        foreach (array('foo', 'bar', 'baz') as $event) {
+            $events->trigger($event);
+            $this->assertContains($event, $this->test->events);
+        }
+    }
+
+    public function testCanDetachListenerFromResource()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('foo', 'bar', array($this, __FUNCTION__));
+        foreach ($events->getListeners('foo', 'bar') as $listener) {
+            // only one; retrieving it so we can detach
+        }
+        $events->detach('foo', $listener);
+        $listeners = $events->getListeners('foo', 'bar');
+        $this->assertEquals(0, count($listeners));
+    }
+
+    public function testCanGetEventsByResource()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('foo', 'bar', array($this, __FUNCTION__));
+        $this->assertEquals(array('bar'), $events->getEvents('foo'));
+    }
+
+    public function testCanGetListenersByResourceAndEvent()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('foo', 'bar', array($this, __FUNCTION__));
+        $listeners = $events->getListeners('foo', 'bar');
+        $this->assertType('Zend_Stdlib_PriorityQueue', $listeners);
+        $this->assertEquals(1, count($listeners));
+    }
+
+    public function testCanClearListenersByResource()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('foo', 'bar', array($this, __FUNCTION__));
+        $events->attach('foo', 'baz', array($this, __FUNCTION__));
+        $events->clearListeners('foo');
+        $this->assertFalse($events->getListeners('foo', 'bar'));
+        $this->assertFalse($events->getListeners('foo', 'baz'));
+    }
+
+    public function testCanClearListenersByResourceAndEvent()
+    {
+        $events = Zend_EventManager_StaticEventManager::getInstance();
+        $events->attach('foo', 'bar', array($this, __FUNCTION__));
+        $events->attach('foo', 'baz', array($this, __FUNCTION__));
+        $events->attach('foo', 'bat', array($this, __FUNCTION__));
+        $events->clearListeners('foo', 'baz');
+        $this->assertType('Zend_Stdlib_PriorityQueue', $events->getListeners('foo', 'baz'));
+        $this->assertEquals(0, count($events->getListeners('foo', 'baz')));
+        $this->assertType('Zend_Stdlib_PriorityQueue', $events->getListeners('foo', 'bar'));
+        $this->assertEquals(1, count($events->getListeners('foo', 'bar')));
+        $this->assertType('Zend_Stdlib_PriorityQueue', $events->getListeners('foo', 'bat'));
+        $this->assertEquals(1, count($events->getListeners('foo', 'bat')));
+    }
+
+    public function testCanPassArrayOfIdentifiersToConstructor()
+    {
+        $identifiers = array('foo', 'bar');
+        $manager = new Zend_EventManager_EventManager($identifiers);
+    }
+
+    public function testListenersAttachedToAnyIdentifierProvidedToEventManagerWillBeTriggered()
+    {
+        $identifiers           = array('foo', 'bar');
+        $manager               = new Zend_EventManager_EventManager($identifiers);
+        $events                = Zend_EventManager_StaticEventManager::getInstance();
+        $this->test            = new stdClass;
+        $this->test->triggered = 0;
+        $events->attach('foo', 'bar', array($this, 'advanceTriggered'));
+        $events->attach('foo', 'bar', array($this, 'advanceTriggered'));
+        $manager->trigger('bar', $this, array());
+        $this->assertEquals(2, $this->test->triggered);
+    }
+
+    /*
+     * Listeners used in tests
+     */
+
+    public function setEventName($e)
+    {
+        $this->test->events[] = $e->getName();
+    }
+
+    public function advanceTriggered($e)
+    {
+        $this->test->triggered++;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_EventManager_StaticEventManagerTest::main') {
+    Zend_EventManager_StaticEventManagerTest::main();
+}

+ 173 - 0
tests/Zend/EventManager/StaticIntegrationTest.php

@@ -0,0 +1,173 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_EventManager_StaticIntegrationTest::main');
+}
+
+require_once 'Zend/EventManager/EventManager.php';
+require_once 'Zend/EventManager/StaticEventManager.php';
+require_once 'Zend/EventManager/TestAsset/ClassWithEvents.php';
+require_once 'Zend/EventManager/TestAsset/StaticEventsMock.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_StaticIntegrationTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        Zend_EventManager_StaticEventManager::resetInstance();
+    }
+
+    public function testCanConnectStaticallyToClassWithEvents()
+    {
+        $this->counter = (object) array('count' => 0);
+        Zend_EventManager_StaticEventManager::getInstance()->attach(
+            'Zend_EventManager_TestAsset_ClassWithEvents', 
+            'foo', 
+            array($this, 'advanceCounter')
+        );
+        $class = new Zend_EventManager_TestAsset_ClassWithEvents();
+        $class->foo();
+        $this->assertEquals(1, $this->counter->count);
+    }
+
+    public function testLocalHandlersAreExecutedPriorToStaticHandlersWhenSetWithSamePriority()
+    {
+        $this->test = (object) array('results' => array());
+        Zend_EventManager_StaticEventManager::getInstance()->attach(
+            'Zend_EventManager_TestAsset_ClassWithEvents', 
+            'foo', 
+            array($this, 'aggregateStatic')
+        );
+        $class = new Zend_EventManager_TestAsset_ClassWithEvents();
+        $class->events()->attach('foo', array($this, 'aggregateLocal'));
+        $class->foo();
+        $this->assertEquals(array('local', 'static'), $this->test->results);
+    }
+
+    public function testLocalHandlersAreExecutedInPriorityOrderRegardlessOfStaticOrLocalRegistration()
+    {
+        $this->test = (object) array('results' => array());
+        Zend_EventManager_StaticEventManager::getInstance()->attach(
+            'Zend_EventManager_TestAsset_ClassWithEvents', 
+            'foo', 
+            array($this, 'aggregateStatic'),
+            10000 // high priority
+        );
+        $class = new Zend_EventManager_TestAsset_ClassWithEvents();
+        $class->events()->attach('foo', array($this, 'aggregateLocal'), 1); // low priority
+        $class->events()->attach('foo', array($this, 'aggregateLocal2'), 1000); // medium priority
+        $class->events()->attach('foo', array($this, 'aggregateLocal3'), 15000); // highest priority
+        $class->foo();
+        $this->assertEquals(array('local3', 'static', 'local2', 'local'), $this->test->results);
+    }
+
+    public function testPassingNullValueToSetStaticConnectionsDisablesStaticConnections()
+    {
+        $this->counter = (object) array('count' => 0);
+        Zend_EventManager_StaticEventManager::getInstance()->attach(
+            'Zend_EventManager_TestAsset_ClassWithEvents', 
+            'foo', 
+            array($this, 'advanceCounter')
+        );
+        $class = new Zend_EventManager_TestAsset_ClassWithEvents();
+        $class->events()->setStaticConnections(null);
+        $class->foo();
+        $this->assertEquals(0, $this->counter->count);
+    }
+
+    public function testCanPassAlternateStaticConnectionsHolder()
+    {
+        $this->counter = (object) array('count' => 0);
+        Zend_EventManager_StaticEventManager::getInstance()->attach(
+            'Zend_EventManager_TestAsset_ClassWithEvents', 
+            'foo', 
+            array($this, 'advanceCounter')
+        );
+        $mockStaticEvents = new Zend_EventManager_TestAsset_StaticEventsMock();
+        $class = new Zend_EventManager_TestAsset_ClassWithEvents();
+        $class->events()->setStaticConnections($mockStaticEvents);
+        $this->assertSame($mockStaticEvents, $class->events()->getStaticConnections());
+        $class->foo();
+        $this->assertEquals(0, $this->counter->count);
+    }
+
+    public function testTriggerMergesPrioritiesOfStaticAndInstanceListeners()
+    {
+        $this->test = (object) array('results' => array());
+        Zend_EventManager_StaticEventManager::getInstance()->attach(
+            'Zend_EventManager_TestAsset_ClassWithEvents', 
+            'foo', 
+            array($this, 'aggregateStatic'),
+            100
+        );
+        $class = new Zend_EventManager_TestAsset_ClassWithEvents();
+        $class->events()->attach('foo', array($this, 'aggregateLocal'), -100);
+        $class->foo();
+        $this->assertEquals(array('static', 'local'), $this->test->results);
+    }
+
+    /*
+     * Listeners used in tests
+     */
+
+    public function advanceCounter($e)
+    {
+        $this->counter->count++;
+    }
+
+    public function aggregateStatic($e)
+    {
+        $this->test->results[] = 'static';
+    }
+
+    public function aggregateLocal($e)
+    {
+        $this->test->results[] = 'local';
+    }
+
+    public function aggregateLocal2($e)
+    {
+        $this->test->results[] = 'local2';
+    }
+
+    public function aggregateLocal3($e)
+    {
+        $this->test->results[] = 'local3';
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_EventManager_StaticIntegrationTest::main') {
+    Zend_EventManager_StaticIntegrationTest::main();
+}

+ 52 - 0
tests/Zend/EventManager/TestAsset/ClassWithEvents.php

@@ -0,0 +1,52 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/EventCollection.php';
+require_once 'Zend/EventManager/EventManager.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_TestAsset_ClassWithEvents
+{
+    protected $events;
+
+    public function events(Zend_EventManager_EventCollection $events = null)
+    {
+        if (null !== $events) {
+            $this->events = $events;
+        }
+        if (null === $this->events) {
+            $this->events = new Zend_EventManager_EventManager(__CLASS__);
+        }
+        return $this->events;
+    }
+
+    public function foo()
+    {
+        $this->events()->trigger(__FUNCTION__, $this, array());
+    }
+}

+ 9 - 0
tests/Zend/EventManager/TestAsset/Functor.php

@@ -0,0 +1,9 @@
+<?php
+
+class Zend_EventManager_TestAsset_Functor
+{
+    public function __invoke($e)
+    {
+        return __METHOD__;
+    }
+}

+ 70 - 0
tests/Zend/EventManager/TestAsset/MockAggregate.php

@@ -0,0 +1,70 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/EventCollection.php';
+require_once 'Zend/EventManager/ListenerAggregate.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_TestAsset_MockAggregate implements Zend_EventManager_ListenerAggregate
+{
+
+    protected $listeners = array();
+    public $priority;
+
+    public function attach(Zend_EventManager_EventCollection $events, $priority = null)
+    {
+        $this->priority = $priority;
+
+        $listeners = array();
+        $listeners[] = $events->attach('foo.bar', array( $this, 'fooBar' ));
+        $listeners[] = $events->attach('foo.baz', array( $this, 'fooBaz' ));
+
+        $this->listeners[ spl_object_hash($events) ] = $listeners;
+
+        return __METHOD__;
+    }
+
+    public function detach(Zend_EventManager_EventCollection $events)
+    {
+        foreach ($this->listeners[ spl_object_hash($events) ] as $listener) {
+            $events->detach($listener);
+        }
+
+        return __METHOD__;
+    }
+
+    public function fooBar()
+    {
+        return __METHOD__;
+    }
+
+    public function fooBaz()
+    {
+        return __METHOD__;
+    }
+}

+ 38 - 0
tests/Zend/EventManager/TestAsset/StaticEventsMock.php

@@ -0,0 +1,38 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/EventManager/StaticEventCollection.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_EventManager
+ * @subpackage UnitTests
+ * @group      Zend_EventManager
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_EventManager_TestAsset_StaticEventsMock implements Zend_EventManager_StaticEventCollection
+{
+    public function getListeners($id, $event)
+    {
+        return array();
+    }
+}

+ 60 - 0
tests/Zend/Stdlib/AllTests.php

@@ -0,0 +1,60 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Stdlib_AllTests::main');
+}
+
+require_once 'Zend/Stdlib/CallbackHandlerTest.php';
+require_once 'Zend/Stdlib/PriorityQueueTest.php';
+require_once 'Zend/Stdlib/SplPriorityQueueTest.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @group      Zend_Stdlib
+ */
+class Zend_EventManager_AllTests
+{
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Zend Framework - Zend_Stdlib');
+
+        $suite->addTestSuite('Zend_Stdlib_CallbackHandlerTest');
+        $suite->addTestSuite('Zend_Stdlib_PriorityQueueTest');
+        $suite->addTestSuite('Zend_Stdlib_SplPriorityQueueTest');
+
+        return $suite;
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Stdlib_AllTests::main') {
+    Zend_Stdlib_AllTests::main();
+}

+ 143 - 0
tests/Zend/Stdlib/CallbackHandlerTest.php

@@ -0,0 +1,143 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id:$
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Stdlib_CallbackHandlerTest::main');
+}
+
+require_once 'Zend/Stdlib/CallbackHandler.php';
+require_once 'Zend/Stdlib/TestAsset/SignalHandlers/InstanceMethod.php';
+require_once 'Zend/Stdlib/TestAsset/SignalHandlers/ObjectCallback.php';
+
+/**
+ * @todo       Remove all closures from tests and refactor as methods or functions
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @group      Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_CallbackHandlerTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        if (isset($this->args)) {
+            unset($this->args);
+        }
+        $this->error = false;
+    }
+
+    public function testCallbackShouldStoreMetadata()
+    {
+        $handler = new Zend_Stdlib_CallbackHandler('rand', array('event' => 'foo'));
+        $this->assertEquals('foo', $handler->getMetadatum('event'));
+        $this->assertEquals(array('event' => 'foo'), $handler->getMetadata());
+    }
+
+    public function testCallbackShouldBeStringIfNoHandlerPassedToConstructor()
+    {
+        $handler = new Zend_Stdlib_CallbackHandler('rand');
+        $this->assertSame('rand', $handler->getCallback());
+    }
+
+    public function testCallbackShouldBeArrayIfHandlerPassedToConstructor()
+    {
+        $handler = new Zend_Stdlib_CallbackHandler(array('Zend_Stdlib_TestAsset_SignalHandlers_ObjectCallback', 'test'));
+        $this->assertSame(array('Zend_Stdlib_TestAsset_SignalHandlers_ObjectCallback', 'test'), $handler->getCallback());
+    }
+
+    public function testCallShouldInvokeCallbackWithSuppliedArguments()
+    {
+        $handler = new Zend_Stdlib_CallbackHandler(array( $this, 'handleCall' ));
+        $args   = array('foo', 'bar', 'baz');
+        $handler->call($args);
+        $this->assertSame($args, $this->args);
+    }
+
+    public function testPassingInvalidCallbackShouldRaiseInvalidCallbackExceptionDuringInstantiation()
+    {
+        $this->setExpectedException('Zend_Stdlib_Exception_InvalidCallbackException');
+        $handler = new Zend_Stdlib_CallbackHandler('boguscallback');
+    }
+
+    public function testCallShouldReturnTheReturnValueOfTheCallback()
+    {
+        $handler = new Zend_Stdlib_CallbackHandler(array('Zend_Stdlib_TestAsset_SignalHandlers_ObjectCallback', 'test'));
+        if (!is_callable(array('Zend_Stdlib_TestAsset_SignalHandlers_ObjectCallback', 'test'))) {
+            echo "\nClass exists? " . var_export(class_exists('Zend_Stdlib_TestAsset_SignalHandlers_ObjectCallback'), 1) . "\n";
+            echo "Include path: " . get_include_path() . "\n";
+        }
+        $this->assertEquals('bar', $handler->call(array()));
+    }
+
+    public function testStringCallbackResolvingToClassDefiningInvokeNameShouldRaiseException()
+    {
+        $this->setExpectedException('Zend_Stdlib_Exception_InvalidCallbackException');
+        $handler = new Zend_Stdlib_CallbackHandler('Zend_Stdlib_TestAsset_SignalHandlers_Invokable');
+    }
+
+    public function testStringCallbackReferringToClassWithoutDefinedInvokeShouldRaiseException()
+    {
+        $this->setExpectedException('Zend_Stdlib_Exception_InvalidCallbackException');
+        $class   = new Zend_Stdlib_TestAsset_SignalHandlers_InstanceMethod();
+        $handler = new Zend_Stdlib_CallbackHandler($class);
+    }
+
+    public function errorHandler($errno, $errstr)
+    {
+        $this->error = true;
+    }
+
+    public function testCallbackConsistingOfStringContextWithNonStaticMethodShouldRaiseException()
+    {
+        $this->setExpectedException('Zend_Stdlib_Exception_InvalidCallbackException');
+        $handler     = new Zend_Stdlib_CallbackHandler(array('Zend_Stdlib_TestAsset_SignalHandlers_InstanceMethod', 'handler'));
+    }
+
+    public function testStringCallbackConsistingOfNonStaticMethodShouldRaiseException()
+    {
+        $this->setExpectedException('Zend_Stdlib_Exception_InvalidCallbackException');
+        $handler = new Zend_Stdlib_CallbackHandler('Zend_Stdlib_TestAsset_SignalHandlers_InstanceMethod::handler');
+    }
+
+    public function testCallbackToClassImplementingOverloadingButNotInvocableShouldRaiseException()
+    {
+        $this->setExpectedException('Zend_Stdlib_Exception_InvalidCallbackException');
+        $handler = new Zend_Stdlib_CallbackHandler('foo', array( 'Zend_Stdlib_TestAsset_SignalHandlers_Overloadable', 'foo' ));
+    }
+
+    public function handleCall()
+    {
+        $this->args = func_get_args();
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Stdlib_CallbackHandlerTest::main') {
+    Zend_Stdlib_CallbackHandlerTest::main();
+}

+ 146 - 0
tests/Zend/Stdlib/PriorityQueueTest.php

@@ -0,0 +1,146 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Stdlib_PriorityQueueTest::main');
+}
+
+require_once 'Zend/Stdlib/PriorityQueue.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @group      Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_PriorityQueueTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        $this->queue = new Zend_Stdlib_PriorityQueue();
+        $this->queue->insert('foo', 3);
+        $this->queue->insert('bar', 4);
+        $this->queue->insert('baz', 2);
+        $this->queue->insert('bat', 1);
+    }
+
+    public function testSerializationAndDeserializationShouldMaintainState()
+    {
+        $s = serialize($this->queue);
+        $unserialized = unserialize($s);
+        $count = count($this->queue);
+        $this->assertSame($count, count($unserialized), 'Expected count ' . $count . '; received ' . count($unserialized));
+
+        $expected = array();
+        foreach ($this->queue as $item) {
+            $expected[] = $item;
+        }
+        $test = array();
+        foreach ($unserialized as $item) {
+            $test[] = $item;
+        }
+        $this->assertSame($expected, $test, 'Expected: ' . var_export($expected, 1) . "\nReceived:" . var_export($test, 1));
+    }
+
+    public function testRetrievingQueueAsArrayReturnsDataOnlyByDefault()
+    {
+        $expected = array(
+            'foo',
+            'bar',
+            'baz',
+            'bat',
+        );
+        $test     = $this->queue->toArray();
+        $this->assertSame($expected, $test, var_export($test, 1));
+    }
+
+    public function testCanCastToArrayOfPrioritiesOnly()
+    {
+        $expected = array(
+            3,
+            4,
+            2,
+            1,
+        );
+        $test     = $this->queue->toArray(Zend_Stdlib_PriorityQueue::EXTR_PRIORITY);
+        $this->assertSame($expected, $test, var_export($test, 1));
+    }
+
+    public function testCanCastToArrayOfDataPriorityPairs()
+    {
+        $expected = array(
+            array('data' => 'foo', 'priority' => 3),
+            array('data' => 'bar', 'priority' => 4),
+            array('data' => 'baz', 'priority' => 2),
+            array('data' => 'bat', 'priority' => 1),
+        );
+        $test     = $this->queue->toArray(Zend_Stdlib_PriorityQueue::EXTR_BOTH);
+        $this->assertSame($expected, $test, var_export($test, 1));
+    }
+
+    public function testCanIterateMultipleTimesAndReceiveSameResults()
+    {
+        $expected = array('bar', 'foo', 'baz', 'bat');
+
+        for ($i = 1; $i < 3; $i++) {
+            $test = array();
+            foreach ($this->queue as $item) {
+                $test[] = $item;
+            }
+            $this->assertEquals($expected, $test, 'Failed at iteration ' . $i);
+        }
+    }
+
+    public function testCanRemoveItemFromQueue()
+    {
+        $this->queue->remove('baz');
+        $expected = array('bar', 'foo', 'bat');
+        $test = array();
+        foreach ($this->queue as $item) {
+            $test[] = $item;
+        }
+        $this->assertEquals($expected, $test);
+    }
+
+    public function testCanTestForExistenceOfItemInQueue()
+    {
+        $this->assertTrue($this->queue->contains('foo'));
+        $this->assertFalse($this->queue->contains('foobar'));
+    }
+
+    public function testCanTestForExistenceOfPriorityInQueue()
+    {
+        $this->assertTrue($this->queue->hasPriority(3));
+        $this->assertFalse($this->queue->hasPriority(1000));
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Stdlib_PriorityQueueTest::main') {
+    Zend_Stdlib_PriorityQueueTest::main();
+}

+ 103 - 0
tests/Zend/Stdlib/SplPriorityQueueTest.php

@@ -0,0 +1,103 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id:$
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Zend_Stdlib_SplPriorityQueueTest::main');
+}
+
+require_once 'Zend/Stdlib/SplPriorityQueue.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Stdlib
+ * @subpackage UnitTests
+ * @group      Zend_Stdlib
+ * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Stdlib_SplPriorityQueueTest extends PHPUnit_Framework_TestCase
+{
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite(__CLASS__);
+        $result = PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    public function setUp()
+    {
+        $this->queue = new Zend_Stdlib_SplPriorityQueue();
+        $this->queue->insert('foo', 3);
+        $this->queue->insert('bar', 4);
+        $this->queue->insert('baz', 2);
+        $this->queue->insert('bat', 1);
+    }
+
+    public function testMaintainsInsertOrderForDataOfEqualPriority()
+    {
+        $queue = new Zend_Stdlib_SplPriorityQueue();
+        $queue->insert('foo', 1000);
+        $queue->insert('bar', 1000);
+        $queue->insert('baz', 1000);
+        $queue->insert('bat', 1000);
+
+        $expected = array('foo', 'bar', 'baz', 'bat');
+        $test     = array();
+        foreach ($queue as $datum) {
+            $test[] = $datum;
+        }
+        $this->assertEquals($expected, $test);
+    }
+
+    public function testSerializationAndDeserializationShouldMaintainState()
+    {
+        $s = serialize($this->queue);
+        $unserialized = unserialize($s);
+        $count = count($this->queue);
+        $this->assertSame($count, count($unserialized), 'Expected count ' . $count . '; received ' . count($unserialized));
+
+        $expected = array();
+        foreach ($this->queue as $item) {
+            $expected[] = $item;
+        }
+        $test = array();
+        foreach ($unserialized as $item) {
+            $test[] = $item;
+        }
+        $this->assertSame($expected, $test, 'Expected: ' . var_export($expected, 1) . "\nReceived:" . var_export($test, 1));
+    }
+
+    public function testCanRetrieveQueueAsArray()
+    {
+        $expected = array(
+            'bar', 
+            'foo', 
+            'baz', 
+            'bat',
+        );
+        $test     = $this->queue->toArray();
+        $this->assertSame($expected, $test, var_export($test, 1));
+    }
+}
+
+if (PHPUnit_MAIN_METHOD == 'Zend_Stdlib_SplPriorityQueueTest::main') {
+    Zend_Stdlib_SplPriorityQueueTest::main();
+}

+ 8 - 0
tests/Zend/Stdlib/TestAsset/SignalHandlers/InstanceMethod.php

@@ -0,0 +1,8 @@
+<?php
+class Zend_Stdlib_TestAsset_SignalHandlers_InstanceMethod
+{
+    public function handler()
+    {
+        return __FUNCTION__;
+    }
+}

+ 8 - 0
tests/Zend/Stdlib/TestAsset/SignalHandlers/Invokable.php

@@ -0,0 +1,8 @@
+<?php
+class Zend_Stdlib_TestAsset_SignalHandlers_Invokable
+{
+    public function __invoke()
+    {
+        return __FUNCTION__;
+    }
+}

+ 8 - 0
tests/Zend/Stdlib/TestAsset/SignalHandlers/ObjectCallback.php

@@ -0,0 +1,8 @@
+<?php
+class Zend_Stdlib_TestAsset_SignalHandlers_ObjectCallback
+{
+    public static function test()
+    {
+        return 'bar';
+    }
+}

+ 8 - 0
tests/Zend/Stdlib/TestAsset/SignalHandlers/Overloadable.php

@@ -0,0 +1,8 @@
+<?php
+class Zend_Stdlib_TestAsset_SignalHandlers_Overloadable
+{
+    public function __call($method, $args)
+    {
+        return $method;
+    }
+}