Yii. Render can work as renderPartial too? - php

In the past, with Yii 1, I use to load partial views with "renderPartial", if you did not use renderPartial, it loaded the "partial view" with layout. Now in Yii2, method renderPartial has been moved to the controller, BUT you also can do:
<?= $this->render('_form', [
'model' => $model,
]) ?>
Inside another view, and it works same way as renderPartial, what kind of sorcery is this? :D. I guess in some level Yii2 checks if it is being loaded inside another view and uses renderPartial but I have not been able to find it. Anyone has found this?
I came to this question, because I was reviewing Pjax, playing with it and I saw a couple examples where the controller used return $this->render but Pjax still worked, as far as I know Pjax stops working if <html> tag or error has been found on the response, am I right?

All the controller and view render() methods are essentially using the same method, part of the view model. It's a key concept in DRY (don't repeat yourself) The view model has this method;
public function render($view, $params = [], $context = null)
{
$viewFile = $this->findViewFile($view, $context);
return $this->renderFile($viewFile, $params, $context);
}
All it does is render the view file specified by $view.
Now in the controller, you have two methods; render() and renderPartial(). The difference is just that in one a layout is applied, and in the other it isn't. The code looks like this;
public function render($view, $params = [])
{
$content = $this->getView()->render($view, $params, $this);
return $this->renderContent($content);
}
The first line in the method does two things. Firstly it gets the view object, then, by chaining, it uses the render() method of that view object to generate the html from that view.
The second line then passes that content on to the renderContent() method of the controller, which applies any layout to the content.
The code for renderPartial() is this;
public function renderPartial($view, $params = [])
{
return $this->getView()->render($view, $params, $this);
}
As you can see, it's exactly the same as the render() method of the controller, except that it doesn't pass anything to the renderContent() method of the controller, it just outputs it.
As for the pjax part of your question, I'm guessing that as long as your pjax doesn't generate any substantial errors, it will work whether you are using $controller->render(), $controller->renderPartial() or $view->render().

Related

Common practice to pass class names to views in Laravel

