首页 > 解决方案 > 如何递归地从多维表单数组转换为相关的实体对象

问题描述

我正在尝试将多维 HTML 表单数组转换为具有一对多关系和嵌套一对多关系的相关实体(数据库)对象类。考虑以下输入示例(人类可读):

order[id]: 1
order[note]: test note
order[ordertime]: 13. Dez. 2018 09:01
order[position][0][id]: 1
order[position][0][ordernumber]: ADSF-11
order[position][0][price]: 45.99
order[position][0][supplier][id]: 1
order[position][0][supplier][name]: test supplier 1
order[position][1][id]: 2
order[position][1][ordernumber]: ADSF-12
order[position][1][price]: 50.99
order[position][1][supplier][id]: 2
order[position][1][supplier][name]: test supplier 2
order[customer][firstname]: Human
order[customer][surname]: Being
order[customer][billingAddress][id]: 1
order[customer][billingAddress][firstname]: Human 2
order[customer][billingAddress][surname]: Being 2
order[customer][billingAddress][street]: test street 1
order[customer][billingAddress][zip]: 99999
order[customer][billingAddress][city]: test city
order[customer][shippingAddress][id]: 2
order[customer][shippingAddress][firstname]: Human 3
order[customer][shippingAddress][surname]: Being 3
order[customer][shippingAddress][street]: test street 100
order[customer][shippingAddress][zip]: 88888
order[customer][shippingAddress][city]: test city 2

我们得到了一个带有空主体的抽象类AbstractEntity,每个实体都对其进行扩展,并且实体具有简单类型的公共成员变量。对于数组,它的访问是私有的,有一些setter方法以及addXX在数组末尾添加一个条目的方法(这就是为什么需要反射以及我们有$method1and的原因$method2)。此外,它还将日期和时间从 inernationalized 解析stringDateTime.

我想像 Doctrine 这样的 ORM 框架样式访问它们,如下所示:

$order->getPosition()[0]->getBillingAddress()->firstname

这是我做主要工作的工人阶级:

<?php
namespace MyApp\Ajax;

use MyApp\Entity\AbstractEntity;
use MyApp\Entity\Repository;

class AjaxRequest
{
    private $inputType;
    private $data;
    private $formatter;
    private $objMapping;
    private $repo;

    public function __construct()
    {
        $this->inputType = strtolower($_SERVER['REQUEST_METHOD']) === 'post' ? \INPUT_POST : \INPUT_GET;
        $this->formatter = new \IntlDateFormatter(
            'de_DE',
            \IntlDateFormatter::LONG,
            \IntlDateFormatter::SHORT,
            null,
            \IntlDateFormatter::GREGORIAN,
            'd. MMM Y HH:mm'
        );
        $this->objMapping = array(
            'order' => "MyApp\\Entity\\Order",
            'position' => "MyApp\\Entity\\Article",
            'supplier' => "MyApp\\Entity\\Supplier",
            'customer' => "MyApp\\Entity\\User",
            'billingAddress' => "MyApp\\Entity\\UserAddress",
            'shippingAddress' => "MyApp\\Entity\\UserAddress"
        );
        $this->repo = new Repository();
    }

    public function save()
    {
        $obj = $this->convertRequestToObj('order');
        $this->data['success'] = $this->repo->save($obj);
        $this->data['data'] = $obj;

        $this->jsonResponse();
    }

    private function jsonResponse()
    {
        header('Content-type: application/json');
        echo json_encode(
            array(
                'success' => $this->data['success'],
                'data' => $this->convertToPublicObjects($this->data['data'])
            )
        );
    }

