如何解决PHP中缺少的对象属性


How to solve the missing object properties in PHP?

这有点哲学,但我认为很多人都遇到了这个问题。目标是访问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房地产访问,但我认为这仍然是一个样板。有什么更简单的方法吗?无论是第三方库、PHP设置还是C扩展,我都不在乎它的工作原理。。。可能的陷阱是什么?

如果我理解正确,你想处理第三方对象,在那里你没有控制权,但你的逻辑需要某些可能不存在于对象上的属性。这意味着,您接受的数据对于您的逻辑来说是无效的(或者应该声明为无效)。然后,检查有效性的负担将进入验证器。我希望您已经有了以下处理第三方数据的最佳实践。:)

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

现在,在验证器中,每当不遵守规则时,都会throw一个描述错误的Exception,并附加携带要使用的信息的Exception属性。稍后,当您在逻辑中的某个地方调用验证器时,您将其放置在try {...}块中,然后catch()您的异常并处理它们,也就是说,编写为这些异常保留的特殊逻辑。一般来说,如果你的逻辑在一个块中变得太大,你想把它作为功能"外包"。引用罗伯特·马丁的伟大著作《干净的代码》,强烈推荐给任何开发者:

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

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

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

  • 问-$data->birthday丢失的情况对您的应用程序有什么意义

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

希望它有帮助:)

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

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);
}

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

编辑:

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

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

就我个人而言,我更喜欢第一种解决方案。它更清晰、更安全。

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

这是一个很长的说法,如果你可以避免在你的数据中有可选的字段,处理PHP中丢失的对象属性的最佳方法是没有任何丢失的对象特性。。。

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

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

你还应该问,你在努力实现什么?清晰安全稳定性最小化代码重复?这些都是有效的目标。累了吗?不那么重要。这表明缺乏disciprine,一个好的程序员总是被讨论的。当然,我会接受这样一个事实,即人们不太可能做某事,如果他们认为这是一件家务事的话。

我的观点是,你的问题的答案可能会因为什么而有所不同。零工作量解决方案可能不可用,所以如果您只是将一个卑微的编程任务交换到另一个,您会解决什么问题吗?

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

在PHP版本8

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

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

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

$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

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

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;

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

$data = '{"name": "Pavel", "job": "programmer"}';
$jsonarray = json_decode($data, true);
$b = $jsonarray["birthday"];    // NULL
function check($temp=null) {
 if(isset($temp))
 return $temp;
else
return null;
}
$b = check($data->birthday);

我遇到了这个问题,主要是从一个支持nosql的api中获取json数据,该api在设计上具有不一致的结构,例如,如果用户有地址,你将获得$user->address,否则地址键就不存在。我没有在模板中放很多isset,而是写了这门课。。。

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->something

这里有很多好的答案,我认为@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

我已经为多级链接编写了一个助手函数,例如,假设你想做一些类似$obj1->obj2->obj3->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;
    }
}