Symfony 3.4 - Rendering Custom Views on Every Page Load - php

I have a large application using SF 3.4 and I need to find a better way to do custom rendering based on user on each page load.
Right now we have listeners for a side menu, a footer, side menu favorites, an application menu, and whether or not you are timed out. These listeners fire a theme event via the render function that adds information pertaining to the users access/favorites/if they are timed out. This cannot be done with roles in twig because we have things like menu favorites that change all the time.
We override the Controller render method to dispatch that theme event and array_merge what we get back with the render function's parameter array like this:
protected function render($view, array $parameters = array(), Response $response = null)
{
$themeEvent = new ExampleThemeEvent($this->getUser(), $this->getMyMenuApplication());
$result = $this->getDispatcher()->dispatch(ExampleThemeEvent::NAME, $themeEvent);
$parameters = array_merge($result->getModel(),$parameters);
return parent::render($view, $parameters, $response);
}
Because of this most of our Controllers extend ExampleThemeController instead of just Controller so every page we want to have these features does.
This works fine, but the render method has been marked final as of SF 3.4 and shouldn't be overridden. What would be the best way to approach this without overriding the render method? Is there another place I can dispatch our theme event?
We have tried onKernelController subscribers/listeners to no avail because we need to add the extra view parameters to the container which cannot be done at that point.
Thank you!

For such user-specific changes that go beyond passing globals to twig, I'd check out embedding controllers. You can render a part of your response with a different controller, responsible only for the favourites for example.

Related

Dealing with Views in Phalcon Controllers