    private function convertToPublicObjects($object)
    {
        $names = array();
        if (is_object($object) && !$object instanceof \DateTimeInterface) {
            $reflection = new \ReflectionClass($object);
            $columns = $reflection->getProperties();
            $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

            foreach ($columns as $column) {
                $colName = $column->getName();
                $method1 = 'get' . ucfirst($colName);
                $method2 = 'is' . ucfirst($colName);

                try {
                    if ($column->isPublic()) {
                        $names[$colName] = $column->getValue($object);
                    } else {
                        if ($reflection->hasMethod($method1) && $this->checkPublicMethods($methods, $method1)) {
                            $names[$colName] = $object->{$method1}();
                        } else {
                            if ($reflection->hasMethod($method2) && $this->checkPublicMethods($methods, $method2)) {
                                $names[$colName] = $object->{$method2}();
                            }
                        }
                    }
                } catch (\ReflectionException $ex) {
                    $names[$colName] = null;
                } catch (\TypeError $exc) {
                    $names[$colName] = null;
                }

                if (array_key_exists($colName, $names) && is_object($names[$colName])) {
                    if ($names[$colName] instanceof \DateTimeInterface) {
                        $names[$colName] = $this->formatter->format($names[$colName]);
                    } else {
                        $names[$colName] = $this->convertToPublicObjects($names[$colName]);
                    }
                } elseif (array_key_exists($colName, $names) && is_array($names[$colName])) {
                    array_walk_recursive($names[$colName], array($this, 'walkReturnArray'));
                }
            }
        }

        return $names;
    }

    private function walkReturnArray(&$item, $key)
    {
        if (is_object($item)) {
            $item = $this->convertToPublicObjects($item);
        }
    }

    /**
     * @param \ReflectionMethod[] $methods
     * @param string $method
     *
     * @return bool
     */
    private function checkPublicMethods(array $methods, string $method)
    {
        $found = false;
        foreach ($methods as $meth) {
            if ($meth->getName() === $method) {
                $found = true;
                break;
            }
        }

        return $found;
    }

    /**
     * Converts ORM like objects from the request from arrays to objects.
     *
     * @param string $key
     *
     * @return AbstractEntity
     */
    private function convertRequestToObj(string $key)
    {
        $ar = filter_input($this->inputType, $key, \FILTER_DEFAULT, \FILTER_REQUIRE_ARRAY);
        $baseObj = new $this->objMapping[$key]();
        $this->mapArrayToObj($ar, $baseObj);

        return $baseObj;
    }

    private function mapArrayToObj(array $ar, AbstractEntity $baseObj)
    {
        foreach ($ar as $column => $value) {
            $reflection = new \ReflectionClass($baseObj);
            $method1 = 'add' . ucfirst($column);
            $method2 = 'set' . ucfirst($column);
            $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

            if (is_array($value)) {
                $newObj = new $this->objMapping[$column]();
                $this->addObjectTo($methods, $method1, $method2, $baseObj, $newObj);
                $reflection = new \ReflectionClass($newObj);
                $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
                foreach ($value as $subCol => $subVal) {
                    $method2 = 'set' . ucfirst($subCol);
                    if (is_array($subVal)) {
                        if (is_numeric($subCol)) {
                            $this->mapArrayToObj($subVal, $newObj);
                        }
                    } else {
                        $this->parseSimpleType($newObj, $column, $value, $methods, $method2);
                    }
                }
            } else {
                $this->parseSimpleType($baseObj, $column, $value, $methods, $method2);
            }
        }
    }

    private function parseSimpleType(AbstractEntity $obj, $column, $value, array $methods, $method2)
    {
        $timestamp = $this->formatter->parse($value);

        if ($timestamp) {
            try {
                $value = new \DateTime($timestamp);
            } catch (\Exception $ex) {
                // nothing to do...
            }
        }

        if ($this->checkPublicMethods($methods, $method2)) {
            $obj->$method2($value);
        } else {
            $obj->{$column} = $value;
        }
    }

    private function addObjectTo(array $methods, $method1, $method2, AbstractEntity $baseObj, AbstractEntity $newObj)
    {
        if ($this->checkPublicMethods($methods, $method1)) {
            $baseObj->$method1($newObj);
        } elseif ($this->checkPublicMethods($methods, $method2)) {
            $baseObj->$method2($newObj);
        } else {
            $baseObj->{$column} = $newObj;
        }
    }

