From 6327d23657bbc7235d42059ada9a6b6f117f1983 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Tue, 28 Feb 2017 11:23:22 +0100 Subject: [PATCH 1/7] Extract saving and deleting of items in own class --- .../DataAccess/DatabaseHelperTrait.php | 39 ++ src/MetaModels/DataAccess/ItemPersister.php | 356 ++++++++++++++++++ src/MetaModels/MetaModel.php | 208 +--------- 3 files changed, 400 insertions(+), 203 deletions(-) create mode 100644 src/MetaModels/DataAccess/DatabaseHelperTrait.php create mode 100644 src/MetaModels/DataAccess/ItemPersister.php 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/ItemPersister.php b/src/MetaModels/DataAccess/ItemPersister.php new file mode 100644 index 000000000..251b074c6 --- /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 = []; + 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', $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 (%3$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/MetaModel.php b/src/MetaModels/MetaModel.php index 66b91d2d0..c712c105f 100644 --- a/src/MetaModels/MetaModel.php +++ b/src/MetaModels/MetaModel.php @@ -31,6 +31,7 @@ use MetaModels\Attribute\IComplex; use MetaModels\Attribute\ISimple as ISimpleAttribute; use MetaModels\Attribute\ITranslated; +use MetaModels\DataAccess\ItemPersister; use MetaModels\Filter\Filter; use MetaModels\Attribute\IAttribute; use MetaModels\Filter\IFilter; @@ -851,159 +852,6 @@ public function getAttributeOptions($strAttribute, $objFilter = null) 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); - } - } - - /** - * 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') - ); - - $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'); - } - - /** @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')); - } - } - /** * {@inheritdoc} */ @@ -1018,30 +866,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 +875,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); } /** From 156a09658167954f009c652a817d3f4d174de606 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Tue, 28 Feb 2017 20:51:13 +0100 Subject: [PATCH 2/7] Refactor out item retrieval into ItemRetriever In addition we also now have an IdResolver --- src/MetaModels/DataAccess/IdResolver.php | 340 +++++++++++ src/MetaModels/DataAccess/ItemRetriever.php | 261 +++++++++ src/MetaModels/MetaModel.php | 619 ++++---------------- 3 files changed, 705 insertions(+), 515 deletions(-) create mode 100644 src/MetaModels/DataAccess/IdResolver.php create mode 100644 src/MetaModels/DataAccess/ItemRetriever.php 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/ItemRetriever.php b/src/MetaModels/DataAccess/ItemRetriever.php new file mode 100644 index 000000000..e67e3d635 --- /dev/null +++ b/src/MetaModels/DataAccess/ItemRetriever.php @@ -0,0 +1,261 @@ + + * @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) + { + $attributeNames = array_diff(array_keys($this->attributes), array_keys($this->simpleAttributes)); + $attributes = array_filter($this->attributes, function ($attribute) use ($attributeNames) { + /** @var IAttribute $attribute */ + return in_array($attribute->getColName(), $attributeNames); + }); + + 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 c712c105f..11795dc16 100644 --- a/src/MetaModels/MetaModel.php +++ b/src/MetaModels/MetaModel.php @@ -28,13 +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; /** @@ -45,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. * @@ -112,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} */ @@ -162,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} */ @@ -577,7 +206,7 @@ public function isTranslated() */ public function hasVariants() { - return $this->arrData['varsupport']; + return (bool) $this->arrData['varsupport']; } /** @@ -646,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} */ @@ -675,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; } @@ -691,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); } /** @@ -712,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(); } /** @@ -754,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(); } /** @@ -777,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); } /** @@ -792,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); } /** @@ -816,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); } /** @@ -838,18 +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); - } + if (null === ($attribute = $this->getAttribute($strAttribute))) { + return []; + } + + if ($objFilter) { + $filteredIds = IdResolver::create($this, $this->getDatabase()) + ->setFilter($objFilter) + ->setSortBy($strAttribute) + ->getIds(); + + return $attribute->getFilterOptions($filteredIds, true); } - return array(); + return $attribute->getFilterOptions(null, true); } /** @@ -911,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; + } + + /** + * Retrieve the database instance to use. + * + * @return \Contao\Database + */ + private function getDatabase() + { + return $this->serviceContainer->getDatabase(); + } - // Update system columns. - if (null !== $item->get('pid')) { - $this->saveSimpleColumn('pid', [$item->get('id')], $item->get('pid')); + /** + * 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; } } From 32e894e632b5177244dcb2592a0c110ca223d53a Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Wed, 1 Mar 2017 03:09:38 +0100 Subject: [PATCH 3/7] We must not ignore simple attributes in additional We also have hybrid attributes (like select) which are both, ISimple and IComplex. We might also have all other kinds of combinations. Therefore we now have the sequence of: - Always fetch simple attributes - Then try translated - Finally try complex --- src/MetaModels/DataAccess/ItemRetriever.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/MetaModels/DataAccess/ItemRetriever.php b/src/MetaModels/DataAccess/ItemRetriever.php index e67e3d635..3c58f2ff6 100644 --- a/src/MetaModels/DataAccess/ItemRetriever.php +++ b/src/MetaModels/DataAccess/ItemRetriever.php @@ -208,10 +208,9 @@ private function fetchRows(array $ids) */ private function fetchAdditionalAttributes(array $ids, array $result) { - $attributeNames = array_diff(array_keys($this->attributes), array_keys($this->simpleAttributes)); - $attributes = array_filter($this->attributes, function ($attribute) use ($attributeNames) { + $attributes = array_filter($this->attributes, function ($attribute) { /** @var IAttribute $attribute */ - return in_array($attribute->getColName(), $attributeNames); + return $attribute instanceof ITranslated || $attribute instanceof IComplex; }); foreach ($attributes as $attributeName => $attribute) { From 5b7f65888d0e5721808a62ed1492225a1b5201ed Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Fri, 17 Mar 2017 11:08:22 +0100 Subject: [PATCH 4/7] Fix #1097 correct parameter index in query --- src/MetaModels/DataAccess/ItemPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MetaModels/DataAccess/ItemPersister.php b/src/MetaModels/DataAccess/ItemPersister.php index 251b074c6..9473c85bb 100644 --- a/src/MetaModels/DataAccess/ItemPersister.php +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -215,7 +215,7 @@ private function saveRawColumns(array $columns, array $ids) ->database ->prepare( sprintf( - 'UPDATE %1$s %%s=? WHERE id IN (%3$s)', + 'UPDATE %1$s %%s=? WHERE id IN (%2$s)', $this->tableName, $this->buildDatabaseParameterList($ids) ) From 263faebc2e7f89df6626fdf4ac180a8c4a1a9055 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Fri, 17 Mar 2017 14:47:07 +0100 Subject: [PATCH 5/7] Fix #1097 correct parameter values in query --- src/MetaModels/DataAccess/ItemPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MetaModels/DataAccess/ItemPersister.php b/src/MetaModels/DataAccess/ItemPersister.php index 9473c85bb..2759bd085 100644 --- a/src/MetaModels/DataAccess/ItemPersister.php +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -215,7 +215,7 @@ private function saveRawColumns(array $columns, array $ids) ->database ->prepare( sprintf( - 'UPDATE %1$s %%s=? WHERE id IN (%2$s)', + 'UPDATE %1$s %%s WHERE id IN (%2$s)', $this->tableName, $this->buildDatabaseParameterList($ids) ) From 187e3833b2f9cd62805367be4b1bb49402a5ee9a Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Tue, 30 May 2017 22:42:27 +0200 Subject: [PATCH 6/7] Force id to be string See contao-community-alliance/dc-general#342 --- src/MetaModels/DataAccess/ItemPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MetaModels/DataAccess/ItemPersister.php b/src/MetaModels/DataAccess/ItemPersister.php index 2759bd085..3ed2730f0 100644 --- a/src/MetaModels/DataAccess/ItemPersister.php +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -191,7 +191,7 @@ private function createNewItem(IItem $item) ->set($data) ->execute() ->insertId; - $item->set('id', $itemId); + $item->set('id', (string) $itemId); // Set the variant group equal to the id. if ($isNewBaseItem) { From d329d9d0a3b35130ee62b22fb2184ff560226738 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Tue, 6 Mar 2018 12:25:02 +0100 Subject: [PATCH 7/7] Add current item to variant ids --- src/MetaModels/DataAccess/ItemPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MetaModels/DataAccess/ItemPersister.php b/src/MetaModels/DataAccess/ItemPersister.php index 3ed2730f0..7e02fdbe4 100644 --- a/src/MetaModels/DataAccess/ItemPersister.php +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -108,7 +108,7 @@ public function saveItem(IItem $item, $timestamp) $language = null; } - $variantIds = []; + $variantIds = [$itemId]; if ($item->isVariantBase()) { $variants = $this->metaModel->findVariantsWithBase([$itemId], null); foreach ($variants as $objVariant) {