From acb32ba190d9d1cc25bb75dacc3013503310c655 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sat, 1 Sep 2018 17:23:34 +0200 Subject: [PATCH] Implement more array functionality --- DataStorage/Database/DataMapperAbstract.php | 350 ++++++++++++------ .../Database/DataMapperAbstractTest.php | 4 +- 2 files changed, 233 insertions(+), 121 deletions(-) diff --git a/DataStorage/Database/DataMapperAbstract.php b/DataStorage/Database/DataMapperAbstract.php index fc6115aae..af4a1dfe5 100644 --- a/DataStorage/Database/DataMapperAbstract.php +++ b/DataStorage/Database/DataMapperAbstract.php @@ -92,16 +92,6 @@ class DataMapperAbstract implements DataMapperInterface */ protected static $hasMany = []; - /** - * Relations. - * - * Relation is defined in the model - * - * @var string[] - * @since 1.0.0 - */ - protected static $hasOne = []; - /** * Relations. * @@ -173,7 +163,6 @@ class DataMapperAbstract implements DataMapperInterface 'createdAt' => [], 'columns' => [], 'hasMany' => [], - 'hasOne' => [], 'ownsOne' => [], 'table' => [], ]; @@ -254,7 +243,6 @@ class DataMapperAbstract implements DataMapperInterface self::$collection['createdAt'][] = $class::$createdAt; self::$collection['columns'][] = $class::$columns; self::$collection['hasMany'][] = $class::$hasMany; - self::$collection['hasOne'][] = $class::$hasOne; self::$collection['ownsOne'][] = $class::$ownsOne; self::$collection['table'][] = $class::$table; @@ -295,7 +283,6 @@ class DataMapperAbstract implements DataMapperInterface self::$createdAt = ''; self::$columns = []; self::$hasMany = []; - self::$hasOne = []; self::$ownsOne = []; self::$table = ''; self::$fields = []; @@ -303,7 +290,6 @@ class DataMapperAbstract implements DataMapperInterface 'primaryField' => [], 'createdAt' => [], 'columns' => [], - 'hasOne' => [], 'ownsMany' => [], 'ownsOne' => [], 'table' => [], @@ -412,7 +398,7 @@ class DataMapperAbstract implements DataMapperInterface foreach (static::$columns as $key => $column) { $propertyName = \stripos($column['internal'], '/') !== false ? \explode('/', $column['internal'])[0] : $column['internal']; - if (isset(static::$hasMany[$propertyName]) || isset(static::$hasOne[$propertyName])) { + if (isset(static::$hasMany[$propertyName])) { continue; } @@ -477,7 +463,7 @@ class DataMapperAbstract implements DataMapperInterface $query->prefix(self::$db->getPrefix())->into(static::$table); foreach (static::$columns as $key => $column) { - if (isset(static::$hasMany[$key]) || isset(static::$hasOne[$key])) { + if (isset(static::$hasMany[$key])) { continue; } @@ -506,7 +492,7 @@ class DataMapperAbstract implements DataMapperInterface $query->insert($column['name'])->value($value, $column['type']); } - } + } // if a table only has a single column = primary key column. This must be done otherwise the query is empty if ($query->getType() === QueryType::NONE) { @@ -711,22 +697,6 @@ class DataMapperAbstract implements DataMapperInterface } } - /** - * Create has one - * - * @param \ReflectionClass $refClass Property name to initialize - * @param object $obj Object to create - * - * @return mixed - * @todo implement??? - * - * @since 1.0.0 - */ - private static function createHasOne(\ReflectionClass $refClass, object $obj) - { - throw new \Exception(); - } - /** * Create owns one * @@ -999,6 +969,69 @@ class DataMapperAbstract implements DataMapperInterface self::updateRelationTable($objsIds, $objId); } + /** + * Update has many + * + * @param array $obj Object to create + * @param mixed $objId Id to set + * @param int $relations Create all relations as well + * @param int $depth Depth of relations to update (default = 1 = none) + * + * @return void + * + * @throws InvalidMapperException + * + * @since 1.0.0 + */ + private static function updateHasManyArray(array &$obj, $objId, int $relations = RelationType::ALL, $depth = 1) : void + { + $objsIds = []; + + foreach (static::$hasMany as $propertyName => $rel) { + $values = $obj[$propertyName]; + + if (!isset(static::$hasMany[$propertyName]['mapper'])) { + throw new InvalidMapperException(); + } + + /** @var string $mapper */ + $mapper = static::$hasMany[$propertyName]['mapper']; + $objsIds[$propertyName] = []; + + foreach ($values as $key => &$value) { + // todo: carefull what if a value is an object or another array? + if (!\is_array($value)) { + // Is scalar => already in database + $objsIds[$propertyName][$key] = $value; + + continue; + } + + $primaryKey = $value[$mapper::$columns[$mapper::$primaryField]['internal']]; + + // already in db + if (!empty($primaryKey)) { + $mapper::updateArray($value, $relations, $depth); + + $objsIds[$propertyName][$key] = $value; + + continue; + } + + // create if not existing + /** @var string $table */ + /** @var array $columns */ + if (static::$hasMany[$propertyName]['table'] === static::$hasMany[$propertyName]['mapper']::$table) { + $value[$mapper::$columns[static::$hasMany[$propertyName]['dst']]['internal']] = $objId; + } + + $objsIds[$propertyName][$key] = $mapper::createArray($value); + } + } + + self::updateRelationTable($objsIds, $objId); + } + /** * Update relation table entry * @@ -1016,8 +1049,8 @@ class DataMapperAbstract implements DataMapperInterface $many = self::getHasManyRaw($objId); foreach (static::$hasMany as $propertyName => $rel) { - $removes = array_diff($many[$propertyName], array_keys($objsIds[$propertyName] ?? [])); - $adds = array_diff(array_keys($objsIds[$propertyName] ?? []), $many[$propertyName]); + $removes = \array_diff($many[$propertyName], \array_keys($objsIds[$propertyName] ?? [])); + $adds = \array_diff(\array_keys($objsIds[$propertyName] ?? []), $many[$propertyName]); if (!empty($removes)) { self::deleteRelationTable($propertyName, $removes, $objId); @@ -1084,6 +1117,30 @@ class DataMapperAbstract implements DataMapperInterface return $mapper::update($obj, $relations, $depth); } + /** + * Update owns one + * + * The reference is stored in the main model + * + * @param string $propertyName Property name to initialize + * @param array $obj Object to update + * @param int $relations Create all relations as well + * @param int $depth Depth of relations to update (default = 1 = none) + * + * @return mixed + * + * @since 1.0.0 + */ + private static function updateOwnsOneArray(string $propertyName, array $obj, int $relations = RelationType::ALL, int $depth = 1) + { + /** @var string $mapper */ + $mapper = static::$ownsOne[$propertyName]['mapper']; + + // todo: delete owned one object is not recommended since it can be owned by by something else? or does owns one mean that nothing else can have a relation to this one? + + return $mapper::updateArray($obj, $relations, $depth); + } + /** * Update owns one * @@ -1110,6 +1167,32 @@ class DataMapperAbstract implements DataMapperInterface return $obj; } + /** + * Update owns one + * + * The reference is stored in the main model + * + * @param string $propertyName Property name to initialize + * @param mixed $obj Object to update + * @param int $relations Create all relations as well + * @param int $depth Depth of relations to update (default = 1 = none) + * + * @return mixed + * + * @since 1.0.0 + */ + private static function updateBelongsToArray(string $propertyName, $obj, int $relations = RelationType::ALL, int $depth = 1) + { + if (\is_array($obj)) { + /** @var string $mapper */ + $mapper = static::$belongsTo[$propertyName]['mapper']; + + return $mapper::updateArray($obj, $relations, $depth); + } + + return $obj; + } + /** * Update object in db. * @@ -1133,7 +1216,6 @@ class DataMapperAbstract implements DataMapperInterface foreach (static::$columns as $key => $column) { $propertyName = \stripos($column['internal'], '/') !== false ? \explode('/', $column['internal'])[0] : $column['internal']; if (isset(static::$hasMany[$propertyName]) - || isset(static::$hasOne[$propertyName]) || $column['internal'] === static::$primaryField ) { continue; @@ -1180,6 +1262,62 @@ class DataMapperAbstract implements DataMapperInterface self::$db->con->prepare($query->toSql())->execute(); } + /** + * Update object in db. + * + * @param array $obj Model to update + * @param mixed $objId Model id + * @param int $relations Create all relations as well + * @param int $depth Depth of relations to update (default = 1 = none) + * + * @return void + * + * @since 1.0.0 + */ + private static function updateModelArray(array $obj, $objId, int $relations = RelationType::ALL, int $depth = 1) : void + { + $query = new Builder(self::$db); + $query->prefix(self::$db->getPrefix()) + ->update(static::$table) + ->where(static::$table . '.' . static::$primaryField, '=', $objId); + + foreach (static::$columns as $key => $column) { + if (isset(static::$hasMany[$key])) { + continue; + } + + $path = $column['internal']; + if (\stripos($column['internal'], '/') !== false) { + $path = \explode('/', $column['internal']); + + \array_shift($path); // todo: why am I doing this? + $path = \implode('/', $path); + } + + $property = ArrayUtils::getArray($path, $obj, '/'); + + if (isset(static::$ownsOne[$path])) { + $id = self::updateOwnsOneArray($column['internal'], $property, $relations, $depth); + $value = self::parseValue($column['type'], $id); + + // todo: should not be done if the id didn't change. but for now don't know if id changed + $query->set([static::$table . '.' . $column['name'] => $value], $column['type']); + } elseif (isset(static::$belongsTo[$path])) { + $id = self::updateBelongsToArray($column['internal'], $property, $relations, $depth); + $value = self::parseValue($column['type'], $id); + + // todo: should not be done if the id didn't change. but for now don't know if id changed + $query->set([static::$table . '.' . $column['name'] => $value], $column['type']); + } elseif ($column['name'] !== static::$primaryField) { + $value = self::parseValue($column['type'], $property); + + $query->set([static::$table . '.' . $column['name'] => $value], $column['type']); + } + } + + self::$db->con->prepare($query->toSql())->execute(); + } + /** * Update object in db. * @@ -1225,6 +1363,50 @@ class DataMapperAbstract implements DataMapperInterface return $objId; } + /** + * Update object in db. + * + * @param array $obj Object reference (gets filled with insert id) + * @param int $relations Create all relations as well + * @param int $depth Depth of relations to update (default = 1 = none) + * + * @return mixed + * + * @since 1.0.0 + */ + public static function updateArray(array &$obj, int $relations = RelationType::ALL, int $depth = 1) + { + self::extend(__CLASS__); + + if (!isset($obj)) { + return null; + } + + $objId = $obj[static::$columns[static::$primaryField]['internal']]; + $update = true; + + if ($depth < 1) { + return $objId; + } + + self::addInitializedArray(static::class, $objId, $obj); + + if (empty($objId)) { + $update = false; + self::createArray($obj, $relations); + } + + if ($relations === RelationType::ALL) { + self::updateHasManyArray($obj, $objId, --$depth); + } + + if ($update) { + self::updateModelArray($obj, $objId, --$depth); + } + + return $objId; + } + /** * Delete has many * @@ -1372,7 +1554,7 @@ class DataMapperAbstract implements DataMapperInterface foreach ($properties as $property) { $propertyName = $property->getName(); - if (isset(static::$hasMany[$propertyName]) || isset(static::$hasOne[$propertyName])) { + if (isset(static::$hasMany[$propertyName])) { continue; } @@ -1575,72 +1757,6 @@ class DataMapperAbstract implements DataMapperInterface } } - /** - * Populate data. - * - * @param mixed $obj Object to add the relations to - * @param int $depth Relation depth - * - * @return void - * - * @todo accept reflection class as parameter - * - * @since 1.0.0 - */ - public static function populateHasOne(&$obj, int $depth = 3) : void - { - $refClass = new \ReflectionClass($obj); - - foreach (static::$hasOne as $member => $one) { - // todo: is that if necessary? performance is suffering for sure! - if ($refClass->hasProperty($member)) { - $refProp = $refClass->getProperty($member); - - if (!($accessible = $refProp->isPublic())) { - $refProp->setAccessible(true); - } - - /** @var string $mapper */ - $mapper = static::$hasOne[$member]['mapper']; - $id = $refProp->getValue($obj); - - if (self::isNullObject($id)) { - continue; - } - - $id = \is_object($id) ? self::getObjectId($id) : $id; - $value = self::getInitialized($mapper, $id) ?? $mapper::get($id, RelationType::ALL, null, $depth); - - $refProp->setValue($obj, $value); - - if (!$accessible) { - $refProp->setAccessible(false); - } - } - } - } - - /** - * Populate data. - * - * @param array $obj Object to add the relations to - * @param int $depth Relation depth - * - * @return void - * - * @todo accept reflection class as parameter - * - * @since 1.0.0 - */ - public static function populateHasOneArray(array &$obj, int $depth = 3) : void - { - foreach (static::$hasOne as $member => $one) { - /** @var string $mapper */ - $mapper = static::$hasOne[$member]['mapper']; - $obj[$member] = self::getInitializedArray($mapper, $obj['member']) ?? $mapper::getArray($obj[$member], RelationType::ALL, $depth); - } - } - /** * Populate data. * @@ -1702,8 +1818,11 @@ class DataMapperAbstract implements DataMapperInterface { foreach (static::$ownsOne as $member => $one) { /** @var string $mapper */ - $mapper = static::$ownsOne[$member]['mapper']; - $obj[$member] = self::getInitializedArray($mapper, $obj[$member]) ?? $mapper::getArray($obj[$member], RelationType::ALL, $depth); + $mapper = static::$ownsOne[$member]['mapper']; + $id = $obj[$member]; + $id = \is_array($id) ? $id[$mapper::$columns[$mapper::$primaryField]['internal']] : $id; + + $obj[$member] = self::getInitializedArray($mapper, $id) ?? $mapper::getArray($id, RelationType::ALL, $depth); } } @@ -1768,8 +1887,11 @@ class DataMapperAbstract implements DataMapperInterface { foreach (static::$belongsTo as $member => $one) { /** @var string $mapper */ - $mapper = static::$belongsTo[$member]['mapper']; - $obj[$member] = self::getInitializedArray($mapper, $obj[$member]) ?? $mapper::getArray($obj[$member], RelationType::ALL, $depth); + $mapper = static::$belongsTo[$member]['mapper']; + $id = $obj[$member]; + $id = \is_array($id) ? $id[$mapper::$columns[$mapper::$primaryField]['internal']] : $id; + + $obj[$member] = self::getInitializedArray($mapper, $id) ?? $mapper::getArray($id, RelationType::ALL, $depth); } } @@ -2320,11 +2442,10 @@ class DataMapperAbstract implements DataMapperInterface } $hasMany = !empty(static::$hasMany); - $hasOne = !empty(static::$hasOne); $ownsOne = !empty(static::$ownsOne); $belongsTo = !empty(static::$belongsTo); - if (!($hasMany || $hasOne || $ownsOne || $belongsTo)) { + if (!($hasMany || $ownsOne || $belongsTo)) { return; } @@ -2334,10 +2455,6 @@ class DataMapperAbstract implements DataMapperInterface self::populateManyToMany(self::getHasManyRaw($key, $relations), $obj[$key], $depth); } - if ($hasOne) { - self::populateHasOne($obj[$key], $depth); - } - if ($ownsOne) { self::populateOwnsOne($obj[$key], $depth); } @@ -2370,11 +2487,10 @@ class DataMapperAbstract implements DataMapperInterface } $hasMany = !empty(static::$hasMany); - $hasOne = !empty(static::$hasOne); $ownsOne = !empty(static::$ownsOne); $belongsTo = !empty(static::$belongsTo); - if (!($hasMany || $hasOne || $ownsOne || $belongsTo)) { + if (!($hasMany || $ownsOne || $belongsTo)) { return; } @@ -2384,10 +2500,6 @@ class DataMapperAbstract implements DataMapperInterface self::populateManyToManyArray(self::getHasManyRaw($key, $relations), $obj[$key], $depth); } - if ($hasOne) { - self::populateHasOneArray($obj[$key], $depth); - } - if ($ownsOne) { self::populateOwnsOneArray($obj[$key], $depth); } diff --git a/tests/DataStorage/Database/DataMapperAbstractTest.php b/tests/DataStorage/Database/DataMapperAbstractTest.php index a2260fc32..375783e5a 100644 --- a/tests/DataStorage/Database/DataMapperAbstractTest.php +++ b/tests/DataStorage/Database/DataMapperAbstractTest.php @@ -240,7 +240,7 @@ class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase // todo test update relations } - /*public function testUpdateArray() + public function testUpdateArray() { $id = BaseModelMapper::createArray($this->modelArray); $modelR = BaseModelMapper::getArray($id); @@ -263,7 +263,7 @@ class DataMapperAbstractTest extends \PHPUnit\Framework\TestCase self::assertEquals($modelR['datetime']->format('Y-m-d'), $modelR2['datetime']->format('Y-m-d')); // todo test update relations - }*/ + } public function testDelete() {