吸气和塞特?


203

我不是PHP开发人员,所以我想知道在PHP中使用纯私有对象(我喜欢的方式)以纯OOP风格使用显式getter / setters是否更受欢迎:

class MyClass {
    private $firstField;
    private $secondField;

    public function getFirstField() {
        return $this->firstField;
    }
    public function setFirstField($x) {
        $this->firstField = $x;
    }
    public function getSecondField() {
        return $this->secondField;
    }
    public function setSecondField($x) {
        $this->secondField = $x;
    }
}

或只是公共领域:

class MyClass {
    public $firstField;
    public $secondField;
}

谢谢


7
在尝试了答案中的一些代码后,我使用了您在问题中使用的代码。多么可悲:-(
sumid

9
PHPstorm ...生成> getter和setter。==胜利
DevDonkey

@DevDonkey根本不是胜利。要保存结构化数据,请改用数组。@:Mark这不是对象或目的。吸气剂和吸气剂是邪恶的:yegor256.com/2014/09/16/getters-and-setters-are-evil.html
Kubo2

Answers:


222

您可以使用php魔术方法 __get__set

<?php
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;
    }

    return $this;
  }
}
?>

15
我想你的意思__get__set。有两个下划线,而不是一个。这是页面右侧的直接链接:php.net/manual/en/… (+1为正确的答案)
Computerish 2010年

28
public如果没有验证/卫生措施,那么对性能有何好处?
KingCrunch

7
@KingCrunch,这只是一个例子。一个非常强大的资源的非常虚构的示例。
戴维斯Peixoto

10
那不是真正的二传手。通常我需要为每个属性的getter实现不同的实现!
sumid

79
请不要:使用魔术方法,您将失去许多IDE(甚至vim)中几乎所有与质量相关的功能:自动完成,显式PHP继承,快速PHP解释以及有用的PHPDoc生成和输出。cf. stackoverflow.com/a/6184893/490589
罗南2013年

113

为什么要使用getter和setter?

  1. 可伸缩性:重构getter比在项目代码中搜索所有var赋值更容易。
  2. 调试:您可以在setter和getter处设置断点。
  3. 清洁器:Magic函数不是写少的好方法,您的IDE不会建议代码。更好地使用模板来快速编写吸气剂。

直接分配和获取/设置者


7
如果您使用@property,则您的IDE将建议代码(已通过PhpStorm 7测试)
Alex2php 2013年

41

Google已经发布了有关PHP优化的指南,其结论是:

无需getter和setter 优化PHP

不,您一定不能使用魔术方法。对于PHP,魔术方法是邪恶的。为什么?

  1. 他们很难调试。
  2. 这会对性能产生负面影响。
  3. 他们需要编写更多代码。

PHP不是Java,C ++或C#。PHP是不同的,扮演着不同的角色。