    private function getNestedObject(AbstractEntity $obj, array $keys, $levelUp = 0)
    {
        if ($levelUp > 0) {
            for ($i = 0; $i < $levelUp; $i++) {
                unset($keys[count($keys) - 1]);
            }
        }

        $innerObj = $obj;
        $lastObj = $obj;

        if (count($keys) > 0) {
            foreach ($keys as $key) {
                if (is_numeric($key)) {
                    $innerObj = $innerObj[$key];
                } else {
                    $method = 'get' . ucfirst($key);
                    $reflection = new \ReflectionClass($innerObj);
                    $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

                    $lastObj = $innerObj;
                    if ($this->checkPublicMethods($methods, $method)) {
                        $innerObj = $innerObj->$method();
                    } else {
                        $innerObj = $innerObj->{$key};
                    }
                }
            }

            if ($innerObj === null) {
                $innerObj = $lastObj;
            }
        }

        return $innerObj;
    }

    private function setNestedObject(array $parsedObjs, array $keys, AbstractEntity $objToAdd)
    {
        $ref = &$parsedObjs;
        foreach ($keys as $key) {
            $ref = &$ref[$key];
        }

        $ref = $objToAdd;

        return $parsedObjs;
    }
}

假设这个例子调用了公共方法save。由于某种原因,它的嵌套是错误的。尽管反过来,从对象到数组 usingconvertToPublicObjects工作正常。

这是我的其他尝试:

绕过参考深度:

/**
 * Converts ORM like objects from the request from arrays to objects.
 *
 * @param string $key
 *
 * @return AbstractEntity
 */
private function convertRequestToObj(string $key)
{
    $ar = filter_input($this->inputType, $key, \FILTER_DEFAULT, \FILTER_REQUIRE_ARRAY);
    $baseObj = new $this->objMapping[$key]();
    $this->mapArrayToObj($ar, $baseObj, $baseObj);

    return $baseObj;
}

private function mapArrayToObj(array $ar, AbstractEntity $baseObj, AbstractEntity $veryBaseObj, $refDepth = '')
{
    foreach ($ar as $column => $value) {
        $reflection = new \ReflectionClass($baseObj);
        $method1 = 'add' . ucfirst($column);
        $method2 = 'set' . ucfirst($column);
        $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

        if (is_array($value) && !is_numeric($column)) {
            $refDepth .= $column .',';

            $newObj = new $this->objMapping[$column]();
            $this->addObjectTo($methods, $method1, $method2, $baseObj, $newObj);

            $this->mapArrayToObj($value, $newObj, $veryBaseObj, $refDepth);
        } elseif (is_array($value) && is_numeric($column)) {
            $refDepth .= $column .',';
            $refKeys = explode(',', substr($refDepth, 0, strrpos($refDepth, ',')));
            $toAddObj = $this->getNestedObject($veryBaseObj, $refKeys);
            $column = substr($refDepth, 0, strrpos($refDepth, ','));
            $column = substr($column, 0, strrpos($column, ','));
            $newObj = new $this->objMapping[$column]();
            $this->addObjectTo($methods, $method1, $method2, $baseObj, $newObj);

            $reflection = new \ReflectionClass($newObj);
            $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

            foreach ($value as $subCol => $subVal) {
                if (is_array($subVal)) {
                    // sanitize strings like userMain,0,1,:
                    $refDepth = substr($refDepth, 0, strrpos($refDepth, ','));
                    $refDepth = substr($refDepth, 0, strrpos($refDepth, ',') + 1);

                    $refDepth .= $subCol . ',';
                    $this->mapArrayToObj($subVal, $newObj, $veryBaseObj, $refDepth);
                } else {
                    $method2 = 'set' . ucfirst($subCol);
                    $this->parseSimpleType($newObj, $subCol, $subVal, $methods, $method2);
                }
            }
            // sanitize strings like position,0,1,:
            $refDepth = substr($refDepth, 0, strrpos($refDepth, ','));
            $refDepth = substr($refDepth, 0, strrpos($refDepth, ',') + 1);
        } else {
            $refDepth = '';
            $this->parseSimpleType($baseObj, $column, $value, $methods, $method2);
        }
    }
}

