diff --git a/src/MetaModels/DataAccess/DatabaseHelperTrait.php b/src/MetaModels/DataAccess/DatabaseHelperTrait.php new file mode 100644 index 000000000..28aca069f --- /dev/null +++ b/src/MetaModels/DataAccess/DatabaseHelperTrait.php @@ -0,0 +1,39 @@ + + * @copyright 2012-2017 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0 + * @filesource + */ + +namespace MetaModels\DataAccess; + +/** + * This trait provides some helper functions for database access. + */ +trait DatabaseHelperTrait +{ + /** + * Build a list of the correct amount of "?" for use in a db query. + * + * @param array $parameters The parameters. + * + * @return string + */ + private function buildDatabaseParameterList(array $parameters) + { + return implode(',', array_fill(0, count($parameters), '?')); + } +} diff --git a/src/MetaModels/DataAccess/IdResolver.php b/src/MetaModels/DataAccess/IdResolver.php new file mode 100644 index 000000000..41012848f --- /dev/null +++ b/src/MetaModels/DataAccess/IdResolver.php @@ -0,0 +1,340 @@ + + * @copyright 2012-2017 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0 + * @filesource + */ + +namespace MetaModels\DataAccess; + +use Contao\Database; +use MetaModels\Filter\IFilter; +use MetaModels\IMetaModel; + +/** + * This class resolves an id list. + */ +class IdResolver +{ + use DatabaseHelperTrait; + + /** + * The database. + * + * @var Database + */ + private $database; + + /** + * The metamodel we work on. + * + * @var IMetaModel + */ + private $metaModel; + + /** + * The MetaModel table name. + * + * @var string + */ + private $tableName; + + /** + * The filter. + * + * @var IFilter + */ + private $filter; + + /** + * The sort attribute. + * + * @var string + */ + private $sortBy; + + /** + * The sort order. + * + * @var string + */ + private $sortOrder = 'ASC'; + + /** + * The offset. + * + * @var int + */ + private $offset = 0; + + /** + * The limit. + * + * @var int + */ + private $limit = 0; + + /** + * Create a new instance. + * + * @param IMetaModel $metaModel The MetaModel. + * @param Database $database The database. + */ + public function __construct(IMetaModel $metaModel, Database $database) + { + $this->database = $database; + $this->metaModel = $metaModel; + $this->tableName = $metaModel->getTableName(); + } + + /** + * Create a new instance. + * + * @param IMetaModel $metaModel The MetaModel. + * @param Database $database The database. + * + * @return IdResolver + */ + public static function create(IMetaModel $metaModel, Database $database) + { + return new static($metaModel, $database); + } + + /** + * Retrieve filter. + * + * @return IFilter + */ + public function getFilter() + { + return $this->filter; + } + + /** + * Set filter. + * + * @param IFilter $filter The new value. + * + * @return IdResolver + */ + public function setFilter(IFilter $filter = null) + { + $this->filter = $filter; + + return $this; + } + + /** + * Retrieve attribute. + * + * @return string + */ + public function getSortBy() + { + return $this->sortBy; + } + + /** + * Set attribute. + * + * @param string $sortBy The new value. + * + * @return IdResolver + */ + public function setSortBy($sortBy) + { + $this->sortBy = (string) $sortBy; + + return $this; + } + + /** + * Retrieve sort order. + * + * @return string + */ + public function getSortOrder() + { + return $this->sortOrder; + } + + /** + * Set sort order. + * + * @param string $sortOrder The new value. + * + * @return IdResolver + */ + public function setSortOrder($sortOrder) + { + $this->sortOrder = $sortOrder == 'DESC' ? 'DESC' : 'ASC'; + + return $this; + } + + /** + * Retrieve offset. + * + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * Set offset. + * + * @param int $offset The new value. + * + * @return IdResolver + */ + public function setOffset($offset) + { + $this->offset = (int) $offset; + + return $this; + } + + /** + * Retrieve limit. + * + * @return int + */ + public function getLimit() + { + return $this->limit; + } + + /** + * Set limit. + * + * @param int $limit The new value. + * + * @return IdResolver + */ + public function setLimit($limit) + { + $this->limit = (int) $limit; + + return $this; + } + + /** + * Retrieve the id list. + * + * @return string[] + */ + public function getIds() + { + if ([] === $filteredIds = array_filter($this->getMatchingIds())) { + return []; + } + + // If desired, sort the entries. + if (!empty($filteredIds) && null !== $this->sortBy) { + $filteredIds = $this->sortIds($filteredIds); + } + + // Apply limiting then. + if ($this->offset > 0 || $this->limit > 0) { + $filteredIds = array_slice($filteredIds, $this->offset, $this->limit ?: null); + } + return array_unique(array_filter($filteredIds)); + } + + /** + * Fetch the amount of matching items. + * + * @return int + */ + public function count() + { + $filteredIds = $this->getMatchingIds(); + if (count($filteredIds) == 0) { + return 0; + } + + $result = $this + ->database + ->prepare(sprintf( + 'SELECT COUNT(id) AS count FROM %s WHERE id IN(%s)', + $this->tableName, + $this->buildDatabaseParameterList($filteredIds) + )) + ->execute($filteredIds); + + return $result->count; + } + + /** + * Narrow down the list of Ids that match the given filter. + * + * @return array all matching Ids. + */ + private function getMatchingIds() + { + if (null !== $this->filter && null !== ($matchingIds = $this->filter->getMatchingIds())) { + return $matchingIds; + } + + // Either no filter object or all ids allowed => return all ids. + // if no id filter is passed, we assume all ids are provided. + $rows = $this->database->execute('SELECT id FROM ' . $this->tableName); + + return $rows->fetchEach('id'); + } + + /** + * Sort the ids. + * + * @param string[] $filteredIds The id list. + * + * @return array + */ + private function sortIds($filteredIds) + { + switch (true) { + case ('random' === $this->sortBy): + shuffle($filteredIds); + return $filteredIds; + case 'id': + asort($filteredIds); + return $filteredIds; + case (null !== ($attribute = $this->metaModel->getAttribute($this->sortBy))): + return $attribute->sortIds($filteredIds, $this->sortOrder); + case (in_array($this->sortBy, ['pid', 'tstamp', 'sorting'])): + // Sort by database values. + return $this + ->database + ->prepare( + sprintf( + 'SELECT id FROM %s WHERE id IN(%s) ORDER BY %s %s', + $this->tableName, + $this->buildDatabaseParameterList($filteredIds), + $this->sortBy, + $this->sortOrder + ) + ) + ->execute($filteredIds) + ->fetchEach('id'); + default: + // Nothing we can do about this. + } + + return $filteredIds; + } +} diff --git a/src/MetaModels/DataAccess/ItemPersister.php b/src/MetaModels/DataAccess/ItemPersister.php new file mode 100644 index 000000000..7e02fdbe4 --- /dev/null +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -0,0 +1,356 @@ + + * @copyright 2012-2017 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0 + * @filesource + */ + +namespace MetaModels\DataAccess; + +use Contao\Database; +use MetaModels\Attribute\IAttribute; +use MetaModels\Attribute\IComplex; +use MetaModels\Attribute\ISimple; +use MetaModels\Attribute\ITranslated; +use MetaModels\IItem; +use MetaModels\IMetaModel; + +/** + * This class handles the raw database interaction for MetaModels. + * + * @internal Not part of the API. + */ +class ItemPersister +{ + use DatabaseHelperTrait; + + /** + * The metamodel we work on. + * + * @var IMetaModel + */ + private $metaModel; + + /** + * The MetaModel table name. + * + * @var string + */ + private $tableName; + + /** + * The used database. + * + * @var Database + */ + private $database; + + /** + * Create a new instance. + * + * @param IMetaModel $metaModel The metamodel we work on. + * @param Database $database The used database. + */ + public function __construct(IMetaModel $metaModel, Database $database) + { + $this->metaModel = $metaModel; + $this->tableName = $metaModel->getTableName(); + $this->database = $database; + } + + /** + * Save an item into the database. + * + * @param IItem $item The item to save to the database. + * @param int|null $timestamp Optional parameter for use own timestamp. + * This is useful if save a collection of models and all shall have + * the same timestamp. + * + * @return void + */ + public function saveItem(IItem $item, $timestamp) + { + $baseAttributes = false; + $item->set('tstamp', $timestamp); + if (null === $item->get('id')) { + $baseAttributes = true; + $this->createNewItem($item); + } + + $itemId = $item->get('id'); + $data = ['tstamp' => $item->get('tstamp')]; + // Update system columns. + if (null !== $item->get('pid')) { + $data['pid'] = $item->get('pid'); + } + if (null !== $item->get('sorting')) { + $data['sorting'] = $item->get('sorting'); + } + $this->saveRawColumns($data, [$itemId]); + unset($data); + + if ($this->metaModel->isTranslated()) { + $language = $this->metaModel->getActiveLanguage(); + } else { + $language = null; + } + + $variantIds = [$itemId]; + if ($item->isVariantBase()) { + $variants = $this->metaModel->findVariantsWithBase([$itemId], null); + foreach ($variants as $objVariant) { + /** @var IItem $objVariant */ + $variantIds[] = $objVariant->get('id'); + } + $this->saveRawColumns(['tstamp' => $item->get('tstamp')], $variantIds); + } + + $this->updateVariants($item, $language, $variantIds, $baseAttributes); + + // Tell all attributes that the model has been saved. Useful for alias fields, edit counters etc. + foreach ($this->metaModel->getAttributes() as $objAttribute) { + if ($item->isAttributeSet($objAttribute->getColName())) { + $objAttribute->modelSaved($item); + } + } + } + + /** + * Remove an item from the database. + * + * @param IItem $item The item to delete from the database. + * + * @return void + */ + public function deleteItem(IItem $item) + { + $idList = [$item->get('id')]; + // Determine if the model is a variant base and if so, fetch the variants additionally. + if ($item->isVariantBase()) { + $variants = $this->metaModel->findVariants([$item->get('id')], null); + foreach ($variants as $variant) { + /** @var IItem $variant */ + $idList[] = $variant->get('id'); + } + } + // Complex and translated attributes shall delete their values first. + $this->deleteAttributeValues($idList); + // Now make the real rows disappear. + $this + ->database + ->prepare(sprintf( + 'DELETE FROM %s WHERE id IN (%s)', + $this->tableName, + $this->buildDatabaseParameterList($idList) + )) + ->execute($idList); + } + + + /** + * Create a new item in the database. + * + * @param IItem $item The item to be created. + * + * @return void + */ + private function createNewItem(IItem $item) + { + $data = ['tstamp' => $item->get('tstamp')]; + + $isNewBaseItem = false; + if ($this->metaModel->hasVariants()) { + // No variant group is given, so we have a complete new base item this should be a workaround for these + // values should be set by the GeneralDataMetaModel or whoever is calling this method. + if (null === $item->get('vargroup')) { + $item->set('varbase', '1'); + $item->set('vargroup', '0'); + $isNewBaseItem = true; + } + $data['varbase'] = $item->get('varbase'); + $data['vargroup'] = $item->get('vargroup'); + } + + /** @noinspection PhpUndefinedFieldInspection */ + $itemId = $this + ->database + ->prepare('INSERT INTO ' . $this->tableName . ' %s') + ->set($data) + ->execute() + ->insertId; + $item->set('id', (string) $itemId); + + // Set the variant group equal to the id. + if ($isNewBaseItem) { + $this->saveRawColumns(['vargroup' => $item->get('id')], [$item->get('id')]); + $item->set('vargroup', $item->get('id')); + } + } + + /** + * Update the values of a native columns for the given ids. + * + * @param string[] $columns The column names to update (i.e. tstamp) as key, the values as value. + * + * @param string[] $ids The ids of the items that shall be updated. + * + * @return void + */ + private function saveRawColumns(array $columns, array $ids) + { + $this + ->database + ->prepare( + sprintf( + 'UPDATE %1$s %%s WHERE id IN (%2$s)', + $this->tableName, + $this->buildDatabaseParameterList($ids) + ) + ) + ->set($columns) + ->execute($ids); + } + + /** + * Update the variants with the value if needed. + * + * @param IItem $item The item to save. + * @param string $activeLanguage The language the values are in. + * @param string[] $variantIds The ids of all variants. + * @param bool $baseAttributes If also the base attributes get updated as well. + * + * @return void + */ + private function updateVariants(IItem $item, $activeLanguage, array $variantIds, $baseAttributes) + { + list($variant, $invariant) = $this->splitAttributes($item, $baseAttributes); + + // Override in variants. + foreach ($variant as $attributeName => $attribute) { + $this->saveAttributeValues($attribute, $variantIds, $item->get($attributeName), $activeLanguage); + } + // Save invariant ones now. + $ids = [$item->get('id')]; + foreach ($invariant as $attributeName => $attribute) { + $this->saveAttributeValues($attribute, $ids, $item->get($attributeName), $activeLanguage); + } + } + + /** + * Update an attribute for the given ids with the given data. + * + * @param IAttribute $attribute The attribute to save. + * @param array $ids The ids of the rows that shall be updated. + * @param mixed $data The data to save in raw data. + * @param string $language The language code to save. + * + * @return void + * + * @throws \RuntimeException When an unknown attribute type is encountered. + */ + private function saveAttributeValues($attribute, array $ids, $data, $language) + { + // Call the serializeData for all simple attributes. + if ($attribute instanceof ISimple) { + $data = $attribute->serializeData($data); + } + + $arrData = array(); + foreach ($ids as $intId) { + $arrData[$intId] = $data; + } + + // Check for translated fields first, then for complex and save as simple then. + if ($language && $attribute instanceof ITranslated) { + $attribute->setTranslatedDataFor($arrData, $language); + return; + } + if ($attribute instanceof IComplex) { + $attribute->setDataFor($arrData); + return; + } + if ($attribute instanceof ISimple) { + $attribute->setDataFor($arrData); + return; + } + + throw new \RuntimeException( + 'Unknown attribute type, can not save. Interfaces implemented: ' . + implode(', ', class_implements($attribute)) + ); + } + + /** + * Delete the values in complex and translated attributes. + * + * @param string[] $idList The list of item ids to remove. + * + * @return void + */ + private function deleteAttributeValues(array $idList) + { + $languages = null; + if ($this->metaModel->isTranslated()) { + $languages = $this->metaModel->getAvailableLanguages(); + } + foreach ($this->metaModel->getAttributes() as $attribute) { + if ($attribute instanceof IComplex) { + /** @var IComplex $attribute */ + $attribute->unsetDataFor($idList); + continue; + } + if ($attribute instanceof ITranslated) { + foreach ($languages as $language) { + $attribute->unsetValueFor($idList, $language); + } + continue; + } + } + } + + /** + * Split the attributes into variant and invariant ones and filter out all that do not need to get updated. + * + * @param IItem $item The item to save. + * @param bool $baseAttributes If also the base attributes get updated as well. + * + * @return array + */ + private function splitAttributes(IItem $item, $baseAttributes) + { + $variant = []; + $invariant = []; + foreach ($this->metaModel->getAttributes() as $attributeName => $attribute) { + // Skip unset attributes. + if (!$item->isAttributeSet($attribute->getColName())) { + continue; + } + if ($this->metaModel->hasVariants()) { + if ($attribute->get('isvariant')) { + $variant[$attributeName] = $attribute; + continue; + } + if (!$baseAttributes && $item->isVariant()) { + // Skip base attribute. + continue; + } + } + $invariant[$attributeName] = $attribute; + } + + return [$variant, $invariant]; + } +} diff --git a/src/MetaModels/DataAccess/ItemRetriever.php b/src/MetaModels/DataAccess/ItemRetriever.php new file mode 100644 index 000000000..3c58f2ff6 --- /dev/null +++ b/src/MetaModels/DataAccess/ItemRetriever.php @@ -0,0 +1,260 @@ + + * @copyright 2012-2017 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0 + * @filesource + */ + +namespace MetaModels\DataAccess; + +use Contao\Database; +use MetaModels\Attribute\IAttribute; +use MetaModels\Attribute\IComplex; +use MetaModels\Attribute\ISimple; +use MetaModels\Attribute\ITranslated; +use MetaModels\IItems; +use MetaModels\IMetaModel; +use MetaModels\Item; +use MetaModels\Items; +use RuntimeException; + +/** + * This class handles the item retrieval + * + * @internal Not part of the API. + */ +class ItemRetriever +{ + use DatabaseHelperTrait; + + /** + * The metamodel we work on. + * + * @var IMetaModel + */ + private $metaModel; + + /** + * The MetaModel table name. + * + * @var string + */ + private $tableName; + + /** + * The used database. + * + * @var Database + */ + private $database; + + /** + * The attribute names. + * + * @var IAttribute[] + */ + private $attributes; + + /** + * The simple attribute names. + * + * @var ISimple[] + */ + private $simpleAttributes; + + /** + * Create a new instance. + * + * @param IMetaModel $metaModel The metamodel we work on. + * @param Database $database The used database. + */ + public function __construct(IMetaModel $metaModel, Database $database) + { + $this->metaModel = $metaModel; + $this->tableName = $metaModel->getTableName(); + $this->database = $database; + $this->setAttributes(array_keys($metaModel->getAttributes())); + } + + /** + * Set the attribute names. + * + * @param string[] $attributeNames The attribute names. + * + * @return ItemRetriever + */ + public function setAttributes(array $attributeNames) + { + $this->attributes = []; + $this->simpleAttributes = []; + + foreach ($this->metaModel->getAttributes() as $name => $attribute) { + if (!in_array($name, $attributeNames)) { + continue; + } + $this->attributes[$name] = $attribute; + if ($attribute instanceof ISimple) { + $this->simpleAttributes[$name] = $attribute; + } + } + + return $this; + } + + /** + * This method is called to retrieve the data for certain items from the database. + * + * @param IdResolver $resolver The ids of the items to retrieve the order of ids is used for sorting of the + * return values. + * + * @return IItems a collection of all matched items, sorted by the id list. + */ + public function findItems(IdResolver $resolver) + { + $ids = $resolver->getIds(); + + if (!$ids) { + return new Items([]); + } + + $result = $this->fetchRows($ids); + // Determine "independent attributes" (complex and translated) and inject their content into the row. + $result = $this->fetchAdditionalAttributes($ids, $result); + $items = []; + foreach ($result as $entry) { + $items[] = new Item($this->metaModel, $entry); + } + + $objItems = new Items($items); + + return $objItems; + } + + /** + * Fetch the "native" database rows with the given ids. + * + * @param string[] $ids The ids of the items to retrieve the order of ids is used for sorting of the return + * values. + + * @return array an array containing the database rows with each column "deserialized". + * + * @SuppressWarnings(PHPMD.Superglobals) + * @SuppressWarnings(PHPMD.CamelCaseVariableName) + */ + private function fetchRows(array $ids) + { + // If we have an attribute restriction, make sure we keep the system columns. See #196. + $system = ['id', 'pid', 'tstamp', 'sorting']; + if ($this->metaModel->hasVariants()) { + $system[] = 'varbase'; + $system[] = 'vargroup'; + } + $attributes = array_merge($system, array_keys($this->simpleAttributes)); + + $rows = $this + ->database + ->prepare( + sprintf( + 'SELECT %1$s FROM %2$s WHERE id IN (%3$s) ORDER BY FIELD(id,%3$s)', + implode(', ', $attributes), + $this->tableName, + $this->buildDatabaseParameterList($ids) + ) + ) + ->execute(array_merge($ids, $ids)); + + if (0 === $rows->numRows) { + return []; + } + + $result = []; + do { + $data = []; + foreach ($system as $key) { + $data[$key] = $rows->$key; + } + foreach ($this->simpleAttributes as $key => $attribute) { + $data[$key] = $attribute->unserializeData($rows->$key); + } + $result[$rows->id] = $data; + } while ($rows->next()); + + return $result; + } + + /** + * This method is called to retrieve the data for certain items from the database. + * + * @param string[] $ids The ids of the items to retrieve the order of ids is used for sorting of the + * return values. + * @param array $result The current values. + * + * @return array an array of all matched items, sorted by the id list. + * + * @throws RuntimeException When an attribute is neither translated nor complex. + */ + private function fetchAdditionalAttributes(array $ids, array $result) + { + $attributes = array_filter($this->attributes, function ($attribute) { + /** @var IAttribute $attribute */ + return $attribute instanceof ITranslated || $attribute instanceof IComplex; + }); + + foreach ($attributes as $attributeName => $attribute) { + /** @var IAttribute $attribute */ + $attributeName = $attribute->getColName(); + + switch (true) { + case ($attribute instanceof ITranslated): + $attributeData = $this->fetchTranslatedAttributeValues($attribute, $ids); + break; + case ($attribute instanceof IComplex): + $attributeData = $attribute->getDataFor($ids); + break; + default: + throw new RuntimeException('Unknown attribute type ' . get_class($attribute)); + } + + foreach (array_keys($result) as $id) { + $result[$id][$attributeName] = isset($attributeData[$id]) ? $attributeData[$id] : null; + } + } + + return $result; + } + + /** + * This method is called to retrieve the data for certain items from the database. + * + * @param ITranslated $attribute The attribute to fetch the values for. + * + * @param string[] $ids The ids of the items to retrieve the order of ids is used for sorting of the return + * values. + * + * @return array an array of all matched items, sorted by the id list. + */ + private function fetchTranslatedAttributeValues(ITranslated $attribute, array $ids) + { + $attributeData = $attribute->getTranslatedDataFor($ids, $this->metaModel->getActiveLanguage()); + $missing = array_diff($ids, array_keys($attributeData)); + + if ($missing) { + $attributeData += $attribute->getTranslatedDataFor($missing, $this->metaModel->getFallbackLanguage()); + } + + return $attributeData; + } +} diff --git a/src/MetaModels/MetaModel.php b/src/MetaModels/MetaModel.php index 66b91d2d0..11795dc16 100644 --- a/src/MetaModels/MetaModel.php +++ b/src/MetaModels/MetaModel.php @@ -28,12 +28,14 @@ namespace MetaModels; -use MetaModels\Attribute\IComplex; -use MetaModels\Attribute\ISimple as ISimpleAttribute; -use MetaModels\Attribute\ITranslated; +use MetaModels\DataAccess\DatabaseHelperTrait; +use MetaModels\DataAccess\IdResolver; +use MetaModels\DataAccess\ItemPersister; +use MetaModels\DataAccess\ItemRetriever; use MetaModels\Filter\Filter; use MetaModels\Attribute\IAttribute; use MetaModels\Filter\IFilter; +use MetaModels\Filter\Rules\SimpleQuery; use MetaModels\Filter\Rules\StaticIdList; /** @@ -44,13 +46,12 @@ * * This class handles all attribute definition instantiation and can be queried for a view instance to certain entries. * - * @SuppressWarnings(PHPMD.ExcessiveClassLength) * @SuppressWarnings(PHPMD.TooManyPublicMethods) - * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) - * We disable these for the moment - to be changed in MetaModel 2.1. */ class MetaModel implements IMetaModel { + use DatabaseHelperTrait; + /** * Information data of this MetaModel instance. * @@ -111,36 +112,6 @@ public function setServiceContainer($serviceContainer) return $this; } - /** - * Retrieve the database instance to use. - * - * @return \Contao\Database - */ - protected function getDatabase() - { - return $this->serviceContainer->getDatabase(); - } - - /** - * Try to unserialize a value. - * - * @param string $value The string to process. - * - * @return mixed - */ - protected function tryUnserialize($value) - { - if (!is_array($value) && (substr($value, 0, 2) == 'a:')) { - $unSerialized = unserialize($value); - } - - if (isset($unSerialized) && is_array($unSerialized)) { - return $unSerialized; - } - - return $value; - } - /** * {@inheritdoc} */ @@ -161,347 +132,6 @@ public function hasAttribute($strAttributeName) return array_key_exists($strAttributeName, $this->arrAttributes); } - /** - * Determine if the given attribute is a complex one. - * - * @param IAttribute $objAttribute The attribute to test. - * - * @return bool true if it is complex, false otherwise. - */ - protected function isComplexAttribute($objAttribute) - { - return $objAttribute instanceof IComplex; - } - - /** - * Determine if the given attribute is a simple one. - * - * @param IAttribute $objAttribute The attribute to test. - * - * @return bool true if it is simple, false otherwise. - */ - protected function isSimpleAttribute($objAttribute) - { - return $objAttribute instanceof ISimpleAttribute; - } - - /** - * Determine if the given attribute is a translated one. - * - * @param IAttribute $objAttribute The attribute to test. - * - * @return bool true if it is translated, false otherwise. - */ - protected function isTranslatedAttribute($objAttribute) - { - return $objAttribute instanceof ITranslated; - } - - /** - * Retrieve all attributes implementing the given interface. - * - * @param string $interface The interface name. - * - * @return array - */ - protected function getAttributeImplementing($interface) - { - $result = array(); - foreach ($this->getAttributes() as $colName => $attribute) { - if ($attribute instanceof $interface) { - $result[$colName] = $attribute; - } - } - - return $result; - } - - /** - * This method retrieves all complex attributes from the current MetaModel. - * - * @return IComplex[] all complex attributes defined for this instance. - */ - protected function getComplexAttributes() - { - return $this->getAttributeImplementing('MetaModels\Attribute\IComplex'); - } - - /** - * This method retrieves all simple attributes from the current MetaModel. - * - * @return ISimpleAttribute[] all simple attributes defined for this instance. - */ - protected function getSimpleAttributes() - { - return $this->getAttributeImplementing('MetaModels\Attribute\ISimple'); - } - - /** - * This method retrieves all translated attributes from the current MetaModel. - * - * @return ITranslated[] all translated attributes defined for this instance. - */ - protected function getTranslatedAttributes() - { - return $this->getAttributeImplementing('MetaModels\Attribute\ITranslated'); - } - - /** - * Narrow down the list of Ids that match the given filter. - * - * @param IFilter|null $objFilter The filter to search the matching ids for. - * - * @return array all matching Ids. - */ - protected function getMatchingIds($objFilter) - { - if ($objFilter) { - $arrFilteredIds = $objFilter->getMatchingIds(); - if ($arrFilteredIds !== null) { - return $arrFilteredIds; - } - } - - // Either no filter object or all ids allowed => return all ids. - // if no id filter is passed, we assume all ids are provided. - $objRow = $this->getDatabase()->execute('SELECT id FROM ' . $this->getTableName()); - - return $objRow->fetchEach('id'); - } - - /** - * Convert a database result to a result array. - * - * @param \Database\Result $objRow The database result. - * - * @param string[] $arrAttrOnly The list of attributes to return, if any. - * - * @return array - */ - protected function convertRowsToResult($objRow, $arrAttrOnly = array()) - { - $arrResult = array(); - - while ($objRow->next()) { - $arrData = array(); - - foreach ($objRow->row() as $strKey => $varValue) { - if ((!$arrAttrOnly) || (in_array($strKey, $arrAttrOnly))) { - $arrData[$strKey] = $varValue; - } - } - - /** @noinspection PhpUndefinedFieldInspection */ - $arrResult[$objRow->id] = $arrData; - } - - return $arrResult; - } - - /** - * Build a list of the correct amount of "?" for use in a db query. - * - * @param array $parameters The parameters. - * - * @return string - */ - protected function buildDatabaseParameterList($parameters) - { - return implode(',', array_fill(0, count($parameters), '?')); - } - - /** - * Fetch the "native" database rows with the given ids. - * - * @param string[] $arrIds The ids of the items to retrieve the order of ids is used for sorting of the return - * values. - * - * @param string[] $arrAttrOnly Names of the attributes that shall be contained in the result, defaults to array() - * which means all attributes. - * - * @return array an array containing the database rows with each column "deserialized". - * - * @SuppressWarnings(PHPMD.Superglobals) - * @SuppressWarnings(PHPMD.CamelCaseVariableName) - */ - protected function fetchRows($arrIds, $arrAttrOnly = array()) - { - $parameters = array_merge($arrIds, $arrIds); - $objRow = $this->getDatabase() - ->prepare( - sprintf( - 'SELECT * FROM %s WHERE id IN (%s) ORDER BY FIELD(id,%s)', - $this->getTableName(), - $this->buildDatabaseParameterList($arrIds), - $this->buildDatabaseParameterList($arrIds) - ) - ) - ->execute($parameters); - - /** @noinspection PhpUndefinedFieldInspection */ - if ($objRow->numRows == 0) { - return array(); - } - - // If we have an attribute restriction, make sure we keep the system columns. See #196. - if ($arrAttrOnly) { - $arrAttrOnly = array_merge($GLOBALS['METAMODELS_SYSTEM_COLUMNS'], $arrAttrOnly); - } - - return $this->convertRowsToResult($objRow, $arrAttrOnly); - } - - /** - * This method is called to retrieve the data for certain items from the database. - * - * @param ITranslated $attribute The attribute to fetch the values for. - * - * @param string[] $ids The ids of the items to retrieve the order of ids is used for sorting of the return - * values. - * - * @return array an array of all matched items, sorted by the id list. - */ - protected function fetchTranslatedAttributeValues(ITranslated $attribute, $ids) - { - $attributeData = $attribute->getTranslatedDataFor($ids, $this->getActiveLanguage()); - $missing = array_diff($ids, array_keys($attributeData)); - - if ($missing) { - $attributeData += $attribute->getTranslatedDataFor($missing, $this->getFallbackLanguage()); - } - - return $attributeData; - } - - /** - * This method is called to retrieve the data for certain items from the database. - * - * @param string[] $ids The ids of the items to retrieve the order of ids is used for sorting of the - * return values. - * - * @param array $result The current values. - * - * @param string[] $attrOnly Names of the attributes that shall be contained in the result, defaults to array() - * which means all attributes. - * - * @return array an array of all matched items, sorted by the id list. - */ - protected function fetchAdditionalAttributes($ids, $result, $attrOnly = array()) - { - $attributes = $this->getAttributeByNames($attrOnly); - $attributeNames = array_intersect( - array_keys($attributes), - array_keys(array_merge($this->getComplexAttributes(), $this->getTranslatedAttributes())) - ); - - foreach ($attributeNames as $attributeName) { - $attribute = $attributes[$attributeName]; - - /** @var IAttribute $attribute */ - $attributeName = $attribute->getColName(); - - // If it is translated, fetch the translated data now. - if ($this->isTranslatedAttribute($attribute)) { - /** @var ITranslated $attribute */ - $attributeData = $this->fetchTranslatedAttributeValues($attribute, $ids); - } else { - /** @var IComplex $attribute */ - $attributeData = $attribute->getDataFor($ids); - } - - foreach (array_keys($result) as $id) { - $result[$id][$attributeName] = isset($attributeData[$id]) ? $attributeData[$id] : null; - } - } - - return $result; - } - - /** - * This method is called to retrieve the data for certain items from the database. - * - * @param int[] $arrIds The ids of the items to retrieve the order of ids is used for sorting of the - * return values. - * - * @param string[] $arrAttrOnly Names of the attributes that shall be contained in the result, defaults to array() - * which means all attributes. - * - * @return \MetaModels\IItems a collection of all matched items, sorted by the id list. - */ - protected function getItemsWithId($arrIds, $arrAttrOnly = array()) - { - $arrIds = array_unique(array_filter($arrIds)); - - if (!$arrIds) { - return new Items(array()); - } - - if (!$arrAttrOnly) { - $arrAttrOnly = array_keys($this->getAttributes()); - } - - $arrResult = $this->fetchRows($arrIds, $arrAttrOnly); - - // Give simple attributes the chance for editing the "simple" data. - foreach ($this->getSimpleAttributes() as $objAttribute) { - // Get current simple attribute. - $strColName = $objAttribute->getColName(); - - // Run each row. - foreach (array_keys($arrResult) as $intId) { - // Do only skip if the key does not exist. Do not use isset() here as "null" is a valid value. - if (!array_key_exists($strColName, $arrResult[$intId])) { - continue; - } - $value = $arrResult[$intId][$strColName]; - $value2 = $objAttribute->unserializeData($arrResult[$intId][$strColName]); - // Deprecated fallback, attributes should deserialize themselves for a long time now. - if ($value === $value2) { - $value2 = $this->tryUnserialize($value); - if ($value !== $value2) { - trigger_error( - sprintf( - 'Attribute type %s should implement method unserializeData() and serializeData().', - $objAttribute->get('type') - ), - E_USER_DEPRECATED - ); - } - } - // End of deprecated fallback. - $arrResult[$intId][$strColName] = $value2; - } - } - - // Determine "independent attributes" (complex and translated) and inject their content into the row. - $arrResult = $this->fetchAdditionalAttributes($arrIds, $arrResult, $arrAttrOnly); - $arrItems = array(); - foreach ($arrResult as $arrEntry) { - $arrItems[] = new Item($this, $arrEntry); - } - - $objItems = new Items($arrItems); - - return $objItems; - } - - /** - * Clone the given filter or create an empty one if no filter has been passed. - * - * @param IFilter|null $objFilter The filter to clone. - * - * @return IFilter the cloned filter. - */ - protected function copyFilter($objFilter) - { - if ($objFilter) { - $objNewFilter = $objFilter->createCopy(); - } else { - $objNewFilter = $this->getEmptyFilter(); - } - return $objNewFilter; - } - /** * {@inheritdoc} */ @@ -576,7 +206,7 @@ public function isTranslated() */ public function hasVariants() { - return $this->arrData['varsupport']; + return (bool) $this->arrData['varsupport']; } /** @@ -645,27 +275,6 @@ public function getAttributeById($intId) return null; } - /** - * Retrieve all attributes with the given names. - * - * @param string[] $attrNames The attribute names, if empty all attributes will be returned. - * - * @return IAttribute[] - */ - protected function getAttributeByNames($attrNames = array()) - { - if (empty($attrNames)) { - return $this->arrAttributes; - } - - $result = array(); - foreach ($attrNames as $attributeName) { - $result[$attributeName] = $this->arrAttributes[$attributeName]; - } - - return $result; - } - /** * {@inheritdoc} */ @@ -674,9 +283,17 @@ public function findById($intId, $arrAttrOnly = array()) if (!$intId) { return null; } - $objItems = $this->getItemsWithId(array($intId), $arrAttrOnly); - if ($objItems && $objItems->first()) { - return $objItems->getItem(); + $database = $this->getDatabase(); + $retriever = new ItemRetriever($this, $database); + $resolver = new IdResolver($this, $database); + $resolver + ->setFilter($this->getEmptyFilter()->addFilterRule(new StaticIdList([$intId]))) + ->setLimit(1); + $items = $retriever + ->setAttributes($arrAttrOnly ?: array_keys($this->arrAttributes)) + ->findItems($resolver); + if ($items && $items->first()) { + return $items->getItem(); } return null; } @@ -690,18 +307,19 @@ public function findByFilter( $intOffset = 0, $intLimit = 0, $strSortOrder = 'ASC', - $arrAttrOnly = array() + $arrAttrOnly = [] ) { - return $this->getItemsWithId( - $this->getIdsFromFilter( - $objFilter, - $strSortBy, - $intOffset, - $intLimit, - $strSortOrder - ), - $arrAttrOnly - ); + $database = $this->getDatabase(); + $retriever = new ItemRetriever($this, $database); + $resolver = IdResolver::create($this, $database); + $resolver + ->setFilter($objFilter) + ->setSortOrder($strSortOrder) + ->setSortBy($strSortBy) + ->setLimit($intLimit) + ->setOffset($intOffset); + + return $retriever->setAttributes($arrAttrOnly ?: array_keys($this->arrAttributes))->findItems($resolver); } /** @@ -711,41 +329,13 @@ public function findByFilter( */ public function getIdsFromFilter($objFilter, $strSortBy = '', $intOffset = 0, $intLimit = 0, $strSortOrder = 'ASC') { - if ([] === $arrFilteredIds = array_filter($this->getMatchingIds($objFilter))) { - return []; - } - - // If desired, sort the entries. - if ($strSortBy != '') { - if ($objSortAttribute = $this->getAttribute($strSortBy)) { - $arrFilteredIds = $objSortAttribute->sortIds($arrFilteredIds, $strSortOrder); - } elseif ('id' === $strSortBy) { - asort($arrFilteredIds); - } elseif (in_array($strSortBy, array('pid', 'tstamp', 'sorting'))) { - // Sort by database values. - $arrFilteredIds = $this - ->getDatabase() - ->prepare( - sprintf( - 'SELECT id FROM %s WHERE id IN(%s) ORDER BY %s %s', - $this->getTableName(), - $this->buildDatabaseParameterList($arrFilteredIds), - $strSortBy, - $strSortOrder - ) - ) - ->execute($arrFilteredIds) - ->fetchEach('id'); - } elseif ($strSortBy == 'random') { - shuffle($arrFilteredIds); - } - } - - // Apply limiting then. - if ($intOffset > 0 || $intLimit > 0) { - $arrFilteredIds = array_slice($arrFilteredIds, $intOffset, $intLimit ?: null); - } - return array_values($arrFilteredIds); + return IdResolver::create($this, $this->getDatabase()) + ->setFilter($objFilter) + ->setSortOrder($strSortOrder) + ->setSortBy($strSortBy) + ->setLimit($intLimit) + ->setOffset($intOffset) + ->getIds(); } /** @@ -753,22 +343,7 @@ public function getIdsFromFilter($objFilter, $strSortBy = '', $intOffset = 0, $i */ public function getCount($objFilter) { - $arrFilteredIds = $this->getMatchingIds($objFilter); - if (count($arrFilteredIds) == 0) { - return 0; - } - - $objRow = $this - ->getDatabase() - ->prepare(sprintf( - 'SELECT COUNT(id) AS count FROM %s WHERE id IN(%s)', - $this->getTableName(), - $this->buildDatabaseParameterList($arrFilteredIds) - )) - ->execute($arrFilteredIds); - - /** @noinspection PhpUndefinedFieldInspection */ - return $objRow->count; + return IdResolver::create($this, $this->getDatabase())->setFilter($objFilter)->count(); } /** @@ -776,12 +351,9 @@ public function getCount($objFilter) */ public function findVariantBase($objFilter) { - $objNewFilter = $this->copyFilter($objFilter); - - $objRow = $this->getDatabase()->execute('SELECT id FROM ' . $this->getTableName() . ' WHERE varbase=1'); - - $objNewFilter->addFilterRule(new StaticIdList($objRow->fetchEach('id'))); - return $this->findByFilter($objNewFilter); + $filter = $this->copyFilter($objFilter); + $filter->addFilterRule(new SimpleQuery('SELECT id FROM ' . $this->getTableName() . ' WHERE varbase=1')); + return $this->findByFilter($filter); } /** @@ -791,21 +363,20 @@ public function findVariants($arrIds, $objFilter) { if (!$arrIds) { // Return an empty result. - return $this->getItemsWithId(array()); + return new Items([]); } - $objNewFilter = $this->copyFilter($objFilter); - $objRow = $this - ->getDatabase() - ->prepare(sprintf( + $filter = $this->copyFilter($objFilter); + $filter->addFilterRule(new SimpleQuery( + sprintf( 'SELECT id,vargroup FROM %s WHERE varbase=0 AND vargroup IN (%s)', $this->getTableName(), $this->buildDatabaseParameterList($arrIds) - )) - ->execute($arrIds); + ), + $arrIds + )); - $objNewFilter->addFilterRule(new StaticIdList($objRow->fetchEach('id'))); - return $this->findByFilter($objNewFilter); + return $this->findByFilter($filter); } /** @@ -815,21 +386,20 @@ public function findVariantsWithBase($arrIds, $objFilter) { if (!$arrIds) { // Return an empty result. - return $this->getItemsWithId(array()); + return new Items([]); } - $objNewFilter = $this->copyFilter($objFilter); + $filter = $this->copyFilter($objFilter); - $objRow = $this - ->getDatabase() - ->prepare(sprintf( + $filter->addFilterRule(new SimpleQuery( + sprintf( 'SELECT id,vargroup FROM %1$s WHERE vargroup IN (SELECT vargroup FROM %1$s WHERE id IN (%2$s))', $this->getTableName(), $this->buildDatabaseParameterList($arrIds) - )) - ->execute($arrIds); + ), + $arrIds + )); - $objNewFilter->addFilterRule(new StaticIdList($objRow->fetchEach('id'))); - return $this->findByFilter($objNewFilter); + return $this->findByFilter($filter); } /** @@ -837,171 +407,20 @@ public function findVariantsWithBase($arrIds, $objFilter) */ public function getAttributeOptions($strAttribute, $objFilter = null) { - $objAttribute = $this->getAttribute($strAttribute); - if ($objAttribute) { - if ($objFilter) { - $arrFilteredIds = $this->getMatchingIds($objFilter); - $arrFilteredIds = $objAttribute->sortIds($arrFilteredIds, 'ASC'); - return $objAttribute->getFilterOptions($arrFilteredIds, true); - } else { - return $objAttribute->getFilterOptions(null, true); - } - } - - return array(); - } - - /** - * Update the value of a native column for the given ids with the given data. - * - * @param string $strColumn The column name to update (i.e. tstamp). - * - * @param array $arrIds The ids of the rows that shall be updated. - * - * @param mixed $varData The data to save. If this is an array, it is automatically serialized. - * - * @return void - */ - protected function saveSimpleColumn($strColumn, $arrIds, $varData) - { - if (is_array($varData)) { - $varData = serialize($varData); - } - - $this - ->getDatabase() - ->prepare( - sprintf( - 'UPDATE %s SET %s=? WHERE id IN (%s)', - $this->getTableName(), - $strColumn, - implode(',', $arrIds) - ) - ) - ->execute($varData); - } - - /** - * Update an attribute for the given ids with the given data. - * - * @param IAttribute $objAttribute The attribute to save. - * - * @param array $arrIds The ids of the rows that shall be updated. - * - * @param mixed $varData The data to save in raw data. - * - * @param string $strLangCode The language code to save. - * - * @return void - * - * @throws \RuntimeException When an unknown attribute type is encountered. - */ - protected function saveAttribute($objAttribute, $arrIds, $varData, $strLangCode) - { - // Call the serializeData for all simple attributes. - if ($this->isSimpleAttribute($objAttribute)) { - /** @var \MetaModels\Attribute\ISimple $objAttribute */ - $varData = $objAttribute->serializeData($varData); - } - - $arrData = array(); - foreach ($arrIds as $intId) { - $arrData[$intId] = $varData; - } - - // Check for translated fields first, then for complex and save as simple then. - if ($strLangCode && $this->isTranslatedAttribute($objAttribute)) { - /** @var ITranslated $objAttribute */ - $objAttribute->setTranslatedDataFor($arrData, $strLangCode); - } elseif ($this->isComplexAttribute($objAttribute)) { - // Complex saving. - $objAttribute->setDataFor($arrData); - } elseif ($this->isSimpleAttribute($objAttribute)) { - $objAttribute->setDataFor($arrData); - } else { - throw new \RuntimeException( - 'Unknown attribute type, can not save. Interfaces implemented: ' . - implode(', ', class_implements($objAttribute)) - ); - } - } - - /** - * Update the variants with the value if needed. - * - * @param IItem $item The item to save. - * - * @param string $activeLanguage The language the values are in. - * - * @param int[] $allIds The ids of all variants. - * - * @param bool $baseAttributes If also the base attributes get updated as well. - * - * @return void - */ - protected function updateVariants($item, $activeLanguage, $allIds, $baseAttributes = false) - { - foreach ($this->getAttributes() as $strAttributeId => $objAttribute) { - // Skip unset attributes. - if (!$item->isAttributeSet($objAttribute->getColName())) { - continue; - } - - if (!$baseAttributes && $item->isVariant() && !($objAttribute->get('isvariant'))) { - // Skip base attribute. - continue; - } - - if ($item->isVariantBase() && !($objAttribute->get('isvariant'))) { - // We have to override in variants. - $arrIds = $allIds; - } else { - $arrIds = array($item->get('id')); - } - $this->saveAttribute($objAttribute, $arrIds, $item->get($strAttributeId), $activeLanguage); + if (null === ($attribute = $this->getAttribute($strAttribute))) { + return []; } - } - /** - * Create a new item in the database. - * - * @param IItem $item The item to be created. - * - * @return void - */ - protected function createNewItem($item) - { - $arrData = array - ( - 'tstamp' => $item->get('tstamp') - ); + if ($objFilter) { + $filteredIds = IdResolver::create($this, $this->getDatabase()) + ->setFilter($objFilter) + ->setSortBy($strAttribute) + ->getIds(); - $blnNewBaseItem = false; - if ($this->hasVariants()) { - // No variant group is given, so we have a complete new base item this should be a workaround for these - // values should be set by the GeneralDataMetaModel or whoever is calling this method. - if ($item->get('vargroup') === null) { - $item->set('varbase', '1'); - $item->set('vargroup', '0'); - $blnNewBaseItem = true; - } - $arrData['varbase'] = $item->get('varbase'); - $arrData['vargroup'] = $item->get('vargroup'); + return $attribute->getFilterOptions($filteredIds, true); } - /** @noinspection PhpUndefinedFieldInspection */ - $intItemId = $this - ->getDatabase() - ->prepare('INSERT INTO ' . $this->getTableName() . ' %s') - ->set($arrData) - ->execute() - ->insertId; - $item->set('id', $intItemId); - - // Add the variant group equal to the id. - if ($blnNewBaseItem) { - $this->saveSimpleColumn('vargroup', array($item->get('id')), $item->get('id')); - } + return $attribute->getFilterOptions(null, true); } /** @@ -1018,30 +437,8 @@ public function saveItem($objItem, $timestamp = null) // @codingStandardsIgnoreEnd } - $baseAttributes = $this->saveBaseColumns($objItem, $timestamp ?: \time()); - if ($this->isTranslated()) { - $strActiveLanguage = $this->getActiveLanguage(); - } else { - $strActiveLanguage = null; - } - - $arrAllIds = array(); - if ($objItem->isVariantBase()) { - $objVariants = $this->findVariantsWithBase(array($objItem->get('id')), null); - foreach ($objVariants as $objVariant) { - /** @var IItem $objVariant */ - $arrAllIds[] = $objVariant->get('id'); - } - } - - $this->updateVariants($objItem, $strActiveLanguage, $arrAllIds, $baseAttributes); - - // Tell all attributes that the model has been saved. Useful for alias fields, edit counters etc. - foreach ($this->getAttributes() as $objAttribute) { - if ($objItem->isAttributeSet($objAttribute->getColName())) { - $objAttribute->modelSaved($objItem); - } - } + $persister = new ItemPersister($this, $this->getDatabase()); + $persister->saveItem($objItem, $timestamp ?: \time()); } /** @@ -1049,32 +446,8 @@ public function saveItem($objItem, $timestamp = null) */ public function delete(IItem $objItem) { - $arrIds = array($objItem->get('id')); - // Determine if the model is a variant base and if so, fetch the variants additionally. - if ($objItem->isVariantBase()) { - $objVariants = $objItem->getVariants(new Filter($this)); - foreach ($objVariants as $objVariant) { - /** @var IItem $objVariant */ - $arrIds[] = $objVariant->get('id'); - } - } - - // Complex attributes shall delete their values first. - foreach ($this->getAttributes() as $objAttribute) { - if ($this->isComplexAttribute($objAttribute)) { - /** @var IComplex $objAttribute */ - $objAttribute->unsetDataFor($arrIds); - } - } - // Now make the real row disappear. - $this - ->getDatabase() - ->prepare(sprintf( - 'DELETE FROM %s WHERE id IN (%s)', - $this->getTableName(), - $this->buildDatabaseParameterList($arrIds) - )) - ->execute($arrIds); + $persister = new ItemPersister($this, $this->getDatabase()); + $persister->deleteItem($objItem); } /** @@ -1109,31 +482,49 @@ public function getView($intViewId = 0) } /** - * Save the base columns of an item and return true if it is a new item. + * Clone the given filter or create an empty one if no filter has been passed. * - * @param IItem $item The item to save. - * @param int $timestamp The timestamp to use. + * @param IFilter|null $objFilter The filter to clone. * - * @return bool + * @return IFilter the cloned filter. */ - private function saveBaseColumns(IItem $item, $timestamp) + private function copyFilter($objFilter) { - $isNew = false; - $item->set('tstamp', $timestamp); - if (!$item->get('id')) { - $isNew = true; - $this->createNewItem($item); + if ($objFilter) { + $objNewFilter = $objFilter->createCopy(); + } else { + $objNewFilter = $this->getEmptyFilter(); } + return $objNewFilter; + } - // Update system columns. - if (null !== $item->get('pid')) { - $this->saveSimpleColumn('pid', [$item->get('id')], $item->get('pid')); + /** + * Retrieve the database instance to use. + * + * @return \Contao\Database + */ + private function getDatabase() + { + return $this->serviceContainer->getDatabase(); + } + + /** + * Try to unserialize a value. + * + * @param string $value The string to process. + * + * @return mixed + */ + private function tryUnserialize($value) + { + if (!is_array($value) && (substr($value, 0, 2) == 'a:')) { + $unSerialized = unserialize($value); } - if (null !== $item->get('sorting')) { - $this->saveSimpleColumn('sorting', [$item->get('id')], $item->get('sorting')); + + if (isset($unSerialized) && is_array($unSerialized)) { + return $unSerialized; } - $this->saveSimpleColumn('tstamp', [$item->get('id')], $item->get('tstamp')); - return $isNew; + return $value; } }