Search code examples
phpjsonlithiumcontent-negotiation

Lithium content negotiation displays all data - how to filter it out?


I have app/controllers/UsersController.php that does a simple Users::find('all'); in the index action.

The path /users/index renders plain 'ol HTML output of the users data. The path /users/index.json render the JSON equivalent of the HTML output which is great except for the fact that it also exposes the password (which is hashed, but still...).

I see two options to avoid this:

  1. Explicitly specify fields in my finder.
  2. Filter Media::render() and unset any sensitive data.

I feel #2 may be easier to maintain, in the long run. Any opinions? Is there a third, better, alternative?

This is how I've implemented #2:

<?php

namespace app\controllers;

use \lithium\net\http\Media;

class UsersController extends \lithium\action\Controller {
    protected function _init() {
        Media::applyFilter('render', function($self, $params, $chain) {
            if ($params['options']['type'] === 'json') {
                foreach ($params['data']['users'] as $user) {
                    $user->set([
                        'password' => null,
                        'salt' => null
                    ]);
                }
            }
            return $chain->next($self, $params, $chain);
        });
        parent::_init();
    }
}
?>

Any advice would be appreciated.


Solution

  • This question could have a lot of answers and ways to do it, depending on your app, maintainability, elegance of your architecture, etc... In the case you want only to remove sensible fields like the user password, your solutions do the job.

    But!

    Filtering Media::render() doesn't seems to be a good idea at all. You are mixing concerns here, and you'll end up with a bloated filter where you tweak an object to remove what you don't want to expose in your json responses.

    using fields could be not good enough if you have to dot it each time, for each controller in your app. And worse, if your entities have 30+ fields, and depending on the current user, show different pieces of information (OMG)! You'll end up with a bloated controller, where, again, you are mixing concerns and responsibilities: find() is responsible of reading your data, and fields thing is only to change the presentation (sort of view) of your data.

    So? What could we do?

    duplication controller logic
    You could separate the filtering logic in your controller by enclosing it into a if ($this->request->is('json')) { ... } That means the same controller action respond differently if the request is html or json (a public api).
    This isn't good too :)
    A slightly better approach, is to split things a bit by having duplicated controllers => The first set is responsible for you json api, and the second for your "classic" controllers that respond to html.
    You could do this easily with Lithium by adding a controllers/api namespace, and reconfiguring the Dispatcher to use this path in case of a json request/response.

    li3_jbuilder
    I'm not that happy with duplicating controllers in some cases. A better approach is to use the V part of the MVC but this time to render json responses, and handle those as first class objects: json views !
    This could be done easily by tweaking Media class configuration, and having a fallback mechanism (if a *.json.php is not found, json_encode the object without filtering fields).
    I built li3_jbuilder for Lithium, to make it easy to build json responses, nest objects, make use of helpers, and move the "presentation" aspect to the view layer.
    Jbuilder is inspired by Rails' jbuilder. FYI, the ruby community got RABL too.

    Presenter Pattern
    While this approach seems simple, there is another interesting one, more object oriented: Use Presenter pattern (or Decorator).
    A User Model, is associated to a UserPresenter class (plain old php class), responsible for providing objects to be "presented", especially in json responses (or anywhere in your app).
    Presenters help you to clean up complex view logic too, are testable, and very flexible.
    The presenter needs to know about the model and the view it will be dealing with so you'll pass these in to an initialize method and assign them to instance variables.
    Just google for "Presenter pattern", or "Rails presenters" (the only framework I used that make use of this pattern), to know more on the subject