Passed, named or query string params?

This is a question often asked. It’s also a question I had to find my own answers to – and to redefine those answers over time.

So here are my five cents regarding this topic:

Persistence: Passed params

You use passed params where there is a definite order in your URL regarding those params. So the second one never goes without the first one etc.
One example is a blog with date posts and you could just use the year or year/month or even year/month/day to select posts:

/**
 * url: /posts/index/year/month/day/
 */
public function index($year = null, $month = null, $day = null) {}

Other examples: /controller/action/country/city/ or /controller/action/page/paragraph/

So basically, when the order is fixed and you always need all previous params to use a specific param.

Note: you can also access them using $this->request->params['pass'][0...x]. But since you always know the order, its usually easier to directly pass them into your method as variables.

Outdated flexibility: Named params

For me they are still in use in my places – mainly in older projects of mine.

Advantages

They work well with named routing and custom routes. You can easily make named params into passed ones:

// in the routes.php
Router::connect('/specials/offer/:slug', array('controller'=>'products', 'action'=>'view'), array('pass'=>array('slug')));

// url array
array('controller' => 'products', 'action' => 'view', 'slug' => 'kitchen-knife')

// resulting "pretty" url instead of "/products/view/slug:kitchen-knife/"
/specials/offers/kitchen-knife/

Issues

The encoding of named params can break urls. And, basically, they always violate the HTTP spec.
They are cake specific (no other application uses volatile params this way) and no real standard.

Imagine what happens when you pass named params (via search/userinput) that contain special chars like /, &, … That will break the URL.
You would need to manually encode and decode them all the time. Quite the pain. Using mod_rewrite you will also have to adjust the amount of
encoding/decoding to the amount of htaccess files invoked in apache (as that also triggers the encoding/decoding). That is a known issue with URLs and apache. For me it was a nightmare do work with it properly without the URL being easily breakable.

Working with extensions can be quite the mess, as well:

// url array
array('ext' => 'json', 'foo' => 'bar')

// resulting url
/controller/action/foo:bar.json

To sum it up: Migrate them to query strings (see the next paragraph).

Flexibility the right way: Query strings

This is the way to go for 2.x and especially for 3.x.

When to use them

If the order is irrelevant or if they can be combined in different ways. In many cases like pagination not all query strings are needed to get the desired result. So the are usually always optional.

Advantages

They do not have the encoding issues of named params and are pretty much the way all other web apps also use such volatile params. So it is the de facto standard for web apps.

With query strings the short-routed action names (usually all index actions) don’t pop up anymore as long as you don’t use passed params:

/some_controller (index action of SomeController)
/some_controller/index/page:2/... (wrong)
/some_controller?page=2&... (nice)

They also work well with extensions:

