Shopware 6: Which event should I subscribe to get all variants of the product on the Home Page? - shopware

There is a slider on the homepage where the products are listed. Here I want to show the colors of the variants that the products have. Which event should I subscribe to get all variants of the product on the Home Page?

You could decorate the CMS element resolver. You'll need to inject the repository of the configurator settings, as you won't be able to get all variants from the resolved product, as it will be one of the variants and only the actual parent has associations to all variants.
<service id="MyPlugin\Core\Content\Product\Cms\ProductSliderCmsElementResolverDecorator" decorates="Shopware\Core\Content\Product\Cms\ProductSliderCmsElementResolver">
<argument type="service" id="MyPlugin\Core\Content\Product\Cms\ProductSliderCmsElementResolverDecorator.inner"/>
<argument type="service" id="product_configurator_setting.repository"/>
</service>
class ProductSliderCmsElementResolverDecorator implements CmsElementResolverInterface
{
private CmsElementResolverInterface $decorated;
private EntityRepositoryInterface $productConfiguratorSettingRepository;
public function __construct(
CmsElementResolverInterface $decorated,
EntityRepositoryInterface $productConfiguratorSettingRepository
) {
$this->decorated = $decorated;
$this->productConfiguratorSettingRepository = $productConfiguratorSettingRepository;
}
public function getType(): string
{
return $this->decorated->getType();
}
public function collect(CmsSlotEntity $slot, ResolverContext $resolverContext): ?CriteriaCollection
{
return $this->decorated->collect($slot, $resolverContext);
}
public function enrich(CmsSlotEntity $slot, ResolverContext $resolverContext, ElementDataCollection $result): void
{
$this->decorated->enrich($slot, $resolverContext, $result);
// results from product streams
$entitySearchResult = $result->get('product-slider-entity-fallback_' . $slot->getUniqueIdentifier());
if ($entitySearchResult === null) {
// manual results
$entitySearchResult = $result->get('product-slider_' . $slot->getUniqueIdentifier());
}
if ($entitySearchResult === null) {
return;
}
/** #var ProductEntity $product */
foreach ($entitySearchResult->getElements() as $product) {
if (!$product->getParentId()) {
continue;
}
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productId', $product->getParentId()));
$criteria->addAssociation('option.group');
$configuratorSettings = $this->productConfiguratorSettingRepository->search(
$criteria,
$resolverContext->getSalesChannelContext()->getContext()
)->getElements();
$variants = [];
/** #var ProductConfiguratorSettingEntity $configuratorSetting */
foreach ($configuratorSettings as $configuratorSetting) {
$variants[$configuratorSetting->getOption()->getGroup()->getName()][] = $configuratorSetting->getOption()->getName();
}
$product->addExtension('variantsArray', new ArrayStruct($variants));
}
}
}
Then in the template you can use the extension for those products that have variants.
{% if product.extensions.variantsArray is defined %}
<ul>
{% for group, values in product.extensions.variantsArray.all() %}
<li>
{{ group }}:
{% for value in values %}
{{ value }}{% if not loop.last %},{% endif %}
{% endfor %}
</li>
{% endfor %}
</ul>
{% endif %}

Related

AngularFire Quickstart Not Returning Data From Firestore