里面有 if 分支:

/**
 * Converts ORM like objects from the request from arrays to objects.
 *
 * @param string $key
 *
 * @return AbstractEntity
 */
private function convertRequestToObj(string $key)
{
    $ar = filter_input($this->inputType, $key, \FILTER_DEFAULT, \FILTER_REQUIRE_ARRAY);
    $baseObj = new $this->objMapping[$key]();
    $this->mapArrayToObj($ar, $baseObj, $baseObj);

    return $baseObj;
}

private function mapArrayToObj(array $ar, AbstractEntity $baseObj, AbstractEntity $veryBaseObj, $refDepth = '')
{
    foreach ($ar as $column => $value) {
        $reflection = new \ReflectionClass($baseObj);
        $method1 = 'add' . ucfirst($column);
        $method2 = 'set' . ucfirst($column);
        $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

        if (is_array($value)) {
            $refDepth .= $column .',';
            $refDepthBackup = $refDepth;
            $refKeys = explode(',', substr($refDepth, 0, strrpos($refDepth, ',')));
            if (is_numeric($column)) {
                $column = substr($refDepth, 0, strrpos($refDepth, ','));
                $column = substr($column, 0, strrpos($column, ','));
                $method1 = 'add' . ucfirst($column);
                $toAddObj = $this->getNestedObject($veryBaseObj, $refKeys, 2);
                // sanitize strings like position,0,1,:
                $refDepth = substr($refDepth, 0, strrpos($refDepth, ','));
                $refDepth = substr($refDepth, 0, strrpos($refDepth, ',') + 1);
            } else {
                $toAddObj = $baseObj;
            }

            $reflection = new \ReflectionClass($toAddObj);
            $method1 = 'add' . ucfirst($column);
            $method2 = 'set' . ucfirst($column);
            $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

            $newObj = new $this->objMapping[$column]();
            $this->addObjectTo($methods, $method1, $method2, $toAddObj, $newObj);

            $this->mapArrayToObj($value, $newObj, $veryBaseObj, $refDepthBackup);
        } else {
            $refDepth = '';
            $this->parseSimpleType($baseObj, $column, $value, $methods, $method2);
        }
    }
}

使用内部foreach循环:

/**
 * Converts ORM like objects from the request from arrays to objects.
 *
 * @param string $key
 *
 * @return AbstractEntity
 */
private function convertRequestToObj(string $key)
{
    $ar = filter_input($this->inputType, $key, \FILTER_DEFAULT, \FILTER_REQUIRE_ARRAY);
    $baseObj = new $this->objMapping[$key]();
    $this->mapArrayToObj($ar, $baseObj);

    return $baseObj;
}

private function mapArrayToObj(array $ar, AbstractEntity $baseObj)
{
    foreach ($ar as $column => $value) {
        $reflection = new \ReflectionClass($baseObj);
        $method1 = 'add' . ucfirst($column);
        $method2 = 'set' . ucfirst($column);
        $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);

        if (is_array($value)) {
            $newObj = new $this->objMapping[$column]();
            $this->addObjectTo($methods, $method1, $method2, $baseObj, $newObj);
            $reflection = new \ReflectionClass($newObj);
            $methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
            foreach ($value as $subCol => $subVal) {
                $method2 = 'set' . ucfirst($subCol);
                if (is_array($subVal)) {
                    if (is_numeric($subCol)) {
                        $this->mapArrayToObj($subVal, $newObj);
                    }
                } else {
                    $this->parseSimpleType($newObj, $column, $value, $methods, $method2);
                }
            }
        } else {
            $this->parseSimpleType($baseObj, $column, $value, $methods, $method2);
        }
    }
}

