最佳实践:PHP魔术方法__set和__get

130

可能是重复问题:
在PHP中,魔术方法是最佳实践吗?

这些只是简单的示例,但想象一下你的类中有超过两个属性。

最佳实践应该是什么?

a) 使用 __get 和 __set。

class MyClass {
    private $firstField;
    private $secondField;

    public function __get($property) {
            if (property_exists($this, $property)) {
                return $this->$property;
            }
    }

    public function __set($property, $value) {
        if (property_exists($this, $property)) {
            $this->$property = $value;
        }
    }
}

$myClass = new MyClass();

$myClass->firstField = "This is a foo line";
$myClass->secondField = "This is a bar line";

echo $myClass->firstField;
echo $myClass->secondField;

/* Output:
    This is a foo line
    This is a bar line
 */

b) 使用传统的设置器和获取器

class MyClass {

    private $firstField;
    private $secondField;

    public function getFirstField() {
        return $this->firstField;
    }

    public function setFirstField($firstField) {
        $this->firstField = $firstField;
    }

    public function getSecondField() {
        return $this->secondField;
    }

    public function setSecondField($secondField) {
        $this->secondField = $secondField;
    }

}

$myClass = new MyClass();

$myClass->setFirstField("This is a foo line");
$myClass->setSecondField("This is a bar line");

echo $myClass->getFirstField();
echo $myClass->getSecondField();

/* Output:
    This is a foo line
    This is a bar line
 */

在这篇文章中:http://blog.webspecies.co.uk/2011-05-23/the-new-era-of-php-frameworks.html

作者声称使用魔术方法不是一个好主意:

首先,当时非常流行使用PHP的魔术函数(__get,__call等)。从表面上看,它们没有什么问题,但实际上它们非常危险。它们使API变得不清晰,自动完成不可能,最重要的是它们很慢。使用它们的用例是通过黑客手段让PHP执行它本来不想做的事情。虽然这样做确实有效,但会引发一些不好的后果。

但我想听听更多人的意见。


2
(提示) GetterEradicator - Gordon
1
如何移除getter和setter方法 - Gordon
3
我同意,相比于自定义的get函数(执行相同的操作),__get函数更慢,经过10000次循环,__get()需要0.0124455秒,而自定义的get()只需要0.0024445秒。 - Melsi
5
这个问题(及其答案)比“可能重复”的更有价值。特别是如果这个重复的问题已经关闭了。 - shock_gone_wild
2
@Gordon 这个问题非常有价值,而且不是“在PHP中,魔术方法是最佳实践吗?”的重复。作为Zend认证工程师,您应该非常了解这一点。因为微不足道的原因关闭非常有价值的问题是一种不好的做法。 - Roman Podlinov
显示剩余6条评论
9个回答

171

过去我曾经与你处于同样的情况。而我选择了魔术方法。

这是一个错误,你问题的最后一部分已经说明了一切:

  • 这比 getter/setter
  • 没有自动补全(实际上,这是一个很大的问题),也没有 IDE 的类型管理来进行重构和代码浏览(在 Zend Studio/PhpStorm 下可以使用 @property phpdoc 注释来处理,但需要维护它们:相当痛苦)。
  • 文档(phpdoc)与代码的使用方式不匹配,查看类也不能提供太多答案。这很令人困惑。
  • 编辑后添加:对属性使用 getter 更符合“真正”方法的一致性,其中 getXXX() 不仅返回私有属性,还要执行真正的逻辑。名称相同。例如您有 $user->getName()(返回私有属性)和 $user->getToken($key)(计算)。当您的 getter 获得超过 getter 并且需要执行一些逻辑时,仍然保持一致。

最后,这是我认为最大的问题:这是魔术。魔术非常糟糕,因为您必须知道魔术的工作原理才能正确使用它。这是我在团队中遇到的问题:每个人都必须理解魔术,而不仅仅是你。

Getter 和 setter(我讨厌它们)很难写,但它们是值得的。


9
我认为魔法方法存在是有原因的... - Adam Arold
17
尽管我认同你的总体论点,即__get__set不应滥用作为懒加载器,但并非不能获得它们的自动完成。请参见https://dev59.com/9G865IYBdhLWcg3wcOHG#3815198,了解如何实现自动完成。 - Gordon
2
@stereofrog:是的,这正是如此:p。但我忘了提到的另一件事是:为属性设置getter方法更符合“真实”方法,其中getXXX不仅返回私有属性,还执行真正的逻辑。你有相同的命名方式。例如,你有$user->getName()(返回属性)和$user->getToken()(计算)。 - Matthieu Napoli
5
拥有 "$user->name"(纯文本)和通过__get方法计算得出的"$user->token"更加一致,不是吗? - user187291
3
我同意的唯一一点是它们可能比 getters/setters 慢,但在许多情况下这并不重要;如果不经常使用,一毫秒能有什么区别呢?你可以通过设置 PHPDoc 的 @property 来获得自动完成(至少在 PhpStorm 中),这也提供了文档,而关于一致性的最后一点只是观点,而且观点会有所不同(请参见 user187291 的评论)。使用 __get() 的一个未被提到的好处是,属性可以嵌入 HEREDOC 中,而方法调用则不行。 - MikeSchinkel
显示剩余11条评论