10
我倾向于同意这个想法。那$dog->name = 'fido'$dog->setName('fido')。当实际变异的特性(例如:$dog->increaseAge(1)我能打造出来,做了必要的验证和变异该属性的方法,但是,并非所有的行为确实需要在这个意义上的突变。
查理Schliesser

11
这篇文章不是说“ 不要 ”,而是说“天真的设置者和获取者”。
Brett Santore 2014年


13
可以肯定地说,由Google撰写的标题为“ PHP性能提示”的文章并非旨在建议良好的编码风格,而是建议快速执行代码。处理setter和getter的章节标记为“避免编写幼稚的setter和getters”,而代码示例正是这样:Naive。它设置并仅获取变量而无需任何验证。这种设置器/获取器是没有用的。在setter内部进行验证(只需在方法参数中使用类型提示)将使setter / getter有用,因为现在您的代码知道其处理的内容。
2016年

3
这就像说内联样式更好。当然性能更好,但是代码更好吗?我不知道Google工程师还是会使用php
Claudiu Creanga

13

封装在任何OO语言中都很重要,流行与否无关。在动态类型的语言(如PHP)中,它特别有用,因为没有很少的方法可以确保不使用setter的情况下属性为特定类型。

在PHP中,这有效:

class Foo {
   public $bar; // should be an integer
}
$foo = new Foo;
$foo->bar = "string";

在Java中,它不会:

class Foo {
   public int bar;
}
Foo myFoo = new Foo();
myFoo.bar = "string"; // error

使用魔术方法(__get__set)也可以,但是仅当访问的可见性比当前作用域低的属性可以访问时。如果使用不当,它很容易在尝试调试时使您头痛。


7
Getter和Setter不会带来封装。封装==对象使用自己的数据执行某项操作,而不是将其提供给外部。getter和setter并不是在PHP等动态类型化语言中强制类型化的工具。
smentek

14
@smentek:您显然至少缺少封装真正的一半。
netcoder 2011年

2
作为对此的更新,PHP 7.4将提供类型化属性支持。因此,您可以在第一个示例中声明$barintwiki.php.net/rfc/typed_properties_v2
凯文(Kevin)

7

如果您喜欢使用__call函数,则可以使用此方法。它与

  • GET => $this->property()
  • 设置=> $this->property($value)
  • GET => $this->getProperty()
  • 设置=> $this->setProperty($value)

卡尔斯达斯

public function __call($name, $arguments) {

    //Getting and setting with $this->property($optional);

    if (property_exists(get_class($this), $name)) {


        //Always set the value if a parameter is passed
        if (count($arguments) == 1) {
            /* set */
            $this->$name = $arguments[0];
        } else if (count($arguments) > 1) {
            throw new \Exception("Setter for $name only accepts one parameter.");
        }

        //Always return the value (Even on the set)
        return $this->$name;
    }

    //If it doesn't chech if its a normal old type setter ot getter
    //Getting and setting with $this->getProperty($optional);
    //Getting and setting with $this->setProperty($optional);
    $prefix = substr($name, 0, 3);
    $property = strtolower($name[3]) . substr($name, 4);
    switch ($prefix) {
        case 'get':
            return $this->$property;
            break;
        case 'set':
            //Always set the value if a parameter is passed
            if (count($arguments) != 1) {
                throw new \Exception("Setter for $name requires exactly one parameter.");
            }
            $this->$property = $arguments[0];
            //Always return the value (Even on the set)
            return $this->$name;
        default:
            throw new \Exception("Property $name doesn't exist.");
            break;
    }
}

2
@ krzysztof-przygoda:这种“魔术方法”总是要付出代价的。他们必须使用递归,property_exists(get_class($this), $name)并且递归很慢。y可以通过缓存来减轻这种情况,但是它仍然比手动创建getter和setter还要慢。我只写了这作为替代。我实际上不建议使用“魔术方法”。创建getter和setter的额外时间通常很少。
J-Rou

7

除了这里已经很受人尊敬的答案外,我还要扩展没有设置器/获取器的PHP。

PHP没有getter和setter语法。如 Dave所指出的,它提供了子类或魔术方法来允许“钩住”并覆盖属性查找过程。

Magic使我们懒惰的程序员可以在我们积极参与项目并亲密了解项目的同时,用更少的代码来做更多的事情,但这通常是以可读性为代价的。

性能由于在PHP中强制使用类似getter / setter的代码体系结构而导致的每个不必要的功能,在调用时都会涉及其自身的内存堆栈框架,并浪费CPU周期。

可读性:代码库会导致代码行膨胀,因为更多的LOC意味着更多的滚动,这会影响代码导航。

偏爱:就我个人而言,根据我的经验,只要当时没有明显的长期利益,我就将静态代码分析的失败视为避免走魔术之路的标志。

谬论:

一个常见的论点是可读性。例如,$someobject->width它比容易阅读$someobject->width()。但是,与可能假定为的行星circumference或行星不同widthstatic例如$someobject,需要宽度函数的对象实例,很可能需要测量对象实例宽度。
因此,可读性的提高主要是由于主张性的命名方案,而不是通过隐藏输出给定属性值的函数来实现的。

__get / __set使用:

  • 属性值的预验证和预卫生

  • 字符串,例如

    "
    some {mathsobj1->generatelatex} multi
    line text {mathsobj1->latexoutput}
    with lots of variables for {mathsobj1->generatelatex}
     some reason
    "

    在这种情况下,generatelatex将遵循动作名+方法名的命名方案

  • 特殊的,明显的情况

    $dnastringobj->homeobox($one_rememberable_parameter)->gattaca->findrelated()
    $dnastringobj->homeobox($one_rememberable_parameter)->gttccaatttga->findrelated()

注意: PHP选择不实现getter / setter语法。我并不是说吸气剂/装甲剂通常是不好的。


6
class MyClass {
    private $firstField;
    private $secondField;
    private $thirdField;

    public function __get( $name ) {
        if( method_exists( $this , $method = ( 'get' . ucfirst( $name  ) ) ) )
            return $this->$method();
        else
            throw new Exception( 'Can\'t get property ' . $name );
    }

    public function __set( $name , $value ) {
        if( method_exists( $this , $method = ( 'set' . ucfirst( $name  ) ) ) )
            return $this->$method( $value );
        else
            throw new Exception( 'Can\'t set property ' . $name );
    }

    public function __isset( $name )
    {
        return method_exists( $this , 'get' . ucfirst( $name  ) ) 
            || method_exists( $this , 'set' . ucfirst( $name  ) );
    }

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

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

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

$obj = new MyClass();

echo $obj->firstField; // works
$obj->firstField = 'value'; // works

echo $obj->getFirstField(); // works
$obj->setFirstField( 'value' ); // not works, method is protected

echo $obj->secondField; // works
echo $obj->getSecondField(); // not works, method is private

$obj->secondField = 'value'; // not works, setter not exists

echo $obj->thirdField; // not works, property not exists

isset( $obj->firstField ); // returns true
isset( $obj->secondField ); // returns true
isset( $obj->thirdField ); // returns false

准备!


样板太多。想象一下,每个班级都有这种东西。避免使用IMO
DarkNeuron,2015年

出于您提到的相同原因,PHP不支持getter和setter。此类型的任何实现都会严重影响服务器端脚本的性能。
joas,2016年

我认为这与“私有”财产背道而驰。您可以封装它们,但也可以直接访问。
KorayKüpe19年

@KorayKüpe仅在定义了吸气剂的情况下。我经常使用这种封装(进行了许多改进),并且效果很好。您还可以扩展该类,并轻松地在所有代码中使用它。
joas,

5

那么,PHP确实有神奇的方法__get__set__isset__unset,这始终是一个开始。proper(适当吗?)OO属性比魔术方法还重要。PHP实现的主要问题是,所有不可访问的属性都会调用魔术方法。这意味着在确定名称是否实际上是对象的属性时,必须在魔术方法中重复自己(例如,通过调用property_exists())。除非所有类都从ie继承,否则您不能用基类真正解决此一般问题。ClassWithProperties,因为PHP缺少多重继承。

相反,Python新样式类为您提供了property(),可让您显式定义所有属性。C#具有特殊的语法。

http://en.wikipedia.org/wiki/Property_(编程)


1
调用property_exists,class_vars或array_key_exists(即检查属性是否确实存在)只是为了避免运行时致命错误的一个步骤。我不确定不被区别是否与重复编码相同。
戴维斯·皮克斯托

1
很公平。但是在Python和C#中,不需要重复。我认为这是一种优势。
伊曼纽尔·兰德霍尔姆

4

我使用魔术方法__call进行了实验。不知道我是否应该张贴它(因为其他答案和评论中的所有“请勿使用魔术方法”警告),但我将其留在这里..以防万一有人发现它有用。


public function __call($_name, $_arguments){
    $action  = substr($_name, 0, 4);
    $varName = substr($_name, 4);

    if (isset($this->{$varName})){
        if ($action === "get_") return $this->{$varName};
        if ($action === "set_") $this->{$varName} = $_arguments[0];
    }
}

只需在类中添加该方法,现在您可以输入:

class MyClass{
    private foo = "bar";
    private bom = "bim";
    // ...
    // public function __call(){ ... }
    // ...
}
$C = new MyClass();

// as getter
$C->get_foo(); // return "bar"
$C->get_bom(); // return "bim"

// as setter
$C->set_foo("abc"); // set "abc" as new value of foo
$C->set_bom("zam"); // set "zam" as new value of bom


这样,您就可以获取/设置类中的所有内容(如果存在的话),如果只需要几个特定元素,则可以使用“白名单”作为过滤器。

例:

private $callWhiteList = array(
    "foo" => "foo",
    "fee" => "fee",
    // ...
);

public function __call($_name, $_arguments){
    $action  = substr($_name, 0, 4);
    $varName = $this->callWhiteList[substr($_name, 4)];

    if (!is_null($varName) && isset($this->{$varName})){
        if ($action === "get_") return $this->{$varName};
        if ($action === "set_") $this->{$varName} = $_arguments[0];
    }
}

现在,您只能获取/设置“ foo”和“ fee”。
您还可以使用该“白名单”来分配自定义名称以访问您的var。
例如,

private $callWhiteList = array(
    "myfoo" => "foo",
    "zim" => "bom",
    // ...
);

使用该列表,您现在可以键入:

class MyClass{
    private foo = "bar";
    private bom = "bim";
    // ...
    // private $callWhiteList = array( ... )
    // public function __call(){ ... }
    // ...
}
$C = new MyClass();

// as getter
$C->get_myfoo(); // return "bar"
$C->get_zim(); // return "bim"

// as setter
$C->set_myfoo("abc"); // set "abc" as new value of foo
$C->set_zim("zam"); // set "zam" as new value of bom




就这样。


Doc: 在对象上下文中调用不可访问的方法时会触发__call()


所有这些“魔术”解决方案的问题在于它们仅使用这些魔术方法,因为它们已经存在,并且它们很有用,因为所提出的问题是一种简单的通用方法。一旦离开了这一类通用问题,您将遇到无法通过简单的魔术方法解决的特定要求,但将需要非常复杂的魔术方法-或单独的编码设置器和获取器。
2016年

2

阅读其他建议后,我倾向于说:

作为GENERIC规则,你不会总是定义制定者所有的性质,特别是“内部”的人(信号灯,内部标志...)。显然,只读属性将没有setter方法,因此某些属性将仅具有getter方法。这就是__get()缩小代码的地方:

  • 为所有类似的属性定义一个__get()(全局全局获取器),
  • 将它们分成数组,以便:
    • 它们将具有共同的特征:货币价值将/可能会采用正确的格式设置,日期采用特定格式(ISO,US,Intl。)等。
    • 代码本身可以验证使用这种神奇的方法仅读取现有和允许的属性。
    • 每当您需要创建一个新的相似属性时,只需对其进行声明并将其名称添加到适当的数组即可。这比定义一个新的getter 更快,也许在类代码中反复重复一些代码。

是! 我们也可以编写一个私有方法来做到这一点,但是再一次,我们将有许多声明的方法(++ memory)最终调用另一个始终相同的方法。为什么不编写单个方法来统治所有对象呢?[是的!双关语意在意!:)]

魔术设置器也只能响应特定的属性,因此可以仅通过一种方法针对无效值筛选所有日期类型的属性。如果在数组中列出了日期类型属性,则可以轻松定义它们的设置器。当然,只是一个例子。情况太多了。

关于可读性 ...好吧...那是另一场辩论:我不喜欢被IDE所束缚(实际上,我不使用它们,它们倾向于告诉我(并强迫我)如何写...,我喜欢编码“美女”)。我倾向于保持一致的命名方式,因此使用ctags和其他一些辅助工具对我来说就足够了……无论如何:一旦完成了所有这些神奇的设置者和getter的工作,我就会写出其他过于具体或“特殊”的设置者可以通过__set()方法进行概括。这涵盖了我获取和设置属性所需的全部内容。当然:并不总是有一个共同点,或者有这么几个属性不值得编写一种神奇方法的麻烦,然后仍然有旧的传统的setter / getter对。

编程语言就是:人类人工语言。因此,它们每个都有自己的语调或重音,语法和风格,因此我不会假装使用与Java或C#相同的“重音”来编写Ruby或Python代码,也不会写类似的JavaScript或PHP Perl或SQL ...以应有的方式使用它们。


1

一般而言,第一种方法在总体上更受欢迎,因为具有先验编程知识的人可以轻松地过渡到PHP并以面向对象的方式完成工作。第一种方法更通用。我的建议是坚持使用多种语言尝试过的和正确的方法。然后,当并且如果您使用其他语言,您将准备完成某些工作(而不是花时间重新发明轮子)。


0

在netbeans约定中有多种创建源代码的方法。很好 它使思考变得更轻松=== FALSE。只需使用传统字体,特别是如果您不确定应该封装哪个属性,不封装哪个属性。我知道,这是一个boi .... pla ...代码,但是对于调试工作和许多其他人来说,这是更好,更清晰的方法。不要花大量的时间来学习如何制作简单的吸气剂和吸气剂。如果使用魔术,则不能实现过多的设计模式,如demeter-rule等。在特定情况下,您可以使用magic_calls或用于小型,快速和清晰的解决方案。当然,您也可以通过这种方式为设计模式提供解决方案,但是为什么使您的生活更加困难。


0

验证+格式化/派生值

设置器可让您验证数据,而设置器可让您格式化或导出数据。对象使您可以将数据及其验证和格式代码封装到鼓励DRY的整齐的程序包中。

例如,考虑以下包含生日的简单类。

class BirthDate {

    private $birth_date;

    public function getBirthDate($format='Y-m-d') {
        //format $birth_date ...
        //$birth_date = ...
        return $birth_date;
    }

    public function setBirthDate($birth_date) {                   
        //if($birth_date is not valid) throw an exception ...          
        $this->birth_date = $birth_date;
    }

    public function getAge() {
        //calculate age ...
        return $age;
    }

    public function getDaysUntilBirthday() {
        //calculate days until birth days
        return $days;
    }
}

您将要验证所设置的值是

  • 有效日期
  • 不在将来

而且您不想在整个应用程序(或与此相关的多个应用程序)上进行此验证。相反,将成员变量设置为受保护或私有(以便使设置器成为唯一的访问点)并在设置器中进行验证比较容易,因为这样一来,无论对象的哪个部分,您都将知道对象包含有效的出生日期。应用对象的来源,如果要添加更多验证,则可以将其添加到单个位置。

你可能想补充一点,在同一个成员变量即操作多个格式化getAge()getDaysUntilBirthday()你可能要执行一个可配置的格式getBirthDate()取决于语言环境。因此,我宁愿经由一致干将访问值,而不是混合$date->getAge()使用$date->birth_date

扩展对象时,getter和setter也很有用。例如,假设您的应用程序需要在某些地方允许150岁以上的生日,但在其他地方则不允许。解决该问题而不重复任何代码的一种方法是扩展BirthDate对象并将附加的验证放入设置器中。

class LivingBirthDate extends BirthDate {

    public function setBirthDate($birth_date) {
        //if $birth_date is greater than 150 years throw an exception
        //else pass to parent's setter
        return parent::setBirthDate($birth_date);
    }
}

但是,很多时候您需要一起(而不是单独)验证属性。我认为允许“设置者”意味着您也没有捕获上下文。验证应在上下文的基础上进行。同样,您将被迫在拥有的每个方法上检查一些“ isValid”标志(如果为假,则执行验证)。话虽如此,setter确实提供了一种有用的类型提示方式,例如,当您拥有要成为Value Object(即Money)的属性时。
prograhammer

我不明白您被迫检查“ isValid”标志是什么意思?如果设置值的唯一方法是通过执行验证的设置器,则可以通过成功设置数据来知道数据是有效的。如果需要一起验证属性,则可以编写一个通用的验证方法,这些属性的设置者将调用这些方法。
FuzzyTree 2015年

假设您有一个setHired和的员工班级setHireDate。让某人设置聘用日期而不将员工也设置为聘用无效。但是您无法强制执行此操作。如果您在这些设置方法之一中实施它,则比强制执行“设置”的顺序要多,这需要从开发人员那里读取更多的代码才能知道。然后,当您执行类似的方法时$employee->promote($newPosition);,必须检查一个标志以查看是否已完成验证或假定尚未完成验证,然后再次进行验证(冗余)。
prograhammer

而是捕获交互。也许$employee->updateWorkStatus($hired, $hireDate);或者说更先进$employee->adminUpdate(\Employee\AdminUpdateDTO $dto);。现在,您可以在所需的上下文中进行验证,并确定是否需要任何额外的验证。
prograhammer

updateWorkStatus本质上是一个setter函数,它设置2而不是1的值,但是概念是相同的。这是这样做的一种方法,但是您也可以将上下文验证放入一个通用方法中,该方法仅在需要验证的所有属性都已设置时才运行,即setHiredDate和setHired都将调用验证的上下文部分,但只能运行如果isset hired和isset hiredDate均为true。
FuzzyTree 2015年

0

这篇文章不是专门关于__get__set而是__call除了方法调用之外,是相同的想法。通常,出于注释和帖子中概述的原因,我不会使用任何允许重载的魔术方法,但是我最近遇到了我使用的第三方API,该API使用了SERVICE和SUB-SERVICE这个示例:

http://3rdparty.api.com?service=APIService.doActionOne&apikey=12341234

重要的是,在这种情况下,此API的所有功能除了子操作外都相同doActionOne。这个想法是,开发人员(我自己和其他使用此类的人)可以通过名称来调用子服务,而不是像这样:

$myClass->doAction(array('service'=>'doActionOne','args'=>$args));

我可以改为:

 $myClass->doActionOne($args);

要硬编码这将是很多重复(此示例与代码非常相似):

public function doActionOne($array)
    {
        $this->args     =   $array;
        $name           =   __FUNCTION__;
        $this->response =   $this->executeCoreCall("APIService.{$name}");
    }

public function doActionTwo($array)
    {
        $this->args     =   $array;
        $name           =   __FUNCTION__;
        $this->response =   $this->executeCoreCall("APIService.{$name}");
    }

public function doActionThree($array)
    {
        $this->args     =   $array;
        $name           =   __FUNCTION__;
        $this->response =   $this->executeCoreCall("APIService.{$name}");
    }

protected function executeCoreCall($service)
    {
        $cURL = new \cURL();
        return $cURL->('http://3rdparty.api.com?service='.$service.'&apikey='.$this->api.'&'.http_build_query($this->args))
                    ->getResponse();
    }

但是,借助神奇的方法,__call()我能够使用动态方法访问所有服务:

public function __call($name, $arguments)
    {
        $this->args     =   $arguments;
        $this->response =   $this->executeCoreCall("APIService.{$name}");   
        return $this;
    }

动态要求返回数据的好处是,如果供应商添加了另一个子服务,则不必在类中添加其他方法或创建扩展类,等等。我不确定这是否对任何人,但我想我会显示一个例子,其中__set__get__call等可以是考虑的选择,因为其主要功能是数据的返回。


编辑:

巧合的是,发布后几天我看到了这一点,它准确地概述了我的情况。它不是我所指的API,但是方法的应用程序是相同的:

我是否正确使用api?


-2

更新:不要使用此答案,因为这是我在学习时发现的非常愚蠢的代码。只需使用简单的getter和setter,那就更好了。


我通常使用该变量名作为函数名,并向该函数添加可选参数,以便当调用方填充该可选参数时,然后将其设置为属性并返回$ this对象(链接),然后当该可选参数未被指定时调用者,我只是将属性返回给调用者。

我的例子:

class Model
{
     private $propOne;
     private $propTwo;

     public function propOne($propVal = '')
     {
          if ($propVal === '') {
              return $this->propOne;
          } else {
              $this->propOne = $propVal;
              return $this;
          }
     }

     public function propTwo($propVal = '')
     {
          if ($propVal === '') {
              return $this->propTwo;
          } else {
              $this->propTwo = $propVal;
              return $this;
          }
     }
}

现在的问题仍然存在:如何将属性设置为空字符串?以及如何检测将属性设置为空字符串实际上失败并作为吸气剂工作?试想一下HTML表单将空字段发送为字符串...并且不:使用其他值(例如NULL)作为默认值不能解决问题。
2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.