Followed steps in AngularFire Quickstart
Add authentication as described in 5. Getting started with Firebase Authentication
When I set my Firestore rules to limit read and write access to authenticated users, I either get an error or I get nothing. For more details, see Issue #2838 filed in the GitHub repository.
My environment is:
Angular CLI: 12.0.1
Node: 14.17.0
Package Manager: npm 7.13.0
AngularFire: 6.1.5
Firebase: 8.6.1
Firebase Tools: 9.11.0
OS: Ubuntu 20.04.2 LTS (linux x64)
My Firestore rules are:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if request.auth != null;
allow write: if request.auth != null;
}
}
}
app.component.ts
import { Component, OnInit } from '#angular/core';
import { AngularFirestore } from '#angular/fire/firestore';
import { AngularFireAuth } from '#angular/fire/auth';
import firebase from 'firebase/app';
import { Observable } from 'rxjs';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular Fire Quickstart';
userDisplayName: string | null = null;
leagues$: Observable<any[]> = new Observable<any[]>();
constructor(
private firestore: AngularFirestore,
public auth: AngularFireAuth) {}
ngOnInit() {
// Recommended in Firebase documentation
this.auth.onAuthStateChanged((user) => {
if (user) {
this.userDisplayName = user.displayName;
this.leagues$ = this.firestore.collection('Leagues').valueChanges();
} else {
this.userDisplayName = null;
this.leagues$ = new Observable<any[]>();
}
});
}
login() {
this.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
}
logout() {
this.auth.signOut();
}
}
app.component.html
<div *ngIf="userDisplayName != null; else showLogin">
<p>Hello {{userDisplayName}}.
<button (click)="logout()">Logout</button>
</p>
<ul>
<li *ngFor="let league of leagues$ | async">
{{ league.name }} - {{ league.location }}
</li>
</ul>
</div>
<ng-template #showLogin>
<p>Please login.</p>
<button (click)="login()">Login</button>
</ng-template>
Resolved by subscribing to the Observable and changing my template to watch an array managed by the subscription. I now get data every time.
Here's the code with the changes:
app.component.ts
...
export class AppComponent implements OnInit {
title = 'Angular Fire Quickstart';
theUser: firebase.User | null = null;
leagueArray: Array<any> = []; // Eliminated the Observable in favor of an Array
constructor(
private firestore: AngularFirestore,
public auth: AngularFireAuth) {}
ngOnInit() {
this.auth.onAuthStateChanged((user) => {
if (user) {
this.theUser = user;
// Added the subscription and populated the array from there.
this.firestore.collection('Leagues').valueChanges().subscribe((data) => {
data.forEach((item) => {
this.leagueArray.push(item);
});
});
} else {
this.theUser = null;
this.leagueArray = [];
}
});
}
...
app.component.html
<div *ngIf="theUser != null">
<p>Hello {{theUser.displayName}}.
<button (click)="logout()">Logout</button>
</p>
<ul>
<!-- Watch the array instead of an Observable -->
<li *ngFor="let league of leagueArray">
{{ league.name }} - {{ league.location }}
</li>
</ul>
</div>

asp-page-handler helper tag not populating the correct hyperlink

I'm having trouble with asp-page-handler populating the correct link when passing in an route value for an ID and not the handler (so "{int:id}" vs "{handler?}"). (See bottom of image).
I'm expecting something such as:
https://localhost:5001/Emp/Download?id=Matthew.pdf
In my small test app I hardcoded the employee id value into the GetDirectoryReference("E000002/stubs/") and it works fine. (Note that the E000002 is the value that changes dependant upon the logged in person. The value does populate in my OnGetAsync() call so no issue there).
Setup for customer interface GetEmployeePayrollNo:
public async Task<Employee> GetEmployeePayrollNo(int id)
{
return await _context.Employees
.Include(e => e.EmployeePayroll)
.Where(p => p.EmployeeId == id)
.FirstAsync();
}
In this test I'm trying to pass a variable into GetDirectoryReference dependant upon who is logged in.
Not sure what I'm messing up since selecting download or view doesn't even hit on debug mode.
Using Azure File Share to hold documents*
Using Razor pages for folder structure*
Pages
Emp
Paystubs.cshtml
Paystubs.cshtml takes an id route value for the person logged in. In my test application it took a "{handler?}" route value. Not sure if I can use both???
Model
private readonly IConfiguration _configuration;
private readonly ICustomer _customer;
public PaystubsModel(IConfiguration configuration,
ICustomer customer)
{
_configuration = configuration;
_customer = customer;
}
public Employee Employee { get; set; }
public List<AzureFileModel> AzureFileModel { get; private set; } = new List<AzureFileModel>();
public async Task OnGetAsync(int id)
{
Employee = await _customer.GetEmployeePayrollNo(id);
var empPayNo = Employee.EmployeePayroll;
string fileStorageConnection = _configuration.GetValue<string>("FileStorage");
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(fileStorageConnection);
CloudFileShare share = storageAccount.CreateCloudFileClient().GetShareReference("test");
CloudFileDirectory root = share.GetRootDirectoryReference();
CloudFileDirectory dir = root.GetDirectoryReference(empPayNo.EmployeeOnline+"/stubs");
// list all files in the directory
AzureFileModel = await ListSubDir(dir);
}
public static async Task<List<AzureFileModel>> ListSubDir(CloudFileDirectory fileDirectory)
{
// LEFT OUT FOR BREVITY
}
public async Task<IActionResult> OnGetDownload(string fileId)
{
var empPayNo = Employee.EmployeePayroll;
string fileStorageConnection = _configuration.GetValue<string>("FileStorage");
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(fileStorageConnection);
CloudFileShare share = storageAccount.CreateCloudFileClient().GetShareReference("test");
CloudFileDirectory rootDir = share.GetRootDirectoryReference();
CloudFileDirectory dir = rootDir.GetDirectoryReference(empPayNo.EmployeeOnline+"/stubs");
CloudFile file = dir.GetFileReference(fileId);
if (!file.Exists())
{
ModelState.AddModelError(string.Empty, "File not found.");
return Page();
}
else
{
await file.DownloadToStreamAsync(new MemoryStream());
Stream fileStream = await file.OpenReadAsync();
return File(fileStream, file.Properties.ContentType, file.Name);
}
}
Page:
#page "{id:int}" //*******In my test model I was using {handler?} but I need to pass in the employee id to route here to display only the logged in employee. Again, not sure if its possible to use both?
#model NavraePortal.WebApp.Pages.Emp.PaystubsModel
#{
ViewData["Title"] = "Documents";
}
<h1>Pay Stub Copies</h1>
<table class="table table-bordered">
<thead>
<tr>
<th>File Name</th>
<th>File Date</th>
<th>Download</th>
<th>View</th>
</tr>
</thead>
<tbody>
#foreach (var data in Model.AzureFileModel)
{
<tr>
<td>#data.FileName</td>
<td>#data.DateModified</td>
<td>
<a class="btn btn-primary" asp-route-id="#data.FileName" asp-page-handler="Download">Download</a>
</td>
<td>
<a class="btn btn-info" asp-route-id="#data.FileName" asp-page-handler="View">View</a>
</td>
</tr>
}
</tbody>
</table>
Because this id in asp-route-id is not matched with the fileId. In this page, you need to modify it.
<a class="btn btn-primary" asp-route-fileId="#data.FileName" asp-page-handler="Download">Download</a>
Then, this url is updated.