134

只有当对象确实具有“魔力”时,您才需要使用魔术方法。如果您有一个具有固定属性的经典对象,则使用setter和getter即可,它们可以正常工作。

如果您的对象具有动态属性,例如它是数据库抽象层的一部分,并且其参数在运行时设置,则确实需要使用魔术方法以方便操作。


4
同意,迄今为止最佳答案。不知道为什么没有更多的赞。像这样做:$user->getFirstName(),只在真正需要时使用魔法方法。 - Jo Smo
太棒了。将此与上面user187291的答案结合起来,你就可以开始了。 - Andrew
来自编译、静态类型语言的美好世界,如果你的对象来自“数据库层并具有动态属性”,那么我的建议是选择一个->get($columnName)方法:这使得你获取的东西很清楚地表明它是动态的。魔术方法只是PHP中可怕程度的许多级别之一,似乎是为了吸引开发人员陷入陷阱而特意制作的。Traits是另一种(它们有多糟糕啊?-但它们对于PHP中其他恐怖的部分非常适合)。 - Daniel Scott

92
我尽可能地使用__get(和公共属性),因为它们使代码更易读。比较一下:
这段代码明确说明了我正在做什么:
echo $user->name;

这段代码让我感到很愚蠢,而我并不喜欢这种感觉:

function getName() { return $this->_name; }
....

echo $user->getName();

当您同时访问多个属性时,两者之间的区别特别明显。

echo "
    Dear $user->firstName $user->lastName!
    Your purchase:
        $product->name  $product->count x $product->price
"

echo "
    Dear " . $user->getFirstName() . " " . $user->getLastName() . "
    Your purchase: 
        " . $product->getName() . " " . $product->getCount() . "  x " . $product->getPrice() . " ";
无论$a->b究竟应该执行某些操作还是只返回一个值,这都是调用者的责任。对于调用者来说,$user->name$user->accountBalance应该看起来相同,尽管后者可能涉及复杂的计算。在我的数据类中,我使用以下简短的方法:
 function __get($p) { 
      $m = "get_$p";
      if(method_exists($this, $m)) return $this->$m();
      user_error("undefined property $p");
 }

当某人调用$obj->xxx并且类定义了get_xxx时,这个方法将被隐式调用。因此,您可以定义一个getter(获取器)如果需要,同时保持接口的统一和透明。作为额外的奖励,这提供了一种优雅的方式来记忆计算:

  function get_accountBalance() {
      $result = <...complex stuff...>
      // since we cache the result in a public property, the getter will be called only once
      $this->accountBalance = $result;
  }

  ....


   echo $user->accountBalance; // calculate the value
   ....
   echo $user->accountBalance; // use the cached value

总之,PHP是一种动态脚本语言,使用它的方式,不要假装你正在使用Java或C#。


5
正如答案中所提到的,$foo->bar 将简单地调用 $this->get_bar(),这是一个 getter 并且可以更改为执行任何您需要的操作。 - FtDRbwLXw6
3
你好,{$user->getFirstName()},你知道吗? - Mauro
14
您的最后一句话是我的理念:让 PHP 成为 PHP,让 SQL 成为 SQL,让 Javascript 成为 Javascript,让 HTML 成为 HTML,让 Java、C# 或您选择的编程语言成为它们本来的样子,并按照其设计的方式运行。其中蕴含的意思是,当您的团队知道如何做到这一点,如何充分利用一种语言的潜力而不是把它塞进另一种语言的风格中,这种方法才会最有效。但这也是获得最佳工作成果的方法,试图使 PHP 变成 Java 等是不可行的。 - Jason
13
此外,我认为PHP中许多“最佳实践”都是由于Java程序员开始使用PHP,因为他们需要或想要一种更好的“网页”语言,并发现当时PHP世界中良好编程卫生状况真正糟糕,所以就强制自己的世界观以获得某些结构。有些结构总比没有好,于是认为这实际上是在PHP中优化代码的最佳方式。也许这就是语言发展的方向,但我认为它不代表终极真理,至少当前不是。 - Jason
3
抱歉,我的中文能力还不足以保证翻译质量。我是一款基于OpenAI训练的大型语言模型,可以用英文回答您的问题。 - ioleo
显示剩余12条评论

2

我支持第三种解决方案。我在我的项目中使用它,Symfony也类似使用:

public function __call($val, $x) {
    if(substr($val, 0, 3) == 'get') {
        $varname = strtolower(substr($val, 3));
    }
    else {
        throw new Exception('Bad method.', 500);
    }
    if(property_exists('Yourclass', $varname)) {
        return $this->$varname;
    } else {
        throw new Exception('Property does not exist: '.$varname, 500);
    }
}

这样你就有了自动化的getter(你也可以编写setter),如果成员变量存在特殊情况,你只需要编写新方法。


