Question

I'm trying to intercept the ViewModel, prior to it being rendered, and add it to another 'parent' view model - much like the way that the ZF2 layout wraps around the controllers returned content.

The simple (working) way to do this would be in each controller action.

public function dashboardAction() { 

  $dashboardContent = new ViewModel(array('foo' => 'bar'));

  $parent = new ViewModel();
  $parent->setTemplate('foo/bar/parent-template'); 

  $parent->addChild($dashboardContent, 'content');

  return $parent;
}

This works as expected and the 'child' view is correctly nested within the 'parent' in the final output.

As I have a number of controllers/actions that should all behave in the same way (resolved via their route name); I was hoping to encapsulate this in an event listener.

public function onBootstrap(MvcEvent $event)
{
    $application  = $event->getApplication();
    $eventManager = $application->getEventManager();

    $eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'addUserAccountLayout'), -100);
}

public function addUserAccountLayout(EventInterface $event)
{
    $routeMatch = $event->getRouteMatch();
    $controller = $event->getTarget();
    $result     = $event->getResult();

    if (! $result instanceof ViewModel || $result instanceof JsonModel) {
        return;
    }

    if (! $routeMatch instanceof RouteMatch || 0 !== strpos($routeMatch->getMatchedRouteName(), 'zfcuser') || $result->terminate()) {
        return;
    }

    $application    = $event->getApplication();
    $serviceManager = $application->getServiceManager();
    $accountService = $serviceManager->get('JobboardUser\Service\AccountService');

    $user = $accountService->getCurrentUser();

    $parent = new ViewModel();
    $parent->setVariables(compact('user'));
    $parent->setTemplate('jobboard-user/widget/user-account');
    $parent->addChild($result, 'content');

    $event->setResult($parent); 
}

This however is not working; the normal view is rendered (without the parent). My guess is because I am either not using the correct event or event priority OR $event->setResult($view) is not the correct way to assign the result.

How can I modify and then re-assign the view from within an event listener?

Was it helpful?

Solution 2

It's been a little while since I asked this; however I did find the solution I was after. It was indeed related to how the new ViewModel is attached back to the MvcEvent.

Previously I had tried to set the event's result property with no luck :

$mvcEvent->setResult($myNewViewModel);

The key was to first fetch the MvcEvent's own view model and then attach my custom one to it as a child.

$mvcEvent->getViewModel()->addChild($myNewViewModel, 'content');

I've also incorporated the point that @Crisp made regarding the event managers target. Previously, I was attaching to the main application dispatch event meaning that the listener would be called on every dispatch event.

I now specifically target the controllers I want to listen to.

$eventManager->attach(array(
       'JobboardUser\Controller\AccountController',
       'JobboardUser\Controller\ProfileController'
    ), 
    MvcEvent::EVENT_DISPATCH, 
    array($this, 'addUserAccountLayout'),
    -80
 );

The final listener code looks like this

public function addUserAccountLayout(EventInterface $event)
{
    $result = $event->getResult();

    if (! $result instanceof ViewModel || $result instanceof JsonModel) {
        return;
    }

    $application    = $event->getApplication();
    $serviceManager = $application->getServiceManager();
    $accountService = $serviceManager->get('JobboardUser\Service\AccountService');
    $currentUser    = $accountService->getCurrentUser();

    $layout = new ViewModel();

    $layout->setVariables(array('user' => $currentUser));
    $layout->setTemplate('jobboard-user/widget/user-account');
    $layout->addChild($result, 'userContent');

    $event->getViewModel()->addChild($layout, 'content');
}

OTHER TIPS

I can't offer an explanation as to 'why', but a priority of -10 seems to be the sweet spot to get this working with the code you already have.

I do have one suggestion though, instead of listening to every dispatch event triggered by every module controller and then having to check if it's a zfcuser route, you can instead make use of the shared events manager to listen specifically to the ZfcUser controller you're interested in ...

public function onBootstrap(MvcEvent $event)
{
    $application  = $event->getApplication();
    $eventManager = $application->getEventManager();
    $sharedEvents = $eventManager->getSharedManager();

    $sharedEvents->attach(
        'ZfcUser\Controller\UserController', // controller FQCN to listen to
        MvcEvent::EVENT_DISPATCH, 
        array($this, 'addUserAccountLayout'), 
       -10
    );
}

If you do that, you can remove the routematch check 0 !== strpos($routeMatch->getMatchedRouteName(), 'zfcuser') in your callback entirely.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top