【问题标题】:What's a good way to make a PHP website approach the database object oriented?使PHP网站接近数据库对象的好方法是什么?
【发布时间】:2013-11-19 08:16:32
【问题描述】:

请注意,我不是在寻找“使用框架”的答案。我正在尝试从结构上改进我使用 PHP 编写网站和处理数据库的方式。

我正在从头开始构建 Web 服务,没有任何框架。我正在使用 LAMP 堆栈,并且正在尝试学习一些 PHP 的 OO 功能。我以前只用OO做移动应用。

我已经做了几个月了(按计划进行,不用担心)。在此过程中,我遇到了一些结构性问题,这让我想知道使代码面向对象的最佳方法是什么。

几乎所有问题都以某种方式涉及数据库。假设我们有一个类DB 和一个类User。在大多数情况下,我只需要从数据库中获取单个用户的信息。我认为处理它的一个好方法是拥有一个全局 $_db 变量并让 User 对象像这样查询数据库(过于简单):

class User {
    function __construct($id) {
        global $_db;
        $q = $_db->query("SELECT name, mail FROM user WHERE id = ?", $id);
        $this->loadProperties($q);
    }
}

现在假设我们有一个显示用户列表的页面。我仍然想为每个人创建User 对象,但我不想为每个单独的用户查询数据库。

所以,我扩展了User 类以将对象作为参数:

class User {
    function __construct($id) {
        if(is_object($id))
            $q = $id;
        else {
            global $_db;
            $q = $_db->query("SELECT name, mail FROM user WHERE id = ?", $id);
        }

        $this->loadProperties($q);
    }
}

现在我可以创建一个列表,例如最近创建和活跃的 100 个帐户:

$user_list = [];

$q = $_db->query("SELECT name, mail FROM user WHERE banned = 0 ORDER BY date_created DESC LIMIT 100");

while($a = $_db->fetch($q))
    $user_list[] = new User($a);

这一切都很好,除了一个很大的缺点:表user 的数据库查询不再在一个地方,这是一种制作意大利面条的代码。这就是我开始怀疑这是否可以更有效地完成的地方。

所以也许我需要扩展我的DB 对象而不是我的User 对象,例如:

class DB {
    public function getUsers($where) {
        $q = $this->query("SELECT name, mail FROM user WHERE ".$where);

        $users = [];
        while($a = $this->fetch($q))
            $users[] = new User($a);
    }
}

现在我将按如下方式创建用户列表:

$user_list = $_db->getUsers("banned = 0 ORDER BY date_created DESC LIMIT 100");

但现在我使用各种 SQL 查询在各个地方调用 getUsers() 方法,但什么也没解决。我也不想每次都加载相同的属性,所以我的getUsers() 方法必须将整个 SQL 查询作为参数。无论如何,你明白了。

说到加载不同的属性,还有一件事一直困扰着我用 PHP 编写 OO。假设我们的 PHP 对象至少具有数据库行所具有的所有属性。说我有一个方法User::getName()

class User {
    public function getName() {
        return $this->name;
    }
}

此函数将假定已从数据库加载适当的字段。但是,每次创建对象时都预加载所有用户的属性是低效的。有时我只需要用户名。另一方面,此时进入数据库加载这个属性也是低效的。

我必须确保我使用的每种方法都已经加载了相应的属性。从性能的角度来看,这完全有道理,但从 OO 的角度来看,这意味着您必须事先知道要使用哪些方法,这会使其动态性大大降低,并且再次允许意大利面条式代码。

我遇到的最后一件事(至少目前是这样)是如何将实际的用户与new User 区分开来。我想我会使用一个名为Registration 的单独类(同样,过于简单化了):

class Registration {
    function createUser() {
        $form = $this->getSubmittedForm();

        global $_db;
        $_db->query("INSERT INTO user (name, mail) VALUES (?, ?)", $form->name, $form->mail);
        if($_db->hasError)
            return FALSE;

        return $_db->insertedID;
    }
}

但这意味着我必须为每个数据库表创建两个单独的类,并且我有不同的类访问同一个表。更不用说还有一个处理登录会话的第三类也访问用户表。

