Nesting layouts/views keeping the content variable

2019-08-27 16:53发布

问题:

I am trying to nest two (or more) views using the following code. I am struggling to find a way to successfully nest these views without losing the final view content and passing it through the $this->content variable within the last layout, as it just returns an empty string.

core/Framework/Mvc/Controller/BaseActionController.php This is a simple base controller which uses the $frame and $layout variables (so that they can be used within any controller extending this class). The idea is the frame is defined as the page starting with <!DOCTYPE html> and the layout is the HTML which gets displayed in the frame using <?= $this->content; ?>.

namespace Framework\Mvc\Controller;

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

class BaseActionController extends AbstractActionController
{
    protected $frame;
    protected $layout;
    protected $layouts = array();

    public function preDispatch() {...}

    public function dispatch() {..}

    public function postDispatch()
    {
        if ($this->frame !== null) {
            $this->layouts[] = $this->frame;
        }

        if ($this->layout !== null) {
            $this->layouts[] = $this->layout;
        }

        foreach ($this->layouts as $layout) {
            $view = new ViewModel();

            $layoutView = new ViewModel();
            $layoutView->setTemplate($layout);

            $layoutView->addChild($view);
        }
    }
}

module/Application/view/layout/frame.phtml The <?= $this->content; ?> part within this template should echo out the layout.phtml template along with it's own <?= $this->content; ?>.

<?= $this->doctype(); ?>
<html>
    <head>
        <meta charset="utf-8">
        <title>Woohoo, I'm a frame</title>
    </head>
    <body>
        <?= $this->content; ?>
    </body>
</html>

module/Application/view/layout/admin/layout.phtml The $this->content variable should echo out the contents of the module/Users/view/users/test/index.phtml file. At this point, the variable returns an empty string.

<header>
    <img class="logo" src="<?= $this->basePath() ?>/img/logo.png" alt="Company">
    <nav>
        <ul>
            <li><a href="#">Home</a></li>
            <li><a href="#">About</a></li>
            <li><a href="#">Contact</a></li>
        </ul>
    </nav>
</header>
<section>
    <?= $this->content; ?>
</section>
<footer>
    <ul>
        <li><a href="#">Copyright</a></li>
        <li><a href="#">Sitemap</a></li>
        <li><a href="#">Privacy policy</a></li>
    </ul>
</footer>

module/Users/view/users/test/index.phtml

<h1 class="page__title">Test</h1>
<p class="page__content">The final view</p>

Temporary solution (not very nice to write this in each action)

<?php

namespace Users\Controller;

use Framework\Mvc\Controller\BaseActionController;
use Zend\View\Model\ViewModel;

class TestController extends BaseActionController
{
    public function indexAction()
    {
        $view = new ViewModel();
        $view->setTemplate('users/test/index.phtml');

        $adminView = new ViewModel();

        // This layout is defined in the Application module.config.php file
        $adminView->setTemplate('layout/admin');

        $adminView->addChild($view);

        return $adminView;
    }
}

As above, my temporary solution is to choose the template the ViewModel() instance needs, manually. I notice $view->setTemplate(); works but without defining one, $view->getTemplate(); returns an empty string. I am not sure where, in Zend Framework 2, the default template is being defined so I can replicate this within the base controller.

I think the solution I have (temporarily) could work, the only issue being the manual $view->setTemplate('/path/to/my/template.phtml');. If I can replicate how Zend does this, then it should work correctly but I am at a loss passing the $this->content variable into the layout.phtml file with the contents being the final view.

UPDATE:

As suggested by Next Developer, I have added the following:

module/Application/Module.php

<?php

namespace Application;

use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
use Zend\Session\Container;
use Framework\Mvc\View\Http\TemplateInjector;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $app = $e->getApplication();

        $request = $app->getRequest();
        $response = $app->getResponse();

        $eventManager = $app->getEventManager();
        $serviceManager = $app->getServiceManager();

        $session = new Container('locale');
        if (!$session->offsetExists('locale')) {
            $session->offsetSet('locale', \Locale::acceptFromHttp($request->getServer('HTTP_ACCEPT_LANGUAGE')));
        }

        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);

        $serviceManager->get('translator')
            ->setLocale($session->locale)
            ->setFallbackLocale('en_GB');

        $eventManager->getSharedManager()
            ->attach(
                'Zend\Stdlib\DispatchableInterface',
                MvcEvent::EVENT_DISPATCH,
                new TemplateInjector(),
                -80
            );

    }

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

    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }
}

core/Framework/Mvc/View/Http/TemplateInjector.php

<?php

namespace Framework\Mvc\View\Http;

