Question

My site has many "pages". Pages are created for different topics and moderated by users (for example a page for Pirates of the Caribbean or a page for Justin Bieber). The page belongs to a category, for example, Movies or Music. The default URL to a page is http://www.example.com/category-directory/page-id. Optionally, if the moderator wants to establish their own link for a page as an easier means of navigation, they can do so by giving value to the "Page.link" column (for example: http://www.example.com/potc for the movie Pirates of the Caribbean).

I have my routes all set up which took some time in itself. The route checks to see if the first parameter of the URL after ".com" matches the "link" column of any page, if it does, route to that page and the controllers/actions under it. If not, route as usual.

When a user is viewing either http://www.example.com/potc/articles/99 or http://www.example.com/movies/106/articles/99 (given that Pirates of the Caribbean is page with id = 106) then the user is directed to the articles controller, view action, id = 99. 106 is passed as $page_id to the controller's action and also set in the AppController.

The URL array I'm providing in the controller works fine for example if the id does not exist I redirect to 'controller' => 'articles', 'action' => 'index', 'page_id' = $page_id. I am redirected back to http://www.example.com/potc/articles. I don't actually have to define the controller since by default it uses the current controller.

However, I'm having an issue with the pagination links. Say for example I'm on the articles index page and there are 100 articles but I'm only viewing the first 20. I would like the URL to be outputted as http://www.example.com/potc/articles/page:2.

Instead, if I set the URL array as 'controller' => 'articles', 'action' => 'index', 'page_id' => $page_id similar to what I did in the controller I get the link http://www.example.com/articles/page_id:106/page:2 I can understand why this route does not work because I probably have to pass along the value Page.link if it exists and if not pass along Category.directory and Page.id. I've tried doing the latter option using the URL array 'controller' => 'articles', 'action' => 'index', 'category' => $page['Category']['directory'], 'page_id' => $page_id. I have a route to match this but the pagination links are still generated as http://www.example.com/articles/category:movies/page_id:106/page:2

Here are some snippets from my routes.php: https://gist.github.com/9ba0a54a7f4d68803d14

My question is: "Can I use the exact current URL for pagination links (minus reference to current page I am on)?" For example, if I'm viewing http://www.example.com/dynamic-route/controller/id/page:2, the pagination link will automatically go to http://www.example.com/dynamic-route/controller/id/page:3

I put all of my pagination code into an element and I would like to include the element in my view file wherever the pagination is needed.

Right now, if I'm viewing the articles controller, index action, the default URL for the pagination links is http://www.example.com/articles/page:2, but I need it to be http://www.example.com/dynamic-route/articles/page:2

I tried using the

   $this->Paginator->options = array(
                    'url' => 'http://www.example.com/dynamic-route/articles'
   ); 

But the link is output like http://www.example.com/articles/http://www.example.com/dynamic-route/articles/page:2

Edit: Jeztah, you mention putting

'customlink' => '[a-z0-9]{1}([a-z0-9-]{2,}[a-z0-9]{1})?'

in my route. Well I can't simply do this because the route does a query to check to see if the customlink is in the database and if so routes accordingly.

An example of my router file is:

after getting first param of url which would be http://www.domain.com/CUSTOM-LINK, do the following:

if($param1 != "users" && $param1){
    $pagelink = str_replace('-', ' ', $param1);

    $pagesModel = ClassRegistry::init('Page');
    $matchedpage = $pagesModel->find('first', array(
        'conditions' => array('Page.link LIKE' => "$pagelink"), 'recursive' => '-1'
    ));

    if($matchedpage['Page']['id']){
        Router::connect('/' . $param1 . '/:controller', array('action' => 'index', 'page_id' => $matchedpage['Page']['id']), array(
            'pass' => array('page_id'),
            'page_id' => '[0-9]+',
            'persist' => array('page_id'),
        ));

        Router::connect('/' . $param1 . '/:controller/:action/:id', array('page_id' => $matchedpage['Page']['id']), array(
            'pass' => array('page_id', 'id'),
            'page_id' => '[0-9]+',
            'persist' => array('page_id'),
        ));


        Router::connect('/' . $param1 . '/:controller/:id', array('action' => 'view', 'page_id' => $matchedpage['Page']['id']), array(
            'pass' => array('page_id', 'id'),
            'page_id' => '[0-9]+',
            'id' => '[0-9]+',
            'persist' => array('page_id'),
        ));


        Router::connect('/' . $param1 . '/:controller/:action', array('page_id' => $matchedpage['Page']['id']), array(
            'pass' => array('page_id'),
            'page_id' => '[0-9]+',
            'persist' => array('page_id'),
        ));

    } // end if page matches
} // end if param1 is not users