I have a controller method that passes model names and class names to a view. These classes are then instantiated in another controller method. In this case I'm using the Laravel Excel package.
public function index()
{
$exports = [
'Model name 1' => TestExport::class,
'Model name 2' => AnotherExport::class
];
return view('export', compact('exports'));
}
public function download(string $collection)
{
return Excel::download(new $collection(), 'Export.xlsx');
}
My view file then redirects to the download controller method with the specific class name.
#foreach($exports as $name => $collection)
Download
#endforeach
Since I'm learning design patterns, and noticed it would break the DRY rule, I didn't want another controller method or every different Excel file that I downloaded.
Is this good practice or can this be done better?
You can make $exports common to both method and not accept class name from the request.
const EXPORTS = [
'export_name_1' => TestExport::class,
'export_name_2' => AnotherExport::class,
];
public function index()
{
return view('export', compact(self::EXPORTS));
}
public function download(string $collection)
{
if (!isset(self::EXPORTS[$collection]) {
return 'error';
}
$className = self::EXPORTS[$collection];
return Excel::download(new $className(), 'Export.xlsx');
}
Never let request manipulation break your code. For the export_name, you can simply use integers or the array simple index.
View
#foreach($exports as $name => $collection)
Download
#endforeach
To me it feels like your solution is mixing view logic with controller logic.
A controller should handle an incoming request, fetch the right data and then form a response.
A view should handle any logic based on the given data in order to render the output page.
In your case the data you pass to the view is not retrieved, it's just a static list which could be kept in the view just as well. A better solution would be to either:
Have a list of routes (not a list of classes) in your blade template and iterate over that instead.
If the list of routes is not that long, hardcode the whole list instead of making it dynamic.
The main reason for doing so would be to prevent a lot of 'magic' code. For instance in your blade template you expect exports to be an array which contains a list of classes which HAVE to be controllers and which HAVE to have a download function otherwise the code breaks.
An example for solution 1 would look something like:
<?php
// You could consider moving $routes to the controller
$routes = [
action('ExportController#download', TestExport::class),
action('ExportController#download', AnotherExport::class),
];
#foreach($routes as $route)
Download
#endforeach

How to make the controller data override view controller in Laravel?

To build a sidebar that has a lot of dynamic data on it I learned about View composers in Laravel. The problem was that View Composers trigger when the view loads, overriding any data from the controller for variables with the same name. According to the Laravel 5.4 documentation though, I achieve what I want with view creators :
View creators are very similar to view composers; however, they are
executed immediately after the view is instantiated instead of waiting
until the view is about to render. To register a view creator, use the
creator method:
From what I understand, this means if I load variables with the same name in the controller and the creator, controller should override it. However this isn't happening with my code. The view composer:
public function boot()
{
view()->creator('*', function ($view) {
$userCompanies = auth()->user()->company()->get();
$currentCompany = auth()->user()->getCurrentCompany();
$view->with(compact('userCompanies', 'currentCompany'));
});
}
And here is one of the controllers, for example:
public function name()
{
$currentCompany = (object) ['name' => 'creating', 'id' => '0', 'account_balance' => 'N/A'];
return view('companies.name', compact('currentCompany'));
}
the $currentCompany variable in question, for example, always retains the value from the creator rather than being overridden by the one from the controller. Any idea what is wrong here?
After some research, I realized my initial assumption that the data from the view creator can be overwritten by the data from the controller was wrong. The creator still loads after the controller somehow. But I found a simple solution.
For those who want to use View composers/creators for default data that can get overwritten, do an array merge between the controller and composer/creator data at the end of the boot() method of the composer/creator, like so:
$with = array_merge(compact('userCompanies', 'currentCompany', 'currentUser'), $view->getData());
$view->with($with);

Laravel Routing to same view different function

I am trying to return values from a MySQL database into a datatable using Chumper. I am trying to obtain values from 2 tables; but I am having what I believe to be a routing issue as I do not know how to include 2 controller functions to the same view ie in my search view I want to render the result of 2 functions like : for a deeper understanding of what I am trying to accomplish see question : Laravel Chumper Datatable getting data into one datatable from multiple MySQL tables
Route::get('search', array('as' => 'instance.search', 'uses' => 'CRUDController#instances'));
Route::get('search', array('as' => 'accid.search', 'uses' => 'CRUDController#account_ids'));
any ideas ?
Only a single route will ever match any given URL requested of the system. I believe what Laravel will do is choose the second one (as in the second definition will overwrite the first).
You have some options here. All I can really tell from your question is that you want two methods to be executed when that route gets hit. This is indirectly possible, consider:
Route::get('search', 'MyController#instances');
class MyController extends Controller
{
public function instances()
{
$mydata = $this->account_ids();
$myotherdata = $this->getOtherData();
return View::make('myview')
->with('mydata', $mydata)
->with('myotherdata', $myotherdata);
}
private function getOtherData() { /* ... */ }
}
This isn't really clean, though, and eventually will lead to convoluted controller logic which is an anti-pattern in MVC. Fortunately, Laravel let's you use View Composers, which can greatly clean up your controller logic:
public function instances()
{
return View::make('myview');
}
Wow. Nice and simple. Now the view composer part:
// Inside of a service provider...
View::composer('search', 'App\Http\ViewComposers\AViewComposer');
use View;
class AViewComposer
{
public function compose(View $view)
{
$view->with('instances', $this->instances());
$view->with('accountIds', $this->accountIds());
}
public function instances()
{
// generate your instance data here and return it...
return $instances;
}
public function accountIds()
{
// generate your account id data here and return it...
return $accountIds;
}
}
You could take this a step further and inject another class into the constructor of this view composer to completely off-load the responsibility of determining what 'instances' and 'account ids' actually mean, should you need that same functionality elsewhere. This will help you keep your code extremely DRY.
You are having those 2 routes but with same domain so the second one is running over the second one.
Or you change the URL of on of them or you change the method for post instead of get

Zend Framework 2 set custom layout and setTerminate issue

I dont know if this is a bug of ZF2 or I just dont understand it well, but Im very excited why is this happening.
I'm using a solution to change layout of each module globaly by attaching a Dispatch event. (for example from http://framework.zend.com/manual/2.1/en/modules/zend.view.quick-start.html#dealing-with-layouts, last example)
It's working well, but problem is, when in some action I want to setTerminate(true); (for Ajax call) it will not display only content of controller/action template, but only layout template without content! And that is what I'm not expecting.
This is how to simulate this, set layout in dispatch function (instead of attaching event, to make it cleaner) and then setTerminate in controller's action.
public function dispatch(Request $request, Response $response = null)
{
parent::dispatch($request, $response);
$this->layout('layout/new');
}
public function indexAction()
{
$model = new ViewModel();
$model->setTerminal(true);
return $model;
}
Again, I'm expecting that this will display only content of controler/index template, but instead of that, it display only content of layout/new without content.
I tried to set layout in action, and it working how I'm expecting.
public function indexAction()
{
$this->layout('layout/new');
$model = new ViewModel();
$model->setTerminal(true);
return $model;
}
This is working, it display only content of controller/index template and not layout.
So if I'm changing layout globaly (by attaching dispatch event) for each controller it working until I want to use one of these controllers for Ajax call and use setTerminate.
Thanks for help with that.
When you mark your view model as terminal, listener on dispatch event replaces layout view model with view model you returned.
So it is too late to do $this->layout('layout/new'); after dispatch, you are changing template of your view model.
What you should do is attach listener. For example, from controller itself:
protected function attachDefaultListeners()
{
//do not forget to call parent
parent::attachDefaultListeners();
$events = $this->getEventManager();
//attach before action
$events->attach(MvcEvent::EVENT_DISPATCH, array($this, 'changeLayout'), 10);
}
public function changeLayout(MvcEvent $event)
{
$this->layout('layout/new');
}
That will set layout for your controller, but you will be able to change it from action and setTerminal() will work as expected
If you want to only show the content from the actions view file you can use this approach without an issue:
1) Make a new layout, for ajax calls. This will replace layout.phtml
application/layout/ajax-layout.phtml
<?php echo $this->content ?>
2) Modify your action to ovverride the default layout during Ajax calls
Inside your controller/action :
// Don't render base layout if Ajax call
if($this->getRequest()->isXmlHttpRequest()) {
$this->layout('application/layout/ajax-layout');
}
$model = new ViewModel();
return $model;
this will just render your actions content, and override your base layout :)

