ZendFramework2ガイド

解析編

ビューその2(render~finish)

renderイベント

ビューその1」の続きです。
その1では、bootstrapイベントでの初期設定からdispatchイベントでのビューコンポーネントのイベント処理登録までを取り上げました。
その2ではその続きのrenderイベント以降の処理を解説します。



DispatchListener::onDispatchが終了すると、dispatchイベントが終了となり、Applicationクラスのrunメソッドに処理が戻り、dispatchイベントのトリガー以降の処理へ進みます。

vendor/ZF2/library/Zend/Mvc/Application.php
class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    public function run()
    {
                ・
                ・
                ・

        $result = $events->trigger(MvcEvent::EVENT_DISPATCH, $event, $shortCircuit);

        $response = $result->last();
        if ($response instanceof ResponseInterface) {
            $event->setTarget($this);
            $event->setResponse($response);
            $events->trigger(MvcEvent::EVENT_FINISH, $event);
            $this->response = $response;
            return $this;
        }

        $response = $this->response;
        $event->setResponse($response);
        $this->completeRequest($event);

        return $this;
    }

    protected function completeRequest(MvcEvent $event)
    {
        $events = $this->events;
        $event->setTarget($this);
        $events->trigger(MvcEvent::EVENT_RENDER, $event);
        $events->trigger(MvcEvent::EVENT_FINISH, $event);
        return $this;
    }

通常は、

$this->completeRequest($event);

のところまで処理が進みます。
completeRequestではまずrenderイベントが発火されます。

renderイベントで実行される処理は以下の一つのみです。

これはbootstrapイベントの時に、ViewManager::onBootstrapの中でアタッチされていましたね。それがこの時点で実行となります。
ではこのrenderメソッドの中身を見て行きましょう。

vendor/ZF2/library/Zend/Mvc/View/Http/DefaultRenderingStrategy.php
class DefaultRenderingStrategy extends AbstractListenerAggregate
{
    public function __construct(View $view)
    {
        $this->view = $view;
    }


    public function render(MvcEvent $e)
    {
        $result = $e->getResult();
        if ($result instanceof Response) {
            return $result;
        }

        $request   = $e->getRequest();
        $response  = $e->getResponse();
        $viewModel = $e->getViewModel();
        if (!$viewModel instanceof ViewModel) {
            return;
        }

        $view = $this->view;
        $view->setRequest($request);
        $view->setResponse($response);

        try {
            $view->render($viewModel);
        } catch (\Exception $ex) {
            if ($e->getName() === MvcEvent::EVENT_RENDER_ERROR) {
                throw $ex;
            }

            $application = $e->getApplication();
            $events      = $application->getEventManager();
            $e->setError(Application::ERROR_EXCEPTION)
              ->setParam('exception', $ex);
            $events->trigger(MvcEvent::EVENT_RENDER_ERROR, $e);
        }

        return $response;
    }

ここでは結局、

$view->render($viewModel);

メソッドを呼び出しているだけです。
$viewには何が入っているかというと、コンストラクタでZend\View\Viewのインスタンスが渡ってきていた事を思い出して下さい。ViewManagerのgetMvcRenderingStrategyメソッドでの話です。ということで、ここではZend\View\View::renderメソッドを呼び出しているわけです。

vendor/ZF2/library/Zend/View/View.php
namespace Zend\View;

class View implements EventManagerAwareInterface
{