// url array
array('ext' => 'json', '?' => array('foo' => 'bar')

// resulting url
/controller/action.json?foo=bar

Tip: Since CakePHP 2.3 you can use the convenience method CakeRequest::query() to read the URL query array in an error free manner. Any keys that do not exist will return null:

$foo = $this->request->query('foo');
// returns "bar" in our example - or null if no foo key is found

Nice things you can do with them, e.g. passing defaults for forms

Let’s say you link from articles to directly add a comment: /comments/add?article_id=2&title=Foo

public function add() {
    if ($this->request->is('post')) {
    	// save
    } else {
    	$this->request->data['Comment'] = $this->request->query;
    }
}

With that single line you are passing the query string params straight into the form as default values, populating the inputs on GET.
For each POST it will then ignore those, of course.

You could argue that the same was possible with named params. Well, kind of. They were not capable of using chars that needed to be urlescaped.
With query strings you don’t have to even think about it. It all works out of the box with anything you pass along.

Hands on

So how can we get our (new?) 2.x application future-proof and a little bit more 3.x ready?

I use this in my controllers to overwrite "named" as default paramType for the Paginator´:

// in beforeFilter()
$this->paginate['paramType'] = 'querystring';

Note: This works if you do not overwrite the whole paginate array in your methods, but use the same key/value assignments.
Alternatively, this is what I do:

// In your controller index actions
$this->paginate = array(
    'conditions' => ...,
    'contain' => ...,
    ...
) + $this->paginate;

This merges the AppController settings (including the paramType) into it if not overwritten.

Furthermore, my (CakeDC) Search plugin is also now using query strings due to this config setting:

$config['Search'] = array(
    'Prg' => array(
        'commonProcess'=>array('paramType' => 'querystring', 'filterEmpty' => true),
        'presetForm' =>array('paramType' => 'querystring')
    )
);

All other named param checks in my code i simply switched to if ($this->request->query('foo')) etc.
Along with the correct linking, of course:

// old: $this->Html->link('Foo Link', array('action' => 'some', 'foo' => 1))
$this->Html->link('Foo Link', array('action' => 'some', '?' => array('foo' => 1)))

I think that should already cover 99% of all cases.
Beware of special route setups, though, that might need some custom handling.

Tip: Using my Tools plugin CommonComponent you can get a warning if you forgot to remove/upgrade any named params to query strings when enabling Configure key 'App.warnAboutNamedParams'. Set it to true and get error log entries regarding those issues. Helped me to find remaining named params.

Upgrading with BC

In plugins I often had this code:

$limit = null;
if (!empty($this->request->params['named']['limit'])) {
    $limit = $this->request->params['named']['limit']
}

You might not be able to change all dependant apps, right away, though. I often add the query strings while keeping it also named-param compatible.

Now, to upgrade this to query strings while having some BC, all that is needed is:

$limit = $this->request->query('limit');
if (!empty($this->request->params['named']['limit'])) {
    $limit = $this->request->params['named']['limit']
}

And once you don’t need BC anymore, you can simply drop the named param stuff:

$limit = $this->request->query('limit');

Mission accomplished.

Last words

As mentioned above my opinion regarding named params changed over time. I was working with them for years from 1.2 up to 2.2.
But at some point I saw the disadvantages outweigh the advantages. In 2.x there have been also many improvements regarding query strings so that since 2.3 I always use and recommend query strings for new applications. And for legacy ones I recommend upgrading – if possible.

And for 3.x there will be no named params anymore, anyway. So the sooner we start applying the query strings to apps and plugins, the better.

Also, using the extension routing as mentioned above you don’t really have any other choice, anyway.
Passed params like this are not even valid AFAIK:

/some/controller/action.json/bar/y

This is also pretty ugly:

/some/controller/action.json/foo:bar/x:y

The only clean approach here are the query strings appended:

/some/controller/action.json?foo=bar&x=y

Which can easily be read from the controller using CakeRequest::query():

$foo = $this->request->query('foo'); // returns 'bar'
$x = $this->request->query('y'); // returns 'y'

Insight

The 3.x docs state the following:

Named parameters required special handling both in CakePHP as well as any PHP or javascript library that needed to interact with them, as named parameters are not implemented or understood by any library except CakePHP. The additional complexity and code required to support named parameters did not justify their existence, and they have been removed. In their place you should use standard query string parameters or passed arguments

Read more about the usage of the param types above in the documentation.

4.88 avg. rating (96% score) - 8 votes

16 Comments

  1. I don’t like query params because they make the url look messy. I think this article needs a follow up or amend explaining about creating SEO friendly urls using query strings using rewrite rules.

  2. I disagree. It can’t look any more messy than with named params. Especially if you are an outsider, seeing them for the first time.
    You might let you cloud your judgement by what you are used so far in Cake – but giving the in-validness of named params they are not comparable.
    So it doesn’t matter actually if they are nice or whatever. Just don’t use em as if they never existed.

    Using query strings usually doesn’t involve SEO – so it doesn’t necessarily need a follow up. When SEO is relevant you would usually use passed params – or internally make them behave like passed ones via routing.
    Query strings are – as explained above – the way to quickly filter results – and as such don’t need any special love IMO.
    Especially in the backend and such. For revelant frontend parts you can probably use some routing approach to map them to a more passed param style.

  3. Hello Mark, nice article. I have an issue: I have a paginated list, and I am currently using querystrings.
    I have two pages: I delete some items and my pages become single: I have a page not found error in redirection because the redirect still has page2 as url querystring parameter? How to avoid this ?

    Thanks in advance

    Rudy

  4. That is a good point.
    I guess after deleting it would be wise to either jump to first page or to check if there are still enough records to stay on that current page.

    But shouldnt current versions of Cake2 do that automatically? What are you running?

  5. It does that for security reasons if you use Html->url(). Use Router::url() if you don’t want h() to run over it. But then the URL should never be printed in the view.
    So in short: having it escaped is totally valid and even a requirement for valid and secure HTML output. So it depends on your use case and especially where you are using that url.

  6. Thanks Mark, beautifully explained as always – I have a bit more of an insight into what’s going on with the Html helper now. I’m using the url in jquery ajax calls – I realise I could create the whole thing using the js helper but I find it easier to debug when writing the js directly. Thanks again.

  7. Hello
    I am using CakeDC Search plugin , using last version so querystring as paramType option default: when I do search something , the result is parsed into a normal querystring, but once I tried to acess pagination links it return creating named parameters instead again!! And the pagination breaks up! Any idea about that ? Thanks

    Rudy

  8. I found out the same thing just a few days back.
    It might be very well a CakePHP core bug or rather search plugin bug, as it only happens when you supply a custom URL to PaginatorHelper params/options, right?
    Or what is your specific Paginator setup?

    For me, taking those URLs out of it made it function normally again.

    I assume we need to unset() the named params, that are re-formed to passed ones somewhere in the PrgComponent.

  9. My oaginator setup is like :
    public $paginate = array(

        'Post' => array(
            'limit' => 5,
            'contain' => array(
                'CategoriesPosts'
                
            ),
            
            'order' => array(
                'Post.created' => 'desc'
            ),
            'fields' => array('DISTINCT Post.id','Post.acm_name','Post.acm_description','Post.published','Post.status','Post.sort_order'),
            'group' => array('Post.id'),
            'paramType' => 'querystring'
        ),
        
    );
    
  10. Hi Mark
    I have checked about the problems of pagination: it seems that core paginator helper has a problem in _convertUrlKeys function: it does not create the querystring in "$url[‘?’]" with the parameters passed by $this->request->query.
    I have modified the core just to test and adding those values and finally pagination works again.
    But I was unable to remove named parameters from the url.
    This is how the _convertUrlKeys function look like after my small mod ( I think there would be a better and cleaner way to achieve this result but I am in hurry and I needed a solution )

    protected function _convertUrlKeys($url, $type) {
            
            $oldurl = $url;
            
            if ($type === 'named') {
                return $url;
            }
            if (!isset($url['?'])) {
                $url['?'] = array();
            }
                    
            foreach ($this->options['convertKeys'] as $key) {
                if (isset($url[$key])) {
                    $url['?'][$key] = $url[$key];
                    unset($url[$key]);
                    
                    foreach($oldurl as $chiave => $url_val) {
                                        
                        $url['?'][$chiave] = $url_val;
                        
                    }				
                }
            }
    
        
            return $url;
        }
  11. Please open a ticket in GitHub for it. As official bug report, ideally as PR that fixes it (including a test case).

  12. in case anyone’s struggling with the snippet to prepopulate an ‘add’ form under cakePHP 3.x, the query string params need to be passed into the newEntity() call, ie:

    public function add() {
        if ($this->request->is('post')) {
        	// save
        } else {
           $comment = $this->Comments->newEntity($this->request->query);
        }
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.