<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Christopher Boelter <c.boelter@cogizz.de>
 * @author     David Greminger <david.greminger@1up.io>
 * @author     David Maack <david.maack@arcor.de>
 * @author     Martin Treml <github@r2pi.net>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     Chris Raidler <c.raidler@rad-consulting.ch>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Ingolf Steinhardt <info@e-spin.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;
use MetaModels\Attribute\IAttribute;
use MetaModels\Attribute\IComplex;
use MetaModels\Attribute\ISimple as ISimpleAttribute;
use MetaModels\Attribute\ITranslated;
use MetaModels\Filter\Filter;
use MetaModels\Filter\IFilter;
use MetaModels\Filter\Rules\StaticIdList;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * This is the main MetaModel class.
 *
 * @see MetaModelFactory::byId()        to instantiate a MetaModel by its ID.
 * @see MetaModelFactory::byTableName() to instantiate a MetaModel by its table name.
 *
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 */
class MetaModel implements IMetaModel
{
    /**
     * Information data of this MetaModel instance.
     *
     * This is the data from tl_metamodel.
     *
     * @var array
     */
    protected $arrData = array();

    /**
     * This holds all attribute instances.
     *
     * Association is $colName => object
     *
     * @var array
     */
    protected $arrAttributes = array();

    /**
     * The service container.
     *
     * @var IMetaModelsServiceContainer
     */
    protected $serviceContainer;

    /**
     * The database connection.
     *
     * @var Connection
     */
    private $connection;

    /**
     * The event dispatcher.
     *
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * The system columns to preserve.
     *
     * @var string[]
     */
    private $systemColumns;

    /**
     * The cache existing ids.
     *
     * @var array
     */
    private $existingIds = [];