If I go to http://www.domain.com/custom-link/articles, http://www.domain.com/custom-link/articles/1, http://www.domain.com/custom-link/articles/edit/1, etc the proper page is display. If I try to navigate to a page where the custom-link does not exist, continue down my router file and if no other route is matched then throw error message. All good there.

In my ArticlesController.php the redirect method works as expected. Say for example, I try to go to http://www.domain.com/custom-link/articles/99 but id 99 does not exist, I am properly redirected back to http://www.domain.com/custom-link/articles using

$this->redirect(array('action' => 'index', 'page_id' => $page_id), null, true);

However, on the pagination, I set the url options as

array('controller' => 'articles', 'action' => 'index', 'page_id' => $page_id);

where $page_id is set in the AppController from being passed by the Router. Though I don't need to set controller and action because the current will be used. Again, my pagination links appear as http://www.domain.com/articles/page_id:3/page:2 and not what I want which is http://www.domain.com/custom-link/articles/page:2

Edit: Jeztah

I just changed my entire route file to what you had with having a custom-link parameter (I renamed to 'link') and then created a component for use in the app controller to get the page id from the link.

Example route

Router::connect('/:link/:controller/:action/:id', array(), array(
    'pass' => array('link', 'id'),
    'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
    'persist' => array('link'),
));


Router::connect('/:link/:controller/:id', array('action' => 'view'), array(
    'pass' => array('link', 'id'),
    'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
    'id' => '[0-9]+',
    'persist' => array('link'),
));

However, paginating with the url array as array('link' => 'test-example') the url is outputted as http://www.domain.com/articles/link:test-example/page:2. My code is exactly as yours minus renaming "custom-link" to "link". Also, now my route can't differentiate between whether I'm trying to access http://www.domain.com/custom-link which would be 'controller' => 'pages', 'action' => 'view' (his page would basically use the News model and display the latest news for the page) and if I'm trying to access http://www.domain.com/articles which would be 'controller' => 'articles', 'action' => 'index' which would display the latest articles for ALL pages. Note, articles and news are separate models.

Router::connect('/:link', array('controller' => 'pages', 'action' => 'view'), array(
    'pass' => array('link'),
    'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
    'persist' => array('link'),
));

is the same as

Router::connect('/:controller', array('action' => 'index'));

because the check to see whether or not 'link' belongs to a specific page is no longer done in the route.

Edit: Ok I'm making progress. I found out that next and previous pagination links don't take to the paginator->options very well. I deleted the paginator->options and left the url array of the next button empty. The link was outputted as http://www.domain.com/articles/custom-link/articles/page:2. Progess! But is there any way to get ride of the first "articles/"?

My route is

Router::connect('/:link/:controller/*', array(), array(
    'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
));
Was it helpful?

Solution 3

Edit: This is an edited answer after I realized my last answer was flawed when it came to ArticlesConntroller::redirect

For anyone who has managed to read this entire question/comments and care to know the answer. I figured out a solution thanks to Jeztah's assistance whom I will be awarding this bounty to. One of the issues giving me a hard time was the fact that a page could have 2 URLs. For example: http://www.domain.com/potc or http://www.domain.com/movies/106. Basically I used a combination of mine and Jeztah's ideas.

First, I have to perform a check in the routes.php to see if the first param after the .com matches a page's shortcut "link" (example: http://www.domain.com/potc = potc).

// get params of URL
$requestURI = explode('/', $_SERVER['REQUEST_URI']);
$scriptName = explode('/',$_SERVER['SCRIPT_NAME']);

for($i= 0;$i < sizeof($scriptName);$i++) {
      if ($requestURI[$i]     == $scriptName[$i]) {
                unset($requestURI[$i]);
        }
      }
 $param = array_values($requestURI);

// only concerned about first param
$param1 = $param[0];