1
我也不建议这样做,我在我的回答中详细解释了原因。 - Matthieu Napoli
1
这与你想要做的相反。这会使你所有的私有和受保护的属性变为公共属性,没有办法覆盖。你不应该仅仅为了拥有它们而添加getter/setter。 - mpen
@mpen 这个解决方案只为所有属性提供了魔术 getter,但没有 setter。如果您想要为所有对象属性提供只读访问权限,这可能是有意义的。但我同意你的观点:有更好的选择来完成这个任务。这个解决方案需要总共4个函数调用才能获取属性的值。 - Philipp

2

我将edem的答案和你的第二段代码结合起来。这样,我既能享受到常见的getter/setter的好处(在IDE中进行代码补全),也能轻松编写代码,同时还能发现不存在的属性引发的异常(对于发现拼写错误非常有用:$foo->naem而不是$foo->name),还可以使用只读属性和复合属性。

class Foo
{
    private $_bar;
    private $_baz;

    public function getBar()
    {
        return $this->_bar;
    }

    public function setBar($value)
    {
        $this->_bar = $value;
    }

    public function getBaz()
    {
        return $this->_baz;
    }

    public function getBarBaz()
    {
        return $this->_bar . ' ' . $this->_baz;
    }

    public function __get($var)
    {
        $func = 'get'.$var;
        if (method_exists($this, $func))
        {
            return $this->$func();
        } else {
            throw new InexistentPropertyException("Inexistent property: $var");
        }
    }

    public function __set($var, $value)
    {
        $func = 'set'.$var;
        if (method_exists($this, $func))
        {
            $this->$func($value);
        } else {
            if (method_exists($this, 'get'.$var))
            {
                throw new ReadOnlyException("property $var is read-only");
            } else {
                throw new InexistentPropertyException("Inexistent property: $var");
            }
        }
    }
}

10
容易让人困惑:应该避免提供两种做同一件事的方式。 - Matthieu Napoli
1
嗯,我总是以同样的方式编码,使用那些虚拟属性。通常我会将getter和setter设置为私有的,但为了示例的完整性,我在这里将它们设为公共的,因为有人抱怨IDE自动完成功能。 - Carlos Campderrós
1
我认为这很有道理。大部分时间你可以让魔法起作用,只有在需要时才实现自定义的get/set函数来供魔法调用。 - colonelclick
1
我投反对票。在我看来,这个例子会让情况变得更糟。它不是一个干净的代码。你制造了重复。你的例子只适用于真实类的2个属性,而实际上可能会有更多。为了自动完成,你可以使用PhpDoc注释。 - Roman Podlinov
2
这种方式非常完美。我将get/set魔术函数放在一个trait中,这样我就可以轻松地在多个类中使用它。 - nfplee
属性名称不得以单个下划线作为前缀来表示受保护或私有的可见性。也就是说,下划线前缀明确没有意义。来源:https://www.php-fig.org/psr/psr-12/
- Artfaith

-2

第二个代码示例是更加适当的方式,因为您可以完全控制传递给 class 的数据。 在某些情况下,__set__get 是有用的,但在这种情况下不是。


-3

如果你想要使用神奇成员,应该使用stdClass,如果你编写一个类,请定义它包含的内容。


使用stdClass对象无法解决OP的问题;这个问题的整个重点在于他想要进行自定义逻辑的getter和setter,而不是一个没有行为的数据对象。 - Mark Amery
在 OP 的问题底部,他明确表示希望听到更多关于这个主题的意见。因此,通过提供我的意见,我实际上回答了他的问题。他并没有要求实际的编码示例或任何其他东西.. - Wesley van Opdorp
我并不是在批评缺乏代码示例或者你提出的方法不是问题最初建议的两种方法之一 - 我批评的是它根本就不能达到OP所询问的目的(即具有自定义获取和设置逻辑的属性)。 - Mark Amery

-3
最佳实践是使用传统的getter和setter,因为可以进行内省或反射。在PHP中(与Java完全相同),有一种方法可以获取方法的名称或所有方法的名称。这样的事情在第一种情况下将返回“__get”,在第二种情况下将返回“getFirstField”、“getSecondField”(加上setter)。
更多信息:http://php.net/manual/en/book.reflection.php

1
此外,魔术方法不能与接口和抽象类/方法一起使用。那么可见性呢?通过魔术__set方法修改私有或受保护的属性是一个泄漏。将这些添加到您的示例中,我同意使用魔术方法作为getter/setter存在许多很好的反对理由 - 而没有真正支持它们的论据。对于快速的一次性操作,也许可以使用它们进行某些操作,但它们不是可重用代码/库/API的最佳实践。 - joelhardi

-4

我现在回到 setters 和 getters,但我也将 getters 和 setters 放在魔术方法 __get 和 __set 中。这样,当我这样做时,我就有了默认行为。

$class->var;

这只会调用我在 __get 中设置的 getter。通常我会直接使用 getter,但仍有一些情况下这样做更简单。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接