标签: phparraysrecursion

解决方案


我没有使用过 orm,所以不确定这是否是你想要的。

一些提示:

  1. 我使用了 php 5.6.30,所以你的里程可能会有所不同。

  2. OOP是信息隐藏,这意味着教每个班级做什么,没有反思。

  3. 使用字段实现数据驱动框架

  4. 实现魔术 get 和 call 以动态访问数据和对象

  5. 每个类都必须验证它的数据,这里没有实现

  6. 每个类都要抛出和捕获自己的异常,这里不实现

  7. 使用工厂模式来创建数据类。

  8. 该接口定义了订单类外观模式。

  9. 该特征实现了所有订单类的默认方法。

我玩弄了使用 XML 类的想法,但这似乎没问题。

这是实现订单工厂模式的类文件。创建模型对象时,使用工厂类(静态,不要实例化),不要直接实例化类。getValue() 在需要时处理 factory::create。结果是类使用工厂创建自己。

<?php /* ormorder.php */
// Object Relational Mapping (OrmOrder)

// order OrmOrder class interface methods
interface IORM
{
//  function initFields(); // this should not be public?
    function toArray();
    function __get($name);
    function __call($name,$value);
}
// order OrmOrder class trait methods
trait FORM 
{
    protected $fields;
    protected $data;

    function __construct($data) 
    { 
        parent::__construct();
        $this->initFields();
        $this->setData($data);
    }
    // always override, never call 
    protected function initFields(){ $this->fields = null;}
    // sometimes override, never call
    protected   function setData($data)
    {
        foreach($this->fields as $field)
            if(isset($data[$field]))
                $this->data[$field] =   $this->getValue($field,$data[$field]);
    }
    // seldom override, never call
    protected   function getValue($field,$data) { return $data; }
    function toArray(){ return $this->data; }
    final function __get($name)
    {
        if('data' == $name)
            return $this->data;
        return $this->data[$name];
    }
    function __call($name,$value)
    {
        $attr = $value[0];
        $val = $value[1];
        $result = null;
        if(in_array($name, $this->fields))
            if(isset($this->data[$name]))
                if(is_array($this->data[$name]))
                    foreach($this->data[$name] as $obj)
                        if($obj->$attr == $val)
                        {
                            $result = $obj;
                            break;
                        }
        else $result = $this->data[$name];
        return $result;
    }
}
// pacify php parent::_construct()
abstract
class ORMAbstract
{
    function __construct() {}
}
// Main Order class that does (almost) everything
abstract
class Orm extends ORMAbstract implements IORM
{ use FORM;
}
// you should override getValue()
class Order extends Orm
{
}
class Position extends Orm
{
}
class Supplier extends Orm
{
}
class Customer extends Orm
{
}
class Address extends Orm
{
}

// static class to return OrmOrder objects
// keep factory in sync with classes
// Call directly never implement
class OrderFactory
{
    static
    function create($name, $data)
    {
        switch($name)
        {
            case 'supplier': return new Item($data);
            case 'position': return new LineItem($data);
            case 'address': return new Address($data);
            case 'customer': return new Customer($data);
            case 'order': return new Order($data);
            default: return null;
        }
    }
}
?>

模型文件(和主要功能)。从命令提示符运行它

/* 假设 php 设置正确 */

> 订购型号

该文件包含顶级模型,即用于检查数据的订单模型。toArray() 返回一个多维数组。OrderModel 类必须被实例化并传递 (html) 多维数组。

<?php /* ordermodel.php */
require_once('ormorder.php');