    /**
     * Instantiate a MetaModel.
     *
     * @param array $arrData                            The information array, for information on the available
     *                                                  columns, refer to documentation of table tl_metamodel.
     * @param EventDispatcherInterface|null $dispatcher The event dispatcher.
     * @param Connection|null               $connection The database connection.
     */
    public function __construct(
        $arrData,
        EventDispatcherInterface $dispatcher = null,
        Connection $connection = null
    ) {
        foreach ($arrData as $strKey => $varValue) {
            $this->arrData[$strKey] = $this->tryUnserialize($varValue);
        }
        $this->systemColumns = array_key_exists('system_columns', $arrData) ? $arrData['system_columns'] : [];

        $this->connection = $connection;
        $this->dispatcher = $dispatcher;
        if (null === $this->dispatcher) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                'Not passing the event dispatcher as 2nd argument to "' . __METHOD__ . '" is deprecated ' .
                'and will cause an error in MetaModels 3.0',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }
        if (null === $this->connection) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                'Not passing the database connection as 3rd argument to "' . __METHOD__ . '" is deprecated ' .
                'and will cause an error in MetaModels 3.0',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }
    }

    /**
     * {@inheritdoc}
     *
     * @deprecated Inject services via constructor or setter.
     */
    public function getServiceContainer()
    {
        // @codingStandardsIgnoreStart
        @trigger_error(
            '"' .__METHOD__ . '" is deprecated and will get removed.',
            E_USER_DEPRECATED
        );
        // @codingStandardsIgnoreEnd
        return is_callable($this->serviceContainer) ? call_user_func($this->serviceContainer) : $this->serviceContainer;
    }

    /**
     * Set the service container.
     *
     * NOTE: this is deprecated - to prevent triggering deprecation notices, you may pass a closure here which
     * will then return the service container.
     *
     * @param \Closure|IMetaModelsServiceContainer $serviceContainer The service container.
     *
     * @return MetaModel
     *
     * @deprecated Inject services via constructor or setter.
     */
    public function setServiceContainer($serviceContainer, $deprecationNotice = true)
    {
        if ($deprecationNotice) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                '"' .__METHOD__ . '" is deprecated and will get removed.',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }
        $this->serviceContainer = $serviceContainer;

        return $this;
    }

    /**
     * Retrieve the database instance to use.
     *
     * @return \Contao\Database
     *
     * @deprecated Use the doctrine connection instead.
     */
    protected function getDatabase()
    {
        // @codingStandardsIgnoreStart
        @trigger_error(
            '"' .__METHOD__ . '" is deprecated and will get removed.',
            E_USER_DEPRECATED
        );
        // @codingStandardsIgnoreEnd
        return $this->getServiceContainer()->getDatabase();
    }

    /**
     * Try to unserialize a value.
     *
     * @param mixed $value The string to process.
     *
     * @return mixed
     */
    protected function tryUnserialize($value)
    {
        if (!is_array($value)
            && 0 === strpos($value, 'a:')
            && is_array($unSerialized = unserialize($value, ['allowed_classes' => false]))) {
            return $unSerialized;
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function addAttribute(IAttribute $objAttribute)
    {
        if (!$this->hasAttribute($objAttribute->getColName())) {
            $this->arrAttributes[$objAttribute->getColName()] = $objAttribute;
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    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(IComplex::class);
    }

    /**
     * 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(ISimpleAttribute::class);
    }

    /**
     * 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(ITranslated::class);
    }

    /**
     * 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.
        return $this->getConnection()->createQueryBuilder()
            ->select('id')
            ->from($this->getTableName())
            ->execute()
            ->fetchAll(\PDO::FETCH_COLUMN);
    }

    /**
     * Build a list of the correct amount of "?" for use in a db query.
     *
     * @param array $parameters The parameters.
     *
     * @return string
     *
     * @deprecated Obsolete - Use doctrine query builders instead. To be removed in 3.0
     */
    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())
    {
        /** @var QueryBuilder $builder */
        $builder = $this->getConnection()->createQueryBuilder();
        $query   = $builder
            ->select('*')
            ->from($this->getTableName())
            ->where($builder->expr()->in('id', ':values'))
            ->setParameter('values', $arrIds, Connection::PARAM_STR_ARRAY)
            ->orderBy('FIELD(id, :values)')
            ->execute();

        // If we have an attribute restriction, make sure we keep the system columns. See #196.
        if ($arrAttrOnly) {
            $arrAttrOnly = array_merge($this->systemColumns, $arrAttrOnly);
        }

        $result = [];
        while ($row = $query->fetch(\PDO::FETCH_ASSOC)) {
            $data = [];
            foreach ($row as $attribute => $value) {
                if ((!$arrAttrOnly) || (in_array($attribute, $arrAttrOnly, true))) {
                    $data[$attribute] = $value;
                }
            }

            $result[$row['id']] = $data;
        }

        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.
     */
    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] = ($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, $this->dispatcher);
        }

        return new Items($arrItems);
    }

    /**
     * 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}
     */
    public function get($strKey)
    {
        // Try to retrieve via getter method.
        $strGetter = 'get' . $strKey;
        if (method_exists($this, $strGetter)) {
            return $this->$strGetter();
        }

        // Return via raw array if available.
        if (array_key_exists($strKey, $this->arrData)) {
            return $this->arrData[$strKey];
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getTableName()
    {
        return ($this->arrData['tableName'] ?? null);
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return $this->arrData['name'];
    }

    /**
     * {@inheritdoc}
     */
    public function getAttributes()
    {
        return $this->arrAttributes;
    }

    /**
     * {@inheritdoc}
     */
    public function getInVariantAttributes()
    {
        $arrAttributes = $this->getAttributes();
        if (!$this->hasVariants()) {
            return $arrAttributes;
        }
        // Remove all attributes that are selected for overriding.
        foreach ($arrAttributes as $strAttributeId => $objAttribute) {
            if ($objAttribute->get('isvariant')) {
                unset($arrAttributes[$strAttributeId]);
            }
        }
        return $arrAttributes;
    }

    /**
     * {@inheritdoc}
     */
    public function isTranslated()
    {
        return $this->arrData['translated'];
    }

    /**
     * {@inheritdoc}
     */
    public function hasVariants()
    {
        return $this->arrData['varsupport'];
    }

    /**
     * {@inheritdoc}
     */
    public function getAvailableLanguages()
    {
        if ($this->isTranslated()) {
            return array_keys((array) $this->arrData['languages']);
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getFallbackLanguage()
    {
        if ($this->isTranslated()) {
            foreach ($this->arrData['languages'] as $strLangCode => $arrData) {
                if ($arrData['isfallback']) {
                    return $strLangCode;
                }
            }
        }

        return null;
    }

    /**
     * {@inheritdoc}
     *
     * The value is taken from $GLOBALS['TL_LANGUAGE']
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    public function getActiveLanguage()
    {
        $tmp = explode('-', $GLOBALS['TL_LANGUAGE']);
        return array_shift($tmp);
    }

    /**
     * {@inheritdoc}
     */
    public function getAttribute($strAttributeName)
    {
        $arrAttributes = $this->getAttributes();
        return ($arrAttributes[$strAttributeName] ?? null);
    }

    /**
     * {@inheritdoc}
     */
    public function getAttributeById($intId)
    {
        foreach ($this->getAttributes() as $objAttribute) {
            if ($objAttribute->get('id') === $intId) {
                return $objAttribute;
            }
        }
        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}
     */
    public function findById($intId, $arrAttrOnly = array())
    {
        if (!$intId) {
            return null;
        }
        $objItems = $this->getItemsWithId(array($intId), $arrAttrOnly);
        if ($objItems && $objItems->first()) {
            return $objItems->getItem();
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function findByFilter(
        $objFilter,
        $strSortBy = '',
        $intOffset = 0,
        $intLimit = 0,
        $strSortOrder = 'ASC',
        $arrAttrOnly = array()
    ) {
        return $this->getItemsWithId(
            $this->getIdsFromFilter(
                $objFilter,
                $strSortBy,
                $intOffset,
                $intLimit,
                $strSortOrder
            ),
            $arrAttrOnly
        );
    }

    /**
     * {@inheritdoc}
     *
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    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'))) {
                // Build the right key for the cache.
                $sortKey = \sprintf('%s-%s', $strSortBy, \strtolower($strSortOrder));
                // Used the cached ID list, and make a list of wanted ID's with the sorting of the cache.
                if (!isset($this->existingIds[$sortKey])) {
                    $this->existingIds[$sortKey] = [];
                }
                $cacheResult = array_intersect($this->existingIds[$sortKey], $arrFilteredIds);
                // Check if we have all ID's or if we have one missing, now we are using the order of the MM Filter.
                if (array_intersect($arrFilteredIds, $cacheResult) === $arrFilteredIds) {
                    if ($intOffset > 0 || $intLimit > 0) {
                        return array_values(array_slice($cacheResult, $intOffset, $intLimit ?: null));
                    }
                    return array_values($cacheResult);
                }

                // Merge the already known and the new one.
                $fullIdList = array_merge((array) $this->existingIds[$sortKey], $arrFilteredIds);
                $fullIdList = \array_keys(\array_flip($fullIdList));

                // Sort by database values.
                $builder = $this->getConnection()->createQueryBuilder();

                $arrSortedFilteredIds = $builder
                    ->select('id')
                    ->from($this->getTableName())
                    ->where($builder->expr()->in('id', ':values'))
                    ->setParameter('values', $arrFilteredIds, Connection::PARAM_STR_ARRAY)
                    ->orderBy($strSortBy, $strSortOrder)
                    ->execute()
                    ->fetchAll(\PDO::FETCH_COLUMN);

                // Add the new sorted Id's to the cache and use only the wanted.
                $this->existingIds[$sortKey] = $arrSortedFilteredIds;
                $arrFilteredIds              = array_intersect($arrSortedFilteredIds, $arrFilteredIds);
            } elseif ($strSortBy == 'random') {
                shuffle($arrFilteredIds);
            }
        }

        // Apply limiting then.
        if ($intOffset > 0 || $intLimit > 0) {
            $arrFilteredIds = array_slice($arrFilteredIds, $intOffset, $intLimit ?: null);
        }
        return array_values($arrFilteredIds);
    }

    /**
     * {@inheritdoc}
     */
    public function getCount($objFilter)
    {
        $arrFilteredIds = $this->getMatchingIds($objFilter);
        if (0 === count($arrFilteredIds)) {
            return 0;
        }

        $builder = $this->getConnection()->createQueryBuilder();

        return $builder
            ->select('COUNT(id)')
            ->from($this->getTableName())
            ->where($builder->expr()->in('id', ':values'))
            ->setParameter('values', $arrFilteredIds, Connection::PARAM_STR_ARRAY)
            ->execute()
            ->fetch(\PDO::FETCH_COLUMN);
    }

    /**
     * {@inheritdoc}
     */
    public function findVariantBase($objFilter)
    {
        $objNewFilter = $this->copyFilter($objFilter);

        $idList = $this
            ->getConnection()
            ->createQueryBuilder()
            ->select('id')
            ->from($this->getTableName())
            ->where('varbase=1')
            ->execute()
            ->fetchAll(\PDO::FETCH_COLUMN);

        $objNewFilter->addFilterRule(new StaticIdList($idList));
        return $this->findByFilter($objNewFilter);
    }

    /**
     * {@inheritdoc}
     */
    public function findVariants($arrIds, $objFilter)
    {
        if (!$arrIds) {
            // Return an empty result.
            return $this->getItemsWithId(array());
        }
        $objNewFilter = $this->copyFilter($objFilter);

        $builder = $this->getConnection()->createQueryBuilder();

        $idList = $builder
            ->select('id')
            ->from($this->getTableName())
            ->where('varbase=0')
            ->andWhere($builder->expr()->in('vargroup', ':ids'))
            ->setParameter('ids', $arrIds, Connection::PARAM_STR_ARRAY)
            ->execute()
            ->fetchAll(\PDO::FETCH_COLUMN);

        $objNewFilter->addFilterRule(new StaticIdList($idList));
        return $this->findByFilter($objNewFilter);
    }

    /**
     * {@inheritdoc}
     */
    public function findVariantsWithBase($arrIds, $objFilter)
    {
        if (!$arrIds) {
            // Return an empty result.
            return $this->getItemsWithId(array());
        }
        $objNewFilter = $this->copyFilter($objFilter);

        $builder = $this->getConnection()->createQueryBuilder();

        $idList = $builder
            ->select('v.id')
            ->from($this->getTableName(), 'v')
            ->leftJoin('v', $this->getTableName(), 'v2', 'v.vargroup=v2.vargroup')
            ->where($builder->expr()->in('v2.id', ':ids'))
            ->setParameter('ids', $arrIds, Connection::PARAM_STR_ARRAY)
            ->execute()
            ->fetchAll(\PDO::FETCH_COLUMN);

        $objNewFilter->addFilterRule(new StaticIdList($idList));
        return $this->findByFilter($objNewFilter);
    }

    /**
     * {@inheritdoc}
     */
    public function getAttributeOptions($strAttribute, $objFilter = null)
    {
        $objAttribute = $this->getAttribute($strAttribute);
        if ($objAttribute) {
            if ($objFilter) {
                $arrFilteredIds = $this->getMatchingIds($objFilter);
                $arrFilteredIds = $objAttribute->sortIds($arrFilteredIds, 'ASC');
                return $objAttribute->getFilterOptions($arrFilteredIds, true);
            } else {
                return $objAttribute->getFilterOptions(null, true);
            }
        }

        return array();
    }

    /**
     * Update the value of a native column for the given ids with the given data.
     *
     * @param string $strColumn The column name to update (i.e. tstamp).
     *
     * @param array  $arrIds    The ids of the rows that shall be updated.
     *
     * @param mixed  $varData   The data to save. If this is an array, it is automatically serialized.
     *
     * @return void
     */
    protected function saveSimpleColumn($strColumn, $arrIds, $varData)
    {
        if (is_array($varData)) {
            $varData = serialize($varData);
        }

        $builder = $this->getConnection()->createQueryBuilder();

        $builder
            ->update($this->getTableName(), 'v2')
            ->set('v2.' . $strColumn, is_array($varData) ? serialize($varData) : $varData)
            ->where($builder->expr()->in('v2.id', ':ids'))
            ->setParameter('ids', $arrIds, Connection::PARAM_STR_ARRAY)
            ->execute();
    }

    /**
     * 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)
    {
        $data      = ['tstamp' => $item->get('tstamp')];
        $isNewItem = 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');
                $isNewItem = true;
            }
            $data['varbase']  = $item->get('varbase');
            $data['vargroup'] = $item->get('vargroup');
        }

        $connection = $this->getConnection();
        $builder    = $connection->createQueryBuilder();
        $parameters = [];
        foreach (array_keys($data) as $key) {
            $parameters[$key] = ':' . $key;
        }
        $builder
            ->insert($this->getTableName())
            ->values($parameters)
            ->setParameters($data)
            ->execute();

        $item->set('id', $connection->lastInsertId());

        // Add the variant group equal to the id.
        if ($isNewItem) {
            $this->saveSimpleColumn('vargroup', [$item->get('id')], $item->get('id'));
        }
    }

    /**
     * {@inheritdoc}
     */
    public function saveItem($objItem, $timestamp = null)
    {
        if (null === $timestamp) {
            // @codingStandardsIgnoreStart
            @\trigger_error(
                'Not passing a timestamp has been deprecated and will cause an error in MetaModels 3',
                E_USER_DEPRECATED
            );
            // @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);
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    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.
        $builder = $this->getConnection()->createQueryBuilder();

        $builder
            ->delete($this->getTableName())
            ->where($builder->expr()->in('id', ':ids'))
            ->setParameter('ids', $arrIds, Connection::PARAM_STR_ARRAY)
            ->execute();
    }

    /**
     * {@inheritdoc}
     */
    public function getEmptyFilter()
    {
        return new Filter($this);
    }

    /**
     * {@inheritdoc}
     */
    public function prepareFilter($intFilterSettings, $arrFilterUrl)
    {
        // @codingStandardsIgnoreStart
        @trigger_error(
            'Method "' . __METHOD__ . '" is deprecated and will get removed in MetaModels 3.0. ' .
            'Use the "metamodels.filter_setting_factory" service instead.',
            E_USER_DEPRECATED
        );
        // @codingStandardsIgnoreEnd

        $objFilter = $this->getEmptyFilter();
        if ($intFilterSettings) {
            $objFilterSettings = $this->getServiceContainer()->getFilterFactory()->createCollection($intFilterSettings);
            $objFilterSettings->addRules($objFilter, $arrFilterUrl);
        }
        return $objFilter;
    }

    /**
     * {@inheritdoc}
     */
    public function getView($intViewId = 0)
    {
        // @codingStandardsIgnoreStart
        @trigger_error(
            'Method "' . __METHOD__ . '" is deprecated and will get removed in MetaModels 3.0. ' .
            'Use the "metamodels.render_setting_factory" service instead.',
            E_USER_DEPRECATED
        );
        // @codingStandardsIgnoreEnd

        return $this->getServiceContainer()->getRenderSettingFactory()->createCollection($this, $intViewId);
    }

    /**
     * Obtain the doctrine connection.
     *
     * @return Connection
     *
     * @throws \ReflectionException Throws could not connect to database.
     */
    private function getConnection()
    {
        if ($this->connection) {
            return $this->connection;
        }

        $reflection = new \ReflectionProperty(\Contao\Database::class, 'resConnection');
        $reflection->setAccessible(true);

        return $this->connection = $reflection->getValue($this->getDatabase());
    }

    /**
     * Save the base columns of an item and return true if it is a new item.
     *
     * @param IItem $item      The item to save.
     * @param int   $timestamp The timestamp to use.
     *
     * @return bool
     */
    private function saveBaseColumns(IItem $item, $timestamp)
    {
        $isNew = false;
        $item->set('tstamp', $timestamp);
        if (!$item->get('id')) {
            $isNew = true;
            $this->createNewItem($item);
        }

        // Update system columns.
        if (null !== $item->get('pid')) {
            $this->saveSimpleColumn('pid', [$item->get('id')], $item->get('pid'));
        }
        if (null !== $item->get('sorting')) {
            $this->saveSimpleColumn('sorting', [$item->get('id')], $item->get('sorting'));
        }
        $this->saveSimpleColumn('tstamp', [$item->get('id')], $item->get('tstamp'));

        return $isNew;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Cliff Parnitzky <github@cliff-parnitzky.de>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\DcGeneral\DataDefinition\Palette\Condition\Property;

use Contao\System;
use ContaoCommunityAlliance\DcGeneral\Data\ModelInterface;
use ContaoCommunityAlliance\DcGeneral\Data\PropertyValueBag;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\Condition\Property\PropertyConditionInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\LegendInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\PropertyInterface;
use Doctrine\DBAL\Driver\Connection;

/**
 * Condition for the default palette.
 */
class InputScreenRenderModeIs implements PropertyConditionInterface
{
    /**
     * The expected property value.
     *
     * @var string
     */
    protected $desiredState;

    /**
     * The expected property value.
     *
     * @var string[]
     */
    protected static $stateBuffer;

    /**
     * Database connection.
     *
     * @var Connection
     */
    private $connection;

    /**
     * Create a new instance.
     *
     * @param string          $desiredState The desired state.
     *
     * @param Connection|null $connection   Database connection.
     */
    public function __construct($desiredState, Connection $connection = null)
    {
        $this->setRenderMode($desiredState);

        if (null === $connection) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                'Connection is missing. It has to be passed in the constructor. Fallback will be dropped.',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
            $connection = System::getContainer()->get('database_connection');
        }

        $this->connection = $connection;
    }

    /**
     * Set the desired state.
     *
     * @param string $desiredState The desired state.
     *
     * @return InputScreenRenderModeIs
     */
    public function setRenderMode($desiredState)
    {
        $this->desiredState = $desiredState;

        return $this;
    }

    /**
     * Retrieve the desired state.
     *
     * @return string
     */
    public function getRenderMode()
    {
        return $this->desiredState;
    }

    /**
     * Retrieve the type name from an attribute.
     *
     * @param int $value The id of an input screen.
     *
     * @return string
     */
    public function getInputScreenRenderMode($value)
    {
        if (!isset(self::$stateBuffer[$value])) {
            $statement = $this->connection->prepare('SELECT rendermode FROM tl_metamodel_dca WHERE id=? LIMIT 0,1');
            $statement->bindValue(1, $value);
            $statement->execute();

            self::$stateBuffer[$value] = $statement->fetch(\PDO::FETCH_OBJ)->rendermode;
        }

        return self::$stateBuffer[$value];
    }

    /**
     * {@inheritdoc}
     */
    public function match(
        ModelInterface $model = null,
        PropertyValueBag $input = null,
        PropertyInterface $property = null,
        LegendInterface $legend = null
    ) {
        if ($input && $input->hasPropertyValue('pid')) {
            $value = $input->getPropertyValue('pid');
        } elseif ($model) {
            $value = $model->getProperty('pid');
        } else {
            return false;
        }

        return $this->getInputScreenRenderMode($value) == $this->getRenderMode();
    }

    /**
     * {@inheritdoc}
     */
    public function __clone()
    {
    }

    /**
     * Retrieve the service container.
     *
     * @return IMetaModelsServiceContainer
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function getServiceContainer()
    {
        return $GLOBALS['container']['metamodels-service-container'];
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\ViewCombination;

use Doctrine\DBAL\Connection;
use MetaModels\IFactory;

/**
 * This builds the view combinations for an user.
 */
class ViewCombinationBuilder
{
    /**
     * The database connection.
     *
     * @var Connection
     */
    private $connection;

    /**
     * The MetaModels factory.
     *
     * @var IFactory
     */
    private $factory;

    /**
     * Create a new instance.
     *
     * @param Connection $connection The database connection.
     * @param IFactory   $factory    The MetaModels factory.
     */
    public function __construct(Connection $connection, IFactory $factory)
    {
        $this->connection = $connection;
        $this->factory    = $factory;
    }

    /**
     * Retrieve the combinations for the passed user.
     *
     * @param string[] $userGroups The user groups.
     * @param string   $userType   The user type ('fe' or 'be').
     *
     * @return array|null
     *
     * @throws \InvalidArgumentException When the user type is unknown.
     */
    public function getCombinationsForUser($userGroups, $userType)
    {
        $userType = strtolower($userType);
        if ('fe' !== $userType && 'be' !== $userType) {
            throw new \InvalidArgumentException('Unknown user type: ' . $userType);
        }

        return $this->getCombinationsFromDatabase($userGroups, $userType);
    }

    /**
     * Retrieve the palette combinations from the database.
     *
     * @param string $userGroups The user groups of the user to fetch information for.
     * @param string $userType   The user type.
     *
     * @return null|array
     */
    private function getCombinationsFromDatabase($userGroups, $userType)
    {
        if (empty($userGroups)) {
            return null;
        }

        $builder = $this
            ->connection
            ->createQueryBuilder();

        $combinations = $builder
            ->select('*')
            ->from('tl_metamodel_dca_combine')
            ->where($builder->expr()->in($userType . '_group', ':groupList'))
            ->setParameter('groupList', $userGroups, Connection::PARAM_STR_ARRAY)
            ->orWhere($userType . '_group=0')
            ->orderBy('pid')
            ->addOrderBy('sorting')
            ->execute()
            ->fetchAll(\PDO::FETCH_ASSOC);

        $result = [
            'byName' => [],
            'byId' => []
        ];

        foreach ($combinations as $combination) {
            $metaModelId = $combination['pid'];
            if (isset($result['byId'][$metaModelId])) {
                continue;
            }
            $name = $this->factory->translateIdToMetaModelName($metaModelId);

            $result['byId'][$metaModelId] = $result['byName'][$name] = [
                'dca_id'   => $combination['dca_id'],
                'view_id' => $combination['view_id']
            ];
        }

        return $result;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\FilterSetting;

use ContaoCommunityAlliance\DcGeneral\Data\ModelInterface;
use ContaoCommunityAlliance\DcGeneral\EnvironmentInterface;

/**
 * Handles rendering of model from tl_metamodel_filtersetting.
 */
class FilterSettingTypeRendererCore extends AbstractFilterSettingTypeRenderer
{
    /**
     * Retrieve the types this renderer is valid for.
     *
     * @return array
     */
    protected function getTypes()
    {
        return ['idlist', 'simplelookup', 'customsql', 'conditionand', 'conditionor'];
    }

    /**
     * Retrieve the parameters for the label.
     *
     * @param EnvironmentInterface $environment The translator in use.
     *
     * @param ModelInterface       $model       The model.
     *
     * @return array
     */
    protected function getLabelParameters(EnvironmentInterface $environment, ModelInterface $model)
    {
        if ($model->getProperty('type') == 'simplelookup') {
            return $this->getLabelParametersWithAttributeAndUrlParam($environment, $model);
        }
        return $this->getLabelParametersNormal($environment, $model);
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Christopher Bölter <c.boelter@cogizz.de>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @author     binron <rtb@gmx.ch>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Richard Henkenjohann <richardhenkenjohann@googlemail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\DcGeneral\Data;

use ContaoCommunityAlliance\DcGeneral\Data\CollectionInterface;
use ContaoCommunityAlliance\DcGeneral\Data\ConfigInterface;
use ContaoCommunityAlliance\DcGeneral\Data\DCGE;
use ContaoCommunityAlliance\DcGeneral\Data\DefaultCollection;
use ContaoCommunityAlliance\DcGeneral\Data\DefaultConfig;
use ContaoCommunityAlliance\DcGeneral\Data\DefaultFilterOptionCollection;
use ContaoCommunityAlliance\DcGeneral\Data\DefaultLanguageInformation;
use ContaoCommunityAlliance\DcGeneral\Data\DefaultLanguageInformationCollection;
use ContaoCommunityAlliance\DcGeneral\Data\FilterOptionCollectionInterface;
use ContaoCommunityAlliance\DcGeneral\Data\ModelInterface;
use ContaoCommunityAlliance\DcGeneral\Data\MultiLanguageDataProviderInterface;
use Doctrine\DBAL\Connection;
use MetaModels\Attribute\IAttribute;
use MetaModels\Attribute\IComplex;
use MetaModels\Attribute\ITranslated;
use MetaModels\Filter\IFilter;
use MetaModels\IItem;
use MetaModels\IItems;
use MetaModels\IMetaModel;
use MetaModels\Item;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Data driver class for DC_General.
 *
 * @SuppressWarnings(PHPMD.TooManyPublicMethods) - The interface is too complex, maybe split into traits.
 */
class Driver implements MultiLanguageDataProviderInterface
{
    /**
     * Name of current table.
     *
     * @var string
     */
    protected $strTable = null;

    /**
     * The MetaModel this DataContainer is working on.
     *
     * @var IMetaModel
     */
    protected $metaModel = null;

    /**
     * The event dispatcher to pass to items.
     *
     * @var null|EventDispatcherInterface
     */
    private $dispatcher = null;

    /**
     * The current active language.
     *
     * @var string
     */
    protected $strCurrentLanguage;

    /**
     * The database connection.
     *
     * @var Connection|null
     */
    private $connection;

    /**
     * Set dispatcher.
     *
     * @param null|EventDispatcherInterface $dispatcher The new value.
     *
     * @return Driver
     */
    public function setDispatcher($dispatcher)
    {
        $this->dispatcher = $dispatcher;

        return $this;
    }

    /**
     * Set the connection to use.
     *
     * @param Connection $connection The connection.
     *
     * @return Driver
     */
    public function setConnection(Connection $connection)
    {
        $this->connection = $connection;

        return $this;
    }

    /**
     * Delete an item.
     *
     * The given value may be either integer, string or an instance of Model
     *
     * @param mixed $varItem Id or the model itself, to delete.
     *
     * @return void
     *
     * @throws \RuntimeException When an unusable object has been passed.
     */
    public function delete($varItem)
    {
        // Determine the id.
        if (is_object($varItem) && ($varItem instanceof Model)) {
            $objModelItem = $varItem->getItem();
        } else {
            $objModelItem = $this->getMetaModel()->findById($varItem);
        }
        if ($objModelItem) {
            $this->getMetaModel()->delete($objModelItem);
        }
    }

    /**
     * Save a new Version of a record.
     *
     * @param ModelInterface $objModel    The model to be saved.
     *
     * @param string         $strUsername The username that creates the new version.
     *
     * @return void
     *
     * @throws \RuntimeException As this is currently unimplemented, an Exception is thrown.
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function saveVersion(ModelInterface $objModel, $strUsername)
    {
        throw new \RuntimeException('Versioning not supported in MetaModels so far.');
    }

    /**
     * Return a model based of the version information.
     *
     * @param mixed $mixID      The ID of record.
     *
     * @param mixed $mixVersion The ID of the version.
     *
     * @return void
     *
     * @throws \RuntimeException As this is currently unimplemented, an Exception is thrown.
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function getVersion($mixID, $mixVersion)
    {
        throw new \RuntimeException('Versioning not supported in MetaModels so far.');
    }

    /**
     * Set a version as active.
     *
     * @param mixed $mixID      The ID of record.
     *
     * @param mixed $mixVersion The ID of the version.
     *
     * @return void
     *
     * @throws \RuntimeException As this is currently unimplemented, an Exception is thrown.
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function setVersionActive($mixID, $mixVersion)
    {
        throw new \RuntimeException('Versioning not supported in MetaModels so far.');
    }

    /**
     * Return the active version from a record.
     *
     * @param mixed $mixID The ID of the record.
     *
     * @return void
     *
     * @throws \RuntimeException As this is currently unimplemented, an Exception is thrown.
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function getActiveVersion($mixID)
    {
        throw new \RuntimeException('Versioning not supported in MetaModels so far.');
    }

    /**
     * Set a language as active language in Contao and return the previous language.
     *
     * @param string $language The language to set (if any).
     *
     * @return string
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function setLanguage($language = '')
    {
        $previousLanguage = $GLOBALS['TL_LANGUAGE'];
        if (!empty($language) && ($GLOBALS['TL_LANGUAGE'] !== $language)) {
            $GLOBALS['TL_LANGUAGE'] = $language;
        }

        return $previousLanguage;
    }

    /**
     * Retrieve the MetaModel.
     *
     * @return IMetaModel
     *
     * @throws \RuntimeException When the MetaModel could not be retrieved.
     */
    protected function getMetaModel()
    {
        if (!$this->metaModel) {
            if ($this->metaModel === null) {
                throw new \RuntimeException('No MetaModel instance set for ' . $this->strTable);
            }
        }
        return $this->metaModel;
    }

    /**
     * Fetch a single or first record by id or filter.
     *
     * If the model shall be retrieved by id, use $objConfig->setId() to populate the config with an Id.
     *
     * If the model shall be retrieved by filter, use $objConfig->setFilter() to populate the config with a filter.
     *
     * @param ConfigInterface $objConfig The config to use.
     *
     * @return null|ModelInterface
     */
    public function fetch(ConfigInterface $objConfig)
    {
        $backupLanguage = $this->setLanguage($this->getCurrentLanguage());

        if ($objConfig->getId() !== null) {
            $modelId = $objConfig->getId();
        } else {
            $filter  = $this->prepareFilter($objConfig);
            $ids     = $this->getIdsFromFilter($filter, $objConfig);
            $modelId = reset($ids);
        }

        $objItem = $modelId ? $this->getMetaModel()->findById($modelId, $objConfig->getFields() ?: array()) : null;

        $this->setLanguage($backupLanguage);

        if (!$objItem) {
            return null;
        }

        return new Model($objItem);
    }

    /**
     * Set base config with source and other necessary parameter.
     *
     * @param array $arrConfig The configuration to use.
     *
     * @return void
     *
     * @throws \RuntimeException When no source has been defined.
     */
    public function setBaseConfig(array $arrConfig)
    {
        if (!$arrConfig['source']) {
            throw new \RuntimeException('Missing table name.');
        }

        $this->strTable  = $arrConfig['source'];
        $this->metaModel = $arrConfig['metaModel'];
    }

    /**
     * Return empty config object.
     *
     * @return ConfigInterface
     */
    public function getEmptyConfig()
    {
        return DefaultConfig::init();
    }

    /**
     * Fetch an empty single record (new item).
     *
     * @return ModelInterface
     */
    public function getEmptyModel()
    {
        if (!isset($this->dispatcher)) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                'Not setting an "' . EventDispatcherInterface::class .
                '" via "setDispatcher()" is deprecated and will cause an error in MetaModels 3.0.',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }

        $objItem = new Item($this->getMetaModel(), null, $this->dispatcher);
        return new Model($objItem);
    }

    /**
     * Fetch an empty collection.
     *
     * @return CollectionInterface
     */
    public function getEmptyCollection()
    {
        return new DefaultCollection();
    }

    /**
     * Prepare a filter and return it.
     *
     * @param ConfigInterface $configuration The configuration.
     *
     * @return IFilter
     */
    protected function prepareFilter(ConfigInterface $configuration)
    {
        if (null === $this->connection) {
            // @codingStandardsIgnoreStart
            @trigger_error(
                'Not setting a "' . Connection::class .
                '" via "setConnection()" is deprecated and will cause an error in MetaModels 3.0.',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }
        $builder = new FilterBuilder($this->getMetaModel(), $configuration, $this->connection);
        return $builder->build();
    }

    /**
     * Extract the sorting from the given config.
     *
     * @param ConfigInterface $config The configuration to be applied.
     *
     * @return array
     */
    protected function extractSorting($config)
    {
        $sorting = $config->getSorting();

        $sortBy  = key($sorting);
        $sortDir = current($sorting) ?: DCGE::MODEL_SORTING_ASC;

        return array($sortBy, $sortDir);
    }

    /**
     * Fetch the ids via the given filter.
     *
     * @param IFilter         $filter The filter.
     *
     * @param ConfigInterface $config The configuration to be applied.
     *
     * @return string[]
     */
    protected function getIdsFromFilter($filter, $config)
    {
        $sorting = $this->extractSorting($config);

        return $this->getMetaModel()->getIdsFromFilter(
            $filter,
            $sorting[0],
            $config->getStart(),
            $config->getAmount(),
            strtoupper($sorting[1])
        );
    }

    /**
     * Fetch the items via the given filter.
     *
     * @param IFilter         $filter The filter.
     *
     * @param ConfigInterface $config The configuration to be applied.
     *
     * @return IItems|IItem[] The collection of IItem instances that match the given filter.
     */
    protected function getItemsFromFilter($filter, $config)
    {
        $sorting = $this->extractSorting($config);

        return $this->getMetaModel()->findByFilter(
            $filter,
            $sorting[0],
            $config->getStart(),
            $config->getAmount(),
            strtoupper($sorting[1]),
            $config->getFields() ?: array()
        );
    }

    /**
     * Fetch all records (optional filtered, sorted and limited).
     *
     * @param ConfigInterface $objConfig The configuration to be applied.
     *
     * @return CollectionInterface|ModelInterface[]|string[]
     */
    public function fetchAll(ConfigInterface $objConfig)
    {
        $backupLanguage = $this->setLanguage($this->getCurrentLanguage());

        $filter = $this->prepareFilter($objConfig);
        if ($objConfig->getIdOnly()) {
            $this->setLanguage($backupLanguage);

            return $this->getIdsFromFilter($filter, $objConfig);
        }

        $items      = $this->getItemsFromFilter($filter, $objConfig);
        $collection = $this->getEmptyCollection();
        foreach ($items as $objItem) {
            $collection->push(new Model($objItem));
        }
        $this->setLanguage($backupLanguage);

        return $collection;
    }

    /**
     * Retrieve all unique values for the given property.
     *
     * The result set will be an array containing all unique values contained in the MetaModel for the defined
     * attribute in the configuration.
     *
     * Note: this only re-ensembles really used values for at least one data set.
     *
     * The only information being interpreted from the passed config object is the first property to fetch and the
     * filter definition.
     *
     * @param ConfigInterface $objConfig The filter config options.
     *
     * @return FilterOptionCollectionInterface
     *
     * @throws \RuntimeException If improper values have been passed (i.e. not exactly one field requested).
     */
    public function getFilterOptions(ConfigInterface $objConfig)
    {
        $arrProperties = $objConfig->getFields();
        if (count($arrProperties) <> 1) {
            throw new \RuntimeException('objConfig must contain exactly one property to be retrieved.');
        }

        $objFilter = $this->prepareFilter($objConfig);

        $arrValues = $this
            ->getMetaModel()
            ->getAttribute($arrProperties[0])
            ->getFilterOptions($objFilter->getMatchingIds(), true);

        $objCollection = new DefaultFilterOptionCollection();
        foreach ($arrValues as $strKey => $strValue) {
            $objCollection->add($strKey, $strValue);
        }

        return $objCollection;
    }

    /**
     * Return the amount of total items (filtering may be used in the config).
     *
     * @param ConfigInterface $objConfig The filter config options.
     *
     * @return int
     */
    public function getCount(ConfigInterface $objConfig)
    {
        $objFilter = $this->prepareFilter($objConfig);
        return $this->getMetaModel()->getCount($objFilter);
    }

    /**
     * Return a list with all versions for the model with the given Id.
     *
     * @param mixed   $mixID         The ID of the row.
     *
     * @param boolean $blnOnlyActive If true, only active versions will get returned, if false all version will get
     *                               returned.
     *
     * @return CollectionInterface
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function getVersions($mixID, $blnOnlyActive = false)
    {
        // No version support on MetaModels so far, sorry.
        return null;
    }

    /**
     * Determine if a given value is unique within the metamodel.
     *
     * @param string $strField The attribute name.
     *
     * @param mixed  $varNew   The value that shall be checked.
     *
     * @param int    $intId    The (optional) id of the item currently in scope - pass null for new items.
     *
     * @return bool True if the values is not yet contained within the table, false otherwise.
     */
    public function isUniqueValue($strField, $varNew, $intId = null)
    {
        $attribute = $this->getMetaModel()->getAttribute($strField);
        if ($attribute) {
            $matchingIds = $this
                ->prepareFilter(
                    $this->getEmptyConfig()->setFilter(
                        array(
                            array(
                                'operation' => '=',
                                'property' => $attribute->getColName(),
                                'value' => $varNew
                            )
                        )
                    )
                )
                ->getMatchingIds();
            return (count($matchingIds) == 0) || ($matchingIds == array($intId));
        }

        return false;
    }

    /**
     * Reset the fallback field.
     *
     * This clears the given property in all items in the data provider to an empty value.
     *
     * @param string $strField The field to reset.
     *
     * @return void
     *
     * @throws \RuntimeException For invalid ids.
     */
    public function resetFallback($strField)
    {
        // @codingStandardsIgnoreStart
        @trigger_error(__CLASS__ . '::' . __METHOD__ . ' is deprecated - handle resetting manually', E_USER_DEPRECATED);
        // @codingStandardsIgnoreEnd

        $metaModel = $this->getMetaModel();
        $attribute = $metaModel->getAttribute($strField);
        $ids       = $metaModel->getIdsFromFilter(null);

        if ($attribute instanceof IComplex) {
            $attribute->unsetDataFor($ids);
        }
        if ($attribute instanceof ITranslated) {
            $attribute->unsetValueFor($ids, $this->getCurrentLanguage());
        }
        if ($attribute instanceof IAttribute) {
            $data = array();
            foreach ($ids as $id) {
                $data[$id] = null;
            }
            $attribute->setDataFor($data);
        }

        throw new \RuntimeException('Unknown attribute or type ' . $strField);
    }

    /**
     * Save an item to the data provider.
     *
     * If the item does not have an Id yet, the save operation will add it as a new row to the database and
     * populate the Id of the model accordingly.
     *
     * @param ModelInterface $objItem   The model to save back.
     *
     * @param int|null            $timestamp Optional the timestamp.
     *
     * @return ModelInterface The passed model.
     *
     * @throws \RuntimeException When an incompatible item was passed, an Exception is being thrown.
     */
    public function save(ModelInterface $objItem, $timestamp = null)
    {
        if (null === $timestamp) {
            // @codingStandardsIgnoreStart
            @\trigger_error(
                'Not passing a timestamp has been deprecated and will cause an error in MetaModels 3',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }

        if ($objItem instanceof Model) {
            $backupLanguage = $this->setLanguage($this->getCurrentLanguage());

            $objItem->getItem()->save($timestamp);

            $this->setLanguage($backupLanguage);

            return $objItem;
        }

        throw new \RuntimeException('ERROR: incompatible object passed to GeneralDataMetaModel::save()');
    }

    /**
     * Save a collection of items to the data provider.
     *
     * @param CollectionInterface $objItems  The collection containing all items to be saved.
     *
     * @param int                 $timestamp Optional the timestamp.
     *
     * @return void
     *
     * @throws \RuntimeException When an incompatible item was passed.
     */
    public function saveEach(CollectionInterface $objItems, $timestamp = 0)
    {
        if (null === $timestamp) {
            // @codingStandardsIgnoreStart
            @\trigger_error(
                'Not passing a timestamp has been deprecated and will cause an error in MetaModels 3',
                E_USER_DEPRECATED
            );
            // @codingStandardsIgnoreEnd
        }

        foreach ($objItems as $objItem) {
            $this->save($objItem, $timestamp);
        }
    }

    /**
     * Check if the attribute exists in the table and holds a value.
     *
     * @param string $strField The name of the attribute that shall be tested.
     *
     * @return boolean
     */
    public function fieldExists($strField)
    {
        return !!(
            in_array($strField, array('id', 'pid', 'tstamp', 'sorting'))
            || $this->getMetaModel()->getAttribute($strField)
        );
    }

    /**
     * Check if two models have the same values in all properties.
     *
     * @param ModelInterface $objModel1 The first model to compare.
     *
     * @param ModelInterface $objModel2 The second model to compare.
     *
     * @return boolean True - If both models are same, false if not.
     *
     * @throws \InvalidArgumentException If not both models are compatible with this data provider.
     */
    public function sameModels($objModel1, $objModel2)
    {
        if (!($objModel1 instanceof Model && $objModel2 instanceof Model)) {
            throw new \InvalidArgumentException('Passed models are not valid.');
        }

        $objNative1 = $objModel1->getItem();
        $objNative2 = $objModel2->getItem();
        if ($objNative1->getMetaModel() === $objNative2->getMetaModel()) {
            return true;
        }
        foreach ($objNative1->getMetaModel()->getAttributes() as $objAttribute) {
            if ($objNative1->get($objAttribute->getColName()) != $objNative2->get($objAttribute->getColName())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Fetch a variant of a single record by id.
     *
     * @param ConfigInterface $objConfig The config holding the id of the base model.
     *
     * @return null|ModelInterface
     */
    public function createVariant(ConfigInterface $objConfig)
    {
        $objItem = $this->getMetaModel()->findById($objConfig->getId())->varCopy();

        if (!$objItem) {
            return null;
        }

        $model = new Model($objItem);
        $model->setMeta($model::IS_CHANGED, true);

        return $model;
    }

    /**
     * {@inheritdoc}
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function getLanguages($mixID)
    {
        if (!$this->getMetaModel()->isTranslated()) {
            return null;
        }

        $collection = new DefaultLanguageInformationCollection();

        foreach ($this->getMetaModel()->getAvailableLanguages() as $langCode) {
            list($langCode, $country) = explode('_', $langCode, 2);
            $collection->add(new DefaultLanguageInformation($langCode, $country ?: null));
        }

        if (count($collection) > 0) {
            return $collection;
        }

        return null;
    }

    /**
     * {@inheritdoc}
     *
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function getFallbackLanguage($mixID)
    {
        if ($this->getMetaModel()->isTranslated()) {
            $langCode = $this->getMetaModel()->getFallbackLanguage();

            list($langCode, $country) = explode('_', $langCode, 2);
            return new DefaultLanguageInformation($langCode, $country ?: null);
        }

        return null;
    }

    /**
     * Set the current working language for the whole data provider.
     *
     * @param string $strLanguage The new language, use short tag "2 chars like de, fr etc.".
     *
     * @return void
     */
    public function setCurrentLanguage($strLanguage)
    {
        $this->strCurrentLanguage = $strLanguage;
    }

    /**
     * Get the current working language.
     *
     * @return string Short tag for the current working language like de or fr etc.
     */
    public function getCurrentLanguage()
    {
        return $this->strCurrentLanguage;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @author     Stefan Heimes <stefan_heimes@hotmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\Attribute;

use Contao\StringUtil;
use ContaoCommunityAlliance\DcGeneral\Contao\RequestScopeDeterminator;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\BuildWidgetEvent;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\DecodePropertyValueForWidgetEvent;
use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\EncodePropertyValueFromWidgetEvent;
use MetaModels\Attribute\IAttributeFactory;
use MetaModels\Dca\Helper;
use MetaModels\IFactory;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * This class provides the attribute type names.
 */
class NameAndDescriptionListener extends BaseListener
{
    /**
     * The translator.
     *
     * @var TranslatorInterface
     */
    private $translator;

    /**
     * Create a new instance.
     *
     * @param RequestScopeDeterminator $scopeDeterminator The scope determinator.
     * @param IAttributeFactory        $attributeFactory  The attribute factory.
     * @param IFactory                 $factory           The MetaModel factory.
     * @param TranslatorInterface      $translator        The translator.
     */
    public function __construct(
        RequestScopeDeterminator $scopeDeterminator,
        IAttributeFactory $attributeFactory,
        IFactory $factory,
        TranslatorInterface $translator
    ) {
        parent::__construct($scopeDeterminator, $attributeFactory, $factory);
        $this->translator = $translator;
    }

    /**
     * Decode the given value from a serialized language array into the real language array.
     *
     * @param DecodePropertyValueForWidgetEvent $event The event.
     *
     * @return void
     */
    public function decodeValue(DecodePropertyValueForWidgetEvent $event)
    {
        if (!($this->wantToHandle($event) && \in_array($event->getProperty(), ['name', 'description']))) {
            return;
        }

        $metaModel = $this->getMetaModelByModelPid($event->getModel());
        $values    = Helper::decodeLangArray($event->getValue(), $metaModel);

        $event->setValue(unserialize($values, ['allowed_classes' => false]));
    }

    /**
     * Encode the given value from a real language array into a serialized language array.
     *
     * @param EncodePropertyValueFromWidgetEvent $event The event.
     *
     * @return void
     */
    public function encodeValue(EncodePropertyValueFromWidgetEvent $event)
    {
        if (!($this->wantToHandle($event) && \in_array($event->getProperty(), ['name', 'description']))) {
            return;
        }
        $metaModel = $this->getMetaModelByModelPid($event->getModel());
        $values    = Helper::encodeLangArray($event->getValue(), $metaModel);

        $event->setValue($values);
    }

    /**
     * Build the widget for the MCW.
     *
     * @param BuildWidgetEvent $event The event.
     *
     * @return void
     */
    public function buildWidget(BuildWidgetEvent $event)
    {
        if (!($this->wantToHandle($event) && \in_array($event->getProperty()->getName(), ['name', 'description']))) {
            return;
        }

        $metaModel = $this->getMetaModelByModelPid($event->getModel());

        Helper::prepareLanguageAwareWidget(
            $event->getEnvironment(),
            $event->getProperty(),
            $metaModel,
            $this->translator->trans('tl_metamodel_attribute.name_langcode', [], 'contao_tl_metamodel_attribute'),
            $this->translator->trans('tl_metamodel_attribute.name_value', [], 'contao_tl_metamodel_attribute'),
            false,
            StringUtil::deserialize($event->getModel()->getProperty($event->getProperty()->getName()), true)
        );
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Ingolf Steinhardt <info@e-spin.de>
 * @author     David Molineus <david.molineus@netzmacht.de>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\FilterSetting;

use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\Condition\Property\PropertyConditionChain;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\Legend;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\LegendInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\PaletteInterface;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\Property;
use ContaoCommunityAlliance\DcGeneral\DataDefinition\Palette\PropertyInterface;
use ContaoCommunityAlliance\DcGeneral\Factory\Event\BuildDataDefinitionEvent;
use MetaModels\DcGeneral\DataDefinition\Palette\Condition\Property\FilterSettingTypeSubPaletteCondition;
use MetaModels\Filter\Setting\IFilterSettingFactory;

/**
 * This takes care of injecting the sub palettes.
 */
class SubPaletteSubscriber
{
    /**
     * The filter setting factory.
     *
     * @var IFilterSettingFactory
     */
    private $filterFactory;

    /**
     * Create a new instance.
     *
     * @param IFilterSettingFactory $filterFactory The filter setting factory.
     */
    public function __construct(IFilterSettingFactory $filterFactory)
    {
        $this->filterFactory = $filterFactory;
    }

    /**
     * Prepares the sub palettes e. g. add option for translated attributes for different filter types.
     *
     * @param BuildDataDefinitionEvent $event The event.
     *
     * @return void
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    public function prepareSubPalettes(BuildDataDefinitionEvent $event)
    {
        $container = $event->getContainer();
        if ($container->getName() !== 'tl_metamodel_filtersetting') {
            return;
        }

        foreach ($this->filterFactory->getTypeNames() as $filterType) {
            $paletteName = $filterType . '_palettes';

            if (!isset($GLOBALS['TL_DCA']['tl_metamodel_filtersetting'][$paletteName])) {
                continue;
            }

            $palettes    = $container->getPalettesDefinition();
            $typeLegends = $GLOBALS['TL_DCA']['tl_metamodel_filtersetting'][$paletteName];

            foreach ($palettes->getPalettes() as $palette) {
                $this->createConditionsForPalette($palette, $typeLegends);
            }
        }
    }

    /**
     * Create all conditions for the given palette.
     *
     * @param PaletteInterface $palette     The palette.
     * @param array            $typeLegends The type legends.
     *
     * @return void
     */
    private function createConditionsForPalette(PaletteInterface $palette, array $typeLegends)
    {
        $conditions = [];

        foreach ($typeLegends as $value => $legends) {
            // We use an immutable implementation. Using the same condition is save here.
            $valueCondition = new FilterSettingTypeSubPaletteCondition($this->filterFactory, $value);

            foreach ($legends as $legendName => $legendProperties) {
                $legend = $this->getLegend($palette, $legendName);

                foreach ($legendProperties as $propertyName) {
                    $this
                        ->getConditionChain($legend, $propertyName, $conditions)
                        ->addCondition($valueCondition);
                }
            }
        }
    }

    /**
     * Get a legend. Create it if not exists.
     *
     * @param PaletteInterface $palette    The palette.
     * @param string           $legendName The name of the legend.
     *
     * @return LegendInterface
     */
    private function getLegend(PaletteInterface $palette, $legendName)
    {
        if ($palette->hasLegend($legendName)) {
            return $palette->getLegend($legendName);
        }

        $legend = new Legend($legendName);
        $palette->addLegend($legend);

        return $legend;
    }

    /**
     * Get the property chain condition for the property.
     *
     * @param LegendInterface $legend       The legend.
     * @param string          $propertyName The legend property name.
     * @param array           $conditions   Conditions assigned to the properties in this palette.
     *
     * @return PropertyConditionChain
     */
    private function getConditionChain(LegendInterface $legend, $propertyName, array &$conditions)
    {
        // Cache condition chain for each legend property.
        if (isset($conditions[$legend->getName()][$propertyName])) {
            return $conditions[$legend->getName()][$propertyName];
        }

        $property = $this->getLegendProperty($legend, $propertyName);

        // There is no condition assigned to the property. Create an condition chain with an and conjunction
        // and add the condition condition chain for the sub palette with an or condition to it.
        $condition = $this->getVisibleCondition($property);

        $orCondition = new PropertyConditionChain();
        $orCondition->setConjunction(PropertyConditionChain::OR_CONJUNCTION);

        $conditions[$legend->getName()][$propertyName] = $orCondition;

        $condition->addCondition($orCondition);

        return $orCondition;
    }

    /**
     * Get a property from a legend. Create if not exists.
     *
     * @param LegendInterface $legend       The legend.
     * @param string          $propertyName The property name.
     *
     * @return PropertyInterface
     */
    private function getLegendProperty(LegendInterface $legend, $propertyName)
    {
        if ($legend->hasProperty($propertyName)) {
            $property = $legend->getProperty($propertyName);
        } else {
            $property = new Property($propertyName);
            $legend->addProperty($property);
        }

        return $property;
    }

    /**
     * Get the visible condition for a property. Create it if not exists.
     *
     * @param PropertyInterface $property Palette property.
     *
     * @return PropertyConditionChain
     */
    private function getVisibleCondition($property)
    {
        $condition = $property->getVisibleCondition();
        if ($condition instanceof PropertyConditionChain) {
            return $condition;
        }

        $conditionChain = new PropertyConditionChain();
        $property->setVisibleCondition($conditionChain);

        if ($condition) {
            $conditionChain->addCondition($condition);
        }

        return $conditionChain;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\EnvironmentPopulator;

use ContaoCommunityAlliance\Contao\Bindings\ContaoEvents;
use ContaoCommunityAlliance\Contao\Bindings\Events\System\LoadLanguageFileEvent;
use ContaoCommunityAlliance\DcGeneral\EnvironmentInterface;
use ContaoCommunityAlliance\Translator\StaticTranslator;
use ContaoCommunityAlliance\Translator\TranslatorChain;
use MetaModels\ViewCombination\ViewCombination;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * This class populates the translator.
 */
class TranslatorPopulator
{
    use MetaModelPopulatorTrait;

    /**
     * The event dispatcher.
     *
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * The view combination.
     *
     * @var ViewCombination
     */
    private $viewCombination;

    /**
     * Create a new instance.
     *
     * @param EventDispatcherInterface $dispatcher      The event dispatcher.
     * @param ViewCombination          $viewCombination The view combination.
     */
    public function __construct(EventDispatcherInterface $dispatcher, ViewCombination $viewCombination)
    {
        $this->dispatcher      = $dispatcher;
        $this->viewCombination = $viewCombination;
    }

    /**
     * Populate the environment.
     *
     * @param EnvironmentInterface $environment The environment.
     *
     * @return void
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    protected function populate(EnvironmentInterface $environment)
    {
        $translator = $environment->getTranslator();
        if (!($translator instanceof TranslatorChain)) {
            $translatorChain = new TranslatorChain();
            $translatorChain->add($translator);
            $environment->setTranslator($translatorChain);
        } else {
            $translatorChain = $translator;
        }
        $translatorChain->add($translator = new StaticTranslator());

        // Map the tl_metamodel_item domain over to this domain.
        $this->dispatcher->dispatch(
            ContaoEvents::SYSTEM_LOAD_LANGUAGE_FILE,
            new LoadLanguageFileEvent('tl_metamodel_item')
        );

        $definitionName = $environment->getDataDefinition()->getName();
        $this->mapTranslations(
            $GLOBALS['TL_LANG']['tl_metamodel_item'],
            $definitionName,
            $translator
        );
        if (null === $inputScreen = $this->viewCombination->getScreen($definitionName)) {
            return;
        }

        $this->addInputScreenTranslations(
            $translator,
            $inputScreen,
            $definitionName
        );
    }

    /**
     * Map all translation values from the given array to the given destination domain using the optional base key.
     *
     * @param array            $array      The array holding the translation values.
     * @param string           $domain     The target domain.
     * @param StaticTranslator $translator The translator.
     * @param string           $baseKey    The base key to prepend the values of the array with.
     *
     * @return void
     */
    private function mapTranslations($array, $domain, StaticTranslator $translator, $baseKey = '')
    {
        foreach ($array as $key => $value) {
            $newKey = ($baseKey ? $baseKey . '.' : '') . $key;
            if (is_array($value)) {
                $this->mapTranslations($value, $domain, $translator, $newKey);
            } else {
                $translator->setValue($newKey, $value, $domain);
            }
        }
    }

    /**
     * Add the translations for the input screen.
     *
     * @param StaticTranslator $translator    The translator.
     * @param array            $inputScreen   The input screen.
     * @param string           $containerName The container name.
     *
     * @return void
     *
     * @SuppressWarnings(PHPMD.Superglobals)
     * @SuppressWarnings(PHPMD.CamelCaseVariableName)
     */
    private function addInputScreenTranslations(StaticTranslator $translator, $inputScreen, $containerName)
    {
        $currentLocale = $GLOBALS['TL_LANGUAGE'];

        foreach ($inputScreen['legends'] as $legendName => $legendInfo) {
            foreach ($legendInfo['label'] as $langCode => $label) {
                $translator->setValue(
                    $legendName . '_legend',
                    $label,
                    $containerName,
                    $langCode
                );
                if ($currentLocale === $langCode) {
                    $translator->setValue(
                        $legendName . '_legend',
                        $label,
                        $containerName
                    );
                }
            }
        }
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\EventListener\DcGeneral\Table\FilterSetting;

use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Event\GetPropertyOptionsEvent;
use MetaModels\Attribute\IAttribute;
use MetaModels\Filter\Setting\IFilterSettingFactory;

/**
 * This class provides the type options.
 */
class DefaultOptionListener
{
    /**
     * The filter setting factory.
     *
     * @var IFilterSettingFactory
     */
    private $filterFactory;

    /**
     * Create a new instance.
     *
     * @param IFilterSettingFactory $filterFactory The filter setting factory.
     */
    public function __construct(IFilterSettingFactory $filterFactory)
    {
        $this->filterFactory = $filterFactory;
    }

    /**
     * Provide options for default selection.
     *
     * @param GetPropertyOptionsEvent $event The event.
     *
     * @return void
     */
    public function handle(GetPropertyOptionsEvent $event)
    {
        if (('tl_metamodel_filtersetting' !== $event->getEnvironment()->getDataDefinition()->getName())
            || ('defaultid' !== $event->getPropertyName())) {
            return;
        }

        $model = $event->getModel();
        if (!($attributeId = $model->getProperty('attr_id'))) {
            return;
        }
        if (null === $metaModel = $this->filterFactory->createCollection($model->getProperty('fid'))->getMetaModel()) {
            return;
        }

        if (null === $attribute = $metaModel->getAttributeById($attributeId)) {
            return;
        }

        $event->setOptions($this->getOptions($attribute, $model->getProperty('onlyused') ? true : false));
    }

    /**
     * Ensure that all options have a value.
     *
     * @param IAttribute $attribute The options to be cleaned.
     *
     * @param bool       $onlyUsed  Determines if only "used" values shall be returned.
     *
     * @return array
     */
    private function getOptions($attribute, $onlyUsed)
    {
        // Remove empty values.
        $options = [];
        foreach ($attribute->getFilterOptions(null, $onlyUsed) as $key => $value) {
            // Remove html/php tags.
            $value = trim(strip_tags($value));

            if (!empty($value)) {
                $options[$key] = $value;
            }
        }

        return $options;
    }
}


<?php

/**
 * This file is part of MetaModels/core.
 *
 * (c) 2012-2019 The MetaModels team.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * This project is provided in good faith and hope to be usable by anyone.
 *
 * @package    MetaModels/core
 * @author     Christian Schiffler <c.schiffler@cyberspectrum.de>
 * @author     Sven Baumann <baumann.sv@gmail.com>
 * @copyright  2012-2019 The MetaModels team.
 * @license    https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later
 * @filesource
 */

namespace MetaModels\CoreBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Webmozart\PathUtil\Path;

/**
 * Adds the Contao configuration structure.
 */
class Configuration implements ConfigurationInterface
{
    /**
     * The debug flag.
     *
     * @var bool
     */
    private $debug;

    /**
     * The root directory.
     *
     * @var string
     */
    private $rootDir;

    /**
     * Constructor.
     *
     * @param bool   $debug   The debug flag.
     * @param string $rootDir The root directory.
     */
    public function __construct($debug, $rootDir)
    {
        $this->debug   = (bool) $debug;
        $this->rootDir = $rootDir;
    }

    /**
     * Generates the configuration tree builder.
     *
     * @return TreeBuilder
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode    = $treeBuilder->root('metamodels');

        $rootNode
            ->children()
                ->booleanNode('enable_cache')
                    ->defaultValue(!$this->debug)
                ->end()
                ->scalarNode('cache_dir')
                    ->defaultValue('%kernel.cache_dir%' . DIRECTORY_SEPARATOR . 'metamodels')
                ->end()
                ->scalarNode('assets_dir')
                    ->cannotBeEmpty()
                    ->defaultValue($this->resolvePath($this->rootDir . '/assets/metamodels'))
                    ->validate()
                        ->always(function ($value) {
                            return $this->resolvePath($value);
                        })
                    ->end()
                ->end()
                ->scalarNode('assets_web')
                    ->cannotBeEmpty()
                    ->defaultValue('assets/metamodels')
                ->end()
            ->end();

        return $treeBuilder;
    }

    /**
     * Resolves a path.
     *
     * @param string $value The path.
     *
     * @return string
     */
    private function resolvePath($value)
    {
        $path = Path::canonicalize($value);

        if ('\\' === DIRECTORY_SEPARATOR) {
            $path = str_replace('/', '\\', $path);
        }

        return $path;
    }
}