diff --git a/.phpcq.lock b/.phpcq.lock deleted file mode 100644 index 2d695cc..0000000 --- a/.phpcq.lock +++ /dev/null @@ -1 +0,0 @@ -{"plugins":{"doctrine-coding-standard":{"api-version":"1.0.0","version":"1.0.2.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/doctrine-coding-standard/doctrine-coding-standard-1.0.2.0.php","signature":null,"requirements":{"php":{"php":"^7.4 || ^8.0","ext-dom":"*"},"composer":{"doctrine/coding-standard":"^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0 || ^14.0"}},"checksum":{"type":"sha-512","value":"00fab498a6575bf07930e078fd616c0481714570bc1c61ebae4fa277d64c0cb28575aba9190c9731c7bda9f97f57c113516e1eb2920c3b9b7b295e0078be3159"},"tools":{},"composerLock":"{\n \"_readme\": [\n \"This file locks the dependencies of your project to a known state\",\n \"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies\",\n \"This file is @generated automatically\"\n ],\n \"content-hash\": \"40b84043be9c25e6feb38a9e014f1a9f\",\n \"packages\": [\n {\n \"name\": \"dealerdirect/phpcodesniffer-composer-installer\",\n \"version\": \"v1.2.0\",\n \"source\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/PHPCSStandards/composer-installer.git\",\n \"reference\": \"845eb62303d2ca9b289ef216356568ccc075ffd1\"\n },\n \"dist\": {\n \"type\": \"zip\",\n \"url\": \"https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1\",\n \"reference\": \"845eb62303d2ca9b289ef216356568ccc075ffd1\",\n \"shasum\": \"\"\n },\n \"require\": {\n \"composer-plugin-api\": \"^2.2\",\n \"php\": \">=5.4\",\n \"squizlabs/php_codesniffer\": \"^3.1.0 || ^4.0\"\n },\n \"require-dev\": {\n \"composer/composer\": \"^2.2\",\n \"ext-json\": \"*\",\n \"ext-zip\": \"*\",\n \"php-parallel-lint/php-parallel-lint\": \"^1.4.0\",\n \"phpcompatibility/php-compatibility\": \"^9.0 || ^10.0.0@dev\",\n \"yoast/phpunit-polyfills\": \"^1.0\"\n },\n \"type\": \"composer-plugin\",\n \"extra\": {\n \"class\": \"PHPCSStandards\\\\Composer\\\\Plugin\\\\Installers\\\\PHPCodeSniffer\\\\Plugin\"\n },\n \"autoload\": {\n \"psr-4\": {\n \"PHPCSStandards\\\\Composer\\\\Plugin\\\\Installers\\\\PHPCodeSniffer\\\\\": \"src/\"\n }\n },\n \"notification-url\": \"https://packagist.org/downloads/\",\n \"license\": [\n \"MIT\"\n ],\n \"authors\": [\n {\n \"name\": \"Franck Nijhof\",\n \"email\": \"opensource@frenck.dev\",\n \"homepage\": \"https://frenck.dev\",\n \"role\": \"Open source developer\"\n },\n {\n \"name\": \"Contributors\",\n \"homepage\": \"https://github.com/PHPCSStandards/composer-installer/graphs/contributors\"\n }\n ],\n \"description\": \"PHP_CodeSniffer Standards Composer Installer Plugin\",\n \"keywords\": [\n \"PHPCodeSniffer\",\n \"PHP_CodeSniffer\",\n \"code quality\",\n \"codesniffer\",\n \"composer\",\n \"installer\",\n \"phpcbf\",\n \"phpcs\",\n \"plugin\",\n \"qa\",\n \"quality\",\n \"standard\",\n \"standards\",\n \"style guide\",\n \"stylecheck\",\n \"tests\"\n ],\n \"support\": {\n \"issues\": \"https://github.com/PHPCSStandards/composer-installer/issues\",\n \"security\": \"https://github.com/PHPCSStandards/composer-installer/security/policy\",\n \"source\": \"https://github.com/PHPCSStandards/composer-installer\"\n },\n \"funding\": [\n {\n \"url\": \"https://github.com/PHPCSStandards\",\n \"type\": \"github\"\n },\n {\n \"url\": \"https://github.com/jrfnl\",\n \"type\": \"github\"\n },\n {\n \"url\": \"https://opencollective.com/php_codesniffer\",\n \"type\": \"open_collective\"\n },\n {\n \"url\": \"https://thanks.dev/u/gh/phpcsstandards\",\n \"type\": \"thanks_dev\"\n }\n ],\n \"time\": \"2025-11-11T04:32:07+00:00\"\n },\n {\n \"name\": \"doctrine/coding-standard\",\n \"version\": \"14.0.0\",\n \"source\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/doctrine/coding-standard.git\",\n \"reference\": \"897a7dc209e49ee6cf04e689c41112df17967130\"\n },\n \"dist\": {\n \"type\": \"zip\",\n \"url\": \"https://api.github.com/repos/doctrine/coding-standard/zipball/897a7dc209e49ee6cf04e689c41112df17967130\",\n \"reference\": \"897a7dc209e49ee6cf04e689c41112df17967130\",\n \"shasum\": \"\"\n },\n \"require\": {\n \"dealerdirect/phpcodesniffer-composer-installer\": \"^0.6.2 || ^0.7 || ^1.0.0\",\n \"php\": \"^7.4 || ^8.0\",\n \"slevomat/coding-standard\": \"^8.23\",\n \"squizlabs/php_codesniffer\": \"^4\"\n },\n \"type\": \"phpcodesniffer-standard\",\n \"notification-url\": \"https://packagist.org/downloads/\",\n \"license\": [\n \"MIT\"\n ],\n \"authors\": [\n {\n \"name\": \"Benjamin Eberlei\",\n \"email\": \"kontakt@beberlei.de\"\n },\n {\n \"name\": \"Steve Müller\",\n \"email\": \"st.mueller@dzh-online.de\"\n }\n ],\n \"description\": \"The Doctrine Coding Standard is a set of PHPCS rules applied to all Doctrine projects.\",\n \"homepage\": \"https://www.doctrine-project.org/projects/coding-standard.html\",\n \"keywords\": [\n \"checks\",\n \"code\",\n \"coding\",\n \"cs\",\n \"dev\",\n \"doctrine\",\n \"rules\",\n \"sniffer\",\n \"sniffs\",\n \"standard\",\n \"style\"\n ],\n \"support\": {\n \"issues\": \"https://github.com/doctrine/coding-standard/issues\",\n \"source\": \"https://github.com/doctrine/coding-standard/tree/14.0.0\"\n },\n \"time\": \"2025-09-21T18:21:47+00:00\"\n },\n {\n \"name\": \"phpstan/phpdoc-parser\",\n \"version\": \"2.3.2\",\n \"source\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/phpstan/phpdoc-parser.git\",\n \"reference\": \"a004701b11273a26cd7955a61d67a7f1e525a45a\"\n },\n \"dist\": {\n \"type\": \"zip\",\n \"url\": \"https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a\",\n \"reference\": \"a004701b11273a26cd7955a61d67a7f1e525a45a\",\n \"shasum\": \"\"\n },\n \"require\": {\n \"php\": \"^7.4 || ^8.0\"\n },\n \"require-dev\": {\n \"doctrine/annotations\": \"^2.0\",\n \"nikic/php-parser\": \"^5.3.0\",\n \"php-parallel-lint/php-parallel-lint\": \"^1.2\",\n \"phpstan/extension-installer\": \"^1.0\",\n \"phpstan/phpstan\": \"^2.0\",\n \"phpstan/phpstan-phpunit\": \"^2.0\",\n \"phpstan/phpstan-strict-rules\": \"^2.0\",\n \"phpunit/phpunit\": \"^9.6\",\n \"symfony/process\": \"^5.2\"\n },\n \"type\": \"library\",\n \"autoload\": {\n \"psr-4\": {\n \"PHPStan\\\\PhpDocParser\\\\\": [\n \"src/\"\n ]\n }\n },\n \"notification-url\": \"https://packagist.org/downloads/\",\n \"license\": [\n \"MIT\"\n ],\n \"description\": \"PHPDoc parser with support for nullable, intersection and generic types\",\n \"support\": {\n \"issues\": \"https://github.com/phpstan/phpdoc-parser/issues\",\n \"source\": \"https://github.com/phpstan/phpdoc-parser/tree/2.3.2\"\n },\n \"time\": \"2026-01-25T14:56:51+00:00\"\n },\n {\n \"name\": \"slevomat/coding-standard\",\n \"version\": \"8.28.1\",\n \"source\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/slevomat/coding-standard.git\",\n \"reference\": \"66151cfbd25b50e8becd9f809fb704f01fd4d6f2\"\n },\n \"dist\": {\n \"type\": \"zip\",\n \"url\": \"https://api.github.com/repos/slevomat/coding-standard/zipball/66151cfbd25b50e8becd9f809fb704f01fd4d6f2\",\n \"reference\": \"66151cfbd25b50e8becd9f809fb704f01fd4d6f2\",\n \"shasum\": \"\"\n },\n \"require\": {\n \"dealerdirect/phpcodesniffer-composer-installer\": \"^0.7 || ^1.2.0\",\n \"php\": \"^7.4 || ^8.0\",\n \"phpstan/phpdoc-parser\": \"^2.3.2\",\n \"squizlabs/php_codesniffer\": \"^4.0.1\"\n },\n \"require-dev\": {\n \"phing/phing\": \"3.0.1|3.1.2\",\n \"php-parallel-lint/php-parallel-lint\": \"1.4.0\",\n \"phpstan/phpstan\": \"2.1.42\",\n \"phpstan/phpstan-deprecation-rules\": \"2.0.4\",\n \"phpstan/phpstan-phpunit\": \"2.0.16\",\n \"phpstan/phpstan-strict-rules\": \"2.0.10\",\n \"phpunit/phpunit\": \"9.6.34|10.5.63|11.4.4|11.5.50|12.5.14\"\n },\n \"type\": \"phpcodesniffer-standard\",\n \"extra\": {\n \"branch-alias\": {\n \"dev-master\": \"8.x-dev\"\n }\n },\n \"autoload\": {\n \"psr-4\": {\n \"SlevomatCodingStandard\\\\\": \"SlevomatCodingStandard/\"\n }\n },\n \"notification-url\": \"https://packagist.org/downloads/\",\n \"license\": [\n \"MIT\"\n ],\n \"description\": \"Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.\",\n \"keywords\": [\n \"dev\",\n \"phpcs\"\n ],\n \"support\": {\n \"issues\": \"https://github.com/slevomat/coding-standard/issues\",\n \"source\": \"https://github.com/slevomat/coding-standard/tree/8.28.1\"\n },\n \"funding\": [\n {\n \"url\": \"https://github.com/kukulich\",\n \"type\": \"github\"\n },\n {\n \"url\": \"https://tidelift.com/funding/github/packagist/slevomat/coding-standard\",\n \"type\": \"tidelift\"\n }\n ],\n \"time\": \"2026-03-22T17:22:38+00:00\"\n },\n {\n \"name\": \"squizlabs/php_codesniffer\",\n \"version\": \"4.0.1\",\n \"source\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer.git\",\n \"reference\": \"0525c73950de35ded110cffafb9892946d7771b5\"\n },\n \"dist\": {\n \"type\": \"zip\",\n \"url\": \"https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5\",\n \"reference\": \"0525c73950de35ded110cffafb9892946d7771b5\",\n \"shasum\": \"\"\n },\n \"require\": {\n \"ext-simplexml\": \"*\",\n \"ext-tokenizer\": \"*\",\n \"ext-xmlwriter\": \"*\",\n \"php\": \">=7.2.0\"\n },\n \"require-dev\": {\n \"phpunit/phpunit\": \"^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31\"\n },\n \"bin\": [\n \"bin/phpcbf\",\n \"bin/phpcs\"\n ],\n \"type\": \"library\",\n \"notification-url\": \"https://packagist.org/downloads/\",\n \"license\": [\n \"BSD-3-Clause\"\n ],\n \"authors\": [\n {\n \"name\": \"Greg Sherwood\",\n \"role\": \"Former lead\"\n },\n {\n \"name\": \"Juliette Reinders Folmer\",\n \"role\": \"Current lead\"\n },\n {\n \"name\": \"Contributors\",\n \"homepage\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors\"\n }\n ],\n \"description\": \"PHP_CodeSniffer tokenizes PHP files and detects violations of a defined set of coding standards.\",\n \"homepage\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer\",\n \"keywords\": [\n \"phpcs\",\n \"standards\",\n \"static analysis\"\n ],\n \"support\": {\n \"issues\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer/issues\",\n \"security\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy\",\n \"source\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer\",\n \"wiki\": \"https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki\"\n },\n \"funding\": [\n {\n \"url\": \"https://github.com/PHPCSStandards\",\n \"type\": \"github\"\n },\n {\n \"url\": \"https://github.com/jrfnl\",\n \"type\": \"github\"\n },\n {\n \"url\": \"https://opencollective.com/php_codesniffer\",\n \"type\": \"open_collective\"\n },\n {\n \"url\": \"https://thanks.dev/u/gh/phpcsstandards\",\n \"type\": \"thanks_dev\"\n }\n ],\n \"time\": \"2025-11-10T16:43:36+00:00\"\n }\n ],\n \"packages-dev\": [],\n \"aliases\": [],\n \"minimum-stability\": \"stable\",\n \"stability-flags\": {},\n \"prefer-stable\": false,\n \"prefer-lowest\": false,\n \"platform\": {},\n \"platform-dev\": {},\n \"plugin-api-version\": \"2.6.0\"\n}\n"},"composer-normalize":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/composer-normalize/composer-normalize-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-json":"*"},"tool":{"composer-normalize":"^2.1"}},"checksum":{"type":"sha-512","value":"d9abda440b85d501c58abf9c81bf76f417594b397129215ffa8b777e9bb5e5eda37d7661d661db3c8d11c24f20345bc6fbe56f013b3b9435d459d2b94f086e0f"},"tools":{"composer-normalize":{"version":"2.50.0","url":"https://github.com/ergebnis/composer-normalize/releases/download/2.50.0/composer-normalize.phar","requirements":{"php":{"php":"~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0","ext-json":"*"}},"checksum":null,"signature":"https://github.com/ergebnis/composer-normalize/releases/download/2.50.0/composer-normalize.phar.asc"}},"composerLock":null},"composer-require-checker":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/composer-require-checker/composer-require-checker-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.4 || ^8.0"},"tool":{"composer-require-checker":"^3.8 || ^4.0"}},"checksum":{"type":"sha-512","value":"d5415bddfe024c5749d894034583882aee4e5c3e1087815d9fdd81cb5e71630f631a0e35de0ff84b97fbbf738c16ece5f83bd8c00695913eb846aa6f04577dc2"},"tools":{"composer-require-checker":{"version":"4.24.0","url":"https://github.com/maglnet/ComposerRequireChecker/releases/download/4.24.0/composer-require-checker.phar","requirements":{"php":{"php":"~8.4.0 || ~8.5.0","ext-phar":"*"}},"checksum":null,"signature":"https://github.com/maglnet/ComposerRequireChecker/releases/download/4.24.0/composer-require-checker.phar.asc"}},"composerLock":null},"phpcpd":{"api-version":"1.0.0","version":"1.1.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpcpd/phpcpd-1.1.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpcpd":"^6.0"}},"checksum":{"type":"sha-512","value":"1189ce0bf3fade4cb4241f1d96f915ef8fc7651f4450dc79fdf464ee3d6be3009316f0d423ce2d4af9d76ad50807b7fdf4d77bfa6d9ee2c91d6eda32ea214433"},"tools":{"phpcpd":{"version":"6.0.3","url":"https://phar.phpunit.de/phpcpd-6.0.3.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*"}},"checksum":{"type":"sha-256","value":"2cbaea7cfda1bb4299d863eb075e977c3f49055dd16d88529fae5150d48a84cb"},"signature":"https://phar.phpunit.de/phpcpd-6.0.3.phar.asc"}},"composerLock":null},"phploc":{"api-version":"1.0.0","version":"1.0.0.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phploc/phploc-1.0.0.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*","ext-json":"*"},"tool":{"phploc":"^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"}},"checksum":{"type":"sha-512","value":"f67b02d494796adf553cb3dd13ec06c1cb8e53c799954061749424251379541637538199afb3afa3c7a01cabd1cb6f1c53eb621f015dff9644c6c7cbf10c56d1"},"tools":{"phploc":{"version":"7.0.2","url":"https://phar.phpunit.de/phploc-7.0.2.phar","requirements":{"php":{"php":">=7.3","ext-dom":"*","ext-json":"*"}},"checksum":{"type":"sha-256","value":"3d59778ec86faf25fd00e3a329b2f9ad4a3c751ca91601ea7dab70f887b0bf46"},"signature":"https://phar.phpunit.de/phploc-7.0.2.phar.asc"}},"composerLock":null},"phpmd":{"api-version":"1.0.0","version":"1.0.2.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpmd/phpmd-1.0.2.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpmd":"^2.6.1"}},"checksum":{"type":"sha-512","value":"f22280a6dec8dbdd2ec1d83b294f23237fe32c34f4a298e52038e0a7a0074d541635b2b488b1a6098a42d8418a6cd8eb804406ea82b91e362be2b5d11a0915b0"},"tools":{"phpmd":{"version":"2.15.0","url":"https://github.com/phpmd/phpmd/releases/download/2.15.0/phpmd.phar","requirements":{"php":{"php":">=5.3.9","ext-xml":"*"}},"checksum":null,"signature":"https://github.com/phpmd/phpmd/releases/download/2.15.0/phpmd.phar.asc"}},"composerLock":null},"psalm":{"api-version":"1.0.0","version":"1.3.0.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/psalm/psalm-1.3.0.0.php","signature":null,"requirements":{"php":{"php":"^7.4 || ^8.0","ext-dom":"*"},"tool":{"psalm":"^3.0 || ^4.0 || ^5.0 || ^6.0"}},"checksum":{"type":"sha-512","value":"4a550c9226d7bca582d7c10bd87cce01190c96398936b1613421640c83df62ed1c6e0d44c1b39635414ea8cf4a892a6458d27590793238add24e7cb5547e6ffd"},"tools":{"psalm":{"version":"6.16.1","url":"https://github.com/vimeo/psalm/releases/download/6.16.1/psalm.phar","requirements":{"php":{"php":"~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0","ext-SimpleXML":"*","ext-ctype":"*","ext-dom":"*","ext-json":"*","ext-libxml":"*","ext-mbstring":"*","ext-tokenizer":"*"}},"checksum":null,"signature":"https://github.com/vimeo/psalm/releases/download/6.16.1/psalm.phar.asc"}},"composerLock":null},"phpcs":{"api-version":"1.0.0","version":"1.2.1.0","type":"php-file","url":"https://phpcq.github.io/repository/plugin/phpcs/phpcs-1.2.1.0.php","signature":null,"requirements":{"php":{"php":"^7.3 || ^8.0","ext-dom":"*"},"tool":{"phpcs":"^4.0 || ^3.0 || ^2.0","phpcbf":"^4.0 || ^3.0 || ^2.0"}},"checksum":{"type":"sha-512","value":"03f1c6c2d94b79d0e8cbd42996382e0d100c7e07f84c3138fa3a8b394e814ec18ce05cbbd257e527913219b2264f062522e4cf3e3bd402b907b9437d96982b44"},"tools":{"phpcs":{"version":"4.0.1","url":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.1/phpcs.phar","requirements":{"php":{"php":">=7.2.0","ext-simplexml":"*","ext-tokenizer":"*","ext-xmlwriter":"*"}},"checksum":null,"signature":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.1/phpcs.phar.asc"},"phpcbf":{"version":"4.0.1","url":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.1/phpcbf.phar","requirements":{"php":{"php":">=7.2.0","ext-simplexml":"*","ext-tokenizer":"*","ext-xmlwriter":"*"}},"checksum":null,"signature":"https://github.com/PHPCSStandards/PHP_CodeSniffer/releases/download/4.0.1/phpcbf.phar.asc"}},"composerLock":null}},"tools":[]} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b4c27..77aebc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ Changelog [Unreleased] ------------ +### Deprecated + + - Legacy content elements `bs_gridStart`, `bs_gridStop` and `bs_gridSeparator` are deprecated. + Use `bs_grid_wrapper` instead. + +### Added + +- Add support for nested fragments +- Bundle configuration option `enable_legacy_elements` (default: `true`) to disable legacy content elements. + 3.0.5 (2025-03-02) ------------------ diff --git a/README.md b/README.md index a185717..5925acb 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,11 @@ This extension provides Bootstrap 5 grid tools for Contao CMS. Features -------- - - Manage grid definition in your theme settings - - Content elements - - Form elements - - Grid module - - Import/Export with your theme settings - +- Manage grid definition in your theme settings +- Content elements +- Form elements +- Grid module +- Import/Export with your theme settings Changelog --------- @@ -32,7 +31,6 @@ Requirements - PHP ^8.1 - Contao ^4.13 || ^5.3 - Install ------- @@ -73,3 +71,34 @@ class AppKernel } ``` + +Migration +--------- + +To automatically migrate your grid from Start- and Stop-Wrappers to nested fragments, you have to enable the migration +via the bundle configuration. Create or extend the file `config/packages/contao_bootstrap_grid.yaml` in your Symfony +application: + +```yaml +contao_bootstrap_grid: + enable_wrapper_migration: true +``` + +Afterwards you can run the migration in the Contao Manager or via CLI: + +```bash +$ php vendor/bin/contao-console contao:migrate +``` + +Deprecated +---------- + +The legacy content elements `bs_gridStart`, `bs_gridStop` and `bs_gridSeparator` are deprecated and will be removed in +a future major version. Use `bs_grid_wrapper` instead. + +To disable the legacy elements now, set the following configuration: + +```yaml +contao_bootstrap_grid: + enable_legacy_elements: false +``` diff --git a/psalm.xml b/psalm.xml index 4499dac..dc16451 100644 --- a/psalm.xml +++ b/psalm.xml @@ -4,6 +4,7 @@ + diff --git a/src/Component/ContentElement/GridSeparatorElementController.php b/src/Component/ContentElement/GridSeparatorElementController.php index b0bcc83..124ca15 100644 --- a/src/Component/ContentElement/GridSeparatorElementController.php +++ b/src/Component/ContentElement/GridSeparatorElementController.php @@ -21,7 +21,17 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\Translation\TranslatorInterface; -/** @ContentElement("bs_gridSeparator", category="bs_grid", template="ce_bs_gridSeparator") */ +use function sprintf; +use function trigger_error; + +use const E_USER_DEPRECATED; + +/** + * @deprecated Use GridWrapperElementController with bs_grid_wrapper instead. + * Will be removed in a future major version. + * + * @ContentElement("bs_gridSeparator", category="bs_grid", template="ce_bs_gridSeparator") + */ final class GridSeparatorElementController extends AbstractGridElementController { public function __construct( @@ -34,6 +44,15 @@ public function __construct( TranslatorInterface $translator, private readonly RepositoryManager $repositories, ) { + trigger_error( + sprintf( + 'Content element "%s" is deprecated. Use "%s" instead. Will be removed in a future major version.', + 'bs_gridSeparator', + 'bs_grid_wrapper', + ), + E_USER_DEPRECATED, + ); + parent::__construct( $templateRenderer, $scopeMatcher, @@ -110,6 +129,10 @@ protected function getIterator(ContentModel $model): GridIterator|null */ protected function getParent(ContentModel $model): ContentModel|null { + if ($model->ptable === 'tl_content') { + return $this->repositories->getRepository(ContentModel::class)->find($model->pid); + } + return $this->repositories->getRepository(ContentModel::class)->find((int) $model->bs_grid_parent); } } diff --git a/src/Component/ContentElement/GridStartElementController.php b/src/Component/ContentElement/GridStartElementController.php index c26ef45..b37b714 100644 --- a/src/Component/ContentElement/GridStartElementController.php +++ b/src/Component/ContentElement/GridStartElementController.php @@ -5,17 +5,63 @@ namespace ContaoBootstrap\Grid\Component\ContentElement; use Contao\ContentModel; +use Contao\CoreBundle\Security\Authentication\Token\TokenChecker; use Contao\CoreBundle\ServiceAnnotation\ContentElement; use Contao\Model; +use ContaoBootstrap\Core\Helper\ColorRotate; use ContaoBootstrap\Grid\Exception\GridNotFound; use ContaoBootstrap\Grid\GridIterator; +use ContaoBootstrap\Grid\GridProvider; +use Netzmacht\Contao\Toolkit\Response\ResponseTagger; +use Netzmacht\Contao\Toolkit\Routing\RequestScopeMatcher; +use Netzmacht\Contao\Toolkit\View\Template\TemplateRenderer; use Override; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Translation\TranslatorInterface; -/** @ContentElement("bs_gridStart", category="bs_grid", template="ce_bs_gridStart") */ +use function sprintf; +use function trigger_error; + +use const E_USER_DEPRECATED; + +/** + * @deprecated Use GridWrapperElementController with bs_grid_wrapper instead. + * Will be removed in a future major version. + * + * @ContentElement("bs_gridStart", category="bs_grid", template="ce_bs_gridStart") + */ final class GridStartElementController extends AbstractGridElementController { + public function __construct( + TemplateRenderer $templateRenderer, + RequestScopeMatcher $scopeMatcher, + ResponseTagger $responseTagger, + TokenChecker $tokenChecker, + GridProvider $gridProvider, + ColorRotate $colorRotate, + TranslatorInterface $translator, + ) { + trigger_error( + sprintf( + 'Content element "%s" is deprecated. Use "%s" instead. Will be removed in a future major version.', + 'bs_gridStart', + 'bs_grid_wrapper', + ), + E_USER_DEPRECATED, + ); + + parent::__construct( + $templateRenderer, + $scopeMatcher, + $responseTagger, + $tokenChecker, + $gridProvider, + $colorRotate, + $translator, + ); + } + /** {@inheritDoc} */ #[Override] protected function preGenerate( diff --git a/src/Component/ContentElement/GridStopElementController.php b/src/Component/ContentElement/GridStopElementController.php index 59f694c..6a2ff6f 100644 --- a/src/Component/ContentElement/GridStopElementController.php +++ b/src/Component/ContentElement/GridStopElementController.php @@ -21,7 +21,17 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\Translation\TranslatorInterface; -/** @ContentElement("bs_gridStop", category="bs_grid", template="ce_bs_gridStop") */ +use function sprintf; +use function trigger_error; + +use const E_USER_DEPRECATED; + +/** + * @deprecated Use GridWrapperElementController with bs_grid_wrapper instead. + * Will be removed in a future major version. + * + * @ContentElement("bs_gridStop", category="bs_grid", template="ce_bs_gridStop") + */ final class GridStopElementController extends AbstractGridElementController { public function __construct( @@ -34,6 +44,15 @@ public function __construct( TranslatorInterface $translator, private readonly RepositoryManager $repositories, ) { + trigger_error( + sprintf( + 'Content element "%s" is deprecated. Use "%s" instead. Will be removed in a future major version.', + 'bs_gridStop', + 'bs_grid_wrapper', + ), + E_USER_DEPRECATED, + ); + parent::__construct( $templateRenderer, $scopeMatcher, diff --git a/src/Component/ContentElement/GridWrapperElementController.php b/src/Component/ContentElement/GridWrapperElementController.php index 75450e6..96fe5e9 100644 --- a/src/Component/ContentElement/GridWrapperElementController.php +++ b/src/Component/ContentElement/GridWrapperElementController.php @@ -29,10 +29,16 @@ public function __construct( #[Override] protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response { - $template->iterator = $this->getIterator($model); - $template->name = $model->bs_grid_name; - $template->color = $this->colorRotate->getColor('ce:' . $model->id); - $template->isBackend = $this->isBackendScope($request); + if ($this->isBackendScope($request)) { + $template->setName('backend/grid_wildcard'); + + $template->set('title', $model->bs_grid_name); + $template->set('color', $this->colorRotate->getColor('ce:' . $model->id)); + + return $template->getResponse(); + } + + $template->set('iterator', $this->getIterator($model)); return $template->getResponse(); } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..02106bd --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,30 @@ +getRootNode() + ->children() + ->booleanNode('enable_wrapper_migration') + ->defaultFalse() + ->end() + ->booleanNode('enable_legacy_elements') + ->defaultTrue() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/DependencyInjection/ContaoBootstrapGridExtension.php b/src/DependencyInjection/ContaoBootstrapGridExtension.php index 998871b..985f6e8 100644 --- a/src/DependencyInjection/ContaoBootstrapGridExtension.php +++ b/src/DependencyInjection/ContaoBootstrapGridExtension.php @@ -18,6 +18,14 @@ final class ContaoBootstrapGridExtension extends Extension #[Override] public function load(array $configs, ContainerBuilder $container): void { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $container->setParameter( + 'contao_bootstrap.grid.enable_wrapper_migration', + $config['enable_wrapper_migration'], + ); + $loader = new YamlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config'), @@ -26,5 +34,11 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('config.yaml'); $loader->load('services.yaml'); $loader->load('listeners.yaml'); + + if (! $config['enable_legacy_elements']) { + return; + } + + $loader->load('legacy.yaml'); } } diff --git a/src/Listener/Dca/ContentListener.php b/src/Listener/Dca/ContentListener.php index 66498f1..f944bda 100644 --- a/src/Listener/Dca/ContentListener.php +++ b/src/Listener/Dca/ContentListener.php @@ -8,6 +8,7 @@ use Contao\Config; use Contao\ContentModel; use Contao\Controller; +use Contao\CoreBundle\DataContainer\PaletteManipulator; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Image\ImageSizes; use Contao\Database\Result; @@ -70,6 +71,22 @@ public function initializeDca(): void ]; } + public function updatePaletteOnNestedParent(DataContainer $dataContainer): void + { + $input = $this->framework->getAdapter(Input::class); + $currentRecord = $dataContainer->getCurrentRecord(); + + if ($input->get('act') !== 'edit' || $currentRecord === null) { + return; + } + + if ($currentRecord['type'] !== 'bs_gridSeparator' || $currentRecord['ptable'] !== 'tl_content') { + return; + } + + PaletteManipulator::create()->removeField('bs_grid_parent')->applyToPalette('bs_gridSeparator', 'tl_content'); + } + /** * Get all grid parent options. * diff --git a/src/Migration/GridWrapperMigration.php b/src/Migration/GridWrapperMigration.php new file mode 100644 index 0000000..91f1223 --- /dev/null +++ b/src/Migration/GridWrapperMigration.php @@ -0,0 +1,242 @@ +enableMigration === false) { + return false; + } + + $schemaManager = $this->connection->createSchemaManager(); + + if (! $schemaManager->tablesExist(['tl_bs_grid', 'tl_content'])) { + return false; + } + + $count = $this->connection->fetchOne( + "SELECT COUNT(*) FROM tl_content WHERE type = 'bs_gridStart'", + ); + + return $count > 0; + } + + /** @throws Exception */ + #[Override] + public function run(): MigrationResult + { + $this->connection->transactional(function (): void { + while ($startElement = $this->fetchNextGridStart()) { + $this->migrateGridStart($startElement); + } + }); + + return $this->createResult(true); + } + + /** + * @return array|false + * + * @throws Exception + */ + private function fetchNextGridStart(): array|false + { + return $this->connection->fetchAssociative( + "SELECT * FROM tl_content WHERE type = 'bs_gridStart' ORDER BY id LIMIT 1", + ); + } + + /** + * @param array $start + * + * @throws Exception + */ + private function migrateGridStart(array $start): void + { + $startId = (int) $start['id']; + $pid = (int) $start['pid']; + $ptable = (string) $start['ptable']; + $startSorting = (int) $start['sorting']; + $tstamp = (int) $start['tstamp']; + + $stop = $this->connection->fetchAssociative( + "SELECT * FROM tl_content WHERE bs_grid_parent = :parent AND type = 'bs_gridStop'", + ['parent' => $startId], + ); + + $query = <<<'SQL' + SELECT * FROM tl_content + WHERE bs_grid_parent = :parent AND type = 'bs_gridSeparator' + ORDER BY sorting ASC +SQL; + $separators = $this->connection->fetchAllAssociative($query, ['parent' => $startId]); + + $this->connection->executeStatement( + "UPDATE tl_content SET type = 'bs_grid_wrapper' WHERE id = :id", + ['id' => $startId], + ); + + $stopSorting = $stop !== false ? (int) $stop['sorting'] : PHP_INT_MAX; + $bounds = [$startSorting]; + $wrapperSorting = 128; + + foreach ($separators as $sep) { + $bounds[] = (int) $sep['sorting']; + } + + $bounds[] = $stopSorting; + + // Slot 0: elements before first separator (or before stop if no separators) + /** @psalm-suppress PossiblyUndefinedArrayOffset */ + $slot0 = $this->fetchSlotElements($pid, $ptable, $bounds[0], $bounds[1]); + $wrapperSorting = $this->placeSlotElements($slot0, $startId, null, $wrapperSorting, $tstamp); + + // One slot per separator: elements after the separator until next separator or stop + foreach ($separators as $i => $sep) { + $sepId = (int) $sep['id']; + $nextBound = $bounds[$i + 2]; + $slotElements = $this->fetchSlotElements($pid, $ptable, (int) $sep['sorting'], $nextBound); + $wrapperSorting = $this->placeSlotElements($slotElements, $startId, $sepId, $wrapperSorting, $tstamp); + } + + if ($stop === false) { + return; + } + + $this->deleteElement((int) $stop['id']); + } + + /** + * Places slot elements into the wrapper according to count: + * - 0 elements: separator (if any) is deleted + * - 1 element: element moves directly into wrapper, separator (if any) is deleted + * - >1 elements: separator becomes element_group (or new group is created), elements move into it + * + * Returns the next available wrapper sorting value. + * + * @param array> $elements + * + * @throws Exception + */ + private function placeSlotElements( + array $elements, + int $wrapperId, + int|null $separatorId, + int $wrapperSorting, + int $tstamp, + ): int { + if (count($elements) > 1) { + if ($separatorId !== null) { + $this->connection->update( + 'tl_content', + [ + 'pid' => $wrapperId, + 'ptable' => 'tl_content', + 'type' => 'element_group', + 'sorting' => $wrapperSorting, + ], + ['id' => $separatorId], + ); + $groupId = $separatorId; + } else { + $groupId = $this->insertElementGroup($wrapperId, 'tl_content', $wrapperSorting, $tstamp); + } + + $wrapperSorting += 128; + $this->moveElementsToParent($elements, $groupId, 'tl_content'); + } elseif (count($elements) === 1) { + if ($separatorId !== null) { + $this->deleteElement($separatorId); + } + + $this->moveElementsToParent($elements, $wrapperId, 'tl_content', $wrapperSorting); + $wrapperSorting += 128; + } else { + if ($separatorId !== null) { + $this->deleteElement($separatorId); + } + } + + return $wrapperSorting; + } + + /** + * @return array> + * + * @throws Exception + */ + private function fetchSlotElements(int $pid, string $ptable, int $fromSorting, int $toSorting): array + { + $query = <<<'SQL' + SELECT * + FROM tl_content + WHERE pid = :pid AND ptable = :ptable AND sorting > :from AND sorting < :to + ORDER BY sorting ASC +SQL; + + return $this->connection->fetchAllAssociative( + $query, + ['pid' => $pid, 'ptable' => $ptable, 'from' => $fromSorting, 'to' => $toSorting], + ); + } + + /** @throws Exception */ + private function insertElementGroup(int $pid, string $ptable, int $sorting, int $tstamp): int + { + return (int) $this->connection->insert( + 'tl_content', + ['pid' => $pid, 'ptable' => $ptable, 'sorting' => $sorting, 'tstamp' => $tstamp, 'type' => 'element_group'], + ); + } + + /** + * @param array> $elements + * + * @throws Exception + */ + private function moveElementsToParent( + array $elements, + int $newPid, + string $newPtable, + int $startSorting = 128, + ): void { + $sorting = $startSorting; + + foreach ($elements as $element) { + $this->connection->executeStatement( + 'UPDATE tl_content SET pid = :pid, ptable = :ptable, sorting = :sorting WHERE id = :id', + ['pid' => $newPid, 'ptable' => $newPtable, 'sorting' => $sorting, 'id' => $element['id']], + ); + $sorting += 128; + } + } + + /** @throws Exception */ + private function deleteElement(int $elementId): void + { + $this->connection->executeStatement( + 'DELETE FROM tl_content WHERE id = :id', + ['id' => $elementId], + ); + } +} diff --git a/src/Resources/config/legacy.yaml b/src/Resources/config/legacy.yaml new file mode 100644 index 0000000..36ee1ae --- /dev/null +++ b/src/Resources/config/legacy.yaml @@ -0,0 +1,32 @@ +services: + ContaoBootstrap\Grid\Component\ContentElement\GridStartElementController: + arguments: + - '@netzmacht.contao_toolkit.template_renderer' + - '@netzmacht.contao_toolkit.routing.scope_matcher' + - '@netzmacht.contao_toolkit.response_tagger' + - '@contao.security.token_checker' + - '@contao_bootstrap.grid.grid_provider' + - '@contao_bootstrap.core.helper.color_rotate' + - '@translator' + + ContaoBootstrap\Grid\Component\ContentElement\GridSeparatorElementController: + arguments: + - '@netzmacht.contao_toolkit.template_renderer' + - '@netzmacht.contao_toolkit.routing.scope_matcher' + - '@netzmacht.contao_toolkit.response_tagger' + - '@contao.security.token_checker' + - '@contao_bootstrap.grid.grid_provider' + - '@contao_bootstrap.core.helper.color_rotate' + - '@translator' + - '@netzmacht.contao_toolkit.repository_manager' + + ContaoBootstrap\Grid\Component\ContentElement\GridStopElementController: + arguments: + - '@netzmacht.contao_toolkit.template_renderer' + - '@netzmacht.contao_toolkit.routing.scope_matcher' + - '@netzmacht.contao_toolkit.response_tagger' + - '@contao.security.token_checker' + - '@contao_bootstrap.grid.grid_provider' + - '@contao_bootstrap.core.helper.color_rotate' + - '@translator' + - '@netzmacht.contao_toolkit.repository_manager' diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index b49788c..995708d 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -18,38 +18,6 @@ services: - '@contao_bootstrap.grid.grid_provider' - '@contao_bootstrap.core.helper.color_rotate' - ContaoBootstrap\Grid\Component\ContentElement\GridStartElementController: - arguments: - - '@netzmacht.contao_toolkit.template_renderer' - - '@netzmacht.contao_toolkit.routing.scope_matcher' - - '@netzmacht.contao_toolkit.response_tagger' - - '@contao.security.token_checker' - - '@contao_bootstrap.grid.grid_provider' - - '@contao_bootstrap.core.helper.color_rotate' - - '@translator' - - ContaoBootstrap\Grid\Component\ContentElement\GridSeparatorElementController: - arguments: - - '@netzmacht.contao_toolkit.template_renderer' - - '@netzmacht.contao_toolkit.routing.scope_matcher' - - '@netzmacht.contao_toolkit.response_tagger' - - '@contao.security.token_checker' - - '@contao_bootstrap.grid.grid_provider' - - '@contao_bootstrap.core.helper.color_rotate' - - '@translator' - - '@netzmacht.contao_toolkit.repository_manager' - - ContaoBootstrap\Grid\Component\ContentElement\GridStopElementController: - arguments: - - '@netzmacht.contao_toolkit.template_renderer' - - '@netzmacht.contao_toolkit.routing.scope_matcher' - - '@netzmacht.contao_toolkit.response_tagger' - - '@contao.security.token_checker' - - '@contao_bootstrap.grid.grid_provider' - - '@contao_bootstrap.core.helper.color_rotate' - - '@translator' - - '@netzmacht.contao_toolkit.repository_manager' - ContaoBootstrap\Grid\Component\ContentElement\GalleryElementController: arguments: - '@netzmacht.contao_toolkit.template_renderer' @@ -104,3 +72,10 @@ services: - '@database_connection' tags: - { name: 'contao.migration' } + + ContaoBootstrap\Grid\Migration\GridWrapperMigration: + arguments: + - '@database_connection' + - '%contao_bootstrap.grid.enable_wrapper_migration%' + tags: + - { name: 'contao.migration' } diff --git a/src/Resources/contao/dca/tl_content.php b/src/Resources/contao/dca/tl_content.php index c9193db..80cffcd 100644 --- a/src/Resources/contao/dca/tl_content.php +++ b/src/Resources/contao/dca/tl_content.php @@ -13,6 +13,11 @@ 'initializeDca', ]; +$GLOBALS['TL_DCA']['tl_content']['config']['onload_callback'][] = [ + 'contao_bootstrap.grid.listeners.dca.content', + 'updatePaletteOnNestedParent', +]; + $GLOBALS['TL_DCA']['tl_content']['config']['oncopy_callback'][] = [ ContentFixParentRelationListener::class, 'onCopy', diff --git a/src/Resources/contao/templates/twig/backend/grid_wildcard.html.twig b/src/Resources/contao/templates/twig/backend/grid_wildcard.html.twig new file mode 100644 index 0000000..5c4bae3 --- /dev/null +++ b/src/Resources/contao/templates/twig/backend/grid_wildcard.html.twig @@ -0,0 +1,7 @@ +{% trans_default_domain 'contao_modules' %} + +
+ ### {{ ('CTE.bootstrap')|trans }}: {{ ('CTE.' ~ type ~ '.0')|trans }} ### +
+ {{ title }} +
diff --git a/src/Resources/contao/templates/twig/content_element/bs_grid_wrapper.html.twig b/src/Resources/contao/templates/twig/content_element/bs_grid_wrapper.html.twig index 199f371..2999c98 100644 --- a/src/Resources/contao/templates/twig/content_element/bs_grid_wrapper.html.twig +++ b/src/Resources/contao/templates/twig/content_element/bs_grid_wrapper.html.twig @@ -1,34 +1,22 @@ {% extends "@Contao/content_element/_base.html.twig" %} {% block content %} - {% if isBackend %} {% if iterator is not null %} - {% for fragment in nested_fragments %} -
{{ name }} [{{ iterator.current }}]
- {{ content_element(fragment) }} - {% endfor %} +
+ {% for fragment in nested_fragments %} + {% for reset in iterator.resets %} +
+ {% endfor %} +
+ {{ content_element(fragment) }} +
+ {{ iterator.next() }} + {% endfor %} +
{% else %} + {{ 'ERR.bsGridParentMissing'|trans({}, 'contao_default') }} {% for fragment in nested_fragments %} {{ content_element(fragment) }} {% endfor %} {% endif %} - {% else %} - {% if iterator is not null %} -
- {% for fragment in nested_fragments %} - {% for reset in iterator.resets %} -
- {% endfor %} -
- {{ content_element(fragment) }} -
- {% endfor %} -
- {% else %} - {{ 'ERR.bsGridParentMissing'|trans({}, 'contao_default') }} - {% for fragment in nested_fragments %} - {{ content_element(fragment) }} - {% endfor %} - {% endif %} - {% endif %} {% endblock %}