From b30d01628f94228b93706be235331df16adce7e0 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Tue, 28 Feb 2017 11:23:22 +0100 Subject: [PATCH 1/6] Extract saving and deleting of items in own class --- .../DataAccess/DatabaseHelperTrait.php | 39 ++ src/MetaModels/DataAccess/ItemPersister.php | 353 ++++++++++++++++++ src/MetaModels/MetaModel.php | 223 +---------- 3 files changed, 397 insertions(+), 218 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..d7675f9b9 --- /dev/null +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -0,0 +1,353 @@ + + * @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. + * + * @return void + */ + public function saveItem(IItem $item) + { + $baseAttributes = false; + $item->set('tstamp', time()); + 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 8af02a061..f5716c907 100644 --- a/src/MetaModels/MetaModel.php +++ b/src/MetaModels/MetaModel.php @@ -30,6 +30,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; @@ -839,203 +840,13 @@ 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} */ public function saveItem($objItem) { - $baseAttributes = false; - $objItem->set('tstamp', time()); - if (!$objItem->get('id')) { - $baseAttributes = true; - $this->createNewItem($objItem); - } - - // Update system columns. - if ($objItem->get('pid') !== null) { - $this->saveSimpleColumn('pid', array($objItem->get('id')), $objItem->get('pid')); - } - if ($objItem->get('sorting') !== null) { - $this->saveSimpleColumn('sorting', array($objItem->get('id')), $objItem->get('sorting')); - } - $this->saveSimpleColumn('tstamp', array($objItem->get('id')), $objItem->get('tstamp')); - - 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); } /** @@ -1043,32 +854,8 @@ public function saveItem($objItem) */ 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 c93a18de8231abfe77018d651ee289c3a7bf2b72 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Tue, 28 Feb 2017 20:51:13 +0100 Subject: [PATCH 2/6] Refactor out item retrieval into ItemRetriever In addition we also now have an IdResolver --- src/MetaModels/DataAccess/IdResolver.php | 335 +++++++++++ src/MetaModels/DataAccess/ItemRetriever.php | 261 +++++++++ src/MetaModels/MetaModel.php | 609 ++++---------------- 3 files changed, 714 insertions(+), 491 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..082a3d965 --- /dev/null +++ b/src/MetaModels/DataAccess/IdResolver.php @@ -0,0 +1,335 @@ + + * @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() + { + $filteredIds = $this->getMatchingIds(); + + // 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 (null !== ($attribute = $this->metaModel->getAttribute($this->sortBy))): + return $attribute->sortIds($filteredIds, $this->sortOrder); + case (in_array($this->sortBy, ['id', '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 f5716c907..28c394fde 100644 --- a/src/MetaModels/MetaModel.php +++ b/src/MetaModels/MetaModel.php @@ -27,13 +27,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; /** @@ -43,9 +44,13 @@ * @see MetaModelFactory::byTableName() to instantiate a MetaModel by its table name. * * This class handles all attribute definition instantiation and can be queried for a view instance to certain entries. + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class MetaModel implements IMetaModel { + use DatabaseHelperTrait; + /** * Information data of this MetaModel instance. * @@ -106,36 +111,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} */ @@ -156,347 +131,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} */ @@ -571,7 +205,7 @@ public function isTranslated() */ public function hasVariants() { - return $this->arrData['varsupport']; + return (bool) $this->arrData['varsupport']; } /** @@ -640,27 +274,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} */ @@ -669,9 +282,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; } @@ -685,18 +306,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 = new IdResolver($this, $database); + $resolver + ->setFilter($objFilter) + ->setSortOrder($strSortOrder) + ->setSortBy($strSortBy) + ->setLimit($intLimit) + ->setOffset($intOffset); + + return $retriever->setAttributes($arrAttrOnly ?: array_keys($this->arrAttributes))->findItems($resolver); } /** @@ -704,37 +326,13 @@ public function findByFilter( */ public function getIdsFromFilter($objFilter, $strSortBy = '', $intOffset = 0, $intLimit = 0, $strSortOrder = 'ASC') { - $arrFilteredIds = $this->getMatchingIds($objFilter); - - // If desired, sort the entries. - if ($arrFilteredIds && $strSortBy != '') { - if ($objSortAttribute = $this->getAttribute($strSortBy)) { - $arrFilteredIds = $objSortAttribute->sortIds($arrFilteredIds, $strSortOrder); - } elseif (in_array($strSortBy, array('id', '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 $arrFilteredIds; + return IdResolver::create($this, $this->getDatabase()) + ->setFilter($objFilter) + ->setSortOrder($strSortOrder) + ->setSortBy($strSortBy) + ->setLimit($intLimit) + ->setOffset($intOffset) + ->getIds(); } /** @@ -742,22 +340,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(); } /** @@ -765,12 +348,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); } /** @@ -780,21 +360,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); } /** @@ -804,21 +383,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); } /** @@ -826,18 +404,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 []; } - return array(); + if ($objFilter) { + $filteredIds = IdResolver::create($this, $this->getDatabase()) + ->setFilter($objFilter) + ->setSortBy($strAttribute) + ->getIds(); + + return $attribute->getFilterOptions($filteredIds, true); + } + + return $attribute->getFilterOptions(null, true); } /** @@ -888,4 +468,51 @@ public function getView($intViewId = 0) { return $this->getServiceContainer()->getRenderSettingFactory()->createCollection($this, $intViewId); } + + /** + * 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. + */ + private function copyFilter($objFilter) + { + 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(); + } + + /** + * 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 (isset($unSerialized) && is_array($unSerialized)) { + return $unSerialized; + } + + return $value; + } } From 9df53caebaaeacf28aa2b7040fe817644f9c1142 Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Wed, 1 Mar 2017 03:09:38 +0100 Subject: [PATCH 3/6] 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 6ac9da9f04e1efd487a4e03db447f69aea4fbb3f Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Fri, 17 Mar 2017 11:08:22 +0100 Subject: [PATCH 4/6] 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 d7675f9b9..9b75d3f8c 100644 --- a/src/MetaModels/DataAccess/ItemPersister.php +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -212,7 +212,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 f21ba95acd594790b1edef7bdc3c5aefef10550f Mon Sep 17 00:00:00 2001 From: Christian Schiffler Date: Fri, 17 Mar 2017 14:47:07 +0100 Subject: [PATCH 5/6] 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 9b75d3f8c..f676396e3 100644 --- a/src/MetaModels/DataAccess/ItemPersister.php +++ b/src/MetaModels/DataAccess/ItemPersister.php @@ -212,7 +212,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 60388595f3d7c7e965f994d12c8303768e244b95 Mon Sep 17 00:00:00 2001 From: Ingolf Steinhardt Date: Mon, 24 Apr 2017 14:15:44 +0200 Subject: [PATCH 6/6] Hotfix #1005 delete empty items --- .../Attribute/TranslatedReference.php | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/MetaModels/Attribute/TranslatedReference.php b/src/MetaModels/Attribute/TranslatedReference.php index a4dd63d65..14ac70412 100644 --- a/src/MetaModels/Attribute/TranslatedReference.php +++ b/src/MetaModels/Attribute/TranslatedReference.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2016 The MetaModels team. + * (c) 2012-2017 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,7 +16,7 @@ * @author David Maack * @author Stefan Heimes * @author Ingolf Steinhardt - * @copyright 2012-2016 The MetaModels team. + * @copyright 2012-2017 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0 * @filesource */ @@ -321,19 +321,30 @@ public function setTranslatedDataFor($arrValues, $strLangCode) $arrExisting = array_keys($this->getTranslatedDataFor($arrIds, $strLangCode)); $arrNewIds = array_diff($arrIds, $arrExisting); - // Update existing values. - $strQuery = 'UPDATE ' . $this->getValueTable() . ' %s'; + // Update existing values - delete if empty. + $strQueryUpdate = 'UPDATE ' . $this->getValueTable() . ' %s'; + $strQueryDelete = 'DELETE FROM ' . $this->getValueTable(); + foreach ($arrExisting as $intId) { $arrWhere = $this->getWhere($intId, $strLangCode); - $objDB->prepare($strQuery . ($arrWhere ? ' WHERE ' . $arrWhere['procedure'] : '')) - ->set($this->getSetValues($arrValues[$intId], $intId, $strLangCode)) - ->execute(($arrWhere ? $arrWhere['params'] : null)); + + if ($arrValues[$intId]['value'] != '') { + $objDB->prepare($strQueryUpdate . ($arrWhere ? ' WHERE ' . $arrWhere['procedure'] : '')) + ->set($this->getSetValues($arrValues[$intId], $intId, $strLangCode)) + ->execute(($arrWhere ? $arrWhere['params'] : null)); + } else { + $objDB->prepare($strQueryDelete . ($arrWhere ? ' WHERE ' . $arrWhere['procedure'] : '')) + ->execute(($arrWhere ? $arrWhere['params'] : null)); + } } // Insert the new values. - $strQuery = 'INSERT INTO ' . $this->getValueTable() . ' %s'; + $strQueryInsert = 'INSERT INTO ' . $this->getValueTable() . ' %s'; foreach ($arrNewIds as $intId) { - $objDB->prepare($strQuery) + if ($arrValues[$intId]['value'] == '') { + continue; + } + $objDB->prepare($strQueryInsert) ->set($this->getSetValues($arrValues[$intId], $intId, $strLangCode)) ->execute(); }