[Solved] ViewJsonStrategy is registered and set to default, yet some controllers default to a phtml view

zend-view

#1

I have a module which includes mvc controllers that respond with JSON on certain defined endpoints. I have a number of controllers, at least one of which works correctly. At least one other, however, stubbornly returns only a PhpRenderer view with no data passed to it, despite the controller action returning a new JsonModel.

I started with specifying the Json strategy in the module.config.php:

// ... 
    'view_manager' => [
        'display_not_found_reason' => true,
        'display_exceptions'       => true,
        'doctype'                  => 'HTML5',
        'not_found_template'       => 'error/404',
        'exception_template'       => 'error/index',
        'template_map' => [
            'layout/layout'           => __DIR__ . '/../view/layout/layout.phtml',
            'application/index/index' => __DIR__ . '/../view/application/index/index.phtml',
            'error/404'               => __DIR__ . '/../view/error/404.phtml',
            'error/index'             => __DIR__ . '/../view/error/index.phtml',
        ],
        'template_path_stack' => [
            __DIR__ . '/../view',
        ],
        'strategies' => [
            'ViewJsonStrategy',
        ],
    ],

// ...

Then in the common Module.php I copied from the documentation the code needed to register (which I had to modify due to an incorrect call based on a suggested fix on issue 108 - a fix which I do not fully understand):

<?php
namespace Api;

use Zend\Mvc\MvcEvent;

class Module
{
    const VERSION = '3.0.3-dev';

    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }

    /**
     * Listen to the bootstrap event
     *
     * @param EventInterface $e
     * @return array
     */
    public function onBootstrap(MvcEvent $e)
    {
        $app = $e->getApplication();
        $app->getEventManager()->attach('render', [$this, 'registerJsonStrategy'], 100);
    }

    /**
     * @param  MvcEvent $e The MvcEvent instance
     * @return void
     */
    public function registerJsonStrategy(MvcEvent $e)
    {
        $app          = $e->getTarget();
        $locator      = $app->getServiceManager();
        $view         = $locator->get('Zend\View\View');
        $jsonStrategy = $locator->get('ViewJsonStrategy');

        // Attach strategy, which is a listener aggregate, at high priority
        $jsonStrategy->attach($view->getEventManager(), 100);
    }
}

In the controller that returns improperly:

namespace Api\Controller;

// Application specific use statments...
use Zend\View\Model\JsonModel;

class MisbehavingController extends AbstractController
{

    // ...

    protected function allRatings($type = 'Add-Ons')
    {
        try {
            $response = $this->ratings->getAllApprovedRatings($type);
        } catch (\Exception $e) {
            return $this->errorJson($e->getMessage());
        }
        return new JsonModel($response);

    }

}

In the controller that returns correctly:

namespace Api\Controller;

// Application specific use statments...
use Zend\View\Model\JsonModel;

class BehavingController extends AbstractController
{

    // ...

    public function getAppsAction()
    {

        $start       = $this->params()->fromQuery('start', 0);
        $limit       = $this->params()->fromQuery('limit', 80);
        $searchTerm  = $this->params()->fromQuery('searchTerm');
        $filterType  = $this->params()->fromQuery('filterType');
        $sortBy      = $this->params()->fromQuery('sortBy');

        $results = $this->apps->getApps(
            $start,
            $limit,
            $searchTerm,
            $filterType,
            $sortBy
        );

        return new JsonModel($results);
    }

}

Both controllers are instantiated with application-specific dependencies via a Factory class that are not different in kind. The AbstractController that they extend implements the errorJson method thats called in the catch block in the misbehaving instance:

<?php

namespace Api\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\JsonModel;

abstract class AbstractController extends AbstractActionController
{

    protected function errorJson($message, Array $extra = [])
    {
        $response = [];

        $response['error'] = $message;

        if (!empty($extra)) {
            $response = array_merge($extra, $response);
        }

        if ($this->logger) {
            $this->logger->err($message, $extra);
        }
        $this->getResponse()->setStatusCode(400);
        return new JsonModel($response);
    }

}

Initially the misbehaving controller was throwing an Exception when it couldn’t find a default view file. I provided that file and took the opportunity to log out debug_backtrace information, expecting to see something that might hint as to what is going on:

<h2>Last error:</h2>
<pre>
<?php var_dump(error_get_last()); ?>
</pre>

<h2>Debug backtrace:</h2>
<pre>
<?php debug_print_backtrace(0, 5); ?>
</pre>

The last error is NULL and the backtrace (truncated due to out of memory errors trying to show the full stack trace) shows:

#0 include() called at [/var/www/vendor/zendframework/zend-view/src/Renderer/PhpRenderer.php:506]
#1 Zend\View\Renderer\PhpRenderer->render() called at [/var/www/vendor/zendframework/zend-view/src/View.php:207]
#2 Zend\View\View->render(Zend\View\Model\ViewModel Object ([*captureTo] => content,[*children] => Array (),[*options] => Array ([has_parent] => 1),[*template] => api/partners/index,[*terminate] => ,[*variables] => Array ([content] => Placeholder page),[*append] => )) called at [/var/www/vendor/zendframework/zend-view/src/View.php:236]
#3 Zend\View\View->renderChildren(Zend\View\Model\ViewModel Object ([*captureTo] => content,[*children] => Array ([0] => Zend\View\Model\ViewModel Object ([*captureTo] => content,[*children] => Array (),[*options] => Array ([has_parent] => 1),[*template] => api/partners/index,[*terminate] => ,[*variables] => Array ([content] => Placeholder page),[*append] => )),[*options] => Array (),[*template] => layout/layout,[*terminate] => ,[*variables] => Zend\View\Variables Object (),[*append] => )) called at [/var/www/vendor/zendframework/zend-view/src/View.php:200]
#4 Zend\View\View->render(Zend\View\Model\ViewModel Object ([*captureTo] => content,[*children] => Array ([0] => Zend\View\Model\ViewModel Object ([*captureTo] => content,[*children] => Array (),[*options] => Array ([has_parent] => 1),[*template] => api/partners/index,[*terminate] => ,[*variables] => Array ([content] => Placeholder page),[*append] => )),[*options] => Array (),[*template] => layout/layout,[*terminate] => ,[*variables] => Zend\View\Variables Object (),[*append] => )) called at [/var/www/vendor/zendframework/zend-mvc/src/View/Http/DefaultRenderingStrategy.php:105]

What am I missing? Is the application trying to display and error and failing at finding the appropriate error template? Other exceptions and errors have displayed correctly. And there are no items in the log to correspond with access attempts either.


#2

I fell prey to the configuration cache. Leaving a link to the documentation for anyone who may come across a similar issue: https://docs.zendframework.com/zend-config-aggregator/caching/