13

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

为什么不__get呢?
如果您可以声明自己的类,这是一个不错的选择,但在或类似的情况下stdClass则不行。SimpleXML扩展它们不是一个选项,因为您通常不直接实例化这些类,它们是作为 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 属性访问,但我认为它仍然是太多样板文件。有没有更简单的方法?第三方库、PHP 设置、C 扩展,我不在乎它的工作原理……还有哪些可能的陷阱?

4

11 回答 11

6

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

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

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

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

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

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

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

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

希望能帮助到你 :)

于 2013-08-31T07:41:17.747 回答
4

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

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

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

于 2013-09-04T12:57:33.693 回答
3

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

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

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

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

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

我的观点是,您的问题的答案可能会因被问到的原因而异。零努力解决方案可能不可用,因此,如果您只是将一项琐碎的编程任务换成另一项,您是否解决了任何问题?

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

于 2013-08-21T17:10:42.370 回答
1

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

$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
于 2013-09-03T15:33:04.127 回答
1

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

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;
于 2013-09-04T15:07:33.150 回答
1

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

$data = '{"name": "Pavel", "job": "programmer"}';
$jsonarray = json_decode($data, true);
$b = $jsonarray["birthday"];    // NULL
于 2017-10-20T01:04:15.540 回答
1

在 PHP 版本 8 中

您可以Nullsafe按如下方式使用运算符:

$res = $data?->people[0]?->birthday?->year;
于 2021-07-03T08:49:01.240 回答
0
function check($temp=null) {
 if(isset($temp))
 return $temp;

else
return null;
}

$b = check($data->birthday);
于 2013-09-04T13:05:50.290 回答
0

我遇到了这个问题,主要是从 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);

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

$user->firstName->某事

于 2016-09-29T13:06:44.927 回答
0

这里有很多好的答案,我认为@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
于 2020-01-14T17:03:50.643 回答
0

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

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;
    }
}
于 2020-12-19T07:26:36.573 回答