Get http referer URI from a view using twig

I want to know in Twig from what URI the user came. For example:
The user is on the page /en/terms-of-use ('app_default_terms')
User clicks on login
On the register page is a hidden input to send the user back from where he came <input type="hidden" name="_target_path" value="{{ get_uri_from_where_he_came() }}" />
He submits the form and is sent back.
My question is, what is the twig function for this?
Using symfony framework, you can access a special global variable app which hold the request among other attributes.
see http://symfony.com/doc/current/book/templating.html#global-template-variables
So you could use something like :
{% set referer = app.request.server.get('http-referer')|default('/') %}
<input type="hidden" name="_target_path" value="{{ referer }}" />
My suggestion is a bit over-killing but ensures that you don't rely on arbitrary data coming from users.
The idea
You always save the previous route on your application using an event listener; and you implement a /reload path that will send the user back to that route. On login success, you just have to redirect your user to that /reload path and you're good to go.
The implementation
Change namespaces to fit with your application.
LastRouteListener.php
<?php
namespace Fuz\QuickStartBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Fuz\QuickStartBundle\Services\Routing;
class LastRouteListener implements EventSubscriberInterface
{
protected $routing;
public function __construct(Routing $routing)
{
$this->routing = $routing;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
try {
$currentRoute = $this->routing->getCurrentRoute($request);
} catch (ResourceNotFoundException $ex) {
return;
}
if (is_null($currentRoute)) {
return;
}
$session = $request->getSession();
$previousRoute = $session->get('current_route', array());
if ($currentRoute == $previousRoute) {
return;
}
$session->set('previous_route', $previousRoute);
$session->set('current_route', $currentRoute);
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array(array('onKernelRequest', 15)),
);
}
}
services.yml
parameters:
# ...
quickstart.last_route.listener.class: Fuz\QuickStartBundle\EventListener\LastRouteListener
services:
# ...
quickstart.last_route.listener:
class: %quickstart.last_route.listener.class%
arguments: [#quickstart.routing]
tags:
- { name: kernel.event_subscriber }
ReloadController.php
<?php
namespace Fuz\QuickStartBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Fuz\QuickStartBundle\Base\BaseController;
class ReloadController extends BaseController
{
/**
* Get back to the previous route
*
* #Route("/reload", name="reload")
* #Method({"GET"})
*/
public function reloadAction(Request $request)
{
if ($request->getSession()->has('previous_route')) {
$route = $request->getSession()->get('previous_route');
$route['params']['_locale'] = $request->getLocale();
return $this->redirect($this->generateUrl($route['name'], $route['params']));
}
return $this->redirect($this->generateUrl('home'));
}
}
Live
You can clone "symfony-quickstart" from my GitHub if you want to see this implementation live.
https://github.com/ninsuo/symfony-quickstart
Symfony 5.4
This worked for me.
app.request.headers.get('referer')

Writing a Content Part that contains a list of content parts

I've been trying to write a simple accordian widget, where each section of accordian would be its own content part. I have the leafs content part created fine, but I want to create the accordian part which contains a list of the leafs. I havn't been able to find a good tutorial that went over something like this. I'm working on displaying the leafs now, and am running into issues. I'm trying to mimic the comments module. This is what I have. It seems like i am able to get up to the list of leafs in the driver, but i'm not sure what to do with the view. i see comments calls #Display(Model.List) but i have no idea what this is doing.
Edit View (Just using a textbox for one leaf id, need to figure out how to select leaves):
<fieldset>
<legend>Accordian Fields</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.AccordianLeaf)
</div>
<div class="editor-field">
#*#Html.DropDownListFor(m => m.AccordianLeaf,
new System.Web.Mvc.SelectList(service.GetComments(), "Value", "Text"))*#
#Html.TextBoxFor(model => model.AccordianLeaf)
#Html.ValidationMessageFor(model => model.AccordianLeaf)
</div>
</fieldset>
Model:
namespace SuccessCenter.Models
{
public class AccordianRecord : ContentPartRecord
{
public virtual string Title { get; set; }
public virtual int AccordianLeaf { get; set; }
}
public class AccordianPart : ContentPart<AccordianRecord>
{
[Required]
public string Title
{
get { return Retrieve(r => r.Title); }
set { Store(r => r.Title, value); }
}
[Required]
public int AccordianLeaf
{
get { return Retrieve(r => r.AccordianLeaf); }
set { Store(r => r.AccordianLeaf, value); }
}
}
}
Handler:
namespace SuccessCenter.Handlers
{
public class AccordianHandler : ContentHandler
{
public AccordianHandler(IRepository<AccordianRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
}
}
Driver:
namespace SuccessCenter.Drivers
{
[UsedImplicitly]
public class AccordianDriver : ContentPartDriver<AccordianPart>
{
private readonly IAccordian _accordian;
public AccordianDriver(IAccordian accordian)
{
_accordian = accordian;
}
protected override DriverResult Display(AccordianPart part, string displayType, dynamic shapeHelper)
{
//return ContentShape("Parts_Accordian", () => shapeHelper.Parts_Accordian(Title: part.Title, AccordianLeaf: part.AccordianLeaf));
return Combined(
ContentShape("Parts_Accordian",
() =>
{
// create a hierarchy of shapes
var firstLevelShapes = new List<dynamic>();
var allShapes = new Dictionary<int, dynamic>();
var AccordianLeafs = _accordian.AccordianLeafs.ToList();
foreach (var item in AccordianLeafs)
{
var shape = shapeHelper.AccordianLeaf(ContentPart: item, ContentItem: item.ContentItem);
allShapes.Add(item.Id, shape);
}
var list = shapeHelper.List(Items: allShapes);
return shapeHelper.Parts_Accordian(
List: list
);
}));
}
//GET
protected override DriverResult Editor(AccordianPart part, dynamic shapeHelper)
{
return ContentShape("Parts_Accordian_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/Accordian", Model: part, Prefix: Prefix));
}
//POST
protected override DriverResult Editor(AccordianPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
}
View:
#using SuccessCenter.Models;
}<div class="expand-view expanded">
<header class="bg-brand-blue txt-white relative">
<h3 class="txt-left">#Model.List.Title</h3>
<span class="toggle v-align absolute">
<span class="expanded">Colllapse <i class="icons icon-carat-up-wh"></i></span><span class="collapsed">Expand <i class="icons icon-carat-down-wh"></i></span>
</span>
</header>
<section class="default-padding">
#Model.List.AccordianLeaf
</section>
##Display(Model.List)#
The List shape (shapeHelper.List()) takes a range of content item shapes, which you seem to build with shapeHelper.AccordionLeaf(). Therefore you can just display it with the Display method:
#Display(Model.List)
This method will display the List property on your model (your model properties are the ones you give as parameter in shapeHelper.Parts_Accordian(/* model properties */)
I am not sure what you are trying to do in the view, it seems like your want to iterate over the items in the List shape? In that case you can do something like this:
#foreach (var item in Model.List.Items) {
// item here is an AccordionLeaf shape
#Display(item)
}

Nesting layouts/views keeping the content variable in Zend Framework 2

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>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
</header>
<section>
<?= $this->content; ?>
</section>
<footer>
<ul>
<li>Copyright</li>
<li>Sitemap</li>
<li>Privacy policy</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.
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.

Resources