其实这个问题已经有了极为广泛的讨论,也有了相对明确的答案——尽可能使用对象,但是为了以后和人争论有充足的草稿,我要将这件事再次论述一次

首先要明确的是,下文讨论的数组,都是关联数组(map)而非索引数组(list)

PHP 的数组是一个极其优秀的设计,一个 value 可以存储任意类型的东西,并且提供了大量的 array 函数,给予了开发者非常宽松的开发条件,但是与之而来的问题就是,代码变得难以阅读和理解,尤其在调用链路过长的时候,更是让人挠头,还是从具体的代码入手

考虑这个函数

<?php
function show(array $data)
{
    foreach ($data as $key => $value) {
        echo $key . ': ' . $value;
    }
    return;
}

function run(array $data)
{
    show($data);
}

function start(array $data)
{
    run($data);
}

很明显,我们无法得知 data 内部究竟有什么,虽然在开发时非常快速(可能这也是 PHP 效率高的一部分原因),但是如果一个月后来回看的话,这是非常让人费解的代码,如果没有注释的话,我们可能需要一步步 var_dump 才能得知 data 的信息,这显然是令人不爽的

如果说难以阅读是一个重要的问题,那么下一个问题就更加致命,—— 我们无法保证一直书写正确的 key name,这其实是需要 IDE 的帮助的,但是显然的,IDE 并不能分析出这个数组内部有什么东西,毕竟连开发者都需要打印来看看

那么如果用对象的话,可以怎么处理呢

class MyData
{
    protected $name;

    protected $address;

    public function getName()
    {
        return $this->name;
    }

    public function setName(string $name)
    {
        $this->name = $name;
    }

    public function getAddress()
    {
        return $this->address;
    }

    public function setAddress(string $address)
    {
        $this->address = $address;
    }

    public function show()
    {
        echo 'name' . $this->name;
        echo 'address' . $this->address;
    }
}

function show(MyData $data)
{
    $data->show();
}

是的,代码量增加了很多,但是我们可以明确得知 $data 的内部数据,并且提供了安全的类型检查,在 IDE 的帮助下可以彻底避免错误的 key name

事实上,仅仅考虑这种数组传参的情况,也就是 DTO (data to object),不同框架已经给出了各自的解决方案,如 Laravel 和 Hyperf 就提供了 Fluent 类

在结尾我也提供一个适用于 Laravel 和 Hyperf 的 DTO 基类

<?php
class Format
{
    public function __construct(array $data = [])
    {
        foreach ($data as $key => $value) {
            $method = Str::camel('set_' . $key);
            $this->{$method}($value);
        }
    }

    public function __call($name, $args)
    {
        if (substr($name, 0, 3) == 'get') {
            $field = Str::snake(substr($name, 3));

            return $this->{$field} ?? null;
        }

        if (substr($name, 0, 3) == 'set') {
            $field = Str::snake(substr($name, 3));
            if (property_exists($this, $field)) {
                $this->{$field} = $args[0] ?? null;
            }

            return $this;
        }

        throw new \Exception('call to undefined method: ' . $name);
    }

    public function toArray()
    {
        $formatArray = [];
        $allProtectedFields = $this->getAllProtected();
        foreach ($allProtectedFields as $k => $field) {
            $formatArray[$field] = $this->{$field};
        }
        return $formatArray;
    }

    public function toArrayNotNull()
    {
        $formatArray = [];
        $allProtectedFields = $this->getAllProtected();
        foreach ($allProtectedFields as $k => $field) {
            $v = $this->{$field};
            if (! is_null($v)) {
                $formatArray[$field] = $v;
            }
        }
        return $formatArray;
    }

    public function getAllProtected()
    {
        $ref = new \ReflectionClass($this);
        $propers = $ref->getProperties();
        $res = [];
        foreach ($propers as $key => $value) {
            /* @var $value \ReflectionProperty */
            if ($value->getModifiers() == $value::IS_PROTECTED) {
                $res[] = $value->name;
            }
        }
        return $res;
    }
}

举个例子

/**
 * @method getName()
 * @method getType()
 * @method getLength()
 * @method getDefault()
 * @method getComment()
 * @method getNotNull()
 *
 * @method setName($value)
 * @method setType($value)
 * @method setLength($value)
 * @method setDefault($value)
 * @method setComment($value)
 * @method setNotNull($value)
 */
class AnalyseTableColumnFormat extends Format
{
    protected $name;

    protected $type;

    protected $length;

    protected $default;

    protected $comment;

    protected $not_null;
}

是的,这个基类只需要定义 protected 的成员属性即可,Format 类已经实现了 getter 和 setter(当然可以自己重写)

但是为了对 IDE 友好,我们还是要加上类注释,对此我也提供了一个自动生成注释的脚本,但是代码写的比较简陋且通用性并不强,就不放出来了…