【问题标题】:How to solve the missing object properties in PHP?如何解决 PHP 中缺少的对象属性?
【发布时间】:2013-08-24 01:42:17
【问题描述】:

这有点哲学,但我想很多人都遇到过这个问题。目标是访问 PHP 中的各种(动态声明的)属性,并在未设置通知时摆脱通知。

为什么不__get
如果您可以声明自己的类,这是一个不错的选择,但不适用于stdClassSimpleXML 或类似的情况。扩展它们不是一个选项,因为您通常不直接实例化这些类,它们是作为 JSON/XML 解析的结果返回的。

示例:

$data = '{"name": "Pavel", "job": "programmer"}';
$object = json_decode($data);

我们有简单的stdClass 对象。问题很明显:

$b = $data->birthday;

该属性未定义,因此发出通知:

PHP Notice:  Undefined property: stdClass::$birthday

如果您认为您是通过解析某些 JSON 获得该对象,这种情况可能会经常发生。天真的解决方案很明显:

$b = isset($data->birthday) ? $data->birthday : null;

但是,将每个访问器都包装到其中时,很快就会感到疲倦。特别是在链接对象时,例如$data->people[0]->birthday->year。检查是否设置了people。检查是否设置了第一个元素。检查是否设置了birthday。检查是否设置了year。感觉有点过头了……

问题: 最后,我的问题来了。
解决这个问题的最佳方法是什么?使通知静音似乎不是最好的主意。并且检查每个属性是困难的。我见过一些解决方案,例如Symfony property access,但我认为它仍然是太多样板文件。有没有更简单的方法?第三方库、PHP 设置、C 扩展,我不在乎它的工作原理……还有哪些可能的陷阱?

【问题讨论】:

  • “但是,当将每个访问器都包装到其中时,人们很快就会感到疲倦。尤其是在链接对象时,例如 $data->people[0]->birthday->year。” 我不明白。即使$data 为空,您也可以安全地执行isset($data->people[0]->birthday->year),而不会产生任何副作用。
  • 这是一个很好的观点,我没有意识到这一点。谢谢! (而且,还有更短的$y = isset($x) ? $x : null;
  • 没有。如果您查看其他语言,PHP 实际上具有最短的形式。在 C 每个示例中,您必须手动检查每个子元素是否为 null。在 Python 中,您必须用 try..except 块包围它。如果你真的想要更短,你总是可以写一个小的包装函数。
  • 好吧,换行试试..except 会有所帮助。问题不是编写单个 isset() 而是一遍又一遍地重复它,在我/你的代码中重复 1000 倍。
  • 你有没有考虑过Reflection class?,我的意思是你可以定义一个类作为模板,然后使用“反射”来检查你的对象。

标签: php properties isset boilerplate


【解决方案1】:

如果我理解正确,您想处理您无法控制的第 3 方对象,但您的逻辑需要对象上可能不存在的某些属性。这意味着,您接受的数据对于您的逻辑是无效的(或应该被宣布为无效的)。然后检查有效性的负担就交给你的validator了。我希望您已经掌握了处理 3rd 方数据的最佳实践。 :)

您可以使用自己的验证器或框架中的一个。一种常见的方法是编写一组规则,您的数据需要遵守这些规则才能有效。

现在在您的验证器中,只要不遵守规则,您就会throw 一个异常描述错误并附加包含您要使用的信息的异常属性。稍后,当您在逻辑中的某个位置调用验证器时,将其放在 try {...} 块中,然后 catch() 您的异常并处理它们,也就是说,编写为这些异常保留的特殊逻辑。作为一般实践,如果您的逻辑在一个块中变得太大,您希望将其“外包”为功能。 引用 Robert Martin 的伟大著作《Clean Code》,强烈推荐给任何开发者:

函数的第一条规则是它们应该很小。第二个是它们应该比那个小。

