User-Defined File Extensions For Views - How?

I have a “list” action which is pretty standard, allowing the user to filter the results using the standard CGridView patterns we’re all familiar with. However I need to be able to output the data in different formats, e.g. JSON/HTML/XML/CSV.

For example:




URL -> Expected output

/accounts/list -> Standard HTML accounts page.

/accounts/list.json -> JSON accounts object. (Used for search suggestions.)

/accounts/list.csv -> CSV accounts file.



The action and data are identical in each case; only presentation differs. (Hence, this is a View issue.)

For reference, my actions() declaration looks like this:




<?php

public function actions()

{

    return [

        'list' =>[

            'class' =>'actions.ViewAction',

            'model' =>[$this, 'search'],

            'load' =>'GET',

            'view' =>'list',

        ],

        ...

    ];

}



(It’s a custom Action class, but it should be clear from the parameters what is going on.)

I don’t want to clutter my actions or controllers with logic for selecting the view. I’m happy to sacrifice a bit of control to simplify the app, especially because this pattern is repeated all over the place. (And not just for lists.)

I decided to use different view files for the different output types, e.g.:

  • list.php = The standard view.

  • list-json.php = The JSON view.

  • list-csv.php = The CSV file view.

  • list-{ext}.php = The general pattern.

I overrode CController::getViewFile() to select a view file based on the requested file extension. (Code below.) Now, given a URL like /accounts.json, my controller checks:

  • Does the view "list-json.php" exist? If yes, render it.

  • Otherwise, render "list.php".

Benefits:

  • Views retain the same basic name ("list"), indicating their relatedness

  • The output type is clearly visible by scanning the views directory

  • No hidden complexity (e.g. ifs / switch statements in the view file)

  • Doesn’t alter standard controller behavior

Has anyone else solved this problem? Is there a more elegant way to handle this?




<?php

class Controller extends CController

{

    ...

    /**

     * Searches for view files with a request-specified file extension.

     * The extension should be specified in the request object. This searches for

     * a view file named using the convention "viewName-ext.php". If the file does not

     * exist, it falls back to the normal "viewName.php".

     * @param string $viewName name of the view to be rendered

     * @return string|false path to the view file, or false if it could not be found

     **/

    public function getViewFile($viewName, $strict=false)

    {

        $request = Yii::app()->getRequest();


        // Look for a view with the proper file extension

        // if one was specified.

        if (null !== ($ext = $request->getQuery('ext'))) {

            $path = parent::getViewFile($viewName.'-'.$ext);

            if (false !== $path) {

                return $path;

            }

        }

        // Fall back to the standard view

        return parent::getViewFile($viewName);

    }

    ...

}



I think your solution is elegant enough :) Though, it would be easier to override render()/renderPartial(), if you don’t have layouts with different extensions. render() is called only once while getViewFile() is called several times during rendering (or maybe I’m wrong). Also you can find this method useful: http://www.yiiframework.com/doc/api/1.1/CController#beforeRender