// do the following routes if url != http://www.domain.com/users/* or http://www.domain.com
if($param1 != "users" && $param1){

    // convert http://www.domain.com/assassins-creed to "assassins creed"
    $pagelink = str_replace('-', ' ', $param1);
    $pagesModel = ClassRegistry::init('Page');
    // check if there is a page whose "link" matches converted link, if so, get the field "id"
    $matched_page_id = $pagesModel->field('id', array('Page.link LIKE' => "$pagelink"));
    // if there is a match, route as follows
    if($matched_page_id){

         // Jeztah's way with some modifications

        // domain.com/potc/articles/edit/1
        Router::connect(
            '/:link/:controller/:action/:id',
            array('page_id' => $matched_page_id, 'category' => 0),
            array(
                'pass' => array('id', 'link', 'category', 'page_id'),
                'link'   => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
                'persist' => array('link'),
                'id' => '[0-9]+'
            )
        );

        // domain.com/potc/articles/1
        Router::connect(
            '/:link/:controller/:id',
            array('action' => 'view', 'page_id' => $matched_page_id, 'category' => 0),
            array(
                'pass' => array('id', 'link', 'category', 'page_id'),
                'link'   => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
                'persist' => array('link'),
                'id' => '[0-9]+'
            )
        );

        // domain.com/potc/articles/add
        Router::connect(
            '/:link/:controller/:action',
            array('page_id' => $matched_page_id, 'category' => 0),
            array(
                'pass' => array('link', 'category', 'page_id'),
                'link'   => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
                'persist' => array('link'),
                'action' => 'add|uncategorized',
            )
        );

        // domain.com/potc/articles OR domain.com/articles/page:2
        Router::connect(
            '/:link/:controller/*',
            array('action' => 'index', 'page_id' => $matched_page_id, 'category' => 0),
            array(
                'pass' => array('link', 'category', 'page_id'),
                'link'   => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
                'persist' => array('link'),
            )
        );
    }  // end if first param matches a page's "link" shortcut
}  // end if first param is not users and is not empty

// do some general site routing
// do some general site routing

// now route for page's that do not have a link shortcut (example: http://www.domain.com/movies/106)

// domain.com/movies/106/articles/edit/1
Router::connect(
    '/:category/:page_id/:controller/:action/:id',
    array('link' => '0'),
    array(
        'pass' => array('id', 'link', 'category', 'page_id'),
        'page_id' => '[0-9]+',
        'id' => '[0-9]+'
    )
);

// domain.com/movies/106/articles/1
Router::connect(
    '/:category/:page_id/:controller/:id',
    array('action' => 'view', 'link' => '0'),
    array(
        'pass' => array('id', 'link', 'category', 'page_id'),
        'page_id' => '[0-9]+',
        'id' => '[0-9]+'
    )
);

// domain.com/movies/106/articles/add
Router::connect(
    '/:category/:page_id/:controller/:action',
    array('link' => '0'),
    array(
        'pass' => array('link', 'category', 'page_id'),
        'page_id' => '[0-9]+',
        'action' => 'add|uncategorized'
    )
);

// domain.com/movies/106/articles OR domain.com/movies/106/articles/page:2
Router::connect(
    '/:category/:page_id/:controller/*',
    array('action' => 'index', 'link' => '0'),
    array(
        'pass' => array('link', 'category', 'page_id', 'controller'),
        'page_id' => '[0-9]+'
    )
);

because page_id is being passed in both the :link routing and the :category routing, I don't have to have a component that get's the page's id as suggested.

Now in my ArticlesController

public function index($link = null, $category = null, $page_id = null, $controller = null) {
    if($page_id){

         // display all articles for a particular page if on
         // http://www.domain.com/potc/articles or http://www.domain.com/movies/106/articles
        $this->set('articles', $this->paginate('Article', array('Article.page_id' => $page_id, 'Article.status <= 5'))); 
    }
    else{

        // display all articles if on http://www.domain.com/articles
        $this->set('articles', $this->paginate('Article', array('Article.status <= 5')));
    }    
} // end index function

public function view($id = null, $link = null, $category = null, $page_id = null) {
    $this->Article->id = $id;
    $article_page = $this->Article->field('page_id', array('id =' => $id));

    if (!$this->Article->exists()) {
        $this->Session->setFlash('This article does not exist');

        // using PageLinkRedirect function of my custom component,
        // determine where to redirect. Either to "potc/articles" or
        // "movies/106/articles";
        $this->redirect('../'.$this->CustomPage->PageLinkRedirect($link, $category, $page_id).'/articles', null, true);
    }
    elseif($article_page != $page_id) {
        $this->Session->setFlash('Invalid article for this page');

        // using PageLinkRedirect function of my custom component,
        // determine where to redirect. Either to "potc/articles" or
        // "movies/106/articles";
        $this->redirect('../'.$this->CustomPage->PageLinkRedirect($link, $category, $page_id).'/articles', null, true);
    }
    else{
        $this->set('title_for_layout', $this->Article->field('title', array('id =' => $id)));
        $this->set('article',  $this->Article->read(null, $id));
        $this->set('comments', $this->paginate($this->Article->Comment, array('Comment.module_id' => $this->viewVars['module_id'], 'Comment.post_id' => $id)));
    } // end else
} // end view function

