锂含量谈判显示所有数据-如何过滤掉


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

我有一个app/controllers/UsersController.php,它在索引操作中执行一个简单的Users::find('all');

路径/users/index呈现用户数据的纯HTML输出。路径/users/index.json呈现HTML输出的JSON等价物,这很好,只是它还公开了密码(经过散列处理,但仍然…)

我认为有两种选择可以避免这种情况:

  1. 在查找器中明确指定fields
  2. 过滤Media::render()并取消设置任何敏感数据

从长远来看,我觉得2号可能更容易保持。有什么意见吗?还有第三种更好的选择吗?

这就是我实现#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();
    }
}
?>

如有任何建议,我们将不胜感激。

这个问题可能有很多答案和方法,具体取决于您的应用程序、可维护性、架构的优雅性等。。。如果您只想删除用户密码等合理的字段,那么您的解决方案就可以完成这项工作。

但是!

过滤Media::render()似乎根本不是一个好主意。您在这里混合了各种关注点,最终会得到一个臃肿的过滤器,您可以在其中调整对象以删除您不想在json响应中暴露的内容。

如果你每次都要为应用程序中的每个控制器点上字段,那么使用字段可能不够好。更糟糕的是,如果您的实体有30多个字段,并且根据当前用户,显示不同的信息(OMG)!你最终会得到一个臃肿的控制器,在这里,你再次混合了关注点和责任:find()负责读取你的数据,而fields只是改变你的数据的表示(某种视图)。

那么呢?我们能做什么?

复制控制器逻辑
您可以通过将控制器中的过滤逻辑封装到if ($this->request->is('json')) { ... }中来分离它这意味着,如果请求是htmljson(公共api),则相同的控制器动作的响应不同
这也不好:)
一个稍微好一点的方法是通过使用重复的控制器来进行拆分=>第一个集合负责jsonapi,第二个集合负责响应html的"经典"控制器
通过添加一个controllers/api名称空间,并在json请求/响应的情况下重新配置Dispatcher以使用此路径,您可以使用Lithium轻松完成此操作。

li3_jbuilder
在某些情况下,我对复制控制器不太满意。更好的方法是使用MVCV部分,但这次是呈现json响应,并将其作为第一类对象处理:json视图
这可以通过调整Media类配置并具有回退机制(如果找不到*.json.php,则json_encode是没有筛选字段的对象)来轻松完成
我为Lithium构建了li3_jbuilder,以便轻松构建json响应、嵌套对象、使用助手,并将"表示"方面移动到视图层
Jbuilder的灵感来自Rails的Jbuilder。仅供参考,ruby社区也有RABL。

演示者模式
虽然这种方法看起来很简单,但还有另一种有趣的、更面向对象的方法:使用Presenter模式(或Decorator)
用户模型与UserPresenter类(普通的旧php类)相关联,负责提供要"呈现"的对象,尤其是在json响应中(或应用程序中的任何位置)
演示者也可以帮助您清理复杂的视图逻辑,可测试且非常灵活
演示者需要了解模型及其将要处理的视图,以便将它们传递给initialize方法,并将它们分配给实例变量
只需在谷歌上搜索"Presenter模式"或"Rails presenters"(我使用的唯一使用该模式的框架),就可以了解更多关于主题的信息

明确指定fields有几个优点:

  • 您无法获得不需要的数据,因此可能会更快
  • 如果你忘了取消设置,就不会意外泄露数据
  • 当您指定需要哪些字段时,如果JSON格式发生更改,您将收到早期警告

这与在SQL中不执行SELECT * FROM的原理类似。

我也遇到了同样的问题,当你在路径中添加.json时,我正在打印电子邮件和密码。

所以,由于我使用MySql,并且我在所有模型中都声明了我的$_schema,所以我做了一个小技巧。。。我在所有想要从数据库请求的字段中添加了一个"public"=>true,并在所有查询中使用它,如下所示:

$users = Users::find(array('fields' => Users::publicFields()));

publicFields方法如下所示:

public static function publicFields() {
    $self = static::_object();
    $className = $self->meta()['name'];
    $schema = $self->schema();
    $fields = array_filter($schema->fields(), function($var) {
        return !empty($var['public']);
    });
    $names = array_keys($fields);
    for ($i = 0, $iMax = count($names); $i < $iMax; $i++) {
        $names[$i] = $className . '.' . $names[$i];
    }
    return $names;
}

此处相同。unset(var)方法非常肮脏且危险。

我需要所有视图中的users对象来呈现主菜单并进行一些用户交互。Controller::Render方法通过$this->set()提供了这个额外的类。

在我的用户控制器中,我创建了一个新的php类"DSMember"。这个对象占用了我在视图中需要的一些公共属性。此处不提供密码和安全相关内容。

因此,在用户表示逻辑(DS=Display)和核心相关内容之间有了清晰的界限。

class DSMember
{
    public $id;
    public $profile;
    public $uuid;
    public $messages;
    function __construct ($user) //$user is the Users::Object
    {
        $this->id = $user->id;
        $this->uuid = $user->uuid;
        $this->profile = $user->user_profile;
        $this->messages = $user->messages;
    }
}

渲染方法重载::

public function render (array $options = array())
    {
        if ($this->session)
        {
            $member = new DSMember ($this->member);
            $this->set (compact ('member'));
        }
        parent::render ($options);
    }

因此,DSMember对象在所有HTML视图和JSON呈现输出中都可用。热门用户模型是隐藏的。

上面提到了很多不同的方法。对于我的应用程序来说,这种方式似乎很好。