I am working on a newly created Phalcon project, and I don't really know how to actually use multiples views.
What is the entry point? I don't really know when each method in the controller is called, under which conditions, etc.
Where is the control flow defined? is it based in the name of the view? or is there a place where you can register them?
Phalcon is a bit different than other well-known PHP frameworks, in that not much is pre-configured or pre-built by default. It's quite loosely-coupled. So you have to decide where and how your control flow will work. This means that you will need to dig deeper in the documentation and also that there could be different way to achieve the same thing.
I'm going to walk you through a simple example and provide references, so you can understand it more.
1) You would start by defining a bootstrap file (or files) that will define the routes, or entry points, and will setup and create the application. This bootstrap file could be called by an index.php file that is the default file served by the web server. Here is an example of how such bootstrap file will define the routes or entry points (note: these are just fragments and do not represent all the things that a bootstrap file should do):
use Phalcon\Di\FactoryDefault;
// initializes the dependency injector of Phalcon framework
$injector = new FactoryDefault();
// defines the routes
$injector->setShared('router', function () {
return require_once('some/path/routes.php');
});
Then it the routes.php file:
use Phalcon\Mvc\Router;
use Phalcon\Mvc\Router\Group as RouterGroup;
// instantiates the router
$router = new Router(false);
// defines routes for the 'users' controller
$user_routes = new RouterGroup(['controller' => 'users']);
$user_routes->setPrefix('/users');
$user_routes->addGet('/show/{id:[0-9]{1,9}}', ['action' => 'show']);
$router->mount($user_routes);
return $router;
Im defining routes in an alternate way, by defining routes groups. I find it to be more easy to organize routes by resource or controller.
2) When you enter the url example.com/users/show/123, the routes above will match this to the controller users and action show. This is specified by the chunks of code ['controller' => 'users'], setPrefix('/users') and '/show/{id:[0-9]{1,9}}', ['action' => 'show']
3) So now you create the controller. You create a file in, let's say, controllers/UsersController.php. And then you create its action; note the name that you used in the route (show) and the suffix of Action:
public function showAction(int $id) {
// ... do all you need to do...
// fetch data
$user = UserModel::findFirst(blah blah);
// pass data to view
$this->view->setVar('user', $user);
// Phalcon automatically calls the view; from the manual:
/*
Phalcon automatically passes the execution to the view component as soon as a particular
controller has completed its cycle. The view component will look in the views folder for
a folder named as the same name of the last controller executed and then for a file named
as the last action executed.
*/
// but in case you would need to specify a different one
$this->view->render('users', 'another_view');
}
There is much more stuff related to views; consult the manual.
Note that you will need to register such controller in the bootstrap file like (Im also including examples on how to register other things):
use Phalcon\Loader;
// registers namespaces and other classes
$loader = new Loader();
$loader->registerNamespaces([
'MyNameSpace\Controllers' => 'path/controllers/',
'MyNameSpace\Models' => 'path/models/',
'MyNameSpace\Views' => 'path/views/'
]);
$loader->register();
4) You will also need to register a few things for the views. In the bootstrap file
use Phalcon\Mvc\View;
$injector->setShared('view', function () {
$view = new View();
$view->setViewsDir('path/views/');
return $view;
});
And this, together with other things you will need to do, particularly in the bootstrap process, will get you started in sending requests to the controller and action/view defined in the routes.
Those were basic examples. There is much more that you will need to learn, because I only gave you a few pieces to get you started. So here are some links that can explain more. Remember, there are several different ways to achieve the same thing in Phalcon.
Bootstrapping:
https://docs.phalconphp.com/en/3.2/di
https://docs.phalconphp.com/en/3.2/loader
https://docs.phalconphp.com/en/3.2/dispatcher
Routing: https://docs.phalconphp.com/en/3.2/routing
Controllers: https://docs.phalconphp.com/en/3.2/controllers
More on Views (from registering to passing data to them, to templating and more): https://docs.phalconphp.com/en/3.2/views
And a simple tutorial to teach you some basic things: https://docs.phalconphp.com/en/3.2/tutorial-rest
The application begins with the routing stage. From there you grab the controller and action from the router, and feed it to the dispatcher. You set the view then call the execute the dispatcher so it access your controller's action. From there you create a new response object and set its contents equal to the view requests, and finally send the response to the client's browser -- both the content and the headers. It's a good idea to do this through Phalcon rather than echoing directly or using PHP's header(), so it's only done at the moment you call $response->send(); This is best practice because it allows you to create tests, such as in phpunit, so you can test for the existence of headers, or content, while moving off to the next response and header without actually sending anything so you can test stuff. Same idea with exit; in code, is best to avoid so you can write tests and move on to the next test without your tests aborting on the first test due to the existence of exit.
As far as how the Phalcon application works, and in what steps, it's much easier to follow the flow by looking at manual bootstrapping:
https://docs.phalconphp.com/en/3.2/application#manual-bootstrapping
At the heart of Phalcon is the DI, the Dependency Injection container. This allows you to create services, and store them on the DI so services can access each other. You can create your own services and store them under your own name on the DI, there's nothing special about the names used. However depending on the areas of Phalcon you used, certain services on the DI are assumed like "db" for interacting with your database. Note services can be set as either shared or not shared on the DI. Shared means it implements singleton and keeps the object alive for all calls afterwards. If you use getShared, it does a similar thing even if it wasn't initially a shared service. The getShared method is considered bad practice and the Phalcon team is talking about removing the method in future Phalcon versions. Please rely on setShared instead.
Regarding multiple views, you can start with $this->view->disable(); from within the controller. This allows you to disable a view so you don't get any content generated to begin with from within a controller so you can follow how views work from within controllers.
Phalcon assumes every controller has a matching view under /someController/someView followed by whatever extension you registered on the view, which defaults to .volt but can also be set to use .phtml or .php.
These two correspond to:
Phalcon\Mvc\View\Engine\Php and Phalcon\Mvc\View\Engine\Volt
Note that you DON'T specify the extension when looking for a template to render, Phalcon adds this for you
Phalcon also uses a root view template index.volt, if it exists, for all interactions with the view so you can use things like the same doctype for all responses, making your life easier.
Phalcon also offers you partials, so from within a view you can render a partial like breadcrumbs, or a header or footer which you'd otherwise be copy-pasting into each template. This allows you to manage all pages from the same template so you're not repeating yourself.
As far as which view class you use within Phalcon, there's two main choices:
Phalcon\Mvc\View and Phalcon\Mvc\View\Simple
While similar, Phalcon\Mvc\View gives you a multiple level hierarchy as described before with a main template, and a controller-action based template as well as some other fancy features. As far as Phalcon\Mvc\View\Simple, it's much more lightweight and is a single level.
You should be familiar with hierarchical rendering:
https://docs.phalconphp.com/en/3.2/views#hierarchical-rendering
The idea is with Phalcon\Mvc\View that you have a Main Layout (if this template exists) usually stored in /views/index.volt, which is used on every page so you can toss in your doctypes, the title (which you would set with a variable the view passed in), etc. You'd have a Controller Layout, which would be stored under /views/layouts.myController.volt and used for every action within a controller (if this template exists), finally you'd have the Action Layout which is used for the specific action of the controller in /views/myController/myAction.volt.
There are all types of ways you can break from Phalcon's default behavior. You can do the earlier stated $this->view->disable(); so you can do everything manually yourself so Phalcon doesn't assume anything about the view template. You can also use ->pick to pick which template to use if it's going to be different than the controller and action it's ran in.
You can also return a response object from within a controller and Phalcon will not try to render the templates and use the response object instead.
For example you might want to do:
return $this->response->redirect('index/index');
This would redirect the user's browser to said page. You could also do a forward instead which would be used internally within Phalcon to access a different controller and/or action.
You can config the directory the views are stored with setViewsDir. You can also do this from within the controller itself, or even within the view as late as you want, if you have some exceptions due to a goofy directory structure.
You can do things like use $this->view->setTemplateBefore('common') or $this->view->setTemplateAfter('common'); so you can have intermediate templates.
At the heart of the view hierarchy is <?php echo $this->getContent(); ?> or {{ content() }} if you're using Volt. Even if you're using Volt, it gets parsed by Phalcon and generates the PHP version with $this->getContent(), storing it in your /cache/ directory, before it is executed.
The idea with "template before" is that it's optional if you need another layer of hierarchy between your main template and your controller template. Same idea with "template after" etc. I would advise against using template before and after as they are confusing and partials are better suited for the task.
It all depends on how you want to organize your application structure.
Note you can also swap between your main template to another main template if you need to swap anything major. You could also just toss in an "if" statement into your main template to decide what to do based on some condition, etc.
With all that said, you should be able to read the documentation and make better sense of how to utilize it:
https://docs.phalconphp.com/en/3.2/api/Phalcon_Mvc_View