我理解您在处理永恒的issets 时的挫败感,并在此处看到问题的原因,每次您需要编写一个处理程序来处理不存在的这个或那个属性的技术问题。该技术问题在您的抽象层次结构中处于非常低的级别,并且为了正确处理它,您必须一直沿抽象链向上到达对您的逻辑有意义的更高步骤。在不同的抽象层次之间跳跃总是很困难,尤其是相距甚远的地方。这也是使您的代码难以维护的原因,建议避免使用。 理想情况下,您的整个架构设计为一棵树,其中位于其节点上的控制器只知道从它们向下延伸的边缘。

例如,回到你的例子,问题是 -

  • Q - 缺少$data->birthday 的情况对您的应用意味着什么?

含义将取决于当前抛出异常的函数想要实现什么。那是处理您的异常的一个方便的地方。

希望对你有帮助:)

【讨论】:

    【解决方案2】:

    一种解决方案(我不知道它是否是更好的解决方案,但一种可能的解决方案)是创建一个这样的函数:

    function from_obj(&$type,$default = "") {
        return isset($type)? $type : $default;
    }
    

    然后

    $data   = '{"name": "Pavel", "job": "programmer"}';
    $object = json_decode($data);
    
    $name   = from_obj( $object->name      , "unknown");
    $job    = from_obj( $object->job       , "unknown");
    $skill  = from_obj( $object->skills[0] , "unknown");
    $skills = from_obj( $object->skills    , Array());
    
    echo "Your name is $name. You are a $job and your main skill is $skill";
    
    if(count($skills) > 0 ) {
        echo "\n\nYour skills: " . implode(",",$skills);
    }
    

    我认为这很方便,因为您在脚本的顶部有您想要的和应该是什么(数组、字符串等)

    编辑:

    另一种解决方案。您可以创建一个扩展 ArrayObject 的 Bridge 类:

    class ObjectBridge extends ArrayObject{
        private $obj;
        public function __construct(&$obj) {
            $this->obj = $obj;
        }
    
        public function __get($a) {
            if(isset($this->obj->$a)) {
                return $this->obj->$a;
            }else {
                // return an empty object in order to prevent errors with chain call
                $tmp = new stdClass();
                return new ObjectBridge($tmp);
            }
        }
        public function __set($key,$value) {
            $this->obj->$key = $value;
        }
        public function __call($method,$args) {
            call_user_func_array(Array($this->obj,$method),$args);
        }
        public function __toString() {
            return "";
        }
    }
    
    $data   = '{"name": "Pavel", "job": "programmer"}';
    $object = json_decode($data);
    
    $bridge = new ObjectBridge($object);
    
    echo "My name is {$bridge->name}, I have " . count($bridge->skills). " skills and {$bridge->donald->duck->is->paperinik}<br/>";  
    // output: My name is Pavel, I have 0 skills and 
    // (no notice, no warning)
    
    // we can set a property
    $bridge->skills = Array('php','javascript');
    
    // output: My name is Pavel, my main skill is php
    echo "My name is {$bridge->name}, my main skill is {$bridge->skills[0]}<br/>";
    
    
    // available also on original object
    echo $object->skills[0]; // output: php
    

    我个人更喜欢第一种解决方案。更清晰,更安全。

    【讨论】:

      【解决方案3】:

      具有可选字段的数据格式很难处理。如果您有第三方访问或提供数据,它们尤其会出现问题,因为很少有足够的文档来全面涵盖导致字段出现或消失的所有原因。当然,排列往往更难测试,因为编码人员不会本能地意识到这些字段可能存在。

      这是一个很长的说法,如果您可以避免在数据中包含可选字段,那么在 PHP 中处理丢失对象属性的最佳方法是不要丢失任何对象属性...

      如果您正在处理的数据不取决于您,那么我会考虑在所有字段上强制使用默认值,可能通过辅助函数或原型模式的某种疯狂变体。您可以构建一个数据模板,其中包含数据所有字段的默认值,并将其与真实数据合并。

      但是,如果你这样做,你会失败吗,除非?(这是另一种需要牢记的编程哲学。)我想可以证明提供安全的默认参数可以满足数据验证的要求对于任何缺失的字段。但特别是在处理第三方数据时,您应该对使用默认值涂抹的任何字段保持高度的偏执。将其设置为 null 太容易了——在此过程中——无法理解 为什么它一开始就丢失了。

      你还应该问你想要达到什么目标?明晰?安全?稳定?最少的代码重复?这些都是有效的目标。累了吗?少这样。这表明缺乏纪律,而优秀的程序员总是受到纪律处分。当然,我会接受人们不太可能做某事,如果他们认为这是一件苦差事。

      我的意思是,您的问题的答案可能会有所不同,具体取决于为什么。零努力解决方案可能不可用,所以如果您只是将一项琐碎的编程任务换成另一项,您能解决什么问题吗?

      如果您正在寻找一种系统化的解决方案,以保证数据始终采用您指定的格式,从而减少处理该数据的代码中的逻辑测试数量,那么我上面的建议可能是帮助。但它不会没有成本和努力。

      【讨论】:

      • 谢谢。对于默认值和模板 +1。我也很乐意听到其他方法。
      【解决方案4】:

      已经给出了最好的答案,但这里是一个懒惰的答案:

      $data = '{"name": "Pavel", "job": "programmer"}';
      $object = json_decode($data);
      if(
          //...check mandatory properties: !isset($object->...)&&
          ){
          //error
      }
      error_reporting(E_ALL^E_NOTICE);//Yes you're right, not the best idea...
      $b = $object->birthday?:'0000-00-00';//thanks Elvis (php>5.3)
      //Notice that if your default value is "null", you can just do $b = $object->birthday;
      //assign other vars here
      error_reporting(E_ALL);
      //Your code
      

      【讨论】:

        【解决方案5】:

        使用代理对象 - 它只会添加一个小类和每个对象实例化一行来使用它。

        class ProxyObj {
           protected $obj;
           public function __construct( $obj ) {
              $this->_obj = $obj;
           }
           public function __get($key) {
             if (isset($this->_obj->$key)) {
                return $this->_obj->$key;
             }
             return null;
           }
           public function __set($key, $value) {
              $this->_obj->$key = $value;
           }
        }
        
        $proxy = new ProxyObj(json_decode($data));
        $b = $proxy->birthday;
        

        【讨论】:

        • 这里的问题是嵌套对象。 __get() 应该检查($this-&gt;_obj-&gt;$key instanceof StdClass) 是否为真,如果为真则返回return new self($this-&gt;_obj-&gt;$key);
        • 另外,如果user 不存在,返回null 将导致$obj-&gt;user-&gt;birthday 失败。但是,您不能在其位置返回 ProxyObj,因为这样 $obj-&gt;user-&gt;birthdayBlah 将返回一个对象。 __toString 可以在这里提供帮助,但不能完全解决问题。
        【解决方案6】:

        您可以将 JSON 对象解码为数组:

        $data = '{"name": "Pavel", "job": "programmer"}';
        $jsonarray = json_decode($data, true);
        $b = $jsonarray["birthday"];    // NULL
        

        【讨论】:

        • 旁注,我的观点是,在处理外部数据时这是一个很好的简单答案,但在处理内部数据时可能会很麻烦——尤其是对于嵌套对象。例如:假设您有一个 stdObject,它具有另一个 stdObject 作为属性。您可以安全地解码两者。但是,如果该属性是一个类的实例,则您无法在没有更改该实例的副作用的情况下进行解码。您可能会丢失数组中的数据或引发错误。但是,如果您对数组进行外部请求,则解码是安全的,因为它最初只是序列化的数据。
        • 作为一个真实的例子,这在 WooCommerce 中可能会发生。访问购物车将返回一个 cart_items 数组。每个 cart_item 本身就是一个数组,其中一个字段是 WC_Product_Simple Object 类的一个实例。因此,更改购物车或购物车项目的格式也会更改 WC_Product_Simple 对象的格式,这可能会导致问题......
        【解决方案7】:

        在 PHP 版本 8 中

        您可以使用Nullsafe 运算符,如下所示:

        $res = $data?->people[0]?->birthday?->year;
        

        【讨论】:

          【解决方案8】:
          function check($temp=null) {
           if(isset($temp))
           return $temp;
          
          else
          return null;
          }
          
          $b = check($data->birthday);
          

          【讨论】:

          • 小心返回 null - 它会产生副作用,建议 Bob 大叔避免。
          • 这不起作用,因为错误会在函数调用之前引发。
          【解决方案9】:

          我遇到了这个问题,主要是从一个 nosql 支持的 api 获取 json 数据,该 api 在设计上具有不一致的结构,例如,如果用户有一个地址,你将得到 $user->address 否则地址键就是'那里。我写了这个类,而不是在我的模板中放入大量的 issets...

          class GracefulData
          {
              private $_path;
              public function __construct($d=null,$p='')
              {
                  $this->_path=$p;
                  if($d){
                      foreach(get_object_vars($d) as $property => $value) {
                          if(is_object($d->$property)){
                              $this->$property = new GracefulData($d->$property,$this->_path . '->' . $property);
                          }else{
                              $this->$property = $value;
                          }
                      }
                  }
              }
              public function __get($property) {
                  return new GracefulData(null,$this->_path . '->' . $property);
              }
              public function __toString() {
                  Log::info('GracefulData: Invalid property accessed' . $this->_path);
                  return '';
              }
          }
          

          然后像这样实例化它

          $user = new GracefulData($response->body);
          

          它将优雅地处理对现有和非现有属性的嵌套调用。但它无法处理的是,如果您访问现有 non-object 属性的子项,例如

          $user->firstName->某事

          【讨论】:

            【解决方案10】:

            这里有很多好的答案,我认为@Luca 的答案是最好的答案之一 - 我扩展了他的一点,以便我可以传入一个数组或对象并让它创建一个易于使用的对象。这是我的:

            <?php
            
            namespace App\Libraries;
            
            use ArrayObject;
            use stdClass;
            
            class SoftObject extends ArrayObject{
                private $obj;
            
                public function __construct($data) {
                    if(is_object($data)){
                        $this->obj = $data;
                    }elseif(is_array($data)){
                        // turn it into a multidimensional object
                        $this->obj = json_decode(json_encode($data), false);
                    }
                }
            
                public function __get($a) {
                    if(isset($this->obj->$a)) {
                        return $this->obj->$a;
                    }else {
                        // return an empty object in order to prevent errors with chain call
                        $tmp = new stdClass();
                        return new SoftObject($tmp);
                    }
                }
            
                public function __set($key, $value) {
                    $this->obj->$key = $value;
                }
            
                public function __call($method, $args) {
                    call_user_func_array(Array($this->obj,$method),$args);
                }
            
                public function __toString() {
                    return "";
                }
            }
            
            // attributions: https://stackoverflow.com/questions/18361594/how-to-solve-the-missing-object-properties-in-php | Luca Rainone
            

            【讨论】:

              【解决方案11】:

              我已经为多级链接编写了一个辅助函数,例如,假设您想做类似$obj1-&gt;obj2-&gt;obj3-&gt;obj4 的操作,如果其中一个层未定义或为空,我的助手将返回空字符串

              class MyUtils
              {
                  // for $obj1->obj2->obj3: MyUtils::nested($obj1, 'obj2', 'obj3')
                  // returns '' if some of tiers is null
                  public static function nested($obj1, ...$tiers)
                  {
                      if (!isset($obj1)) return '';
                      $a = $obj1;
                      for($i = 0; $i < count($tiers); $i++){
                          if (isset($a->{$tiers[$i]})) {
                              $a = $a->{$tiers[$i]};
                          } else {
                              return '';
                          }
                      }
                      return $a;
                  }
              }
              

              【讨论】:

                猜你喜欢
                • 2021-10-13
                • 1970-01-01
                • 1970-01-01
                • 2018-05-22
                • 2023-03-14
                • 2016-01-30
                • 2010-10-29
                • 1970-01-01
                • 2012-09-08
                相关资源
                最近更新 更多