// sample database, development only, delete in production
$data['order'][0]['id'] = 0;
$data['order'][0]['note'] = 'test orders';
$data['order'][0]['date'] = '23 Mar 13';
$data['order'][0]['customer'][0]['id'] = 1;
$data['order'][0]['customer'][0]['account'] = '3000293826';
$data['order'][0]['customer'][0]['name'] = 'John Doe';
$data['order'][0]['customer'][0]['billing'][0] = 'Sand Castle';
$data['order'][0]['customer'][0]['billing'][1] = '1 beach street';
$data['order'][0]['customer'][0]['billing'][2] = 'strand';
$data['order'][0]['customer'][0]['billing'][3] = 'Lagoon';
$data['order'][0]['customer'][0]['billing'][4] = 'Fairy Island';
$data['order'][0]['customer'][0]['billing'][5] = '55511';
$data['order'][0]['customer'][0]['delivery'][0] = 'Nine Acres';
$data['order'][0]['customer'][0]['delivery'][1] = '3 corn field';
$data['order'][0]['customer'][0]['delivery'][2] = 'Butterworth';
$data['order'][0]['customer'][0]['delivery'][3] = 'Foam Vale';
$data['order'][0]['customer'][0]['delivery'][4] = 'Buttress Lake';
$data['order'][0]['customer'][0]['delivery'][5] = '224433';
$data['order'][0]['customer'][0]['items'][0]['supplier'] = '4000392292';
$data['order'][0]['customer'][0]['items'][0]['stock'] = '2000225571';
$data['order'][0]['customer'][0]['items'][0]['quantity'] = 5;
$data['order'][0]['customer'][0]['items'][0]['unitprice'] = 35.3;
$data['order'][0]['customer'][0]['items'][1]['supplier'] = '4000183563';
$data['order'][0]['customer'][0]['items'][1]['stock'] = '2000442279';
$data['order'][0]['customer'][0]['items'][1]['quantity'] = 12;
$data['order'][0]['customer'][0]['items'][1]['unitprice'] = 7.4;

// Top level Order management class
// could also be an OrmOrder class
class OrderModel
{
    private $orders;

    function __construct($data)
    {
        foreach($data['order'] as $order)
            $this->orders[] = OrderFactory::create('order',$order);
    }
    function __call($name,$value)
    {
        $o = null;
        $attribute = $value[0];
        $val = $value[1];
        foreach($this->orders as $order)
        {
            if($order->$attribute == $val)
            {
                $o = $order;
                break;
            }
        }
        return $o;
    }
    function toArray()
    {
        $data = null;
        foreach($this->orders as $order)
            $data['order'][] = $order->toArray();
        return $data;
    }
}
/* development only, delete in production */
function main($data)
{
    $model = new OrderModel($data);
    echo $model->order('id',12)->note;
    var_dump($model->order('date',
    '23 Mar 13')->customer('account','3000293826')->delivery->data);
//  var_dump($model->toArray());
}
main($data);
?>

输出应类似于:

PHP Notice:  Trying to get property 'note' of non-object in C:\Users\Peter\Docum
ents\php\ordermodel.php on line 70

Notice: Trying to get property 'note' of non-object in C:\Users\Peter\Documents\
php\ordermodel.php on line 70
array(6) {
  [0]=>
  string(10) "Nine Acres"
  [1]=>
  string(12) "3 corn field"
  [2]=>
  string(11) "Butterworth"
  [3]=>
  string(9) "Foam Vale"
  [4]=>
  string(13) "Buttress Lake"
  [5]=>
  string(6) "224433"
}

希望这可以进行您正在寻找的那种检查,可能与 Doctrine 不同,但可能足够接近有用。

  • 更新 *

要在您的代码中实现答案,请尝试以下操作:

<?PHP
    require_once('ordermodel.php');
   /*..... */
    private function jsonResponse()
    {
        header('Content-type: application/json');
        echo json_encode(
            array(
                'success' => $this->data['success'],
                'data' => new OrderModel($this->data['data'])
            )
        );
    }
?>

推荐阅读