URL routing patterns for frontend pages in PHP

Im creating a MySQL database driven PHP (W)CMS application which follows the MVC pattern. First take a look at the framework:
The MVC framework handles the request and decides what to load/call based on the URL, like: http://domain.com/user/details/121 will load and instantiate a User controller object, and calls its details(121) method with the userid passed as a parameter, and then instantiate a User_Model and "ask" it for the detailed data of the user with the 121 userid, and at last display the result with a View. This is the basic concept of an MVC architecture. Nothing particular, everything is clear at this point.
Whereas this will be a CMS, I want to handle a Page model. A user with the nesessary permissions (mostly admin and/or root) can perform basic CRUD operations and other stuff on a page, for example:
I can create a page with the:
tile = 'About us' (this will be displayed as a headline of the page or the title of the browser tab like eg.: HTML title and h1 tags)
URL denomination = '*about_us*' (this will be the URI endpoint, like: http://domain.com/about_us)
reference name = 'Who we are' (This is the text displayed in the menubar)
page content = 'lorem ipsum...' (The actual content of the page...by a WYSIWYG html text editor)
and much more options like structuring the pages, to assign sub pages under a parent page, or making a page startpage (which means if I set 'About us' as a start page, then http://domain.com will automaticall load that page content)...
Or I can modify these properties, even I can delete a page...etc.
The MVC framework makes no difference between handling a frontend and a backend call.
For example we have some requests:
http://domain.com/user/details/121
http://domain.com/about_us
http://domain.com/our_products/1255
The first will load a backend controller as I detailed before,
but the others will load a frontend content.
When the Bootstrap loads the appropriate controller/action we look for the actual controller file, in the example above :
/controllers/Users.php
/controllers/About_us.php
/controllers/Our_products.php
The first can be loaded because that is a 'static' controller written before, but the About_us and Our_products are not existing controllers so If it is impossible to load the controller, the bootstrap searches the database if is there a page with the same URL denimination (like: about_us, our_products). If there is, we load a common FrontEndController and display the requested page data, if there isn't, display a 404 error.
I do this because I want the bootstrap to handle all requests the same way, but I dont want to every frontend URL compulsorily contain the FrontEndController (e.g.:http://domain.com/FrontEndController/our_products/1255). So this is how I hide it from the user, so the URL can remain more user friendly. My question is: Is this a good practice? Or are there any other proper ways to do this?
The MVC framework handles the request and decides what to load/call based on the URL
What you would normally is have is some sort of Router and Dispatcher class. The router would accept the the user/details/121, parse it and return a Route.
$route = $router->route( $request->getUri() );
The router could hold config values like the allowed space character in URI's, default allowed characters etc.
You can also add custom routes to the router
$router->addRoutes($routes);
The custom routes can be a simple associative array
$routes['requested-uri'] = 'custom-route'
In the example above you said when they visit the root of the website you want them to actually see the About Us page so that could be done like this:
$router->addRoutes([
'' => 'about-us
]);
Meaning when the URI is ''(blank) then go to the 'about-us' route. It shouldn't do a redirect, just transparently load up a different route while keeping the URI in the clients web browser the same.
Routing can obviously be more complex, using route objects added to a route collection for more advanced custom routing with more control. Some frameworks use annotations and all sort of different ways to achieve flexible routing.
The dispatcher could then accept the route returned from the router and dispatch it. That means verifying if the requested route actually exists i.e does the controller file exist and the requested method in the controller exist.
$view = $dispatcher->dispatch($route);
Inside the Dispatcher::Dispatch() method:
// Check if the controller file exists.
// Instantiate the controller file, preferably using a controller factory.
// Check if the controller method exists.
// Call the controller method
call_user_func_array([$controller, $route->getMethod()], $route->getParams());
$view = $controller->getView();
$action = $route->getAction();
// Call the view method.
if( method_exists($view, $action) ) {
$view->$action();
}
return $view;
I find the following a very easy to understand way of dealing with controller methods/actions. Let's say you have a login controller, the user sends a GET request to it first and a POST request to it when sending the login details in the form.
public function getIndex() { }
public function postIndex() {
$username = $this->request->post('username');
$password = $this->request->post('password');
}
The get and post in front of the method name is the request type, this prevents you having to do something like this
public function index() {
if( $this->request->getType() === 'POST' ) {
$username = $this->request->post('username');
$password = $this->request->post('password');
}
}
It also gives you more control over authorisation(if you do it at the routing layer) because you can easily allow a user to send a GET request to the controller but deny them access to sending a POST request.
Each controller has a one to one relationship with a view. The view get's injected into the controller on construction, preferably using a controller factory.
What would happen when you send a GET request http://domain.com/user/details/121 is the router would break up the URI and turn it into a route targeting the User controller, the getDetails() method with the parameter 121, the dispatcher checks if the controller and method exist, it then calls the method supplying the user ID as an argument, the controller sets the user ID in the view. Below is the User controller.
public function getDetails($userId) {
$this->getView()->setUserId( (int)$userId );
}
The view then has a method called details(). The same name as the method called in the controller, just without the request type in front of it.
The dispatcher then calls the details() method of the view which then fetches the required data.
Setting the title of the page is done in the view, as it is for presentation purposes only.
Part of the view that is related to the User controller
public function details() {
// Fetch the user by using the previously set user ID from the controller.
// If he doesn't exist set an error template, set the response code to 404,
// or redirect. Do whatever you want really.
$this->setTitle('User Details');
// Build template objects, bind the fetched user data to main template.
}
How you implement the setTitle method and all over view related stuff is up to you.
The view sends the response back to the client, whether it is HTML content, JSON, XML, or any other content type.
For example your application lets you search for users and export them to a Microsoft Excel Workbook file(.xlsx) and prompt the user to download it.
The view would:
Fetch the users
Generate the file
Set the HTTP response headers like Content-Type
Send the response

Issue on zend dispatch loop

I'm using Zend Framework to build a website and I'm having some trouble with the dispatch loop.
Normally, Zend Framework URLs are built this way: http://www.domain.com/module/controller/action.
On my website, I'm using customized dynamic URLs which are parsed on the dispatch loop by a custom method. So, each one of these URLs, after being parsed, will execute a specific action of a specific controller and module.
I need to perform some tasks which depend on the module, controller and action that were parsed. The problem is that I'm only being able to know the parsed module, controller and action when dispatchLoopShutdown occurs. The tasks that I need to execute will set some cookies which will make changes on the output that will be sent to the browser.
But, at this point, the view has already been rendered, and the cookies that were set when dispatchLoopShutdown occurs won't change the output accordingly.
So, my question is... is there a way to force the view to be rendered again? Or a way to know what module, controller and action will be executed, before the dispatchLoopShutdown? I've also tried to accomplish this on the postDispatch but the results are the same!
Hope I could explain my problem right.
Thank you for your help.
Here is a good schema about the Zend Framework sequence.
You can know the module, controller and action before the dispatch by using a controller plugin:
<?php
class Custom_Controller_Plugin_CheckRoute extends Zend_Controller_Plugin_Abstract
{
public function preDispatch($request)
{
//Get Request
$controller = $request->controller;
$action = $request->action;
$module = $request->module;
//=> perform actions before dispatch
//Update the Request
$request->setModuleName('default')
->setControllerName('index')
->setActionName('action2');
}
}
?>
I had the same problem. It was solved by Zend_Controller_Plugin_ActionStack. I added some action where implemented logic from dispatchLoopShutdown. This link can be useful http://framework.zend.com/manual/1.12/en/zend.controller.plugins.html#zend.controller.plugins.standard.actionstack

Create a Zend_Navigation_Page_Mvc instance from a Zend_Controller_Request

I have pages in my application that make up the navigation tree. I would like to dynamically insert pages into my navigation using the values of the request. I already have the logic to find the page and then call the addPage() method on it. What I'm looking for is how to easily pass the Zend_Controller_Request values to Zend_Navigation_Page::factory() so I can add that page. Maybe even written as a plugin?
Solution
AngelP got the closest, so I'm giving him credit, but here's my solution:
$request = $this->getRequest();
if ($page = $this->view->siteNav->findBy('id', $page_id)) {
$page->addPage(Zend_Navigation_Page::factory($request->getParams())
->setParams($request->getParams())
->setLabel($this->view->title)
->setVisible(false));
}
This code is executed from a controller action. $this->view->siteNav is an instance of Zend_Navigation that I have in the view. getParams() from the Zend_Controller_Request instance is easily passed to Zend_Navigation_Page::factory() and then to the setParams() method of the Zend_Navigation_Page_Mvc instance.
I have limited resources at the moment so I cannot really check my suggestion, but if you're in you controller, why don't you..
$controller = $this->_request->getControllerName();
$action = $this->_request->getActionName();
$page = new Zend_Navigation_Page( array(
'label' => "Sonny's Page",
'controller' => $controller,
'action' => $action
));
Maybe you could use this as a plugin so that you overload your view? And then add to your Navigation Container?
Cheers,
Angel
Since we don't know the code you have written already, I'm only guessing…
You need to:
retrieve the actual Zend_Navigation container used in navigation() view helper
create new Zend_Navigation_Page instance from array of data retrieved from the request
add the page to the container
assign the new container to the navigation helper
This should be easy. The rest you need to know:
how to write controller plugin with preDispatch method, and put the above there,
how to access current view instance in this plugin (from the view renderer or from application resource/bootstrap)
Then in the plugin you operate on navigation view helper as usual in the view.
Hope this clarified some things.
Why not store the instance in Zend_Registry and then in a postDispatch from either a plugin, module bootstrap or action controller add the pages to the original nav?

Using actionHelper without returning layout in output

I call an action helper in one of my views using the following code
echo $this->action('foo', 'bar');
The fooAction in the barController does its thing and outputs a list of pages. However, the list has the layout in the output again, which is mightily irritating. If I disable the layout in the fooAction, this causes layout to be completely disabled on the live side, as well.
I'm vexed. I could just create a view helper, and there are many ways around this, but out of curiousity I was wondering if anyone had a solution to this.
From the ZF Reference Guide on Action ViewHelper
The API for the Action view helper follows that of most MVC components that invoke controller actions: action($action, $controller, $module = null, array $params = array()). $action and $controller are required; if no module is specified, the default module is assumed.
Modify your controller to accept a param that controls whether the action should disable the layout. When using the action helper, pass this control flag.
On a sidenote: using the Action ViewHelper is considered bad practise as it will go through the entire dispatch process again and this will slow down your app. If possible, try to access the model directly.

Categories