I'm using the Skeleton Zend Framework 2 to build my application.
I'd like to modify the current navigation bar in layout.phtml to show 2 links as standard, then some more links based on user permissions.
How would I go about getting the active module in use (/user for ZfcUser) to display as li class="active", and, how would I go about implementing the navigation items based on the modules loaded?
In controller, you could get active modules like this:
$modules = $this->getEvent()->getApplication()->getServiceManager()->get('modulemanager')->getLoadedModules();
$moduleNames = array_keys($modules);
Then you could check module loaded by module name:
$moduleLoaded = in_array('ZfcUser', $moduleNames); //true or false
Here's the problem... What's your definition of the "active module"? In ZF2, a module is loosely defined by the top-level namespace, but even that's not an absolute, as modules can provide code under several namespaces if they wish.
I wrote a blog post about configuring module-specific layouts where I explain this issue in a bit more detail, as well as demonstrate one possible "solution" to performing actions based on the active module: http://blog.evan.pro/module-specific-layouts-in-zend-framework-2
In that example, I'm attaching to the 'dispatch' event with the event identifier which is the module name (namespace), which is only triggered for the top-level namespace of the controller being dispatched (I specifically added this functionality to ZF2, as it was becoming a common question in the beta period). If you're curious how or why this works, see https://github.com/zendframework/zf2/blob/master/library/Zend/Mvc/Controller/AbstractController.php#L153-159 (specifically line 158 as of writing this).
Alternatively, you could attach to the dispatch event, and get the top-level namespace of the controller being dispatched. Again, there's no guarantee that this is actually the "module name" you're looking for, and it's best to just think about controllers and actions when it comes to dispatching requests rather than "which module is this?"
class Module
{
public function onBootstrap($e) {
$events = $e->getApplication()->getEventManager()->getSharedManager();
$events->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', function($e) {
$controllerClass = get_class($e->getTarget()); // $e->getTarget() is the controller
$controllerTopNamespace = substr($controllerClass, 0, strpos($controllerClass, '\\'))
// Do whatever here, maybe something like:
// $nav = $e->getTarget()->getServiceLocator()->get('navigation');
// $nav->...
});
}
}
In controller, you can check active module in this way
$bIsModule = (bool) $this->getEvent()
->getApplication()
->getServiceManager()
->get('modulemanager')
->getModule('ZfcUser'); // returns true or false
Related
Joomla components use the MVC model. Component Creator is a widely used tool whose paid level supports creation of multi tabled views with SQL import. Also, developers build components from scratch according to Joomla documentation.
I want to build an advanced component that functions as a "dashboard" displaying data from multiple database tables with all the administrator back-end and visitor front-end CRUD (CREATE, READ, UPDATE, DELETE) capabilities of Joomla. This means that I need multiple models (from the MVC philosophy) drawing from multiple database tables shown on the screen simultaneously.
Joomla Documentation suggests the following code be inserted into the "controller task-method" to make the information available:
$view = $this->getView( 'model-a', 'html' );
$view->setModel( $this->getModel( 'model-a' ), true );
$view->setModel( $this->getModel( 'model-b' ) );
$view->display();
and then later call upon those models with the in the views display method:
$item1 = $this->get( 'data1' );
$item2 = $this->get( 'data2', 'model-b' );
However, these instructions provided in Joomla documentation are insufficient or incompatible with the component built when following the provided Joomla Hello World Tutorial tutorial or components built from the widely used and popular Component Creator tool. Either the component will fail to load the page when called upon or will not pass the data to the view with a simple copy and paste into any one of the multiple controller created by component creator or Joomla hello world tutorial.
How can I call upon multiple models in the same view for a Joomla 3.X component?
I was able to successfully use multiple models from the same view by making calls directly in the two view files to properly formed models. I did not follow Joomla documentation because I didn't modify either possible controller (one being the controller for the entire component and the other controller being view-specific). I also did not use the functions provided in Joomla documentation, as those produced errors.
According to proper Joomla MVC convention, a view is created by two files in the relevant view directory and subfolder:
/site/views/multiviewname/view.html.php (which passes the model to the view)
/site/views/multiviewname/tmpl/default.php (which has the HTML template)
Both these need to be changed to view data from more than one model at the same time. This works assuming that all of your other views, controllers, and models are built properly, as is done automatically when using the 'Component Creator' tool. My component had hundreds of files, including css, backend administration, installation, language, etc. All of these were build in moments with the component creator tool.
The abridged but still completely functional code is as follows:
/site/views/multiviewname/view.html.php
<?php
jimport('joomla.application.component.view');
class ComponentnameViewMultiviewname extends JViewLegacy
{
// $items is for the default model
protected $items;
// $ItemsOtherModel is for second model. Notice the '$' used here but not elsewhere
protected $ItemsOtherModel;
public function display($tpl = null)
{
$app = JFactory::getApplication();
$this->state = $this->get('State');
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->params = $app->getParams('com_componentname');
// sets default model
$this->setModel( $this->getModel( 'model-a' ), true );
// sets second model & uses 'JModelLegacy,' contrary to documentation
$this->setModel(JModelLegacy::getInstance('model-b', 'componentnameModel'));
// assigns array from the second model to 'ItemsOtherModel.' there is no '$' sign used.
$this->ItemsOtherModel = $this->get('Items','model-b');
parent::display($tpl);
}
}
/site/views/multiviewname/tmpl/default.php
<?php
echo "<h3>Items from default model</h3> ";
echo var_dump($this->items);
echo "<h3>items from secondary model</h3> ";
// notice that the '$' is absent from 'ItemsOtherModel'
echo var_dump($this->ItemsOtherModel);
This breakthrough was only possible after days of research. The paid Component Creator tool was invaluable to start me off with well formed code that adheres to Joomla MVC component standards. After working with and examining all the files for days, the I found the prompt I needed in this google groups thread, calling my attention to the JModelLegacy class, found when searching google for terms from the PHP error message PHP Notice: Undefined index: left on my server when attempting to use the officially documented methods.
This page rendered in the browser simply dumps out all information from the database table to the page, but further development can create the formatted and functional dashboard that I will ultimately need.
This code is for displaying lists of information, as opposed to multiple single items. The Joomla documentation for adding multiple models to one view is set up for multiple single items, rather than arrays of items shown here.
I'm trying to create my own xml sitemap. Everything is done except for the part that I thought was going to be the easiest. How do you get a list of all the pages on the site? I have a bunch of views in a /site folder and a few others. Is there a way to explicitly request their URLs or perhaps via the controllers?
I do not want to make use of an extension
You can use reflection to iterate through all methods of all your controllers:
Yii::import('application.controllers.*');
$urls = array();
$directory = Yii::getPathOfAlias('application.controllers');
$iterator = new DirectoryIterator($directory);
foreach ($iterator as $fileinfo)
{
if ($fileinfo->isFile() and $fileinfo->getExtension() == 'php')
{
$className = substr($fileinfo->getFilename(), 0, -4); //strip extension
$class = new ReflectionClass($className);
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
{
$methodName = $method->getName();
//only take methods that begin with 'action', but skip actions() method
if (strpos($methodName, 'action') === 0 and $methodName != 'actions')
{
$controller = lcfirst(substr($className, 0, strrpos($className, 'Controller')));
$action = lcfirst(substr($methodName, 6));
$urls[] = Yii::app()->createAbsoluteUrl("$controller/$action");
}
}
}
}
You need to know what content you want to include in your sitemap.xml, I don't really think you want to include ALL pages in your sitemap.xml, or do you really want to include something like site.com/article/edit/1 ?
That said, you may only want the result from the view action in your controllers. truth is, you need to know what you want to indexed.
Do not think in terms of controllers/actions/views, but rather think of the resources in your system that you want indexed, be them articles, or pages, they are all in your database or stored somehow, so you can list them, and they have a URI that identifies them, getting the URI is a matter of invoking a couple functions.
There are two possiblities -
Case 1:
You are running a static website then you can find all your HTML inside 1 folder - protected/views/site/pages
http://www.yiiframework.com/wiki/22/how-to-display-static-pages-in-yii/
Case 2:
Website is dynamic. Tasks such as generating and regenerating Sitemaps can be classified into background tasks.
Running background taks can be achieved by emulating the browser which is possible in linux using - WGET, GET or lynx commands
Or, You can create a CronController as a CConsoleCommand. How to use Commands in YII is shown in link below -
http://tariffstreet.com/yii/2012/04/implementing-cron-jobs-with-yii-and-cconsolecommand/
Sitemap is an XML which lists your site's URL. But it does more than that.
It helps you visualize the structure of a website , you may have
category
subcategories.
While making a useful extension, above points can be kept into consideration before design.
Frameworks like Wordpress provide way to generate categorical sitemap.
So the metadata for each page is stored from before and using that metadata it discovers and group pages.
Solution by Reflection suggested by #Pavle is good and should be the way to go.
Consider there may be partial views and you may or may not want to list them as separate links.
So how much effort you want to put into creating the extension is subject to some of these as well.
You may either ask user to list down all variables in config fie and go from there which is not bad or you have to group pages and list using some techniques like reflection and parsing pages and looking for regex.
For ex - Based on module names you can group them first and controllers inside a module can form sub-group.
One first approach could be to iterate over the view files, but then you have to take into account that in some cases, views are not page destinations, but page sections included in another pages by using CController::renderPartial() method. By exploring CController's Class Reference I came upon the CController::actions() method.
So, I have not found any Yii way to iterate over all the actions of a CController, but I used php to iterate over all the methods of a SiteController in one of my projects and filter them to these with the prefix 'action', which is my action prefix, here's the sample
class SiteController extends Controller{
public function actionTest(){
echo '<h1>Test Page!</h1></p>';
$methods = get_class_methods(get_class($this));
// The action prefix is strlen('action') = 6
$actionPrefix = 'action';
$reversedActionPrefix = strrev($actionPrefix);
$actionPrefixLength = strlen($actionPrefix);
foreach ($methods as $index=>$methodName){
//Always unset actions(), since it is not a controller action itself and it has the prefix 'action'
if ($methodName==='actions') {
unset($methods[$index]);
continue;
}
$reversedMethod = strrev($methodName);
/* if the last 6 characters of the reversed substring === 'noitca',
* it means that $method Name corresponds to a Controller Action,
* otherwise it is an inherited method and must be unset.
*/
if (substr($reversedMethod, -$actionPrefixLength)!==$reversedActionPrefix){
unset($methods[$index]);
} else $methods[$index] = strrev(str_replace($reversedActionPrefix, '', $reversedMethod,$replace=1));
}
echo 'Actions '.CHtml::listBox('methods', NULL, $methods);
}
...
}
And the output I got was..
I'm sure it can be furtherly refined, but this method should work for any of the controllers you have...
So what you have to do is:
For each Controller: Filter out all the not-action methods of the class, using the above method. You can build an associative array like
array(
'controllerName1'=>array(
'action1_1',
'action1_2'),
'controllerName2'=>array(
'action2_1',
'action2_2'),
);
I would add a static method getAllActions() in my SiteController for this.
get_class_methods, get_class, strrev and strlen are all PHP functions.
Based on your question:
1. How do you get a list of all the pages on the site?
Based on Yii's way of module/controller/action/action_params and your need to construct a sitemap for SEO.
It will be difficult to parse automatically to get all the urls as your action params varies indefinitely. Though you could simply get controller/action easily as constructed by
Pavle Predic. The complexity comes along when you have customized (SERF) URL rules meant for SEO.
The next best solution is to have a database of contents and you know how to get each content via url rules, then a cron console job to create all the urls to be saved as sitemap.xml.
Hope this helps!
is there a way in Zend to call one controller from another?
I have seen the action stack but that does not seem to work for me and i have read that alot of people think it to be EVIL!
What i am trying to acchive is as follows:
Reports Controller scans through all the modules in the system, it then checks to see if a route has been registered for that module called MODULENAME-reports-run
The controller then reuns that registered route to generate all the reports from all the modules.
The idea is that i can create modules for my application that clients can simple drag and drop into place and the system picks up on the reports.
Your controller should not do any of these things. Your controller should only accept any input from the User Interface and then decide to delegate this to the appropriate classes in your Model.
If you have a ReportController, have it accept any input and forward it to a ReportsService or something else in the Model responsible for generating Reports. It's not the controllers responsibility to generate them.
It should look something like this:
public function generateReportAction()
{
try {
$service = new Model_ReportService;
$service->setReportToGenerate($this->getRequest()->getParam('reportId'));
$this->view->report = $service->generateReport();
} catch (ReportException $e) {
// do something with $e
}
}
If your ReportService needs to generate multiple reports, change the ReportService so it knows how to do that. You could do something like
$service = new Model_ReportService;
$service->setModulesDirectory('something');
$this->view->reports = $service->generateReportsForModules();
Personally I dont think a ReportService should need to know about a Module Directory, so you will want to give the public interface of that Service some more thought. But in general, this is the way to go.
Whatever you do, dont do it in the controller. Controllers ought to be slim.
I was thinking about implementing a logic similar to observer pattern on my website, for implementing hooks.
What I am looking for is something similar to this Best way to allow plugins for a PHP application
However the code there is too limited, as I cant attach multiple hooks to same listener.
I am clueless about how to amplify that code to make it able to listen multiple actions at one event.
Thank You
You can do as ircmaxell suggests: add hooks. But clearly, the information he gave was not enough for you.
If you like learning by example, you may look at the CMS Drupal, wich is not OOP, but uses the observer pattern, called hooks all over the place to allow a modular design.
A hook works as follows:
a piece of php looks for the existence of a specially named function.
If that exists, call it and use its output (or do nothing with it)
For example:
Just before an article gets saved in Drupal, the article-system calls the hook_insert
Every module that has a function in the name of ModuleName_insert, will see that function being called. Example: pirate.module may have a function pirate_insert(). The article system makes a roundtrip along all the modules and sees if ModuleName_insert exists. It will pass by pirate module and finds pirate_insert(). It will then call that function (and pass some arguments along too). As such, allowing the pirate.module to change the article just before insertation (or fire some actions, such as turning the body-text into pirate-speek).
The magic happens in so called user_callbacks. An example:
$hook = 'insert'
foreach (module_implements($hook) as $module) {
$function = $module .'_'. $hook;
$result = call_user_func_array($function, $args);
}
And the function module_implements might look something like:
$list = module_list(FALSE, TRUE, $sort); //looks for files that are considered "modules" or "addons".
foreach ($list as $module) {
if (function_exists($module.'_'.$hook)) { //see if the module has the hook 'registered'
$implementations[$hook][] = $module; //if so: add it to a list with functions to be called.
}
}
Simply add a ' * ' hook, and modify the hook() function to call all the 'hooks' in both the named event and the ' * ' event.
Then, simply do:
add_listener('*', 'mycallback');
Take a look at Spl_Observer.
You said you didn't want OOP, but you can easily implement a non-OOP wrapper around this.
I'm trying to create a widget within the module and then load that widget from 'outside' of the module. More particularly I'm using user module written by someone else. I don't want to have a separate page for displaying a login form, therefore I tried to make a CPortlet/widget (confusion) displaying the login form. Basically, I've moved the code from LoginController into that widget. Then I try to display the widget on some random page by
<?php $this->widget('user.components.LoginForm'); ?>
However, I get an error
CWebApplication does not have a method named "encrypting".
in UserIdentity class in this line:
else if(Yii::app()->controller->module->encrypting($this->password)!==$user->password)
This happens, because I'm basically trying to execute this code within context of the app and not the module. Thus the "Yii::app()->controller->module" trick doesn't really work as expected.
What am I doing wrong:-\
Is there a better way to achieve this. I.e. display that login form in some other page, which is normally displayed by accessing login controller within user module (user/login) or is a widget the right way of doing it?
Thanks.
The quick solution
Ok, so I simply ended up doing
Yii::app()->getModule('user')->encrypting($this->password)
instead of
Yii::app()->controller->module->encrypting($this->password)
Notice that now the module must be called 'user' in the main config, but I think this allows for more flexibility. I.e. we're not bound to only use module functionality within the module.
Additional insight on displaying widget outside of the module scope
After playing more with it that's what I did. In the UserModule.php I've created a method
public static function id() {
return 'user';
}
Then everywhere where I need the module I use
Yii::app()->getModule(UserModule::id())->encrypting($this->password)
I don't like having many imports related to the module like:
'application.modules.user.models.*',
'application.modules.user.components.*',
Because we already have those imports in the UserModule.php:
public function init()
{
// this method is called when the module is being created
// you may place code here to customize the module or the application
// import the module-level models and components
$this->setImport(array(
'user.models.*',
'user.components.*',
));
}
Therefore whenever you know that some piece of functionality will be used outside of the module it's important to make sure the module is loaded. For example, in the LoginForm widget that I am trying to display NOT in one of the module controllers, I have this line of code:
$model = new UserLogin;
However, UserLogin is a model inside of the User module, and in order to be able to autoload this model we first have to make sure the module was initialised:
$module = Yii::app()->getModule(UserModule::id());
$model = new UserLogin;
I hope this will be helpful if you were stuck with the whole modules concept the way I was.
http://www.yiiframework.com/forum/index.php?/topic/6449-access-another-modules-model/ was useful but hard to find =)
You better move that encrypting() into a MyUserIdentiy class which extends CUserIdentity. Whatever the code you take to use, they putting the method in controller is a bad idea and as a result you cannot reuse that code.
The login form should still post to User/Login controller but I guess they use Yii's standard login code and you might want to modify it to use the MyUserIdentity.