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')
Related
I have an AuthorizeView component based on authentication through an OpenID Connect provider, but I want to add an extra layer based on some informations found about the user in the associated database after OIDC authentication. I am wondering whether the logic illustrated below,
1: Would be the smartest way of implementing this?
2: And if so, how to approach the task of building my own authorization view inside an already existing one?
<AuthorizeView>
<Authorized>
<ExtraLayerAuthorized>
<p>Authorized through OIDC and extra layer</p>
</ExtraLayerAuthorized>
<NotExtraLayerAuthorized>
<p>Authorized through OIDC, but NOT extra layer</p>
</NotExtraLayerAuthorized>
</Authorized>
<NotAuthorized>
<p>No authorization</p>
</NotAuthorized>
</AuthorizeView>`
Note sure if the is Server or WASM.
One good way to do this is just to add an additional ClaimsIdentity to the existing ClaimsPrincipal of the standard AuthenticationStateProvider.
Here's how to do it in Server. See the comments for an explanation of what it does.
public class MyAuthenticationStxateProvider : ServerAuthenticationStateProvider
{
public async override Task<AuthenticationState> GetAuthenticationStateAsync()
{
// Call the base to get the AuthState and the user provided in the Security Headers by the server
var authstate = await base.GetAuthenticationStateAsync();
var user = authstate.User;
if (user?.Identity?.IsAuthenticated ?? false)
{
// Do whatever you want here to retrieve the additional user information you want to
// include in the ClaimsPrincipal - probably some form of Identity Service
// Construct a ClaimsIdentity instance to attach to the ClaimsPrincipal
// I just added a role as an example
var myIdentity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "User") });
// Add it to the existing ClaimsPrincipal
user.AddIdentity(myIdentity);
}
// construct a new state with the updated ClaimsPrincipal
// - or an empty one of you didn't get a user in the first place
// All the Authorization components and classes will now use this ClaimsPrincipal
return new AuthenticationState(user ?? new ClaimsPrincipal());
}
}
This is setup to use Auth0 as the authentication provider.
"AllowedHosts": "*",
"Auth0": {
"Domain": "xxxxx.eu.auth0.com",
"ClientId": "xxxxxxxxxxxxxxxxxxxxxx",
}
The service registration looks something like this:
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services
.AddAuth0WebAppAuthentication(options => {
options.Domain = builder.Configuration["Auth0:Domain"];
options.ClientId = builder.Configuration["Auth0:ClientId"];
});
builder.Services.AddAuthorization();
builder.Services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
Login.cshtml.cs
public class LoginModel : PageModel
{
public async Task OnGet(string redirectUri)
{
await HttpContext.ChallengeAsync("Auth0", new
AuthenticationProperties
{ RedirectUri = redirectUri });
}
}
Logout.cshtml.cs
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnGetAsync()
{
await HttpContext.SignOutAsync();
return Redirect("/");
}
}
And then this page demonstrates logging in and out and adding the second identity when we have a valid user.
#page "/"
#inject NavigationManager NavManager
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div>
<button class="btn btn-danger" #onclick=this.LogOut>Log Out</button>
<button class="btn btn-primary" #onclick=this.LogIn>Log In</button>
</div>
<h2>Claims</h2>
<dl>
#foreach (var claim in user.Claims)
{
<dt>#claim.Type</dt>
<dd>#claim.Value</dd>
}
</dl>
#code {
private ClaimsPrincipal user = new ClaimsPrincipal();
[CascadingParameter] private Task<AuthenticationState> authStateProvider { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
var authState = await authStateProvider;
user = authState.User;
}
private void LogIn()
{
var returnUrl = NavManager.Uri;
NavManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);
}
private void LogOut()
{
var returnUrl = NavManager.Uri;
NavManager.NavigateTo($"logout?redirectUri={returnUrl}", forceLoad: true);
}
}
this is my first project with vue and nodejs so please let me know if I there is a missing information.
I'm trying to develop a group chat with Laravel, Vue js and Pusher.
Database Tables and Relations
I want to create a private Channel for each team which is available.
As soon as you click on the group chat, the existing messages get loaded and shown.
When you send a message, the message gets added to the messages table and is also sent successfully to pusher as you can see here:
Pusher message
The message is also added to the message list on the sender but not on the other team members.
Sender
Other team members
The new message is only shown on the other team members when they reload the page. That means that the echo listener doesn't seem to work. What can I do to fix it? What is wrong?
Here is my code:
ChatApp.vue (root component)
<template>
<div class="chat-container row">
<i class="far fa-comments fa-3x"></i>
<div id="chat-app" class="chat-app">
<div class="row mx-0 h-100 overflow-hidden">
<TeamList :teams="teamList" #selected="startConversationWith"/>
<Conversation :team="selectedTeam" :messages="messages" #new="saveNewMessage"/>
</div>
</div>
</div>
</template>
<script>
import MessageList from './MessageList';
import TeamList from './TeamList';
import Conversation from './Conversation';
import MessageTextBox from './MessageTextBox';
export default {
props: {
user: {
type: Object,
required: true
}
},
data() {
return {
messages: [],
teamList: [],
selectedTeam: null,
}
},
mounted() {
Echo.private('messages.1')
.listen('NewMessage', (e) => {
this.handleIncoming(e.message);
});
axios.get('/teams')
.then((response) => {
this.teamList = response.data;
});
},
methods: {
startConversationWith(team) {
axios.get('/conversation/' + team.id)
.then((response) => {
this.messages = response.data;
this.selectedTeam = team;
});
},
saveNewMessage(text) {
this.messages.push(text);
},
handleIncoming(message) {
this.saveNewMessage(message);
return;
}
},
components: {TeamList, MessageList, MessageTextBox, Conversation}
}
</script>
App/Events/NewMessage.php
<?php
namespace App\Events;
use App\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class NewMessage implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* #param Message $message
*/
public function __construct(Message $message)
{
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('messages.' . $this->message->team_id);
}
public function broadcastWith()
{
$this->message->load('team');
return ["message" => $this->message];
}
}
routes/channels.php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('messages.{id}', function ($team_id, $message) {
return true;
// return (int) $team->id === (int) $id;
});
Message model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $guarded = [];
public function team()
{
return $this->belongsTo('App\Team');
}
public function user()
{
return $this->belongsTo('App\User');
}
}
ContactsController
namespace App\Http\Controllers;
use App\Events\NewMessage;
use App\Message;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
class ContactsController extends Controller
{
public function getTeams() {
$teams = Auth::user()->teams;
return response()->json($teams);
}
public function getMessagesFor($id)
{
$messages = Message::where('team_id', $id)->get();
return response()->json($messages);
}
public function send(Request $request) {
$message = Message::create([
'team_id' => $request->team_id,
'user_id' => Auth::user()->id,
'message' => $request->text
]);
broadcast(new NewMessage($message));
return response()->json($message);
}
}
bootstrap.js
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
encrypted: true
});
encrypted is set to true for SSL config. Try setting it to false when configuring laravel echo in bootstrap.js
As you can see in the comments, setting encrypted to false in bootstrap.js solved the problem
I need some help/advise on how to make this work.
I need to pass the model from the view to the controller through an ActionLink
#Html.ActionLink("Radera", "DeleteTraffic", new { model = Model, trafficId = traffic.Id }, new { #class = "btn btn-link NoBorder NoBackGround" })
the method in the controller looks like this.
public ActionResult DeleteTraffic(CalendarModel model, int trafficId)
{
return View("EditDay", model);
}
I have not put any code in the method yet, I've only been debugging it to get the call to work. model is null when I press the button, trafficId is however correctly set. so what have I done wrong?
Edit 1:
I've changed the code according to the suggestions here.
#using (Html.BeginForm("DeleteTraffic", "Calendar", new {trafficId = traffic.Id})) {<input type="submit" value="Radera" class="btn btn-link NoBorder NoBackGround"/>}
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("DeleteTraffic")]
public ActionResult DeleteTraffic(int trafficId)
{
return View("EditDay", Model);
}
but DeleteTraffic is never reched, instead it calls the Main Action for this page.
// GET: Calendar
public ActionResult Calendar()
{
CalendarModel model = new CalendarModel {SelectedDate = DateTime.Today};
if (Request.HttpMethod == "POST")
{
if (!string.IsNullOrEmpty(Request.Form.Get("submit.SelectDate")))
{
model.SelectedDate = Convert.ToDateTime(Request.Form["selectedDate"]);
model.TrafficDates = TrafficData.GeTrafficDatesPerMonth(model.SelectedDate);
Model = model;
return View("EditDay", Model);
}
}
Model = model;
return View(Model);
}
should I just tuck the trafficId into a hiddenfield and use this action for everything? MVC seems so inflexible at times...
First, something like a "delete" should never be handled by GET. Deleting is atomic and should be done utilizing either the POST or DELETE (preferably) verbs. Generally, you also should not just delete something without user confirmation, so the simplest and correct way to handle this would be to have the "delete" link take the user to a view that asks them to confirm deleting the item. On this view, then, you would submit the id of the item to be deleted via a form post:
public ActionResult Delete(int id)
{
var foo = db.Foos.Find(id);
if (foo == null)
{
return new HttpNotFoundResult();
}
return View(foo);
}
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("Delete")]
public ActionResult DeleteConfirm(int id)
{
var foo = db.Foos.Find(id);
if (foo == null)
{
return new HttpNotFoundResult();
}
db.Foos.Remove(foo);
db.SaveChanges();
return RedirectToAction("Index");
}
Then, for your GET action, you would add a Delete.cshtml file:
#model Namespace.To.Foo
<p>Are you sure you want to delete the foo, "#Model.Name"?</p>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.HiddenFor(m => m.Id)
#Html.ActionLink("Cancel", "Index")
<button type="submit">Delete</button>
}
Alternatively (or rather progressively, as you should still have the previous method as a fallback), you could use a JavaScript confirm and AJAX to do this, if you don't want to change pages:
#Html.ActionLink("Radera", "DeleteTraffic", new { id = item.Id }, new { #class = "btn btn-link NoBorder NoBackGround delete", data_id = item.Id })
Then:
<script>
$('.delete').on('click', function () {
var $deleteLink = $(this);
if (confirm('Are you sure?')) {
$.post('/url/for/delete/', { id = $deleteLink.data('id') }, function () {
$deleteLink.closest('tr').remove();
});
}
});
</script>
I'm trying to set up some basic navigation on a web site I'm rewriting and I've run into a brick wall and don't see why this is not working. I'm doing something similar in a half dozen other places but it just ain't working.
What I want to do is if my article has a next and or previous ID I want to show a navigation bar with appropriate forward/reverse navigation arrows or whatever to allow user to navigate pages.
The ViewModel
public class NavViewModel
{
public int NextID { get; set; }
public int PreviousID { get; set; }
public string NextString { get; set; }
public string PreviousString { get; set; }
public bool SelectedMode { get; set; }
public NavViewModel() { }
}
The View
#Html.HiddenFor(model => model.NavigationViewModel.PreviousID)
#Html.HiddenFor(model => model.NavigationViewModel.NextID)
<div class="post-nav">
#if (#Model.NavigationViewModel.PreviousString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { #nvm = Model.NavigationViewModel }))
{
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.PreviousString" />
}
}
#if (#Model.NavigationViewModel.NextString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { nvm = #Model.NavigationViewModel }))
{
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.NextString" />
}
}
</div>
and the Controller
[HttpPost]
public ActionResult SinglePost(NavViewModel nvm)
{
return RedirectToAction("SinglePost", "Article", new { postID = nvm.PreviousID });
}
I've tried passing back the bool, the IDs, the ViewModel and they all come null or containing null values.
I had this code in a PartialView and because it wasn't working I moved it up a level into the calling view and it has the same result.
You have stated you want to navigate to the next and previous items so using forms and inputs and submitting to a POST method is not appropriate. Instead use a link to navigate to a GET method, passing the ID of the previous or next item.
#if (#Model.NavigationViewModel.PreviousString != null)
{
#Html.ActionLink(Model.NavigationViewModel.PreviousString, "SinglePost", "Article", new { postID = Model.NavigationViewModel.PreviousID }, null)
}
#if (#Model.NavigationViewModel.NextString != null)
{
#Html.ActionLink(Model.NavigationViewModel.NextString , "SinglePost", "Article", new { postID = Model.NavigationViewModel.NextID }, null)
}
The reason your code does not work will be obvious when you inspect the html generated for the <form> tag. Your generating an attribute nvm="YourAssembly.NavigationViewModel" (not a route value). If you used the correct overload to generate route values, which would be
using (Html.BeginForm("SinglePost", "Article", new { #nvm = Model.NavigationViewModel }))
it will still fail because it will generate something similar to (depending on you routes) action="/Article/SinglePost?nvm=YourAssembly.NavigationViewModel" so when you post back, the DefaultModelBinder will try to assign the string "YourAssembly.NavigationViewModel" to parameter nvm, but nvm is a complex object, not a string, so binding will fail.
You could make the POST method work by using
using (Html.BeginForm("SinglePost", "Article", Model.NavigationViewModel))
however this is just degrading performance by posting back unnecessary data and if your model contained properties that were complex objects or collections, it would fail anyway, so don't do it.
Finally, if you want to make the link look like a button, then style it using css.
Try to move hidden inputs into the form
<div class="post-nav">
#if (#Model.NavigationViewModel.PreviousString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { #nvm = Model.NavigationViewModel }))
{
#Html.HiddenFor(model => model.NavigationViewModel.PreviousID)
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.PreviousString" />
}
}
#if (#Model.NavigationViewModel.NextString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { nvm = #Model.NavigationViewModel }))
{
#Html.HiddenFor(model => model.NavigationViewModel.NextID)
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.NextString" />
}
}
</div>
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.