PHP 中使用 ArrayObject 的可执行数组结构?

Enforceable array structure in PHP using ArrayObject?

我有一组数据(~30 个属性,都有自己的值数组),我想传递给 PHP 中的各种 classes,我也想强制执行数据的数组结构。多个 classes 将期望此结构是一致的。

由于这些事实,我不能真正依赖标准数组,所以我决定传递一个对象。我查看了 ArrayObject,虽然它允许我 set/get 就好像 class 是一个数组,但我没有看到任何关于强制结构的信息。

是否有现有标准 class 可以处理类似数组结构的强制执行,同时仍被视为数组,例如,基本上是 ArrayObject + 强制执行?

数组结构示例:

$item_type_props = array(
    'phone'     => array('speed' => 1000, 'self_label' => false, 'support_poe' => true, 'bluetooth' => false),
    'pbx'       => array('acd_support' => true, 'max_conn' => 300, max_phones => 600),
    'adapter'   => array('fxo' => 4, 'fxs' => 0, 't1_e1_pri' => 0),
    etc...
);

我知道数组中的每个 属性 都可能是它自己的 class 并通过构造函数和 set/get 强制执行它自己的字段,但后来我突然有了 ~30 classes 只不过是一堆属性,对于仅保存数据来说似乎有些过分。


或者,如果我只是从错误的心态接近这个并且遗漏了一些非常非常明显的东西,那么请指出。我觉得我是,但我的大脑可能正在度假。

虽然您可以自己动手,但我鼓励您使用现有的验证实现。例如,Symfony\Validator 允许您定义嵌套结构和每个级别的要求:

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;

$validator = Validation::createValidator();

$constraint = new Assert\Collection(array(
    // the keys correspond to the keys in the input array
    'name' => new Assert\Collection(array(
      'first_name' => new Assert\Length(array('min' => 101)),
      'last_name' => new Assert\Length(array('min' => 1)),
    )),
    'email' => new Assert\Email(),
    'simple' => new Assert\Length(array('min' => 102)),
    'gender' => new Assert\Choice(array(3, 4)),
    'file' => new Assert\File(),
    'password' => new Assert\Length(array('min' => 60)),
));

$violations = $validator->validate($input, $constraint);

这让您可以将 如何 的详细信息推送到另一个(已经测试过的)级别进行验证,同时让您的代码专注于 为什么 它需要这些数据。对于 Symfony 案例,您可以使用 array 作为存储机制,并使用防火墙从已验证数据中取消验证的设计。


我们可以这样做的一种方式是符号化。假设我们已经实现了一个方法,可能使用 Symfony 的验证器,return 一个经过验证的数组。我们可以使用匈牙利符号来表示我们的结构已经通过验证并且是 "safe":

<?php
$vInput = validate($_GET); // Hungarian notation: any variable beginning with "v" is "validated" and safe to use

function foobar(array $vInput) { ... }

虽然这很高效,但从长远来看不利于维护。因此,您可能会考虑一个允许您利用类型系统的对象包装器:

use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Constraints as Assert;

class ValidatedArray extends \ArrayObject {
    public function construct($input = [], $flags = 0, $iterator_class = 'ArrayIterator') {
        $violations = Validation::createValidator()->validate($array, $this->constraints());
        // throw exception if any violations
        parent::__construct($input, $flags, $iterator_class);
    }

    public function __offsetSet($index, $value) {
        $constraints = $this->constraints()[$index]; // specific constraints to this index
        $violations = Validation::createValidator()->validate($array, $constraints);
        // throw exception on violations
        parent::__offsetSet($index, $value);
    }

    public function constraints() {
        return new Assert\Collection(...);
    }
}

$input = new ValidatedArray($_REQUEST); // first time validation
$input['foo'] = 'bar'; // also subject to validation

您可能希望使此实现成为 abstract 基础 class,具体后代实现 constraints 方法以提供对数组对象本身的特定限制。这提供了一种获取专用数据传输对象的灵活方法。

一般来说,我认为 - 除非您将数据传递到另一个上下文,例如javascript - PHP 应用程序应该在 PHP classes 中很好地组织。这是实施结构的最简单方法。你是对的,这可能会导致带有一堆 getter 和 setter 的非常简单的 DTO,但它肯定会胜过检查数组结构。在您的情况下,数组中似乎也存在关系,否则将它们组合成一个数组根本没有意义。

使用 PHP7 您可以清楚地定义方法签名并强制执行类型,例如

public function setSomething(string $myValue)
{
    $this->something = $myValue;
}

与 return 类型相同:

public function myActionMethod(array $options): ActionRespsonse
{
    // Do something
}

如果您有更复杂的数据类型,我建议您使用 Value Objects。这些只不过是代表更复杂值的简单 PHP classes。例如,一个 phone 数字:

public function setPhoneNumber(PhoneNumber $phoneNumber)
{
    $this->phoneNumber = $phoneNumber;
}

这里的 PhoneNumber 是一个值 Object,它实际上是一个很小的 ​​class,它本身强制执行它的用法:

class PhoneNumber {
    private $phoneNumber;

    public __construct(string $phoneNumber) {
        if (strlen($phoneNumber) != 10) {
            throw new \Exception('Not a valid phone number');
        }

        $this->phoneNumber = $phoneNumber;
     }
}

这也是验证可以与@bishop 的答案联系起来的地方,因为您可以使用现有的验证器来帮助您。您可以在此处找到 Phone 数字的值 Object 的示例(只是用谷歌搜索):Example Phone Number Value Object

我确实觉得您可能出于其他原因将 PHP 数据转换为数组?就像与数据库交互,或将其传递到不同的上下文,例如 Javascript?

在这种情况下,一旦您明确定义了 DTO 和 VO,您就可以考虑将它们序列化,例如to/from JSON。您可以为此使用 Symfony 库,如下所述:Symfony Serializer

如果你真的想要数组,你也可以考虑使用 Marco Pivetta (ocramius) 的库对它们进行水化 to/from 实体,他几乎是水化方面的权威,一种在 Doctrine 中广泛使用的方法:Ocramius Hydrator

很多选择,嗯!?

不过,老实说,恕我直言,通常需要有一个很好的论据来传递这些复杂的数组,因为数组提供的支持功能很少。此外,您的代码很可能很快就会变得难以阅读和维护,因为在对数组进行某种修改或使用其数据元素的每一点,您都需要实施各种检查或 运行 @bishop 描述的验证。我不允许在我们的应用程序代码库中存在类似的东西...

因此,结论是:如果您有一组严格定义的 PHP Object 具有完善的构造函数、属性、getter、setter、构造函数、关系、操作方法以及除此之外的某种类型序列化机制,你的状态很好。其他开发人员也将 'forced' 使用 objects 并提供接口(方法和 VO),从而保证一定程度的可维护性和质量。

顺便说一下,您可以考虑重申 Martin Fowler 对这些事情的看法:Martin Fowler: DTO Martin Fowler: VO Martin Fowler: DTO2