总之,我觉得以上所有方法都可以更有效地完成。最重要的是我想要漂亮的代码。我觉得我错过了从 OO 角度处理数据库的方法。但是我怎样才能在不失去 SQL 查询的动态和强大功能的情况下做到这一点呢?

我期待阅读您在该领域的经验和想法。

更新

似乎你们中的大多数人都谴责我使用global $_db。尽管您已经说服我这不是最好的方法,但对于这个问题的范围,无论我是通过参数、全局还是单例来提供数据库都无关紧要。它仍然是一个单独的类 DB 来处理与数据库的任何交互。

【问题讨论】:

  • 第 1 步。避免使用global
  • 第 2 步。了解Dependency Injection
  • @cwallenpoole 我不认为应该完全避免global。不同的对象将如何访问登录用户的信息?
  • @Robbert 有很多方法。由于您使用的是 OOP,因此您可能有一个单例来管理它,或者至少有一个带有静态变量的方法。
  • @Robbert 至少,将您的 DB 变量包装在一个类或函数中意味着以后不可能意外重新分配,这是一件非常好的事情。

标签: php mysql oop


【解决方案1】:

拥有一个单独的类来处理 SQL 查询并保存获取的数据是很常见的事情。其实就是单一职责原则的真正应用。

我通常做的是保留一个包含有关数据的所有信息的类,在您的情况下是 User 类,将所有用户信息作为字段。

然后是业务层,例如UserDataManager(虽然不建议使用“Manager”作为后缀,您最好在每个场景中找到一个更合适的名称),它在其构造函数中使用pdo对象以避免使用全局变量并拥有所有的 SQL 方法。因此,您将拥有 registerNewUser、findUserById、unsuscribeUser 等方法(方法中“User”的使用可以通过类名隐含并省略)。

希望对你有帮助。

【讨论】:

  • 简单但有效。我喜欢它。在此设置中,您将如何检索登录用户的信息?它会有自己的类LoggedInUser吗?如果是这样,如果登录用户更新他的个人资料,哪个类将处理表单处理?
  • 我认为高估登录用户与高估数据库连接以使其成为单例一样严重错误(请参阅我对 Jérémy 的回答的 cmets)。您只需要认为登录用户将像任何其他用户一样被处理,并且您将它传递到任何需要它的地方。你有一个 getRank() 方法吗?通过用户。你有 canReadPrivateMessage 吗?通过用户。登录的用户就是用户,所以 updateProfile 属于 UserDataManager。当然,您不需要获取其他用户的密码,那又如何?
  • 完全有道理,我深信不疑。我已将此标记为最佳答案。我知道这个问题有很多不同的答案,但我喜欢你用几行代码总结出一个非常可靠的解决方案。
  • 只要您对我的处事方式感到满意,我很高兴能为您提供帮助。如果可以的话,这也是一个非常有趣的话题:c2.com/cgi/wiki?DontNameClassesObjectManagerHandlerOrData
【解决方案2】:

我喜欢使用data mapper 模式(或者至少我认为我就是这样做的)。我已经为一些建立在Silex 上的网站做了这个,尽管它适用于没有框架的情况,因为 Silex 非常轻量级并且不会对你如何构建代码施加太多影响。事实上,我建议您查看 Symfony2/Silex 只是为了获得一些关于如何设计代码的想法。

无论如何,我使用过UserMapper 之类的类。由于我使用的是Doctrine DBAL 库,因此我使用依赖注入给每个映射器一个$db。但据我所知,DBAL 几乎是 PDO 类的包装器,因此您可以注入它。

