I'm trying to convert a multidimensional HTML form array to its related entity (database) object classes with one to many relations and nested one to many relations. Consider the following input exmaple (human readable):
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
We got an abstract class with empty body called AbstractEntity which every entity extends and the entities have public member variables for simple types. For arrays its access is private there are setter methods as well as addXX methods to add one entry at the end of the array (that's why reflection is needed and why we have $method1 and $method2). Additionally it parses date and time from inernationalized string to DateTime.
I would like to access them as in ORM frameworks style like Doctrine like so:
$order->getPosition()[0]->getBillingAddress()->firstname
Here is my worker class that does the main stuff:
<?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;
}
}
Let`s say this example calls the pubic method save. For some reason, it does the nesting wrong. Although the other way around, from objects to array using convertToPublicObjects works fine.
Here are my other tries:
With bypassed reference depth:
/**
* 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);
}
}
}
With if branches inside:
/**
* 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);
}
}
}
With an inner foreach loop:
/**
* 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);
}
}
}