use Zend\Mvc\MvcEvent;
use Zend\View\Model\ModelInterface as ViewModel;

class TemplateInjector
{
    public function __invoke(MvcEvent $event)
    {
        $model = $event->getResult();

        if (!$model instanceof ViewModel) {
            return;
        }

        if ($model->getTemplate()) {
            return ;
        }

        $controller = $event->getTarget();

        if (!is_object($controller)) {
            return;
        }

        // @todo: Clear this mess up

        $namespace = explode('\\', ltrim(get_class($controller), '\\'));

        $controllerClass = array_pop($namespace);

        array_pop($namespace);

        $moduleName = implode('/', $namespace);

        $controller = substr($controllerClass, 0, strlen($controllerClass) - strlen('Controller'));
        $action = $event->getRouteMatch()->getParam('action');

        $model->setTemplate(strtolower($moduleName.'/'.$controller.'/'.$action.'.phtml'));
    }
}

Any changes in the TemplateInjector doesn't seem to change the view, by this time it seems too late. It does however set the template on the view. When making a new instance of $view = new VidewModel(); it uses the template defined in the TemplateInjector class which should allow me to automate the layout process, but the scope of everything being set, it seems too late. I know I can access the controller, the view and the model in the TemplateInjector but no matter how I change the views or add children, it doesn't come out on the front end. If anyone could provide a working example, that would be really helpful.

回答1:

I think the best would be in your case is to override the default template injector with your own. Take a look at this post http://blog.igorvorobiov.com/2014/10/18/creating-a-custom-template-injector-to-deal-with-sub-namespaces-in-zend-framework-2/. It explains pretty much well how to create and setup your own template injector.

Basically, you need to create an event listener and attach it to the event MvcEvent::EVENT_DISPATCH triggered by the current controller. Inside the event listener you can put the logic which determines a path to the requested template. In your case, you can get your child view model by calling $model->getChildrenByCaptureTo('capture'); and set the template name to it as you want.

The default logic which resolves template names can be found here Zend\Mvc\View\Http\InjectTemplateListener::injectTemplate

UPDATE:

Upon discussion with @Titanium, this solution was found to be the correct one.

I have tried to understand you problem, so here's another solution to it.

Replace the previous template injector code with this one:

class TemplateInjector
{
    public function __invoke(MvcEvent $e)
    {
        $model = $e->getResult();

        if (!$model instanceof ViewModel)
        {
            return;
        }

        $controller = $e->getTarget();

        if (!is_object($controller))
        {
            return ;
        }

        if (!$controller instanceof LayoutTemplateProviderInterface)
        {
            return ;
        }

        $frameTemplate = $controller->getFrameTemplate();

        if ($frameTemplate !== null)
        {
            $e->getViewModel()->setTemplate($controller->getFrameTemplate());
        }

        $layoutTemplate = $controller->getLayoutTemplate();

        if ($layoutTemplate !== null)
        {
            $model = $e->getResult();
            $layoutModel = new ViewModel();
            $layoutModel->setTemplate($controller->getLayoutTemplate());
            $layoutModel->addChild($model);
            $e->setResult($layoutModel);
        }
    }
} 

Now, you need to define interface which your base controller class should implement in order to tell the system that you want to use custom templates:

interface LayoutTemplateProviderInterface
{
    public function getFrameTemplate();
    public function getLayoutTemplate();
} 

Then in your base controller you should implement the interface like so:

abstract class BaseController extends AbstractActionController implements  LayoutTemplateProviderInterface
{
    private $frameTemplate = 'layout/layout';
    private $layoutTemplate = 'layout/admin';

    public function getFrameTemplate()
    {
        return $this->frameTemplate;
    }

    public function getLayoutTemplate()
    {
        return $this->layoutTemplate;
    }

    protected function setFrameTemplate($name)
    {
        $this->frameTemplate = $name;
    }

    protected function setLayoutTemplate($name)
    {
        $this->layoutTemplate = $name;
    }
}

The last thing is to change the priority at which our template injector is getting executed.

$eventManager->getSharedManager()
            ->attach(
                'Zend\Stdlib\DispatchableInterface',
                MvcEvent::EVENT_DISPATCH,
                new TemplateInjector(),
                -91
            );

So, our template injector will be executed right after the default one, this allows us to avoid resolving the template name and rely on the default logic.

After all this, your action looks like this:

public function testAction()
{
    return new ViewModel();
}

As you can see you don't have to create nesting views here, it will be done automatically by TemplateInjector.

If you need to change frame template name or layout template within an action you can do it like so:

$this->setFrameTemplate("new/template");
$this->setLayoutTemplate("new/template");

Let me know if this solution solves your problem so I can remove the first one to make this post clearer.