Zend Framework 2 display a view within a view

I have two modules Admin and Login.
I want to display the Login view 'login.phtml' within the admin view 'index.html'
I have the following in the Admin modules indexAction controller
public function indexAction()
{
$login = new LoginController();
$view = new ViewModel(array(
'theloginform' => $login->loginAction(),
));
return $view;
}
In the LoginAction method in the Login controller I return the ViewModel for the 'login.phtml' file.
public function LoginAction() {
$view = new ViewModel();
return $view;
}
The indexAction throws an error as the variable 'theloginform' is an object.
Catchable fatal error: Object of class Zend\View\Model\ViewModel could not be converted to string in...
If i add the following:
$authentication->loginAction()->captureTo('test')
The 'index.phtml' shows a string "content".
I have read that i may need to render the ViewModel before i assign it to the view variable 'theloginform', but i can't seem to get it to work, i have tried the following with no luck.
public function LoginAction() {
$view = new ViewModel();
$renderer = new PhpRenderer();
$resolver = new Resolver\AggregateResolver();
$map = new Resolver\TemplateMapResolver(array(
'login' => __DIR__ . '/../view/login.phtml'
));
$resolver->attach($map);
$view->setTemplate("login");
return $renderer->render($view);
}
If get the following error:
Zend\View\Renderer\PhpRenderer::render: Unable to render template "login"; resolver could not resolve to a file
I have even tried adding the DI into the autoload_classmap.php file but still get the same error, i have double checked the login.phtml file is at the correct path:
'/Login/view/login/login/login.phtml' I even copied it to '/Login/src/Login/view/login.phtml'
Very confused have read then re-read the Zend documentation, i just want to pass a view to another view...
If you need share some view content you can use partials for that:
$this->partial('partial/login.pthml', array()); //add this to your index view
you can read about them here
You may also find some usefull information: How does Zend Framework 2 render partials inside a module?
As per this zf2 documentaion page
Write this in login Action:
public function loginAction()
{
return new ViewModel();
}
And in indexAction :
$view = new ViewModel(
array(
//here any thig you want to assign to index view
)
);
$loginView = new ViewModel(
array(
//here any thig you want to assign to login view
)
);
$loginView->setTemplate('moduleName/controllerName/login');
$view->addChild($loginView, 'login');
return $view
In index.phtml you can just echo login <? echo $this->login ?> where ever you want to display loginView.
In ZF 1.x I would likely recommend you build an action helper that is referenced to a view placeholder or a controller plugin that calls back to loginAction for the form logic.
In Zf2 it looks like action helpers have been replaced by controller plugins and seem to be triggered through the event manager and may need to be aware of one or more of the "managers". However the placeholder view helper still exists and even seems somewhat familiar.
I would suggest you look into building/adapting a controller plugin for your login form display that can then be attached to a placeholder view helper. You might be able to get the required functionality with just a view helper, if you're lucky.
I wish I could help more, but I'm still wading through this mess myself.
Good luck.
In you admin view you have to use the render view helper and echo the script rendered, so you can do echo $this->render($this->theloginform);

Categories