    public function render(Model $model)
    {
        $event   = $this->getEvent();
        $event->setModel($model);
        $events  = $this->getEventManager();
        $results = $events->trigger(ViewEvent::EVENT_RENDERER, $event, function ($result) {
            return ($result instanceof Renderer);
        });
        $renderer = $results->last();
        if (!$renderer instanceof Renderer) {
            throw new Exception\RuntimeException(sprintf(
                '%s: no renderer selected!',
                __METHOD__
            ));
        }

        $event->setRenderer($renderer);
        $events->trigger(ViewEvent::EVENT_RENDERER_POST, $event);

        $model   = $event->getModel();

        if ($model->hasChildren()
            && (!$renderer instanceof TreeRendererInterface
                || !$renderer->canRenderTrees())
        ) {
            $this->renderChildren($model);
        }

        $event->setModel($model);
        $event->setRenderer($renderer);

        $rendered = $renderer->render($model);

        $options = $model->getOptions();
        if (array_key_exists('has_parent', $options) && $options['has_parent']) {
            return $rendered;
        }

        $event->setResult($rendered);

        $events->trigger(ViewEvent::EVENT_RESPONSE, $event);
    }

RendererイベントでRendererを選択

まず最初のほうでrendererイベントが発火されます。

rendererイベントに対しては、Zend\View\Strategy名前空間以下のStrategyクラスのselectRendererメソッドが動きます。
デフォルトでは、

の一つのみが登録されています。
bootstrapイベントのところでも少し説明していますが、このメソッドの中身を見てみると、

vendor/ZF2/library/Zend/View/Strategy/PhpRendererStrategy.php
class PhpRendererStrategy extends AbstractListenerAggregate
{
    public function __construct(PhpRenderer $renderer)
    {
        $this->renderer = $renderer;
    }

    public function selectRenderer(ViewEvent $e)
    {
        return $this->renderer;
    }

というように、Zend\View\Strategy\PhpRendererのインスタンスを返します。
これが、レスポンス生成のためのRendererとしてPhpRendererが選択されたということになるわけです。

ここで、例えばあらかじめJsonStragetyをイベントリスナとして登録しておくと、JsonStragetyのselectRendererも呼び出され、JsonRendererが選択されるわけです。
登録の仕方は機能編の「JSONをレスポンス」で説明している通り、module.config.phpの'view_manager'の'strategies'に設定を追加するだけです。
rendererイベントにはデフォルトではPhpRendererStrategyが登録されていますが、このようにそれ以外のStrategyも追加すると、復数のStrategyがrendererイベントにイベントリスナとして登録されることになります。でも、最終的にはそのうちのどれか一つに決定されるわけです。どのようにしているのでしょうか。
それは、rendererイベントをtriggerしている箇所に秘密があります。

        $results = $events->trigger(ViewEvent::EVENT_RENDERER, $event, function ($result) {
            return ($result instanceof Renderer);
        });

このようになっており、この瞬間に復数のStrategyが登録されている場合は順番に呼び出されていくわけですが、triggerの3番めの引数を見てください。クロージャ関数が指定されています。この3番めの引数は、イベントで呼び出される復数のメソッドの一つ一つが終了するたびに呼び出されます。そしてこのクロージャ関数がtrueを返した時、以降のイベント処理は呼び出さずにそこで中断されます。クロージャ関数の引数に渡されてくるのは、イベント処理として呼び出されたメソッドが返した値です。ここではPhpRendererStrategyのselectRendererメソッドなどが呼び出されているはずですが、このメソッドが返すのはPhpRendererのインスタンスです。これが、instanceof Rendererということで、Rendererのインスタンスである事がチェックされ、そうであればtrueを返す、つまり、そこで処理終了なわけです。

で、ここでPhpRendererStrategy以外のStrategy、たとえばJsonStrategyなどもイベントリスナとして登録されていたとしたらどうなるか。普通に考えると最初に呼び出されたStrategyで決まるように見えます。PhpRendererStrategyであれば無条件にPhpRendererを返していますから、最初に呼び出されたら他にStrategyが登録されていようが関係なくPhpRendererに決定してしまいます。なので、ここで重要になるのはStrategyをイベントリスナとして登録する時の優先度です。

PhpRendererStrategyは優先度1で登録されています。

class PhpRendererStrategy extends AbstractListenerAggregate
{

    public function attach(EventManagerInterface $events, $priority = 1)
    {
        $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority);
        $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority);
    }
class ViewManager extends AbstractListenerAggregate
{

