php - 如何递归地从多维表单数组转换为相关的实体对象
问题描述
我正在尝试将多维 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
在数组末尾添加一个条目的方法(这就是为什么需要反射以及我们有$method1
and的原因$method2
)。此外,它还将日期和时间从 inernationalized 解析string
为DateTime
.
我想像 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);
}
}
}
解决方案
我没有使用过 orm,所以不确定这是否是你想要的。
一些提示:
我使用了 php 5.6.30,所以你的里程可能会有所不同。
OOP是信息隐藏,这意味着教每个班级做什么,没有反思。
使用字段实现数据驱动框架
实现魔术 get 和 call 以动态访问数据和对象
每个类都必须验证它的数据,这里没有实现
每个类都要抛出和捕获自己的异常,这里不实现
使用工厂模式来创建数据类。
该接口定义了订单类外观模式。
该特征实现了所有订单类的默认方法。
我玩弄了使用 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'])
)
);
}
?>
推荐阅读
- adodb - ADODB.Recordset 锁定我的本地源文件
- javascript - 使用 mongoose virtuals 有条件地计算条目
- rust - 你如何操纵 Rust env::args 并避免“借用时临时价值下降”?
- mongodb - MongoDB 聚合:添加定义顺序的自定义字段查询
- c# - 使用开发人员方法在 C# 中创建类库
- python - 如何使用 Python 找到函数的最大 x 值
- python - AWS DynamoDB 数据转为 Python/Boto3/Lamba 中的 json 格式
- c# - Spire PDF SaveToFile 崩溃程序
- prebid.js - 出价前出价适配器未从 DFP 获得响应
- pdf - POS 打印机 - 文件格式