现在您有一个负责 CRUD 操作的 UserMapper。所以我用LoadUser($id)LoadAllUsers() 之类的方法解决了你的第一个问题。然后我会根据数据库中的数据设置new User 上的所有属性。您同样可以拥有CreateUser(User $user)。请注意,在“创建”中,我实际上传递了一个 User 对象并将其映射到数据库。您可以将其称为 PersistUser(User $user) 以使这种区别更加清晰。现在所有的 SQL 查询都在一个地方,您的 User 类只是一个数据集合。 User 不需要来自数据库,您可以创建测试用户或其他任何内容而无需任何修改。 `User 的所有持久化都封装在另一个类中。

我不确定加载用户的所有属性是否总是不好,但如果你想解决这个问题,制作LoadUsername($id) 或其他方法并不难。或者你可以用一组你想加载的属性来做LoadUser($id, array $properties)。如果您的命名是一致的,那么很容易设置如下属性:

// in a foreach, $data is the associative array returned by your SQL
$setter = 'set'.$property;
$user->$setter($data[$property]);

或者(并且?)您可以使用代理对象来解决这个问题。我还没有这样做,但想法是返回一堆UserProxy 对象,它们扩展User。但是他们注入了Mapper,并且他们覆盖了getters 以调用Mapper 以选择更多。也许当您访问代理上的一个属性时,它将通过映射器select 一切(一种称为populateUser($id) 的方法?)然后随后的getters 可以访问内存中的属性。例如,如果您选择所有用户然后需要访问子集上的数据,这可能会有意义。但我认为总的来说,选择所有内容可能更容易。

public function getX()
{
    if (!isset($this->x)) {
        $this->mapper->populateUser($this);
    }
    return $this->x;
}

对于新用户,我说只需执行$user = new User... 并设置所有内容,然后拨打$mapper->persist($user)。您可以将其包装在另一个类中,例如UserFactory->Create($data),它可以返回(持久的)User。或者,如果您愿意,可以将该课程称为 Registration

我有没有提到您应该使用Dependency Injection 来处理所有这些服务(可能像Mappers 和其他像Factories 这样的服务)?也许只需从 Silex 获取 DIC,称为Pimple。或者自己实现一个轻量级的(不难)。

我希望这会有所帮助。这是我从编写大量 PHP 和使用 Syfmony2/Silex 中学到的一些东西的高级概述。祝你好运,很高兴看到像你这样的 PHP 程序员真的在努力“做正确的事情”!如果我可以在任何地方详细说明,请发表评论。希望这个答案对你有所帮助。

【讨论】:

  • 感谢马特的详细回答,这表明我还有很多东西要学。我主要关心的是最小化 SQL 查询的数量和每个对象加载的字段数量,因此我喜欢使用单个对象的想法,该对象具有为不同视图加载不同字段的方法(getUserByIDgetUserProfile 等)。我将深入研究这个项目以将其移动到任何框架,但对于下一个项目,我肯定会再看看你的建议。
  • 就像我说的,你不一定需要迁移到一个框架,但你应该学习其他人是如何解决常见问题的。但是如果你确实想为你的下一个项目使用一个框架,Silex 很不错,因为它很有帮助,而且不会对你强加太多的约定。但它可以很好地处理诸如路由之类的事情。还有其他类似的“微型”框架,但我不熟悉它们。祝你好运!
【解决方案3】:

您应该首先编写一个类作为数据库对象的包装器,它比全局变量更干净(如果您不知道单例模式,请阅读它,并且有很多示例网络上的单例数据库包装器)。然后,您将更好地了解您应该实施的架构。

最好的方法是将数据与数据库的事务分开,这意味着您可以为您的 User 设置两个类;一个只发送查询和获取响应,另一个将通过对象的属性和方法来管理数据。有时,可能还有一些不需要与数据库交互的操作,这些操作也会在这些类中实现。

最后但并非最不重要的一点是,查看 MVC 框架以及它们是如何工作的可能是个好主意(即使您不想使用它);这将使您很好地了解如何构建 Web 应用程序,以及如何以某种方式为您实现这些模式。

【讨论】:

  • 我不确定使用单例反模式是否比使用全局 db 变量好得多。
  • @Arlaud Agbe Pierre 你能综合一下为什么吗?
  • 单例是一种反模式。它们使您对自己感觉良好,因为您使用的是类而不是全局变量,但是它们具有相同的缺陷。如果您真的有兴趣,我会推荐此页面上的前 6 个注释:en.wikipedia.org/wiki/Singleton_pattern#cite_note-1
  • 感谢您的建议杰里米。关于单例:无论我使用的是全局还是其他东西,我仍然有一个处理 PDO 的 DB 对象(如我的问题中所述)。这不是问题的重点。
猜你喜欢
  • 2011-06-16
  • 1970-01-01
  • 1970-01-01
  • 2010-09-10
  • 2012-01-29
  • 2012-09-02
  • 2014-10-27
  • 2013-04-05
  • 2011-08-14
相关资源
最近更新 更多