views:

77

answers:

1

Okay, this will require some setup:

I'm working on a method of using nice post title "slugs" in the URL's of my cakePHP powered blog.

For example: /blog/post-title-here instead of /blog/view_post/123.

Since I'm obviously not going to write a new method for every post, I'm trying to be slick and use CakePHP callbacks to emulate the behavior of PHP 5's __call() magic method. For those who do not know, CakePHP's dispatcher checks to see if a method exists and throws a cakePHP error before __call() can be invoked in the controller.

What I've done so far:

In the interest of full disclosure ('cause I have no Idea why I'm having a problem) I've got two routes:

Router::connect('/blog/:action/*', array('controller' => 'blog_posts'));
Router::connect('/blog/*', array('controller' => 'blog_posts'));

These set up an alias for the BlogPostsController so that my url doesn't look like /blog_posts/action

Then in the BlogPostsController:

public function beforeFilter() {
    parent::beforeFilter();
    if (!in_array($this->params['action'], $this->methods)) {
        $this->setAction('single_post', $this->params['action']);
    }
}
public function single_post($slug = NULL) {
    $post = $this->BlogPost->get_post_by_slug($slug);
    $this->set('post', $post);
    //$this->render('single_post');
}

The beforeFilter catches actions that do not exist and passes them to my single_post method. single_post grabs the data from the model, and sets a variable $post for the view.

There's also an index method that displays the 10 most recent posts.

Here's the confounding part:

You'll notice that there is a $this->render method that is commented-out above.

  1. When I do not call $this->render('single_post'), the view renders once, but the $post variable is not set.
  2. When I do call $this->render('single_post'), The view renders with the $post variable set, and then renders again with it not set. So in effect I get two full layouts, one after the other, in the same document. One with the content, and one without.

I've tried using a method named single_post and a method named __single_post and both have the same problem. I would prefer the end result to be a method named __single_post so that it cannot be accessed directly with the url /blog/single_post.

Also

I've not yet coded error handling for when the post does not exist (so that when people type random things in the url they don't get the single_post view). I plan on doing that after I figure out this problem.

+1  A: 

This doesn't explicitly answer your question, but I'd just forego the whole complexity by solving the problem using only routes:

// Whitelist other public actions in BlogPostsController first,
// so they're not caught by the catch-all slug rule.
// This whitelists BlogPostsController::other() and ::actions(), so
// the URLs /blog/other/foo and /blog/actions/bar still work.
Router::connect('/blog/:action/*',
                array('controller' => 'blog_posts'),
                array('action' => 'other|actions'));

// Connect all URLs not matching the above, like /blog/my-frist-post,
// to BlogPostsController::single_post($slug). Optionally use RegEx to
// filter slug format.
Router::connect('/blog/:slug',
                array('controller' => 'blog_posts', 'action' => 'single_post'),
                array('pass' => array('slug') /*, 'slug' => 'regex for slug' */));

Note that the above routes depend on a bug fix only recently, as of the time of this writing, incorporated into Cake (see http://cakephp.lighthouseapp.com/projects/42648/tickets/1197-routing-error-when-using-regex-on-action). See the edit history of this post for a more compatible solution.

As for the single_post method being accessible directly: I won't. Since the /blog/:slug route catches all URLs that start with /blog/, it'll catch /blog/single_post and invoke BlogPostsController::single_post('single_post'). You will then try to find a post with the slug "single_post", which probably won't exist. In that case, you can throw a 404 error:

function single_post($slug) {
    $post = $this->BlogPost->get_post_by_slug($slug);
    if (!$post) {
        $this->cakeError('error404');
    }

    // business as usual here
}

Error handling: done.

deceze
Hmm... Doesn't seem to work quite right. I'll update my question with how I tried your solution.
Stephen
@Stephen Hmm, just tried it myself, weird indeed. This *may* be a bug in Cake, but I'm not sure. I'd've expected this to work. Anyway, if you define each "other action" separately as different routes, it works fine, so that's a possible workaround. Updated my post to reflect that, also including a bug fix in the slug route.
deceze
That did it, thanks!
Stephen
@Stephen An alternative is to use a specific slug format which you can filter with a regex like `\d+-.+` ('42-my-frist-post') or `s-.+` ('s-my-frist-post') in the slug route. Any URL not matching this regex will automatically fall through to the default `/controller/action/` route, so there'd be no need to whitelist regular actions. Anyway, glad it works.
deceze
@Stephen It *was* a bug: http://cakephp.lighthouseapp.com/projects/42648/tickets/1197-routing-error-when-using-regex-on-action
deceze
How about that!
Stephen