    public function getView()
    {
                ・
                ・
        $this->view->getEventManager()->attach($this->getRendererStrategy());

で、module.config.phpで追加Strategyを設定していると、それを処理しているのはViewManagerのregisterViewStrategiesメソッドです。

class ViewManager extends AbstractListenerAggregate
{

    protected function registerViewStrategies()
    {
        if (!isset($this->config['strategies'])) {
            return;
        }
        $strategies = $this->config['strategies'];

                ・
                ・

        foreach ($strategies as $strategy) {
            if (!is_string($strategy)) {
                continue;
            }

            $listener = $this->services->get($strategy);
            if ($listener instanceof ListenerAggregateInterface) {
                $view->getEventManager()->attach($listener, 100);
            }
        }
    }

よく見ると、追加Strategyについては優先度100で登録されているのがわかります。
つまり、追加Strategyは常にデフォルトのStrategyより優先されることになることがわかります。
しかし、これでは、例えばJsonRendererStrategyを追加し他場合、全てのレスポンスがJSONのシステムなら良いですが、JSONをレスポンスするアクションであったり、HTMLレスポンスするアクションであったりが混ざったシステムの場合、常にJsonRendererが選択されると問題があるように見えます。しかし、そこは問題ないように考えられています。

例えばJsonStrategyのselectRendererメソッドを見るとわかります。

vendor/ZF2/library/Zend/View/Strategy/JsonStrategy.php
namespace Zend\View\Strategy;

class JsonStrategy extends AbstractListenerAggregate
{

    public function selectRenderer(ViewEvent $e)
    {
        $model = $e->getModel();

        if (!$model instanceof Model\JsonModel) {
            return;
        }

        return $this->renderer;
    }

PhpRendererStrategyとは違い、アクションメソッドで返されたのがJsonModelの場合のみ、JsonRendererを返し、そうではない場合はnullを返しています。
これでアクションメソッドがJsonModelではなくViewModelのインスタンスを返した場合にrendererイベント発火時にどうなるか。

まず優先100のJsonStrategy::selectRendererが呼び出され、結果的にnullを返す。するとtriggerの3番めの引数で指定されたクロージャ関数はfalseを返すので、処理継続となり、次の優先1であるPhpRendererStrategy::selectRendererが呼び出され、有無をいわさずPhpRendererのインスタンスを返す。ということで、期待通りの結果となるわけです。
アクションメソッドで返したものがJsonModelの場合であれば、最初にJsonStrategy::selectRendererが呼び出された時点でちゃんとJsonRendererが返され、そこで処理終了となり、やはり期待通り、となります。非常にうまく考えられた仕組みですね。

なので例えば自作のStrategyクラスとそれと対となる自作Rendererクラスを作る場合は、ViewModelもセットで作り、StrategyのselectRendererメソッドは、アクションメソッドが返したViewModelが、自作のViewModelのインスタンスであるかを検査する必要があるわけです。これをしないと、アクションメソッドの戻り値に関わらず、必ず自作のRendererが選択されてしまうという結果になります。

RendererとStrategyとViewModelは1セット、と考えて下さい。

このようにしてrendererイベントはRendererのインスタンスを選択し、返してくれる事になります。

階層構造のViewModelを再帰処理

Rendererの決定後、以下の部分では、そのViewModelが子ViewModelを持っていないかを見て、持っている場合は、子ViewModelの処理を行います。

class View implements EventManagerAwareInterface
{

    public function render(Model $model)
    {
                ・
                ・

        if ($model->hasChildren()
            && (!$renderer instanceof TreeRendererInterface
                || !$renderer->canRenderTrees())
        ) {
            $this->renderChildren($model);
        }
                ・
                ・

        $rendered = $renderer->render($model);

                ・
                ・
    }

    protected function renderChildren(Model $model)
    {
        foreach ($model as $child) {
            if ($child->terminate()) {
                throw new Exception\DomainException('Inconsistent state; child view model is marked as terminal');
            }
            $child->setOption('has_parent', true);
            $result  = $this->render($child);        // 再帰的にrenderメソッド呼び出し
            $child->setOption('has_parent', null);
            $capture = $child->captureTo();
            if (!empty($capture)) {
                if ($child->isAppend()) {
                    $oldResult=$model->{$capture};
                    $model->setVariable($capture, $oldResult . $result);
                } else {
                    $model->setVariable($capture, $result);
                }
            }
        }
    }

renderChildrenメソッドで行っていることは、親ViewModelが持つ全ての子ViewModel全てについて、個別にrenderメソッドを呼び出しています。つまり、render→renderChildrenは再帰的に処理され、Rendererはツリー構造で無限に階層化出来る構造になっていることがわかります。

実際のところ、普通に実装すると、レイアウトテンプレートとアクションテンプレートの2階層構造がデフォルトになっているため、一度、renderChildrenが呼び出されることになります。

Rendererクラスでレスポンス生成

renderメソッドのメインはRendererクラスのrenderメソッドを実行し、レスポンスHTMLを生成することです。
デフォルトのRendererであるPhpRendererを見てみましょう。

class PhpRenderer implements Renderer, TreeRendererInterface
{

    public function render($nameOrModel, $values = null)
    {
        if ($nameOrModel instanceof Model) {
            $model       = $nameOrModel;
            $nameOrModel = $model->getTemplate();
            if (empty($nameOrModel)) {
                throw new Exception\DomainException(sprintf(
                    '%s: received View Model argument, but template is empty',
                    __METHOD__
                ));
            }
            $options = $model->getOptions();
            foreach ($options as $setting => $value) {
                $method = 'set' . $setting;
                if (method_exists($this, $method)) {
                    $this->$method($value);
                }
                unset($method, $setting, $value);
            }
            unset($options);

            // Give view model awareness via ViewModel helper
            $helper = $this->plugin('view_model');
            $helper->setCurrent($model);

            $values = $model->getVariables();
            unset($model);
        }

        // find the script file name using the parent private method
        $this->addTemplate($nameOrModel);
        unset($nameOrModel); // remove $name from local scope

        $this->__varsCache[] = $this->vars();

        if (null !== $values) {
            $this->setVars($values);
        }
        unset($values);

        // extract all assigned vars (pre-escaped), but not 'this'.
        // assigns to a double-underscored variable, to prevent naming collisions
        $__vars = $this->vars()->getArrayCopy();
        if (array_key_exists('this', $__vars)) {
            unset($__vars['this']);
        }
        extract($__vars);
        unset($__vars); // remove $__vars from local scope

        while ($this->__template = array_pop($this->__templates)) {
            $this->__file = $this->resolver($this->__template);
            if (!$this->__file) {
                throw new Exception\RuntimeException(sprintf(
                    '%s: Unable to render template "%s"; resolver could not resolve to a file',
                    __METHOD__,
                    $this->__template
                ));
            }
            try {
                ob_start();
                $includeReturn = include $this->__file;
                $this->__content = ob_get_clean();
            } catch (\Exception $ex) {
                ob_end_clean();
                throw $ex;
            }
            if ($includeReturn === false && empty($this->__content)) {
                throw new Exception\UnexpectedValueException(sprintf(
                    '%s: Unable to render template "%s"; file include failed',
                    __METHOD__,
                    $this->__file
                ));
            }
        }

        $this->setVars(array_pop($this->__varsCache));

        return $this->getFilterChain()->filter($this->__content); // filter output
    }

引数にはViewModelのインスタンスが入ってきています。このViewModelは言うまでもなく、アクションメソッドで注入されたデータを持っています。

長いメソッドですが、まず重要な部分のみを順に見て行きましょう。

    public function render($nameOrModel, $values = null)
    {
        if ($nameOrModel instanceof Model) {
            $model       = $nameOrModel;
                ・
                ・
            $values = $model->getVariables();

        }
                ・
                ・
        if (null !== $values) {
            $this->setVars($values);
        }
                ・
                ・
        $__vars = $this->vars()->getArrayCopy();
                ・
                ・
        extract($__vars);
                ・
                ・

ViewModelのデータをテンプレートに展開するのに係る部分のみを抜粋しました。

最後のextract関数は、引数の配列データを、変数に展開する関数です。配列のキーを全て変数名とした変数を作り出し、そこに値が入るわけです。
つまり、例えば

array('hoge' => 123, 'fuga' => 'piypiyo');

という配列だとすると、

$hoge = 123;
$fuga = 'piyopiyo';

という状態に展開します。

$__varsには、最終的にViewModelが持っていたデータが入っているということがわかると思います。
なので、アクションメソッドでViewModelに注入されたデータはここで全て変数に展開されることになるわけです。

この状態で、それらの変数をechoするテンプレートを読みこめば、テンプレートの変数部分が値に置き換わる仕組みです。
テンプレートを読み込むのに関わる部分が以下です。

    public function render($nameOrModel, $values = null)
    {
                ・
                ・
        while ($this->__template = array_pop($this->__templates)) {
            $this->__file = $this->resolver($this->__template);
                ・
                ・
            try {
                ob_start();
                $includeReturn = include $this->__file;
        $this->__content = ob_get_clean();
                ・
                ・
        }

$this->__contentに、テンプレートの内容が文字列として格納されます。

そして最終的にはこの$this->__contentがこのrenderメソッドの戻り値として返してrenderメソッドは終了します。

Responseイベント

Zend\View\Viewクラスのrenderメソッドに戻ると、PhpRendererクラスのrenderの戻り値にはテンプレートから得たHTMLが返ってきていることになります。
それをまずはViewEventのインスタンスに注入します。
その後、responseイベントを発火しています。

vendor/ZF2/library/Zend/View/View.php
class View implements EventManagerAwareInterface
{

    public function render(Model $model)
    {
                ・
                ・

        $rendered = $renderer->render($model);

        $options = $model->getOptions();
        if (array_key_exists('has_parent', $options) && $options['has_parent']) {
            return $rendered;
        }

        $event->setResult($rendered);

        $events->trigger(ViewEvent::EVENT_RESPONSE, $event);
    }

responseイベントに登録されている処理は1つのみです。

vendor/ZF2/library/Zend/View/Strategy/PhpRendererStrategy.php
class PhpRendererStrategy extends AbstractListenerAggregate
{

    public function injectResponse(ViewEvent $e)
    {
        $renderer = $e->getRenderer();
        if ($renderer !== $this->renderer) {
            return;
        }

        $result   = $e->getResult();
        $response = $e->getResponse();

        if (empty($result)) {
            $placeholders = $renderer->plugin('placeholder');
            foreach ($this->contentPlaceholders as $placeholder) {
                if ($placeholders->containerExists($placeholder)) {
                    $result = (string) $placeholders->getContainer($placeholder);
                    break;
                }
            }
        }
        $response->setContent($result);
    }

ここではZend\Http\PhpEnvironment\Responseクラスのインスタンスに、Rendererで生成されたレスポンス文字列をセットしているだけです。Rendererで生成されたレスポンスは、View::renderメソッドでViewEventのインスタンスに注入されているので、ViewEventから取り出しています。

以上でApplicationクラスのcompleteRequestメソッドで発火されたrenderの処理が終了です。

finishイベント

renderイベントが終了すると、Applicationに戻り、すぐにfinishイベントが発火されます。

vendor/ZF2/library/Zend/Mvc/Application.php
class Application implements
    ApplicationInterface,
    EventManagerAwareInterface
{

    protected function completeRequest(MvcEvent $event)
    {
        $events = $this->events;
        $event->setTarget($this);
        $events->trigger(MvcEvent::EVENT_RENDER, $event);
        $events->trigger(MvcEvent::EVENT_FINISH, $event);
        return $this;
    }

finishイベントで実行される処理は1つです。

名前のまま、レスポンスを送信しているようですが、中身を見てみましょう。

vendor/ZF2/library/Zend/Mvc/View/SendResponseListener.php
namespace Zend\Mvc\View;

use Zend\Mvc\SendResponseListener as MvcSendResponseListener;

class SendResponseListener extends MvcSendResponseListener
{
}

Zend\Mvc\SendResponseListenerクラスを継承しているだけのようなのでそちらをみてみましょう。

vendor/ZF2/library/Zend/Mvc/SendResponseListener.php
class SendResponseListener implements
    EventManagerAwareInterface,
    ListenerAggregateInterface
{

    public function sendResponse(MvcEvent $e)
    {
        $response = $e->getResponse();
        if (!$response instanceof Response) {
            return;
        }
        $event = $this->getEvent();
        $event->setResponse($response);
        $event->setTarget($this);
        $this->getEventManager()->trigger($event);
    }

    public function getEvent()
    {
        if (!$this->event instanceof SendResponseEvent) {
            $this->setEvent(new SendResponseEvent());
        }
        return $this->event;
    }

まずこのクラスのgetEventメソッドで得られるのはZend\Mvc\ResponseSender\SendResponseEventクラスのインスタンスです。これはSendResponseListener独自のイベントオブジェクトで、レスポンス送信にかかわるイベント情報を保持するものです。
で、そのSendResponseEventに対して、レスポンスHTMLが注入された状態のZend\Http\PhpEnvironment\Responseのインスタンスをセットしています。

その後、最後の行の、

$this->getEventManager()->trigger($event);

の部分では、SendResponseEventのインスタンスを引数に何かイベントを発火しているようですが、何をやっているのでしょうか。
少しEventManagerのtriggerメソッドを見てみましょう。

class EventManager implements EventManagerInterface
{

    public function trigger($event, $target = null, $argv = array(), $callback = null)
    {
        if ($event instanceof EventInterface) {
            $e        = $event;
            $event    = $e->getName();
            $callback = $target;
        } elseif ($target instanceof EventInterface) {
                ・
                ・
                ・

第1引数の$eventに来るのはSendResponseEventのインスタンスで、Zend\EventManager\EventInterfaceを実装しているので、最初のif条件に引っかかります。そうすると、このSendResponseEventのgetNameメソッドを実行して得られた文字列をイベント名として、そのイベントを実行しているのがわかります。
SendResponseEventのgetNameは、実際はZend\EventManager\Eventに実装されたメソッドで、自身の$nameフィールドの値を返すメソッドです。
SendResponseEventを見てみましょう。

vendor/ZF2/library/Zend/Mvc/ResponseSender/SendResponseEvent.php
class SendResponseEvent extends Event
{
    protected $name = 'sendResponse';

ということで、白化されるのはsendResponseイベントということになります。

sendResponseイベント

sendResponseイベントで実行される処理は、SendResponseListenerのattachDefaultListenersメソッドで登録されている4つです。
attachDefaultListenersメソッドは、SendResponseListenerのsendResponseメソッドの最後の、

$this->getEventManager()->trigger($event);

の部分の、getEventManagerメソッドの中で呼び出されるようになっています。なのでtriggerの直前にattachされているということになります。
attachDefaultListenersメソッドを見てみましょう。

vendor/ZF2/library/Zend/Mvc/SendResponseListener.php
class SendResponseListener implements
    EventManagerAwareInterface,
    ListenerAggregateInterface
{

    protected function attachDefaultListeners()
    {
        $events = $this->getEventManager();
        $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new PhpEnvironmentResponseSender(), -1000);
        $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new ConsoleResponseSender(), -2000);
        $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new SimpleStreamResponseSender(), -3000);
        $events->attach(SendResponseEvent::EVENT_SEND_RESPONSE, new HttpResponseSender(), -4000);
    }

ちょっと特殊な登録の仕方です。
普通は2番目の引数でメソッドを登録します。でもここではクラスのインスタンスそのものを登録しています。このような登録の仕方をした場合、そのクラスにマジックメソッドである__invokeメソッドが存在すれば、それが実行されます。__invokeはクラスのインスタンスの変数に、後ろに()を付けて、関数呼び出しのように呼び出せるメソッドです。

PhpEnvironmentResponseSenderを見てみましょう。

vendor/ZF2/library/Zend/Mvc/ResponseSender/PhpEnvironmentResponseSender.php
namespace Zend\Mvc\ResponseSender;

use Zend\Http\PhpEnvironment\Response;

class PhpEnvironmentResponseSender extends HttpResponseSender
{
    public function __invoke(SendResponseEvent $event)
    {
        $response = $event->getResponse();
        if (!$response instanceof Response) {
            return $this;
        }

        $this->sendHeaders($event)
             ->sendContent($event);
        $event->stopPropagation(true);
        return $this;
    }
}

__invokeメソッドが存在していますね。このメソッドがイベント発火時に動くわけです。

他に3つのクラスがattachされていますが、これら全て同様に__invokeメソッドが存在します。
というわけで、sendResponseイベントで動くのは4つ、と言いたいところですが、通常はPhpEnvironmentResponseSenderのみです。どういうことかというと、PhpEnvironmentResponseSenderの中身を見ればわかります。

最後のほうに、

$event->stopPropagation(true);

という処理がありますが、SendResponseEventのインスタンスに処理ストップのフラグを立てています。こうしておくと、このメソッドが終了した後、EventManagerで次の処理へ移る前に、このフラグを見ていて、trueになっているとそこで中断し、以降のイベント処理は全てスキップされます。では必ず一つ目しか処理されないじゃないかというとそういうわけでもなく、__invokeメソッドの最初の判定を見て分かる通り、SendResponseEventからgetResponseで得られるのがこのクラスで意図していものと異なる場合はすぐに抜けています。そんな感じで4つの処理のどれかに引っかかるということです。通常はほとんど一つ目のPhpEnvironmentResponseSenderで引っかかるでしょう。

で、PhpEnvironmentResponseSenderで最終的にやっていることは、

$this->sendHeaders($event)->sendContent($event);

です。
sendHeadersは継承元であるHttpResponseSenderに定義されています。
sendContentは更にHttpResponseSenderの継承元であるAbstractResponseSenderに定義されています。

vendor/ZF2/library/Zend/Mvc/ResponseSender/AbstractResponseSender.php
namespace Zend\Mvc\ResponseSender;

use Zend\Http\Header\MultipleHeaderInterface;

abstract class AbstractResponseSender implements ResponseSenderInterface
{
    public function sendHeaders(SendResponseEvent $event)
    {
        if (headers_sent() || $event->headersSent()) {
            return $this;
        }

        $response = $event->getResponse();

        foreach ($response->getHeaders() as $header) {
            if ($header instanceof MultipleHeaderInterface) {
                header($header->toString(), false);
                continue;
            }
            header($header->toString());
        }

        $status = $response->renderStatusLine();
        header($status);

        $event->setHeadersSent();
        return $this;
    }
}
vendor/ZF2/library/Zend/Mvc/ResponseSender/HttpResponseSender.php
namespace Zend\Mvc\ResponseSender;

use Zend\Http\Response;

class HttpResponseSender extends AbstractResponseSender
{
    public function sendContent(SendResponseEvent $event)
    {
        if ($event->contentSent()) {
            return $this;
        }
        $response = $event->getResponse();
        echo $response->getContent();
        $event->setContentSent();
        return $this;
    }
}

sendHeadersメソッドはResponseのインスタンスからレスポンスヘッダ情報を取得し、header関数でレスポンスヘッダを出力しています。
sendContentメソッドはResponseのインスタンスからレスポンスのコンテンツ、つまりHTMLを取得し、echoしています。

以上でfinishイベントの処理が終了します。
これでApplicationのrunメソッドの処理も全て終わり、全体の処理が終了となります。