My pagination is stored in an element but I load the element at the bottom of the Articles index.ctp view

<?php echo $this->element('all/pagination', array('pagination_url' => 'articles')); ?>

I would place the same code for each of my different controllers but change out the word "articles" (example : "reviews", "news/topics", etc)

My pagination.ctp element looks like this

<?php
if($this->params['paging'][key($this->params['paging'])]['pageCount'] > 1){
    $pagination_class = "pagination_enabled";
}
else{
    $pagination_class = "pagination_disabled";
}
 ?>
<div class="<?php echo $pagination_class; ?>">
<?php echo $this->Paginator->counter(array(
    'format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}')
)); 
?>
<ul>
<?php

// $link is set in AppController from the link param, same with category and page_id
if($link){
    $this->Paginator->options = array('url' => array('controller' => null, $link, $pagination_url));

    // This outputs http://www.domain.com/potc/articles where articles
    // is from the passed $pagination_url when I loaded the element
    // in my Articles index.ctp view. I have to set controller = null
    // or else the link will output as http://www.domain.com/articles/potc/articles
}
else{
    $this->Paginator->options = array('url' => array('controller' => null, 'link' => null, $category, $page_id, $pagination_url));

    // This outputs http://www.domain.com/movies/106/articles where articles
    // is from the passed $pagination_url when I loaded the element
    // in my Articles index.ctp view. I have to set controller = null AND
    // link = null or else the link will output as
    // http://www.domain.com/articles/0/movies/9/articles
} 
echo $this->Paginator->prev('< ' . __('previous'), array('tag' => 'li'), null, array('class' => 'prev disabled'));
if($this->Paginator->numbers){echo $this->Paginator->numbers(array('tag' => 'li', 'separator' => ''));}
echo $this->Paginator->next(__('next') . ' >', array('tag' => 'li'), null, array('class' => 'next disabled'));
?>
</ul></div>

OTHER TIPS

It's best to use relative paths for this:

   $this->Paginator->options = array(
                    'url' => '/dynamic-route'
   );
Or:
   $this->Paginator->options = array(
                    'url' => '/dynamic-route/articles'
   ); 
Or:
   $this->Paginator->options = array(
                    'url' => array('controller'=>'dynamic-route')
   );

Or Start building custom Routes in your Router file http://api.cakephp.org/class/router#method-Routerurl

If you construct your routes properly, this should all work automatically:

In your routes.php

/**
 * Movies view/details page
 */
Router::connect(
    '/:link',
    array(
         'controller'  => 'movies',
         'action'      => 'view',
    ),
    array(
         /**
          * Custom Link:
          * lowercase alphanumeric and dashes, but NO leading/trailing dash
          * should be at least 4 characters long
          *
          * IMPORTANT:
          * custom links should *NOT* overlap with actual controller
          * names, like 'movies, articles' etc!
          */
         'link'   => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',

         // Define what should be passed to the 'view' action as arguments
         'pass'         => array('link'),

         /**
          * Define what parameters should be automatically preserved
          * when creating URLs/links
          */
         'persist' => array('link'),
    )
);

/**
 * Article view/details page
 */
Router::connect(
    '/:link/:controller/:page_id',
    array(
         'controller'  => 'articles',
         'action'      => 'view',
    ),
    array(
         /**
          * Specify the 'controllers' that are allowed to be put
          * after link. You can add more, for example
          * 'reviews' to show reviews for a movie. this
          * will result in Reviews::view([page_id]) to be shown
          */
         'controller' => 'articles',
         'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
         'page_id'  => '[1-9]{1}[0-9]{0,}',

         // Define what should be passed to the 'view' action as arguments
         'pass'       => array('page_id', 'link'),

         /**
          * Define what parameters should be automatically preserved
          * when creating URLs/links
          */
         'persist' => array('page_id', 'link'),
    )
);

/**
 * Article overview/index page
 */
Router::connect(
    '/:link/:controller/*',
    array(
         'controller'  => 'articles',
         'action'      => 'index',
    ),
    array(
         'controller' => 'articles',
         'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',

         // Define what should be passed to the 'index' action as arguments
         'pass'       => array('link'),

         /**
          * Define what parameters should be automatically maintained
          * when creating URLs/links
          */
         'persist' => array('page_id', 'link'),
    )
);

