I'm turning a well-working piece of my CakePHP (v 2.44) application into a plugin, and I'm getting the strangest behavior when throwing exceptions from within a controller in my plugin: the exception handler/renderer is starting to use my main site's layout in app/View/Layouts/mylayout.ctp, and then interrupts it with the default layout from app/View/Layouts/error.ctp. Here's an extract:
<div><ul><li class="jsdnavpopup blog-menu-categories">
<a href='/blog/categories'>All Categories</a><nav>
<!DOCTYPE html PUBLIC "{trimmed for space}">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>CakePHP: the rapid development php framework:Error Page</title>
If you notice where it looks like a new header is sent right in the middle of Cake's composing the layout. Right at the point where my layout is interrupted I load am element, and the element uses requestAction (no caching in my dev environment). If I remove the element, then my layout is rendered even more until the next element using requestAction is encountered. These requestAction calls aren't requesting an action from the plugin that raised the exception.
I'm using all of the default error handlers from app/Config/core.php. The only thing that's different is specifying $this->layout in the error views.
Now if I recreate the original error layouts and views in the plugin's View folder, things work like I would expect. But for experimental purposes I put a renamed copy of my main site's layout there, and the same thing. Any time an element using requestAction is encountered the exception interrupts the layout.
Any ideas?
I'll reiterate that everything works perfectly when throwing exceptions from anywhere in the app that's not a plugin.
Here's how I do it (as requested in the comments on the OP's question)
Inside my layout, I put the following piece of code where I want the top menu to appear:
<?php echo $this->element('Menu.top_navigation'); ?>
This is quite simple, and allows the HTML designers in my team to put the menu wherever they want to.
Of course there is more happening under the hood. Here are the other files you'll need to make this work:
Plugins/Menu/View/Elements/top_navigation.ctp:
$thisMenu = ClassRegistry::init('Menu.Menu');
$menus = $thisMenu->display_menu_widget(array('position'=> 'top'));
echo $this->element('Menu.display_ul', array('menus' => $menus));
Note that this is just a wrapper -- you can make more of these for footer navigation, or even make an element that allows you to pass in the position as an argument.
Plugins/Menu/View/Elements/display_ul.ctp:
<?php
if(empty($depth)){
$depth = 0;
}
switch($depth){
case 0:
$classes = "dropdown";
break;
default:
$classes = "";
break;
}
?>
<ul class="<?php echo $classes ?> depth_<?php echo $depth ?>">
<?php foreach ($menus as $menu): ?>
<li>
<a href="<?php echo $menu['MenuItem']['url']; ?>">
<?php echo $menu['MenuItem']['title']; ?>
</a>
<?php
if(count($menu['children']) > 0){
echo $this->element(
'Menu.display_ul',
array(
'menus' => $menu['children'],
'depth' => $depth + 1
)
);
}
?>
</li>
<?php endforeach; ?>
</ul>
Plugins/Menu/Model/Menu.php:
/**
* display_menu_widget method
*
* #param array $options
* #return void
*/
public function display_menu_widget($options = array()) {
$defaults = array(
'position' => 'top'
);
$settings = array_merge($defaults, $options);
$this->recursive = 0;
$menuItems = array();
$conditions = array(
'Menu.position' => $settings['position']
);
$menuDetails = $this->find('first', array('recursive' => -1, 'conditions' => $conditions));
$menuPosition = $menuDetails[$this->alias]['position'];
$parentId = $menuDetails[$this->alias][$this->primaryKey];
$conditions = array(
$this->MenuItem->alias . '.menu_id' => $parentId
);
$this->MenuItem->recursive = 0;
$cacheName = 'default' . 'ModelMenuItem';
$cacheKey = $this->generateCacheName(array('type' => 'threaded', 'conditions' => $conditions));
$menuItems = Cache::read($cacheKey, $cacheName);
if(empty($menuItems) || PWFunctions::forceCacheFlush()){
$menuItems = $this->MenuItem->find('threaded', array('conditions' => $conditions));
Cache::write($cacheKey, $menuItems, $cacheName);
}
return $menuItems;
}
There's probably more in that menu function than you need, I'm using some aggressive caching routines to reduce database load, I'd suggest only using the bare bones of that function before worrying about caching.
This solution works well for me, and I think that it's the solution that violates the MVC concepts the least -- when you, as a designer, are in HTML mode and think "Hey I need to display the top menu" then it's just one line in your view, and you're not having to worry about modifying controllers or models.
I've used this pattern throughout my current CakePHP projects, and it seems to work quite well.
Related
I'm using a custom pagination template with CakePHP 3.7
The template is in config/infinite-scroll-paginator.php. I want to use this with the Infinite Scroll jquery plugin. Therefore all that's required is 1 link per paginated page which contains the URL to the "next" page (i.e. next set of results).
So in the file above I have:
$config = [
'nextActive' => '<a class="pagination__next" aria-label="Next" href="{{url}}">{{text}}</a>'
];
In my Controller I start by telling it to use the custom pagination template:
public $helpers = [
'Paginator' => ['templates' => 'infinite-scroll-paginator']
];
Then when I want to paginate my data I use a custom finder (findFilters() which is in Model/Table/FiltersTable.php):
public $paginate = [
'finder' => 'filters',
];
I can then use this to get paginated data:
$this->loadModel('Filters');
$rf_keywords = trim($this->request->getQuery('rf_keywords'));
// Pagination
$page = $this->request->getQuery('page') ? (int)$this->request->getQuery('page') : 1;
$this->paginate = ['page' => $page, 'limit' => 100];
$finder = $this
->Filters
->find('filters', [
'rf_keywords' => $rf_keywords
]);
$data = $this->paginate($finder);
$this->set('data', $data);
Then in my template I output the paginator HTML:
<?= $this->Paginator->next(); ?>
All of this works fine. I get a "next" link for each page. Say if I have 10 pages I get URL's:
/get-filters/?page=1
/get-filters/?page=2
...
/get-filters/?page=10
The issue is that on the 10th page there is a "next" link and the URL of it is just
/get-filters
There are no more pages to output but I still get a link, albeit an invalid one.
How can I stop it from outputting the link on the last page? I thought that the paginator component would take care of this since it's aware how many pages there are based on my limit value in $this->paginate?
Use $this->Paginator->hasNext() which returns true or false depending on whether there is a "next page".
In the template:
<?php if ($this->Paginator->hasNext()): ?>
<?= $this->Paginator->next(); ?>
<?php endif; ?>
In CakePHP 2.x there is a HTML Helper which allows for creating hyperlinks within Views.
If I use this...
<?php
echo $this->Html->link('Navigation',
array('controller' => 'navigations', 'action' => 'foo')
);
?>
... it will generate a URL to /navigations/foo (NavigationsController::foo).
However if I use index as the action
<?php
echo $this->Html->link('Navigation',
array('controller' => 'navigations', 'action' => 'index')
);
?>
The URL becomes /navigations.
I understand about "index" being a default when it comes to webpages. However, I actually need the URL to be /navigations/index, or at the very least have a trailing slash (/navigations/).
Is this possible?
Fix the AJAX URLs instead
With regards to the explanation in your comment, I would argue that the "correct" way of fixing this problem is to use proper root relative URLs for the AJAX calls, as changing the URL structure only cloakes the underlying problem, that is targeting non-unique resources.
Personally I alwas use Router::url() to generate the URLs, either in the specific script files when serving them via PHP, or by writing out configuration in for example the layout, being it globally so that the scripts can access it when needed:
<script>
var config = {
navigationDataUrl: <?php echo json_encode(
Router::url(array('controller' => 'Navigations', 'action' => 'get_data'))
) ?>
};
</script>
or by configuring possibly existing JS objects.
<script>
navigation.dataUrl = <?php echo json_encode(
Router::url(array('controller' => 'Navigations', 'action' => 'get_data'))
) ?>;
</script>
Connect routes with an explicit index part
That being said, for the sake of completion, it is possible to force the index part to not be dropped, by connecting routes that explicitly define that part, as opposed to using the :action route element, like:
Router::connect('/:controller/index', array('action' => 'index'));
which would match/catch URL arrays like yours before they are being handled by CakePHPs default controller index catchall route, which looks like:
Router::connect('/:controller', array('action' => 'index'), array('defaultRoute' => true));
https://github.com/cakephp/cakephp/blob/2.10.6/lib/Cake/Config/routes.php#L72
I am currently building a wordpress theme. For the menu, I just want to display all the pages that are created in the order that they are created in (i.e. I don't want users to have to go into the 'menu' section of the site and create their own).
I tried using wp_page_menu and this worked HOWEVER I need to add a walker class to expand the functionality, which you can't do with this function. So I need a way to display all my pages with using wp_nav_menu as my code - is there a way to do this?
Here is my code currently:
<nav id="nav">
<?php wp_nav_menu( array( 'walker' => new Clapton_Nav_Walker ) ); ?>
</nav>
Try this
functions.php this below code register a menu for use
<?php register_nav_menu( 'main_menu', 'Primary Menu' ); ?>
// remove <?php ?> tags if it already there
index.php or wherever you want, below code output the menu
<?php wp_nav_menu( array( 'theme_location' => 'main_menu','walker' => new Clapton_Nav_Walker ) ); ?>
Build menu in WordPress Dashboar -> Appearance -> Menus -> Manage Locations
I'm trying to use Elements in Cakephp 2.0 without luck. I have a model named Post, a controller named Posts and various views. In the layout I would like to include (for every page/view) a box with the most recent news (say 2).
So I created the element dok-posts.ctp
<?php $posts = $this->requestAction('posts/recentnews'); ?>
<?php foreach($posts as $post): ?>
<div class="Post">
....
In my PostsController I added the function recentnews()
public function recentnews(){
$posts = $this->Post->find('all',array('order' => 'Post.created DESC','limit' => 2));
if ($this->request->is('requested')) {
return $posts;
} else {
$this->set('posts', $posts);
}
}
In my layout, default.ctp I call my element
<?php echo $this->element('dok-posts'); ?>
The problem is that I get this message
Invalid argument supplied for foreach() [APP\View\Elements\dok-posts.ctp, line 9]
Debugging in dok-posts.php, right after the $this->requestAction, gives me an empty line. It seems that the recentnews function is not returning anything (debugging in the function returns an array with the posts found). Can anyone please tell me what am I doing wrong?
Since you found out that the action is actually called,
$posts = $this->requestAction('posts/recentnews');
is working correctly. Here, for clarity and extended configuration options (for later changes to the code), I suggest you to use a Router array instead of an URL
$posts = $this -> requestAction(array(
'controller' => 'posts',
'action' => 'recentnews'
));
Now to your actual problem...
Since you say, it always goes into the else branch,
$this->request->is('requested')
might not work as expected. Try this (it works perfect for me):
if (!empty($this -> request -> params['requested'])) {
return $posts;
}
In your app controller create beforefilter and getNewsElement function.
public function beforeFilter() {
parent::beforeFilter();
$data = $this->getNewsElement();
$this->set('posts',$data);
}
function getNewsElement(){
# Put Post model in uses
$posts = $this->Post->find('all',array('order' => 'Post.created DESC','limit' => 2));
return $posts;
}
dok-posts.ctp
#Remove <?php $posts = $this->requestAction('posts/recentnews'); ?>
<?php foreach($posts as $post): ?>
<div class="Post">
....
In PostsController
public function recentnews(){
$posts = $this->getNewsElement();
$this->set('posts', $posts);
}
This will solve your problem!
Try
<?php $posts = $this->requestAction('/posts/recentnews'); ?>
(note the leading forward slash)
I have followed the example in the Cakephp's guide. From the moment it does not passes the if statement it seems that there is a problem with the control.
$this->request->is('requested')
So I removed the
is->('requested')
adding
->params['requested']
and it works.
Thank you all for your help (especially wnstnsmth for the solution).
Try this
<?php $this->requestAction('/controllerName/functionName');?>
I have tried many times to use this plugin and I failed.
I am following documentation, but it does not work for me.
I am posting the simple code here, to know what wrong I am doing.
1-I put this plugin in this folder app/plugins
2- I add TinyMce helper to articles_controller
<?php
class ArticlesController extends AppController {
// good practice to include the name variable
var $name = 'articles';
// load any helpers used in the views
var $helpers = array('Html', 'Form','TinyMce.TinyMce');
/**
* index()
* main index page of the formats page
* url: /formats/index
*/
function index(){
// get all formats from database where status = 1
$articles = $this->Article->find("all") ;
$this->set('articles', $articles);
}
function admin_add() {
// if the form data is not empty
if (!empty($this->data)) {
// initialise the format model
$this->Article->save($this->data);
// set a flash message
$this->Session->setFlash('The Format has been saved');
// redirect
$this->redirect(array('action'=>'index'));
} else {
// set a flash message
$this->Session->setFlash('The Format could not be saved. Please, try again.','default', array('class' => 'flash_bad'));
}
}
}
?>
3- in the view file articles/admin_add.ctp I added the editor
// i think the problem in this code
<?php $this->TinyMce->editor(array(
'theme' => 'advanced'
)); ?>
<div class="formats form">
<?php echo $form->create('Article');?>
<fieldset>
<legend>Add a article</legend>
<?php
// create the form inputs
echo $this->Form->input('title');
echo $this->Form->input('content'); ?>
</fieldset>
<?php echo $form->end('Add');?>
</div>
<ul class="actions">
<li><?php echo $html->link('List Articles', array('action'=>'index'));?></li>
</ul>
You need to put tinymce files into your js assets
Then you have to add into section of your layout.
Then you'll need to init tinymce according to example provided on tinymce website (ex: full tinymce layout) and configure it according to your requirements.
I'd personally would not rely on such cake plugins, when actions required to get things working are not many and they are simple enough.