diff --git a/.github/workflows/diagnostics.yml b/.github/workflows/diagnostics.yml new file mode 100644 index 000000000..5a6384fe4 --- /dev/null +++ b/.github/workflows/diagnostics.yml @@ -0,0 +1,49 @@ +name: DC General + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + php: [7.3, 7.4] + contao: [~4.4.0, ~4.9.0] + + steps: + - name: PHP ${{ matrix.php }} ${{ matrix.contao }} Pull source + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + # see https://github.com/shivammathur/setup-php + - name: PHP ${{ matrix.php }} ${{ matrix.contao }} Setup PHP. + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: PHP ${{ matrix.php }} ${{ matrix.contao }} Cache composer cache directory + uses: actions/cache@v1 + env: + cache-name: composer-cache-dir + with: + path: ~/.cache/composer + key: ${{ runner.os }}-build-${{ env.cache-name }} + + - name: PHP ${{ matrix.php }} ${{ matrix.contao }} Cache vendor directory + uses: actions/cache@v1 + env: + cache-name: composer-vendor + with: + path: vendor + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + - name: PHP ${{ matrix.php }} ${{ matrix.contao }} Install composer dependencies + run: composer update --prefer-dist --no-interaction --no-suggest + + - name: PHP ${{ matrix.php }} ${{ matrix.contao }} Run tests + run: ant -keep-going diff --git a/.gitignore b/.gitignore index d20d8c2c5..bb4ec17e4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ composer.lock .sass-cache build/ .phpunit.result.cache + +# PHPCQ and related tools +.php_cs.cache +build.properties diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e6017b991..000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -dist: xenial - -addons: - apt: - packages: - - ant-optional - -language: php - -php: - - "7.4" - - "7.3" - - "7.2" - -env: - - CONTAO_VERSION='contao/core-bundle ~4.10.0' - - CONTAO_VERSION='contao/core-bundle ~4.9.0' - -matrix: - fast_finish: true - exclude: - - php: "7.1" - env: CONTAO_VERSION='contao/core-bundle ~4.9.0' - - php: "7.1" - env: CONTAO_VERSION='contao/core-bundle ~4.10.0' - -before_install: - - echo "memory_limit = -1" > travis.php.ini && phpenv config-add travis.php.ini - -install: - - travis_retry composer self-update && composer --version - - travis_retry composer require $CONTAO_VERSION --no-update - - travis_retry composer update --prefer-dist --no-interaction - -script: ant -keep-going - -# Hack to make things work again - we can not use a shallow repository. -git: - depth: 2147483647 - -cache: - directories: - - vendor diff --git a/README.md b/README.md index 1f60fc716..ec6fbac14 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/contao-community-alliance/dc-general.png)](https://travis-ci.org/contao-community-alliance/dc-general) +[![Build Status](https://github.com/contao-community-alliance/dc-general/actions/workflows/diagnostics.yml/badge.svg)](https://github.com/contao-community-alliance/dc-general/actions) [![Latest Version tagged](http://img.shields.io/github/tag/contao-community-alliance/dc-general.svg)](https://github.com/contao-community-alliance/dc-general/tags) [![Latest Version on Packagist](http://img.shields.io/packagist/v/contao-community-alliance/dc-general.svg)](https://packagist.org/packages/contao-community-alliance/dc-general) [![Installations via composer per month](http://img.shields.io/packagist/dm/contao-community-alliance/dc-general.svg)](https://packagist.org/packages/contao-community-alliance/dc-general) diff --git a/composer.json b/composer.json index e31335d99..26bb50f52 100644 --- a/composer.json +++ b/composer.json @@ -81,5 +81,11 @@ }, "scripts": { "php-cs-fixer": "php-cs-fixer fix --rules=@PSR2" + }, + "config": { + "allow-plugins": { + "contao-components/installer": false, + "contao/manager-plugin": true + } } } diff --git a/src/Cache/Http/InvalidCacheTags.php b/src/Cache/Http/InvalidCacheTags.php index 8b817460b..501117df1 100644 --- a/src/Cache/Http/InvalidCacheTags.php +++ b/src/Cache/Http/InvalidCacheTags.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2020 Contao Community Alliance. + * (c) 2013-2021 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,7 +12,8 @@ * * @package contao-community-alliance/dc-general * @author Sven Baumann - * @copyright 2013-2020 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2021 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -146,7 +147,9 @@ private function addRelatedModelsTag(ModelInterface $model): void /** @var DefaultCollection $parentModels */ $parentModels = $dataProvider - ->fetchAll($dataProvider->getEmptyConfig()->setFilter($parentCondition->getInverseFilterFor($model))); + ->fetchAll( + $dataProvider->getEmptyConfig()->setFilter((array) $parentCondition->getInverseFilterFor($model)) + ); foreach ($parentModels as $parentModel) { $this->addModelTag($parentModel); } diff --git a/src/Contao/Subscriber/FallbackResetSubscriber.php b/src/Contao/Subscriber/FallbackResetSubscriber.php index b4fa5fda3..2fef1feb2 100644 --- a/src/Contao/Subscriber/FallbackResetSubscriber.php +++ b/src/Contao/Subscriber/FallbackResetSubscriber.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2019 Contao Community Alliance. + * (c) 2013-2021 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,13 +14,15 @@ * @author Christian Schiffler * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2021 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ namespace ContaoCommunityAlliance\DcGeneral\Contao\Subscriber; +use ContaoCommunityAlliance\DcGeneral\Controller\ModelCollector; use ContaoCommunityAlliance\DcGeneral\Data\ConfigInterface; use ContaoCommunityAlliance\DcGeneral\Data\ModelManipulator; use ContaoCommunityAlliance\DcGeneral\Event\AbstractModelAwareEvent; @@ -160,9 +162,11 @@ private function determineFilterConfig(AbstractModelAwareEvent $event) ); if (null !== $parentFilter) { - $parentConfig = $dataProvider->getEmptyConfig()->setFilter($parentFilter->getInverseFilterFor($model)); - $parentProvider = $environment->getDataProvider($parentFilter->getSourceName()); - $parent = $parentProvider->fetchAll($parentConfig)->get(0); + $parent = (new ModelCollector($environment))->searchParentOf($model); + if ($parent === null) { + return null; + } + return $dataProvider->getEmptyConfig()->setFilter($parentFilter->getFilter($parent)); } diff --git a/src/Contao/View/Contao2BackendView/ActionHandler/ParentedListViewShowAllHandler.php b/src/Contao/View/Contao2BackendView/ActionHandler/ParentedListViewShowAllHandler.php index 8e18809da..a8dc4408c 100644 --- a/src/Contao/View/Contao2BackendView/ActionHandler/ParentedListViewShowAllHandler.php +++ b/src/Contao/View/Contao2BackendView/ActionHandler/ParentedListViewShowAllHandler.php @@ -576,7 +576,7 @@ private function getGrandParentId( $grandParentProvider = $environment->getDataProvider($grandParentName); $config = $grandParentProvider->getEmptyConfig(); - $config->setFilter($relationship->getInverseFilterFor($parentModel)); + $config->setFilter((array) $relationship->getInverseFilterFor($parentModel)); $parents = $grandParentProvider->fetchAll($config); diff --git a/src/Contao/View/Contao2BackendView/ButtonRenderer.php b/src/Contao/View/Contao2BackendView/ButtonRenderer.php index 875c80e5a..ef5190f00 100644 --- a/src/Contao/View/Contao2BackendView/ButtonRenderer.php +++ b/src/Contao/View/Contao2BackendView/ButtonRenderer.php @@ -4,6 +4,7 @@ * This file is part of contao-community-alliance/dc-general. * * (c) 2013-2021 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,7 +17,8 @@ * @author Sven Baumann * @author Ingolf Steinhardt * @author Richard Henkenjohann - * @copyright 2013-2021 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -124,13 +126,10 @@ public function __construct(EnvironmentInterface $environment) $this->circularModelIds = []; // We must only check for CUT operation here as pasting copy'ed parents is allowed. - $cutItems = \array_filter( - $this->clipboardItems, - function ($item) { - /** @var ItemInterface $item */ - return $item->getAction() === $item::CUT; - } - ); + $cutItems = \array_filter($this->clipboardItems, function ($item) { + /** @var ItemInterface $item */ + return $item->getAction() === $item::CUT; + }); $cutModels = $controller->getModelsFromClipboardItems($cutItems); $collector = new ModelCollector($environment); foreach ($cutModels as $model) { diff --git a/src/Contao/View/Contao2BackendView/Subscriber/WidgetBuilder.php b/src/Contao/View/Contao2BackendView/Subscriber/WidgetBuilder.php index af2913813..f18ee719c 100644 --- a/src/Contao/View/Contao2BackendView/Subscriber/WidgetBuilder.php +++ b/src/Contao/View/Contao2BackendView/Subscriber/WidgetBuilder.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2021 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,7 +16,7 @@ * @author Stefan Heimes * @author Sven Baumann * @author Richard Henkenjohann - * @copyright 2013-2021 Contao Community Alliance. + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -483,9 +483,6 @@ private function prepareWidgetAttributes(ModelInterface $model, PropertyInterfac ], 'options' => $this->getOptionsForWidget($property, $model), 'eval' => $propExtra, - // @codingStandardsIgnoreStart - // 'foreignKey' => null - // @codingStandardsIgnoreEnd ]; if (isset($propExtra['reference'])) { diff --git a/src/Contao/View/Contao2BackendView/TreePicker.php b/src/Contao/View/Contao2BackendView/TreePicker.php index f8bcd3b09..0e4eaada4 100644 --- a/src/Contao/View/Contao2BackendView/TreePicker.php +++ b/src/Contao/View/Contao2BackendView/TreePicker.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2021 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,7 +16,9 @@ * @author Tristan Lins * @author Sven Baumann * @author Richard Henkenjohann - * @copyright 2013-2021 Contao Community Alliance. + * @author Ingolf Steinhardt + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ diff --git a/src/Contao/View/Contao2BackendView/Widget/FileTree.php b/src/Contao/View/Contao2BackendView/Widget/FileTree.php index c337e0634..ba5791b8d 100644 --- a/src/Contao/View/Contao2BackendView/Widget/FileTree.php +++ b/src/Contao/View/Contao2BackendView/Widget/FileTree.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2021 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,7 +16,7 @@ * @author Stefan Heimes * @author Ingolf Steinhardt * @author Sven Baumann - * @copyright 2013-2021 Contao Community Alliance. + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -370,19 +370,20 @@ protected function renderIcon($model, $imagesOnly = false, $downloads = false) return Image::getHtml($file->icon) . ' ' . $info; } - return $this->generateGalleryImage($model, $file, $info); + return $this->generateGalleryImage($file, $info); } /** * Generate an image for use as gallery listing. * - * @param FilesModel $model The file model in use. * @param File $file The image file being rendered. * @param string $info The image information. * * @return string + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - private function generateGalleryImage($model, File $file, $info) + private function generateGalleryImage(File $file, $info) { if ($file->viewWidth && $file->viewHeight && ($file->isSvgImage diff --git a/src/Controller/ModelCollector.php b/src/Controller/ModelCollector.php index 840c5e767..e95c5eb88 100644 --- a/src/Controller/ModelCollector.php +++ b/src/Controller/ModelCollector.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2020 Contao Community Alliance. + * (c) 2013-2021 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,7 +14,8 @@ * @author Christian Schiffler * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2021 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -96,6 +97,13 @@ class ModelCollector */ private $parentProviderName; + /** + * The default data provider name. + * + * @var string + */ + private $defaultProviderName; + /** * Create a new instance. * @@ -120,6 +128,11 @@ public function __construct(EnvironmentInterface $environment) if (!$this->rootCondition instanceof RootConditionInterface) { throw new DcGeneralRuntimeException('No root condition specified for hierarchical mode.'); } + + if ($this->environment->getParentDataDefinition()) { + $this->parentProviderName = $basicDefinition->getParentDataProvider(); + $this->parentProvider = $this->environment->getDataProvider($this->parentProviderName); + } } if (BasicDefinitionInterface::MODE_PARENTEDLIST === $this->definitionMode) { $this->parentProviderName = $basicDefinition->getParentDataProvider(); @@ -196,12 +209,17 @@ public function getModel($modelId, $providerName = null) * @param ModelInterface $model The model to search the parent for. * @param CollectionInterface $models The collection to search in. * - * @return ModelInterface + * @return ModelInterface|null + * + * @throws DcGeneralInvalidArgumentException When the model does not originate from the child provider. */ public function searchParentOfIn(ModelInterface $model, CollectionInterface $models) { + $this->guardModelOriginatesFromProvider($model); + foreach ($models as $candidate) { /** @var ModelInterface $candidate */ + foreach ($this->relationships->getChildConditions($candidate->getProviderName()) as $condition) { if ($condition->matches($candidate, $model)) { return $candidate; @@ -222,11 +240,15 @@ public function searchParentOfIn(ModelInterface $model, CollectionInterface $mod /** * Search the parent model for the given model. * + * If the model is part of a hierarchical structure the parent node is determined instead of a possible available + * parent relationship. + * * @param ModelInterface $model The model for which the parent shall be retrieved. * * @return ModelInterface|null * * @throws DcGeneralInvalidArgumentException When a root model has been passed or not in hierarchical mode. + * @throws DcGeneralInvalidArgumentException When the model does not originate from the child provider. */ public function searchParentOf(ModelInterface $model) { @@ -386,17 +408,13 @@ private function internalCollectChildrenOf(ModelInterface $model, $providerName */ private function searchParentOfInParentedMode(ModelInterface $model) { - if ($this->defaultProviderName !== $model->getProviderName()) { - throw new DcGeneralInvalidArgumentException( - 'Model originates from ' . $model->getProviderName() . - ' but is expected to be from ' . $this->defaultProviderName . - ' can not determine parent.' - ); - } + $this->guardParentProviderDefined(); $condition = $this->relationships->getChildCondition($this->parentProviderName, $this->defaultProviderName); - // This is pretty expensive, we fetch all models from the parent provider here. - // This can be much faster by using the inverse condition if present. + if (null !== ($inverseFilter = $condition->getInverseFilterFor($model))) { + return $this->parentProvider->fetch($this->parentProvider->getEmptyConfig()->setFilter($inverseFilter)); + } + foreach ($this->parentProvider->fetchAll($this->parentProvider->getEmptyConfig()) as $candidate) { if ($condition->matches($candidate, $model)) { return $candidate; @@ -412,16 +430,30 @@ private function searchParentOfInParentedMode(ModelInterface $model) * @param ModelInterface $model The model to search the parent of. * * @return ModelInterface|null - * - * @throws DcGeneralInvalidArgumentException When a root model has been passed. */ private function searchParentOfInHierarchical(ModelInterface $model) { - if ($this->isRootModel($model)) { - throw new DcGeneralInvalidArgumentException('Invalid condition, root models can not have parents!'); + $this->guardRootProviderDefined(); + + foreach ($this->relationships->getChildConditions() as $condition) { + // Skip conditions where the destination is not the provider + if ($this->defaultProviderName !== $condition->getDestinationName()) { + continue; + } + + if (null === ($inverseFilter = $condition->getInverseFilterFor($model))) { + continue; + } + + $provider = $this->environment->getDataProvider($condition->getSourceName()); + $config = $provider->getEmptyConfig()->setFilter($inverseFilter); + $parent = $this->environment->getDataProvider($condition->getSourceName())->fetch($config); + + if (null !== $parent) { + return $parent; + } } - // Start from the root data provider and walk through the whole tree. - // To speed up, some conditions have an inverse filter - we should use them! + $config = $this->rootProvider->getEmptyConfig()->setFilter($this->rootCondition->getFilterArray()); return $this->searchParentOfIn($model, $this->rootProvider->fetchAll($config)); @@ -475,4 +507,58 @@ private function isRootModel(ModelInterface $model) { return (null !== $this->rootCondition) && $this->rootCondition->matches($model); } + + /** + * Guards that a root provider is defined. + * + * @return void + * + * @throws DcGeneralInvalidArgumentException When not root provider is defined. + */ + private function guardRootProviderDefined(): void + { + if (null === $this->rootProvider) { + throw new DcGeneralInvalidArgumentException( + 'Invalid configuration. The root data provider must be defined!' + ); + } + } + + /** + * Guards that a parent provider is defined. + * + * @return void + * + * @throws DcGeneralInvalidArgumentException When not root provider is defined. + */ + private function guardParentProviderDefined(): void + { + if (null === $this->parentProvider) { + throw new DcGeneralInvalidArgumentException( + 'Invalid configuration. The parent data provider must be defined!' + ); + } + } + + /** + * This guard checks if the model belongs to the configured data provider. + * + * @param ModelInterface $model The model to check. + * + * @return void + * + * @throws DcGeneralInvalidArgumentException When model is not for the configured provider. + */ + private function guardModelOriginatesFromProvider(ModelInterface $model): void + { + if ($this->defaultProviderName === $model->getProviderName()) { + return; + } + + throw new DcGeneralInvalidArgumentException( + 'Model originates from ' . $model->getProviderName() . + ' but is expected to be from ' . $this->defaultProviderName . + ' can not determine parent.' + ); + } } diff --git a/src/DataDefinition/ModelRelationship/ParentChildCondition.php b/src/DataDefinition/ModelRelationship/ParentChildCondition.php index 6eaa72d95..ebc95f97d 100644 --- a/src/DataDefinition/ModelRelationship/ParentChildCondition.php +++ b/src/DataDefinition/ModelRelationship/ParentChildCondition.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2019 Contao Community Alliance. + * (c) 2013-2021 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,7 +15,8 @@ * @author Stefan Heimes * @author Tristan Lins * @author Sven Baumann - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2021 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -352,6 +353,10 @@ public function getInverseFilterFor($child) $result[] = $applied; } + if ([] === $result) { + return null; + } + return $result; } diff --git a/src/DataDefinition/Palette/Condition/Property/DumpingPropertyCondition.php b/src/DataDefinition/Palette/Condition/Property/DumpingPropertyCondition.php index c836dea3a..2cb7b546a 100644 --- a/src/DataDefinition/Palette/Condition/Property/DumpingPropertyCondition.php +++ b/src/DataDefinition/Palette/Condition/Property/DumpingPropertyCondition.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2019 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,7 +14,8 @@ * @author Christian Schiffler * @author Tristan Lins * @author Sven Baumann - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -52,6 +53,8 @@ public function __construct($propertyCondition) /** * {@inheritdoc} + * + * @SuppressWarnings(PHPMD.DevelopmentCodeFragment) */ public function match( ModelInterface $model = null, diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php index 35b4a45fe..f80d7599d 100644 --- a/src/Resources/contao/config/config.php +++ b/src/Resources/contao/config/config.php @@ -1,12 +1,9 @@ * @author Andreas Isaak * @author Sven Baumann - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ -/** - * JS - */ +use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Widget\FileTree; +use ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\TreePicker; + +// JS if (defined('TL_MODE') && TL_MODE === 'BE') { $GLOBALS['TL_JAVASCRIPT']['cca.dc-general.generalDriver_src'] = 'bundles/ccadcgeneral/js/generalDriver_src.js'; - $GLOBALS['TL_JAVASCRIPT']['cca.dc-general.vanillaGeneral'] = 'bundles/ccadcgeneral/js/vanillaGeneral.js'; + $GLOBALS['TL_JAVASCRIPT']['cca.dc-general.vanillaGeneral'] = 'bundles/ccadcgeneral/js/vanillaGeneral.js'; } $GLOBALS['BE_FFL']['DcGeneralTreePicker'] = TreePicker::class; diff --git a/src/Resources/contao/languages/de/default.php b/src/Resources/contao/languages/de/default.php index ac80ae63f..31901e7e8 100644 --- a/src/Resources/contao/languages/de/default.php +++ b/src/Resources/contao/languages/de/default.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2020 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,11 +16,13 @@ * @author Tristan Lins * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2013-2020 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ +// @codingStandardsIgnoreStart $GLOBALS['TL_LANG']['MSC']['language'] = 'Sprache'; $GLOBALS['TL_LANG']['MSC']['showSelected'] = 'Sprache wechseln'; $GLOBALS['TL_LANG']['MSC']['treePicker'] = 'Tabelle: %s'; @@ -37,3 +39,4 @@ $GLOBALS['TL_LANG']['MSC']['no_properties_available'] = 'Bitte überprüfen Sie die ausgewählten Eigenschaften!'; $GLOBALS['TL_LANG']['MSC']['select_models'] = 'Elemente auswählen'; $GLOBALS['TL_LANG']['MSC']['dc_general_disabled'] = 'Gesperrt: %s'; +// @codingStandardsIgnoreEnd diff --git a/src/Resources/contao/languages/en/default.php b/src/Resources/contao/languages/en/default.php index 0583e002e..928eb9205 100644 --- a/src/Resources/contao/languages/en/default.php +++ b/src/Resources/contao/languages/en/default.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2020 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,11 +16,13 @@ * @author Tristan Lins * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2013-2020 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ +// @codingStandardsIgnoreStart $GLOBALS['TL_LANG']['MSC']['language'] = 'Language'; $GLOBALS['TL_LANG']['MSC']['showSelected'] = 'Switch language'; $GLOBALS['TL_LANG']['MSC']['treePicker'] = 'Table: %s'; @@ -37,3 +39,4 @@ $GLOBALS['TL_LANG']['MSC']['no_properties_available'] = 'Please check your selected properties!'; $GLOBALS['TL_LANG']['MSC']['select_models'] = 'Select models'; $GLOBALS['TL_LANG']['MSC']['dc_general_disabled'] = 'Disabled: %s'; +// @codingStandardsIgnoreEnd diff --git a/src/deprecated-autoload.php b/src/deprecated-autoload.php index 32a9f7c78..9eedacc46 100644 --- a/src/deprecated-autoload.php +++ b/src/deprecated-autoload.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2019 Contao Community Alliance. + * (c) 2013-2022 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,7 +13,8 @@ * @package contao-community-alliance/dc-general * @author Sven Baumann * @author Richard Henkenjohann - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2022 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -28,11 +29,13 @@ spl_autoload_register( function ($class) { static $classes = [ + // @codingStandardsIgnoreStart Line exceeds 120 characters '\ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Exception\DefinitionException' => DefinitionException::class, '\ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Exception\EditOnlyModeException' => EditOnlyModeException::class, '\ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Exception\NotCreatableException' => NotCreatableException::class, '\ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Exception\NotDeletableException' => NotDeletableException::class, '\ContaoCommunityAlliance\DcGeneral\Contao\View\Contao2BackendView\Subscriber\ColorPickerWizardSubscriber' => ColorPickerWizardListener::class, + // @codingStandardsIgnoreEnd ]; if (isset($classes[$class])) { diff --git a/tests/Controller/ModelCollectorTest.php b/tests/Controller/ModelCollectorTest.php index cae05711c..a4b31e09d 100644 --- a/tests/Controller/ModelCollectorTest.php +++ b/tests/Controller/ModelCollectorTest.php @@ -3,7 +3,7 @@ /** * This file is part of contao-community-alliance/dc-general. * - * (c) 2013-2019 Contao Community Alliance. + * (c) 2013-2021 Contao Community Alliance. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,7 +13,8 @@ * @package contao-community-alliance/dc-general * @author Christian Schiffler * @author Sven Baumann - * @copyright 2013-2019 Contao Community Alliance. + * @author David Molineus + * @copyright 2013-2021 Contao Community Alliance. * @license https://github.com/contao-community-alliance/dc-general/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ @@ -25,6 +26,8 @@ use ContaoCommunityAlliance\DcGeneral\Data\CollectionInterface; use ContaoCommunityAlliance\DcGeneral\Data\ConfigInterface; use ContaoCommunityAlliance\DcGeneral\Data\DataProviderInterface; +use ContaoCommunityAlliance\DcGeneral\Data\DefaultCollection; +use ContaoCommunityAlliance\DcGeneral\Data\DefaultModel; use ContaoCommunityAlliance\DcGeneral\Data\ModelId; use ContaoCommunityAlliance\DcGeneral\Data\ModelIdInterface; use ContaoCommunityAlliance\DcGeneral\Data\ModelInterface; @@ -32,10 +35,13 @@ use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\BasicDefinitionInterface; use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\ModelRelationshipDefinitionInterface; use ContaoCommunityAlliance\DcGeneral\DataDefinition\Definition\PropertiesDefinitionInterface; +use ContaoCommunityAlliance\DcGeneral\DataDefinition\ModelRelationship\ParentChildCondition; +use ContaoCommunityAlliance\DcGeneral\DataDefinition\ModelRelationship\ParentChildConditionInterface; use ContaoCommunityAlliance\DcGeneral\DataDefinition\ModelRelationship\RootConditionInterface; use ContaoCommunityAlliance\DcGeneral\EnvironmentInterface; use ContaoCommunityAlliance\DcGeneral\Exception\DcGeneralRuntimeException; use ContaoCommunityAlliance\DcGeneral\Test\TestCase; +use Generator; /** * Test case for the relationship manager. @@ -208,6 +214,328 @@ public function testCollectSiblingsOf() self::assertSame($collection, $collector->collectSiblingsOf($model)); } + /** + * Provides data for the testSearchParentOfInWithoutRecursion test. + * + * @return Generator + */ + public function provideForTestSearchParentOfInWithoutRecursion(): Generator + { + $collection = new DefaultCollection(); + $collection->push($parentA = $this->createModel('parent', 1)); + $collection->push($parentB = $this->createModel('parent', 2)); + + yield [ + $parentB, + $this->createModel('child', 1, ['pid' => 2]), + $collection, + ]; + + yield [ + $parentA, + $this->createModel('child', 1, ['pid' => 1]), + $collection + ]; + + yield [ + null, + $this->createModel('child', 1, ['pid' => 3]), + $collection + ]; + } + + /** + * Tests the searchParentOfIn method without recursion. + * + * @param ModelInterface|null $expected The expected parent. + * @param ModelInterface $model The given instance of the model. + * @param CollectionInterface $candidates The given candidates of the parent for the model. + * + * @return void + * + * @dataProvider provideForTestSearchParentOfInWithoutRecursion + */ + public function testSearchParentOfInWithoutRecursion( + ?ModelInterface $expected, + ModelInterface $model, + CollectionInterface $candidates + ): void { + $definition = $this->mockDefinitionContainer(); + + $basicDefinition = $this->mockBasicDefinition(); + $basicDefinition->method('getDataProvider')->willReturn('child'); + $basicDefinition->method('getParentDataProvider')->willReturn('parent'); + $basicDefinition->method('getMode')->willReturn(BasicDefinitionInterface::MODE_PARENTEDLIST); + $definition->method('getBasicDefinition')->willReturn($basicDefinition); + + $relationships = $this->mockRelationshipDefinition(); + $conditions = [$this->createParentChildCondition('parent', 'child')]; + $relationships->method('getChildConditions')->willReturn($conditions); + $definition->method('getModelRelationshipDefinition')->willReturn($relationships); + + $environment = $this->getMockForAbstractClass(EnvironmentInterface::class); + $environment->method('getDataDefinition')->willReturn($definition); + + $config = $this->getMockForAbstractClass(ConfigInterface::class); + $config + ->method('setFilter') + ->willReturn($config); + + $parentProvider = $this->getMockForAbstractClass(DataProviderInterface::class); + $parentProvider->method('getEmptyConfig')->willReturn($config); + $parentProvider->method('fetchAll')->with($config)->willReturn(new DefaultCollection()); + + $provider = $this->getMockForAbstractClass(DataProviderInterface::class); + $provider->method('getEmptyConfig')->willReturn($config); + $provider->method('fetchAll')->with($config)->willReturn(new DefaultCollection()); + + $environment->method('getDataProvider')->willReturnCallback( + function (string $name) use ($provider, $parentProvider) { + switch ($name) { + case 'child': + return $provider; + case 'parent': + return $parentProvider; + + default: + throw new \RuntimeException(); + } + } + ); + + $collector = new ModelCollector($environment); + + self::assertSame($expected, $collector->searchParentOfIn($model, $candidates)); + } + + /** + * Provides data for the testSearchParentOfWithRecursion test. + * + * @return Generator + */ + public function provideForTestSearchParentOfInWithRecursion(): Generator + { + $parents = new DefaultCollection(); + $parents->push($parentA = $this->createModel('parent', 1, ['pid' => 10])); + $parents->push($parentB = $this->createModel('parent', 2, ['pid' => 11])); + + $grandParents = new DefaultCollection(); + $grandParents->push($this->createModel('grandparent', 10)); + $grandParents->push($this->createModel('grandparent', 11)); + + yield [ + $parentB, + $this->createModel('child', 1, ['pid' => 2]), + $parents, + $grandParents, + ]; + + yield [ + $parentA, + $this->createModel('child', 1, ['pid' => 1]), + $parents, + $grandParents, + ]; + + yield [ + null, + $this->createModel('child', 1, ['pid' => 3]), + $parents, + $grandParents, + ]; + } + + /** + * Tests the searchParentOfIn method without recursion. + * + * @param ModelInterface|null $expected The expected parent. + * @param ModelInterface $model The given instance of the model. + * @param CollectionInterface $parents The given candidates of the parent for the model. + * @param CollectionInterface $grandParents The given candidates of the parent for the model. + * + * @return void + * + * @dataProvider provideForTestSearchParentOfInWithRecursion + */ + public function testSearchParentOfInWithRecursion( + ?ModelInterface $expected, + ModelInterface $model, + CollectionInterface $parents, + CollectionInterface $grandParents + ): void { + $definition = $this->mockDefinitionContainer(); + $basicDefinition = $this->mockBasicDefinition(); + $basicDefinition->method('getDataProvider')->willReturn('child'); + $basicDefinition->method('getParentDataProvider')->willReturn('parent'); + $basicDefinition->method('getMode')->willReturn(BasicDefinitionInterface::MODE_PARENTEDLIST); + $definition->method('getBasicDefinition')->willReturn($basicDefinition); + + $relationships = $this->mockRelationshipDefinition(); + $relationships->method('getChildConditions')->willReturnCallback( + function (string $providerName): array { + switch ($providerName) { + case 'grandparent': + return [$this->createParentChildCondition('grandparent', 'parent')]; + + case 'parent': + return [$this->createParentChildCondition('parent', 'child')]; + + default: + throw new \RuntimeException(); + } + } + ); + + $definition->method('getModelRelationshipDefinition')->willReturn($relationships); + + $environment = $this->getMockForAbstractClass(EnvironmentInterface::class); + $environment->method('getDataDefinition')->willReturn($definition); + + $config = $this->getMockForAbstractClass(ConfigInterface::class); + $config + ->method('setFilter') + ->willReturn($config); + + $parentProvider = $this->getMockForAbstractClass(DataProviderInterface::class); + $parentProvider->method('getEmptyConfig')->willReturn($config); + $parentProvider->method('fetchAll')->with($config)->willReturn($parents); + + $config = $this->getMockForAbstractClass(ConfigInterface::class); + $config + ->method('setFilter') + ->willReturn($config); + + $grandParentProvider = $this->getMockForAbstractClass(DataProviderInterface::class); + $grandParentProvider->method('getEmptyConfig')->willReturn($config); + $grandParentProvider->method('fetchAll')->with($config)->willReturn(new DefaultCollection()); + + $provider = $this->getMockForAbstractClass(DataProviderInterface::class); + $provider->method('getEmptyConfig')->willReturn($config); + $provider->method('fetchAll')->with($config)->willReturn(new DefaultCollection()); + + $environment->method('getDataProvider')->willReturnCallback( + function (string $name) use ($provider, $parentProvider, $grandParentProvider) { + switch ($name) { + case 'child': + return $provider; + case 'parent': + return $parentProvider; + case 'grandparent': + return $grandParentProvider; + + default: + throw new \RuntimeException(); + } + } + ); + + $collector = new ModelCollector($environment); + self::assertSame($expected, $collector->searchParentOfIn($model, $grandParents)); + } + + /** + * Provides data for the testSearchParentOfInHierarchical test. + * + * @return Generator + */ + public function provideForTestSearchParentOfInHierarchicalByInverseFilter(): Generator + { + $parentNodeA = $this->createModel('node', 10); + $parentNodeB = $this->createModel('node', 11); + + yield [ + $parentNodeA, + $this->createModel('node', 1, ['pid' => 10, 'parentId' => 1]), + ]; + + yield [ + $parentNodeB, + $this->createModel('node', 1, ['pid' => 11, 'parentId' => 2]), + ]; + + yield [ + null, + $this->createModel('node', 1, ['pid' => 12, 'parentId' => 1]), + ]; + } + + /** + * Tests the searchParentOfIn method without recursion. + * + * @param ModelInterface|null $expected The expected parent. + * @param ModelInterface $model The given instance of the model. + * + * @return void + * + * @dataProvider provideForTestSearchParentOfInHierarchicalByInverseFilter + */ + public function testSearchParentOfInHierarchicalByInverseFilter( + ?ModelInterface $expected, + ModelInterface $model + ): void { + $definition = $this->mockDefinitionContainer(); + $basicDefinition = $this->mockBasicDefinition(); + $basicDefinition->method('getDataProvider')->willReturn('node'); + $basicDefinition->method('getRootDataProvider')->willReturn('node'); + $basicDefinition->method('getParentDataProvider')->willReturn('parent'); + $basicDefinition->method('getMode')->willReturn(BasicDefinitionInterface::MODE_HIERARCHICAL); + $definition->method('getBasicDefinition')->willReturn($basicDefinition); + + $relationships = $this->mockRelationshipDefinition(); + $relationships->method('getChildConditions')->willReturn( + [ + $this->createParentChildCondition('parent', 'node'), + $this->createParentChildCondition('node', 'node'), + ] + ); + + $rootCondition = $this->getMockForAbstractClass(RootConditionInterface::class); + $relationships->method('getRootCondition')->willReturn($rootCondition); + + $definition->method('getModelRelationshipDefinition')->willReturn($relationships); + + $environment = $this->getMockForAbstractClass(EnvironmentInterface::class); + $environment->method('getDataDefinition')->willReturn($definition); + + $config = $this->getMockForAbstractClass(ConfigInterface::class); + $config->method('setFilter')->willReturn($config); + + $parentProvider = $this->getMockForAbstractClass(DataProviderInterface::class); + $parentProvider->method('getEmptyConfig')->willReturn($config); + $parentProvider + ->expects($this->once()) + ->method('fetch') + ->willReturn(null); + + $provider = $this->getMockForAbstractClass(DataProviderInterface::class); + $provider->method('getEmptyConfig')->willReturn($config); + $provider + ->expects($this->once()) + ->method('fetch')->willReturn($expected); + + $provider + ->expects(null === $expected ? $this->once() : $this->never()) + ->method('fetchAll') + ->willReturn(new DefaultCollection()); + + $environment->method('getDataProvider')->willReturnCallback( + function (string $name) use ($provider, $parentProvider) { + switch ($name) { + case 'node': + return $provider; + case 'parent': + return $parentProvider; + + default: + throw new \RuntimeException(); + } + } + ); + + $collector = new ModelCollector($environment); + self::assertSame($expected, $collector->searchParentOf($model)); + } + /** * Mock a basic definition. * @@ -247,4 +575,52 @@ private function mockPropertiesDefinition() { return $this->getMockForAbstractClass(PropertiesDefinitionInterface::class); } + + private function createModel(string $providerName, int $id, array $properties = []): ModelInterface + { + $model = new DefaultModel(); + $model->setID($id); + $model->setProviderName($providerName); + $model->setPropertiesAsArray($properties); + + return $model; + } + + private function createParentChildCondition( + string $sourceName, + string $destinationName + ): ParentChildConditionInterface { + $condition = new ParentChildCondition(); + $condition->setSourceName($sourceName); + $condition->setDestinationName($destinationName); + $condition->setSetters( + [ + [ + 'to_field' => 'pid', + 'from_field' => 'id', + ], + ] + ); + $condition->setInverseFilterArray( + [ + [ + 'local' => 'pid', + 'remote' => 'id', + 'operation' => '=', + ], + ] + ); + + $condition->setFilterArray( + [ + [ + 'local' => 'pid', + 'remote' => 'id', + 'operation' => '=', + ], + ] + ); + + return $condition; + } }