When opening http://example.com/potc you'll be taken to Movies::view('potc')

When opening http://example.com/potc/articles you'll be taken to Articles::index('potc')

When opening http://example.com/potc/articles/page:2 you'll also be taken to Articles::index('potc'). The 'page' will be passed as named parameter

When opening http://example.com/potc/articles/99 you'll be taken to Articles::view(99, 'potc')

In your Articles/index.ctp view:

debug($this->Html->link('current page', array()));
// outputs:
// <a href="/potc/articles/">current page</a>

debug($this->Html->link('next page', array('page' => 2)));
// outputs:
// <a href="/potc/articles/page:2">next page</a>

debug($this->Html->link('view article', array('action' => 'view', 'page_id' => 99)));
// outputs:
// <a href="/potc/articles/99">view article</a>

Visiting a pagination-link (http://example.com/potc/articles/page:2/sort:title/dir:desc)

debug($this->request);

Outputs:

object(CakeRequest) {
    params => array(
        'plugin' => null,
        'controller' => 'articles',
        'action' => 'index',
        'named' => array(
            'page' => '2',
            'sort' => 'title',
            'dir' => 'desc'
        ),
        'pass' => array(
            (int) 0 => 'potc'
        ),
        'link' => 'potc',
        (int) 0 => 'page:2/sort:title/dir:desc',
        'paging' => array(
            'Article' => array(
                'page' => (int) 1,
                'current' => (int) 0,
                'count' => (int) 3,
                'prevPage' => false,
                'nextPage' => false,
                'pageCount' => (int) 1,
                'order' => array(),
                'limit' => (int) 10,
                'options' => array(
                    'page' => (int) 2,
                    'sort' => 'title',
                    'order' => array(),
                    'conditions' => array()
                ),
                'paramType' => 'named'
            )
        ),
        'models' => array(
            'Article' => array(
                'plugin' => null,
                'className' => 'Article'
            )
        )
    )
    data => array()
    query => array()
    url => 'potc/articles/page:2/sort:title/dir:desc'
    base => ''
    webroot => '/'
    here => '/potc/articles/page:2/sort:title/dir:desc'
}

As you can see, all the information you need be in the request-object

[UPDATE after modifycation by OP]

I see that you're using ':id' in this route:

/:link/:controller/:action/:id

But you didn't include a regular expression for that. Also, I don't think you want to use this route, because for this route to match, you'll have to include the action in your URL. for example:

/test-example/articles/view/1

A better option for this route, will be this route from my original example:

/:link/:controller/:page_id

This will route to:

Controller::view('page_id', 'link')

As I mentioned before, the 'order' in which the routes are added, determines what is matched first, so if you've added your routes before the CakePHP default routes are included, your routes will be prefered.

In this case; /test-example/ /pages/

Will always be matched by '/:link', thus /pages/view/test-example, also if you pass a valid controllername(!) On order to prevent this, you should add your routes after the CakePHP default routes are included.

If you add the route /:link/:controller/

Then this url: /test-example/something

Will only be matched if 'something' is a known controller

/:link/:controller/:action/:id

Will match if 'controller' is a known controller, any action will be 'accepted', but will cause a 'not found exception' if you try to access it. Also (as mentioned above) you should add a regular expression for 'id'

The last route you've added:

Router::connect('/:link/:controller/*', array(), array(
    'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
));

Is missing a default 'action' parameter, therefore will probably not be matched by CakePHP, this should probably be:

Router::connect('/:link/:controller/*', 
    array(
        'action' => 'index',
    ), array(
    'link' => '[a-z0-9]{1}([a-z0-9\-]{2,}[a-z0-9]{1})?',
));

This way, it will match:

/any-link/controllername/pass1
/any-link/controllername/pass1/pass2
etc.

Were 'pass1, pass2' will be sent to the controller as parameters to the action, for example:

/test-example/articles/some/thing/else

Will be routed to:

ArticlesController::index('some', 'thing', 'else');

I thing the best order of your routes will be:

  1. Cake Default routes
  2. /:link (-> pages::view('link'))
  3. /:link/:controller/:id (-> controller::view('id', 'link))
  4. /:link/:controller/* (-> controller::index('link'))

Note the order of the last two routes; Route 3 will only match if 'id' is a numeric value, causing the 'view' action to be routed Route 4 will match 'anything' after controller, and will show the 'index' action of the controller

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top