Initial Drupal 11 with DDEV setup
This commit is contained in:
@ -0,0 +1,20 @@
|
||||
name: ddev-drupal-contrib
|
||||
repository: ddev/ddev-drupal-contrib
|
||||
version: 1.0.1
|
||||
dependencies:
|
||||
- ddev/ddev-selenium-standalone-chrome
|
||||
install_date: "2025-07-07T14:50:14-04:00"
|
||||
project_files:
|
||||
- commands/web/eslint
|
||||
- commands/web/expand-composer-json
|
||||
- commands/web/nightwatch
|
||||
- commands/web/phpcbf
|
||||
- commands/web/phpcs
|
||||
- commands/web/phpstan
|
||||
- commands/web/phpunit
|
||||
- commands/web/poser
|
||||
- commands/web/stylelint
|
||||
- commands/web/symlink-project
|
||||
- config.contrib.yaml
|
||||
global_files: []
|
||||
removal_actions: []
|
||||
@ -0,0 +1,19 @@
|
||||
name: ddev-selenium-standalone-chrome
|
||||
repository: ddev/ddev-selenium-standalone-chrome
|
||||
version: 1.2.3
|
||||
install_date: "2025-07-07T14:50:58-04:00"
|
||||
project_files:
|
||||
- docker-compose.selenium-chrome.yaml
|
||||
- config.selenium-standalone-chrome.yaml
|
||||
global_files: []
|
||||
removal_actions:
|
||||
- |
|
||||
#ddev-nodisplay
|
||||
#ddev-description:Remove docker-compose.selenium-chrome_extras.yaml file
|
||||
if [ -f docker-compose.selenium-chrome_extras.yaml ]; then
|
||||
if grep -q '#ddev-generated' docker-compose.selenium-chrome_extras.yaml; then
|
||||
rm -f docker-compose.selenium-chrome_extras.yaml
|
||||
else
|
||||
echo "Unwilling to remove '$DDEV_APPROOT/.ddev/docker-compose.selenium-chrome_extras.yaml' because it does not have #ddev-generated in it; you can manually delete it if it is safe to delete."
|
||||
fi
|
||||
fi
|
||||
22
web/modules/contrib/devel/.ddev/commands/web/eslint
Executable file
22
web/modules/contrib/devel/.ddev/commands/web/eslint
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run eslint inside the web container
|
||||
## Usage: eslint [flags] [args]
|
||||
## Example: "ddev eslint"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if "$DDEV_DOCROOT/core/node_modules/.bin/eslint" --version >/dev/null ; then
|
||||
# Configure prettier
|
||||
test -e .prettierrc.json || ln -s $DDEV_DOCROOT/core/.prettierrc.json .
|
||||
test -e .prettierignore || echo '*.yml' > .prettierignore
|
||||
# Change directory to the project root folder
|
||||
cd "$DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH/${DDEV_SITENAME//-/_}" || exit
|
||||
"$DDEV_COMPOSER_ROOT/$DDEV_DOCROOT/core/node_modules/.bin/eslint" --config="../../../core/.eslintrc.passing.json" --no-error-on-unmatched-pattern --ignore-pattern="*.es6.js" --resolve-plugins-relative-to=$DDEV_COMPOSER_ROOT/$DDEV_DOCROOT/core --ext=.js,.yml . "$@"
|
||||
else
|
||||
echo "eslint is not available. You may need to 'ddev exec \"cd $DDEV_DOCROOT/core && yarn install\"'"
|
||||
exit 1
|
||||
fi
|
||||
24
web/modules/contrib/devel/.ddev/commands/web/expand-composer-json
Executable file
24
web/modules/contrib/devel/.ddev/commands/web/expand-composer-json
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Add Drupal core and other needed dependencies.
|
||||
## Usage: expand-composer-json [flags] [PROJECT_NAME]
|
||||
## Example: "ddev expand-composer-json ctools"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
export _WEB_ROOT=$DDEV_DOCROOT
|
||||
cd "$DDEV_COMPOSER_ROOT" || exit
|
||||
curl -OL https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/scripts/expand_composer_json.php
|
||||
_ddev_drupal_contrib_empty_composer=false
|
||||
if [[ ! -f composer.json ]]; then
|
||||
echo "{}" > composer.json
|
||||
_ddev_drupal_contrib_empty_composer=true
|
||||
fi
|
||||
php expand_composer_json.php "$DDEV_SITENAME"
|
||||
rm -f expand_composer_json.php
|
||||
if [ "$_ddev_drupal_contrib_empty_composer" = true ]; then
|
||||
rm -f composer.json
|
||||
fi
|
||||
12
web/modules/contrib/devel/.ddev/commands/web/nightwatch
Executable file
12
web/modules/contrib/devel/.ddev/commands/web/nightwatch
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run nightwatch inside the web container
|
||||
## Usage: nightwatch [flags] [args]
|
||||
## Example: "ddev nightwatch"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
yarn --cwd "$DDEV_DOCROOT/core" test:nightwatch "$DDEV_APPROOT/$DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH/" "$@"
|
||||
17
web/modules/contrib/devel/.ddev/commands/web/phpcbf
Executable file
17
web/modules/contrib/devel/.ddev/commands/web/phpcbf
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run phpcbf inside the web container
|
||||
## Usage: phpcbf [flags] [args]
|
||||
## Example: "ddev phpcbf" or "ddev phpcbf -n"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if ! command -v phpcbf >/dev/null; then
|
||||
echo "phpcbf is not available. You may need to 'ddev composer install'"
|
||||
exit 1
|
||||
fi
|
||||
test -e phpcs.xml.dist || curl -OL https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/assets/phpcs.xml.dist
|
||||
phpcbf -s --report-full --report-summary --report-source $DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH "$@"
|
||||
17
web/modules/contrib/devel/.ddev/commands/web/phpcs
Executable file
17
web/modules/contrib/devel/.ddev/commands/web/phpcs
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run phpcs inside the web container
|
||||
## Usage: phpcs [flags] [args]
|
||||
## Example: "ddev phpcs" or "ddev phpcs -n"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if ! command -v phpcs >/dev/null; then
|
||||
echo "phpcs is not available. You may need to 'ddev composer install'"
|
||||
exit 1
|
||||
fi
|
||||
test -e phpcs.xml.dist || curl -OL https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/assets/phpcs.xml.dist
|
||||
phpcs -s --report-full --report-summary --report-source $DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH --ignore=*/.ddev/* "$@"
|
||||
27
web/modules/contrib/devel/.ddev/commands/web/phpstan
Executable file
27
web/modules/contrib/devel/.ddev/commands/web/phpstan
Executable file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run phpstan inside the web container
|
||||
## Usage: phpstan [flags] [args]
|
||||
## Example: "ddev phpstan" or "ddev phpstan -n"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if ! command -v phpstan >/dev/null; then
|
||||
echo "phpstan is not available. You may need to 'ddev poser'"
|
||||
exit 1
|
||||
fi
|
||||
test -e phpstan.neon || curl -OL https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/assets/phpstan.neon
|
||||
# See https://git.drupalcode.org/project/gitlab_templates/-/commit/a107b7f1f79af12e0b09f70be47b68e3f69b4504
|
||||
sed -i 's/BASELINE_PLACEHOLDER/phpstan-baseline.neon/g' phpstan.neon
|
||||
# Add an empty baseline file to ensure it exists.
|
||||
test -e phpstan-baseline.neon || touch phpstan-baseline.neon
|
||||
|
||||
EXTENSION_DIRECTORY=$DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH/${DDEV_SITENAME//-/_}
|
||||
cd "$EXTENSION_DIRECTORY" || exit 1
|
||||
# Ensure PHPStan configuration is symlinked from project root.
|
||||
ln -s $DDEV_DOCROOT/phpstan.neon 2>/dev/null || true
|
||||
ln -s $DDEV_DOCROOT/phpstan-baseline.neon 2>/dev/null || true
|
||||
phpstan analyze . "$@"
|
||||
24
web/modules/contrib/devel/.ddev/commands/web/phpunit
Executable file
24
web/modules/contrib/devel/.ddev/commands/web/phpunit
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run phpunit inside the web container
|
||||
## Usage: phpunit [flags] [args]
|
||||
## Example: "ddev phpunit" or "ddev phpunit --stop-on-failure"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if ! command -v phpunit >/dev/null; then
|
||||
echo "phpunit is not available. You may need to 'ddev composer install'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# CHECK for local config.
|
||||
if [ -f "phpunit.xml" ]; then
|
||||
# Defer to local config
|
||||
phpunit "$@"
|
||||
else
|
||||
# Bootstrap Drupal tests and run all custom module tests.
|
||||
phpunit --bootstrap $PWD/$DDEV_DOCROOT/core/tests/bootstrap.php $DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH "$@"
|
||||
fi
|
||||
17
web/modules/contrib/devel/.ddev/commands/web/poser
Executable file
17
web/modules/contrib/devel/.ddev/commands/web/poser
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Expand composer.json and run composer install.
|
||||
## Usage: poser [flags] [args]
|
||||
## Example: "ddev poser" or "ddev poser --prefer-source"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
export COMPOSER=composer.contrib.json
|
||||
.ddev/commands/web/expand-composer-json
|
||||
composer install "$@"
|
||||
# The -f flag suppresses errors if lock file does not exist.
|
||||
rm -f composer.contrib.json composer.contrib.lock
|
||||
touch $DDEV_DOCROOT/core/.env
|
||||
19
web/modules/contrib/devel/.ddev/commands/web/stylelint
Executable file
19
web/modules/contrib/devel/.ddev/commands/web/stylelint
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Run stylelint inside the web container
|
||||
## Usage: stylelint [flags] [args]
|
||||
## Example: "ddev stylelint"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if $DDEV_DOCROOT/core/node_modules/.bin/stylelint --version >/dev/null ; then
|
||||
# Change directory to the project root folder
|
||||
cd "$DDEV_DOCROOT/$DRUPAL_PROJECTS_PATH/${DDEV_SITENAME//-/_}" || exit
|
||||
"$DDEV_COMPOSER_ROOT/$DDEV_DOCROOT/core/node_modules/.bin/stylelint" --color --config "$DDEV_COMPOSER_ROOT/$DDEV_DOCROOT/core/.stylelintrc.json" "./**/*.css" "$@"
|
||||
else
|
||||
echo "stylelint is not available. You may need to 'ddev exec \"cd $DDEV_DOCROOT/core && yarn install\"'"
|
||||
exit 1
|
||||
fi
|
||||
20
web/modules/contrib/devel/.ddev/commands/web/symlink-project
Executable file
20
web/modules/contrib/devel/.ddev/commands/web/symlink-project
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## Description: Symlink project files into the configured location (defaults to `web/modules/custom/[PROJECT_NAME]`)
|
||||
## Usage: symlink-project [flags] [args]
|
||||
## Example: "ddev symlink-project"
|
||||
## ExecRaw: true
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
export _WEB_ROOT=$DDEV_DOCROOT
|
||||
#todo use more dynamic ref.
|
||||
cd "$DDEV_COMPOSER_ROOT" || exit
|
||||
curl -OL https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/scripts/symlink_project.php
|
||||
|
||||
# Symlink name using underscores.
|
||||
# @see https://www.drupal.org/docs/develop/creating-modules/naming-and-placing-your-drupal-module
|
||||
php symlink_project.php "${DDEV_SITENAME//-/_}"
|
||||
rm -f symlink_project.php
|
||||
21
web/modules/contrib/devel/.ddev/config.contrib.yaml
Normal file
21
web/modules/contrib/devel/.ddev/config.contrib.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
#ddev-generated
|
||||
## Command provided by https://github.com/ddev/ddev-drupal-contrib
|
||||
## To customize this configuration, see:
|
||||
## https://ddev.readthedocs.io/en/stable/users/extend/customization-extendibility/
|
||||
web_environment:
|
||||
# To change the Drupal core version, see the README:
|
||||
# https://github.com/ddev/ddev-drupal-contrib/blob/main/README.md#changing-the-drupal-core-version
|
||||
- DRUPAL_CORE=^11
|
||||
- # https://git.drupalcode.org/project/gitlab_templates/-/blob/1.9.6/scripts/expand_composer_json.php?ref_type=tags#L15
|
||||
- IGNORE_PROJECT_DRUPAL_CORE_VERSION=1
|
||||
# To change the location of your project code, see the README:
|
||||
# https://github.com/ddev/ddev-drupal-contrib/blob/main/README.md#changing-the-symlink-location
|
||||
- DRUPAL_PROJECTS_PATH=modules/custom
|
||||
hooks:
|
||||
post-start:
|
||||
- exec-host: |
|
||||
if [[ -f vendor/autoload.php ]]; then
|
||||
ddev symlink-project
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
@ -0,0 +1,29 @@
|
||||
#ddev-generated
|
||||
# Remove the line above if you don't want this file to be overwritten when you run
|
||||
# ddev get ddev/ddev-selenium-standalone-chrome
|
||||
#
|
||||
# This file comes from https://github.com/ddev/ddev-selenium-standalone-chrome
|
||||
#
|
||||
web_environment:
|
||||
- BROWSERTEST_OUTPUT_DIRECTORY=/tmp
|
||||
- BROWSERTEST_OUTPUT_BASE_URL=${DDEV_PRIMARY_URL}
|
||||
- SIMPLETEST_BASE_URL=http://web
|
||||
- SIMPLETEST_DB=mysql://db:db@db/db
|
||||
# Use disable-dev-shm-usage instead of setting shm_usage
|
||||
# https://developers.google.com/web/tools/puppeteer/troubleshooting#tips
|
||||
# The format of chromeOptions is defined at https://chromedriver.chromium.org/capabilities
|
||||
- MINK_DRIVER_ARGS_WEBDRIVER=[\"chrome\", {\"browserName\":\"chrome\",\"goog:chromeOptions\":{\"w3c\":false,\"args\":[\"--disable-gpu\",\"--headless\", \"--no-sandbox\", \"--disable-dev-shm-usage\"]}}, \"http://selenium-chrome:4444/wd/hub\"]
|
||||
# Nightwatch
|
||||
- DRUPAL_TEST_BASE_URL=http://web
|
||||
- DRUPAL_TEST_DB_URL=mysql://db:db@db/db
|
||||
- DRUPAL_TEST_WEBDRIVER_HOSTNAME=selenium-chrome
|
||||
- DRUPAL_TEST_WEBDRIVER_PORT=4444
|
||||
- DRUPAL_TEST_WEBDRIVER_PATH_PREFIX=/wd/hub
|
||||
- DRUPAL_TEST_WEBDRIVER_CHROME_ARGS=--disable-gpu --headless --no-sandbox --disable-dev-shm-usage
|
||||
- DRUPAL_TEST_CHROMEDRIVER_AUTOSTART=false
|
||||
- DRUPAL_NIGHTWATCH_SEARCH_DIRECTORY=../
|
||||
- DRUPAL_NIGHTWATCH_IGNORE_DIRECTORIES=node_modules,vendor,.*,sites/*/files,sites/*/private,sites/simpletest
|
||||
- DRUPAL_NIGHTWATCH_OUTPUT=reports/nightwatch
|
||||
# DTT
|
||||
- DTT_BASE_URL=http://web
|
||||
- DTT_MINK_DRIVER_ARGS=[\"chrome\", {\"browserName\":\"chrome\",\"goog:chromeOptions\":{\"w3c\":false,\"args\":[\"--disable-gpu\",\"--headless\", \"--no-sandbox\", \"--disable-dev-shm-usage\"]}}, \"http://selenium-chrome:4444/wd/hub\"]
|
||||
18
web/modules/contrib/devel/.ddev/config.yaml
Normal file
18
web/modules/contrib/devel/.ddev/config.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
name: devel
|
||||
type: drupal10
|
||||
docroot: web
|
||||
php_version: "8.1"
|
||||
webserver_type: nginx-fpm
|
||||
router_http_port: "80"
|
||||
router_https_port: "443"
|
||||
xdebug_enabled: false
|
||||
additional_hostnames: [ ]
|
||||
additional_fqdns: [ ]
|
||||
database:
|
||||
type: mariadb
|
||||
version: "10.11"
|
||||
use_dns_when_possible: true
|
||||
composer_version: "2"
|
||||
web_environment:
|
||||
- SIMPLETEST_DB=mysql://root:root@ddev-devel-db/db
|
||||
- SIMPLETEST_BASE_URL=http://ddev-devel-web
|
||||
@ -0,0 +1,34 @@
|
||||
#ddev-generated
|
||||
# Remove the line above if you don't want this file to be overwritten when you run
|
||||
# ddev get ddev/ddev-selenium-standalone-chrome
|
||||
#
|
||||
# This file comes from https://github.com/ddev/ddev-selenium-standalone-chrome
|
||||
#
|
||||
services:
|
||||
selenium-chrome:
|
||||
image: seleniarm/standalone-chromium:4.1.4-20220429
|
||||
container_name: ddev-${DDEV_SITENAME}-selenium-chrome
|
||||
expose:
|
||||
# The internal noVNC port, which operates over HTTP so it can be exposed
|
||||
# through the router.
|
||||
- 7900
|
||||
environment:
|
||||
- VIRTUAL_HOST=$DDEV_HOSTNAME
|
||||
- HTTPS_EXPOSE=7900:7900
|
||||
- HTTP_EXPOSE=7910:7900
|
||||
- VNC_NO_PASSWORD=1
|
||||
# To enable VNC access for traditional VNC clients like macOS "Screen Sharing",
|
||||
# uncomment the following two lines.
|
||||
#ports:
|
||||
# - "5900:5900"
|
||||
labels:
|
||||
com.ddev.site-name: ${DDEV_SITENAME}
|
||||
com.ddev.approot: $DDEV_APPROOT
|
||||
volumes:
|
||||
# To enable file uploads in E2E tests.
|
||||
- ${DDEV_APPROOT}:/var/www/html:r
|
||||
- ".:/mnt/ddev_config"
|
||||
|
||||
web:
|
||||
links:
|
||||
- selenium-chrome
|
||||
@ -0,0 +1,5 @@
|
||||
#ddev-generated
|
||||
services:
|
||||
selenium-chrome:
|
||||
external_links:
|
||||
- "ddev-router:${DDEV_PROJECT}.${DDEV_TLD}"
|
||||
36
web/modules/contrib/devel/.gitignore
vendored
Normal file
36
web/modules/contrib/devel/.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
/vendor/
|
||||
vendor
|
||||
/web/
|
||||
/node_modules/
|
||||
.env
|
||||
composer.lock
|
||||
yarn.lock
|
||||
/.editorconfig
|
||||
/.gitattributes
|
||||
|
||||
#PHPUnit output
|
||||
junit.xml
|
||||
.phpunit.result.cache
|
||||
|
||||
# Ignore local overrides.
|
||||
docker-compose.override.yml
|
||||
/.csslintrc
|
||||
/.eslintignore
|
||||
/.eslintrc.json
|
||||
/.ht.router.php
|
||||
/.htaccess
|
||||
/INSTALL.txt
|
||||
/README.txt
|
||||
/autoload.php
|
||||
/example.gitignore
|
||||
/index.php
|
||||
/robots.txt
|
||||
/update.php
|
||||
/web.config
|
||||
/composer.spoons.json
|
||||
/composer.spoons.lock
|
||||
/.env
|
||||
.envrc
|
||||
.envrc.local
|
||||
.composer-plugin.env
|
||||
.idea/
|
||||
39
web/modules/contrib/devel/.gitlab-ci.yml
Normal file
39
web/modules/contrib/devel/.gitlab-ci.yml
Normal file
@ -0,0 +1,39 @@
|
||||
include:
|
||||
# Use CI from drupal.org https://git.drupalcode.org/project/gitlab_templates/
|
||||
# This include centralizes our CI "golden path" https://docs.gitlab.com/ee/ci/yaml/#includefile
|
||||
- remote: https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/includes/include.drupalci.main.yml
|
||||
- remote: https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/includes/include.drupalci.variables.yml
|
||||
- remote: https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/includes/include.drupalci.hidden-variables.yml
|
||||
- remote: https://git.drupalcode.org/project/gitlab_templates/-/raw/default-ref/includes/include.drupalci.workflows.yml
|
||||
|
||||
# Start custom overrides.
|
||||
|
||||
variables:
|
||||
# Needed in order to get our Drupal 10 testing to use a Drush 13 compatible PHP.
|
||||
_TARGET_PHP: 8.3
|
||||
OPT_IN_TEST_NEXT_MAJOR: 1
|
||||
SKIP_CSPELL: 1
|
||||
# Show more log output.
|
||||
# _PHPUNIT_EXTRA: --debug
|
||||
# Convenient, and we have no secrets.
|
||||
_SHOW_ENVIRONMENT_VARIABLES: 1
|
||||
|
||||
phpcs:
|
||||
allow_failure: false
|
||||
eslint:
|
||||
allow_failure: false
|
||||
stylelint:
|
||||
allow_failure: false
|
||||
|
||||
# Disable not needed jobs.
|
||||
# We rely on 'phpstan (max PHP version)' job instead.
|
||||
phpstan:
|
||||
rules:
|
||||
- when: never
|
||||
phpstan (next major):
|
||||
rules:
|
||||
- when: never
|
||||
# We rely on 'phpunit' and 'phpunit (next major)' jobs instead.
|
||||
phpunit (max PHP version):
|
||||
rules:
|
||||
- when: never
|
||||
1
web/modules/contrib/devel/CODEOWNERS
Normal file
1
web/modules/contrib/devel/CODEOWNERS
Normal file
@ -0,0 +1 @@
|
||||
/src/Drush/Commands/ @weitzman
|
||||
339
web/modules/contrib/devel/LICENSE.txt
Normal file
339
web/modules/contrib/devel/LICENSE.txt
Normal file
@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
49
web/modules/contrib/devel/README.md
Normal file
49
web/modules/contrib/devel/README.md
Normal file
@ -0,0 +1,49 @@
|
||||
[[_TOC_]]
|
||||
|
||||
#### Updates
|
||||
|
||||
- July 2025: Use Kint module if you prefer Kint to the built-in Symfony Var-Dumper. Kint
|
||||
support has moved to that module.
|
||||
|
||||
#### Introduction
|
||||
|
||||
Devel module contains helper functions and pages for Drupal developers and
|
||||
inquisitive admins:
|
||||
|
||||
- A block and toolbar for quickly accessing devel pages. Install drupal/navigation_extra_tools for core navigation module integration.
|
||||
- A local task tab added to entities to view their properties
|
||||
- Urls created to view the internal entity properties even when there is no menu tab, for example /devel/paragraph/n
|
||||
- Debug functions for inspecting a variable such as `dpm($variable)`
|
||||
- Debug a SQL query `dpq($query` or print a backtrace `ddebug_backtrace()`
|
||||
- A block for masquerading as other users (useful for testing)
|
||||
- A mail-system class which redirects outbound email to files
|
||||
- Drush commands such as `fn-hook`, `fn-event`, `token`, `uuid`, and `devel-services`
|
||||
- *Devel Generate*. Bulk creates nodes, users, comment, taxonomy, media, menus, block content for development. Has
|
||||
Drush integration.
|
||||
|
||||
This module is safe to use on a production site. Just be sure to only grant
|
||||
_access development information_ permission to developers.
|
||||
|
||||
#### Collaboration
|
||||
- https://gitlab.com/drupalspoons/devel is our workplace for code, MRs, and CI.
|
||||
- Create a personal fork in order to make an MR.
|
||||
- We plan to move bck to drupal.org once it uses Gitlab for issues.
|
||||
- We auto-push back to git.drupalcode.org in order to keep
|
||||
[Security Team](https://www.drupal.org/security) coverage and packages.drupal.org integration.
|
||||
- Chat with us at [#devel](https://drupal.slack.com/archives/C012WAW1MH6) on Drupal Slack.
|
||||
|
||||
#### Local Development
|
||||
DDEV is configured with https://github.com/ddev/ddev-drupal-contrib for for easy
|
||||
local development, test running, etc.
|
||||
|
||||
#### Version Compatibility
|
||||
| Devel version | Drupal core | PHP | Drush |
|
||||
|---------------|-------------|------|-------|
|
||||
| 5.2+ | 10+ | 8.1+ | 12+ |
|
||||
| 5.0, 5.1 | 9,10 | 8.1+ | 11+ |
|
||||
| 4.x | 8.9+,9 | 7.2+ | 9+ |
|
||||
| 8.x-2.x | 8.x | 7.0+ | 8+ |
|
||||
|
||||
#### Maintainers
|
||||
|
||||
See https://gitlab.com/groups/drupaladmins/devel/-/group_members.
|
||||
40
web/modules/contrib/devel/composer.json
Normal file
40
web/modules/contrib/devel/composer.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "drupal/devel",
|
||||
"description": "Various blocks, pages, and functions for developers.",
|
||||
"type": "drupal-module",
|
||||
"support": {
|
||||
"issues": "https://gitlab.com/drupalspoons/devel/-/issues",
|
||||
"slack": "https://drupal.slack.com/archives/C012WAW1MH6",
|
||||
"source": "https://gitlab.com/drupalspoons/devel"
|
||||
},
|
||||
"license": "GPL-2.0-or-later",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/var-dumper": "^4 || ^5 || ^6 || ^7",
|
||||
"doctrine/common": "^2.7 || ^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"drush/drush": "^13",
|
||||
"firephp/firephp-core": "^0.5.3",
|
||||
"drupal/navigation_extra_tools": "1.0.x-dev"
|
||||
},
|
||||
"suggest": {
|
||||
"drupal/kint": "Kint provides an informative display of arrays/objects. Useful for debugging and developing."
|
||||
},
|
||||
"conflict": {
|
||||
"drush/drush": "<12.5.1",
|
||||
"drupal/core": "<10.3"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/installers": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"cweagans/composer-patches": true,
|
||||
"drupal/core-composer-scaffold": true,
|
||||
"drupalspoons/composer-plugin": true,
|
||||
"phpstan/extension-installer": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
}
|
||||
}
|
||||
10
web/modules/contrib/devel/config/install/devel.settings.yml
Normal file
10
web/modules/contrib/devel/config/install/devel.settings.yml
Normal file
@ -0,0 +1,10 @@
|
||||
page_alter: FALSE
|
||||
raw_names: FALSE
|
||||
error_handlers:
|
||||
1: 1
|
||||
rebuild_theme: FALSE
|
||||
debug_mail_file_format: '%to-%subject-%datetime.mail.txt'
|
||||
debug_mail_directory: 'temporary://devel-mails'
|
||||
devel_dumper: 'default'
|
||||
debug_logfile: 'temporary://drupal_debug.txt'
|
||||
debug_pre: TRUE
|
||||
@ -0,0 +1,8 @@
|
||||
toolbar_items:
|
||||
- 'devel.admin_settings_link'
|
||||
- 'devel.cache_clear'
|
||||
- 'devel.container_info.service'
|
||||
- 'devel.menu_rebuild'
|
||||
- 'devel.reinstall'
|
||||
- 'devel.route_info'
|
||||
- 'devel.run_cron'
|
||||
@ -0,0 +1,10 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
enforced:
|
||||
module:
|
||||
- devel
|
||||
id: devel
|
||||
label: Development
|
||||
description: 'Links related to Devel module.'
|
||||
locked: true
|
||||
59
web/modules/contrib/devel/config/schema/devel.schema.yml
Normal file
59
web/modules/contrib/devel/config/schema/devel.schema.yml
Normal file
@ -0,0 +1,59 @@
|
||||
# Schema for the configuration files of the Devel module.
|
||||
|
||||
devel.settings:
|
||||
type: config_object
|
||||
label: 'Devel settings'
|
||||
mapping:
|
||||
page_alter:
|
||||
type: boolean
|
||||
label: 'Page alter'
|
||||
raw_names:
|
||||
type: boolean
|
||||
label: 'Raw names'
|
||||
error_handlers:
|
||||
label: 'Error handlers'
|
||||
type: sequence
|
||||
sequence:
|
||||
type: integer
|
||||
rebuild_theme:
|
||||
type: boolean
|
||||
label: 'Rebuild theme information'
|
||||
debug_mail_file_format:
|
||||
type: string
|
||||
label: 'Mail debug file format'
|
||||
debug_mail_directory:
|
||||
type: string
|
||||
label: 'Mail debug directory'
|
||||
devel_dumper:
|
||||
type: string
|
||||
label: 'Devel variable dumper'
|
||||
debug_logfile:
|
||||
type: string
|
||||
label: 'Devel debug log file'
|
||||
debug_pre:
|
||||
type: boolean
|
||||
label: 'Wrap debug output in pre tags'
|
||||
|
||||
devel.toolbar.settings:
|
||||
type: config_object
|
||||
label: 'Devel Toolbar settings'
|
||||
mapping:
|
||||
toolbar_items:
|
||||
type: sequence
|
||||
label: 'Toolbar items'
|
||||
sequence:
|
||||
type: string
|
||||
|
||||
block.settings.devel_switch_user:
|
||||
type: block_settings
|
||||
label: 'Switch user block'
|
||||
mapping:
|
||||
list_size:
|
||||
type: integer
|
||||
label: 'List size'
|
||||
include_anon:
|
||||
type: boolean
|
||||
label: 'Include Anonymous user'
|
||||
show_form:
|
||||
type: boolean
|
||||
label: 'Show search form'
|
||||
28
web/modules/contrib/devel/css/devel.css
Normal file
28
web/modules/contrib/devel/css/devel.css
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Dumpers
|
||||
*/
|
||||
.devel-dumper .details-wrapper {
|
||||
overflow: auto;
|
||||
max-height: 450px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch User block
|
||||
*/
|
||||
.region-content .block-devel-switch-user ul,
|
||||
.site-footer .block-devel-switch-user ul {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.region-content .block-devel-switch-user ul li,
|
||||
.site-footer .block-devel-switch-user ul li {
|
||||
display: block;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
.devel-switchuser-form {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
42
web/modules/contrib/devel/css/devel.toolbar.css
Normal file
42
web/modules/contrib/devel/css/devel.toolbar.css
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @file
|
||||
* Styling for devel toolbar module.
|
||||
*/
|
||||
|
||||
.toolbar .toolbar-tray-vertical .edit-devel-toolbar {
|
||||
padding: 1em;
|
||||
text-align: right; /* LTR */
|
||||
}
|
||||
|
||||
[dir="rtl"] .toolbar .toolbar-tray-vertical .edit-devel-toolbar {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-tray-horizontal .edit-devel-toolbar {
|
||||
float: right; /* LTR */
|
||||
}
|
||||
|
||||
[dir="rtl"] .toolbar .toolbar-tray-horizontal .edit-devel-toolbar {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-tray-horizontal .toolbar-menu {
|
||||
float: left; /* LTR */
|
||||
}
|
||||
|
||||
[dir="rtl"] .toolbar .toolbar-tray-horizontal .toolbar-menu {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-bar .toolbar-icon-devel::before {
|
||||
background-image: url(../icons/bebebe/cog.svg);
|
||||
}
|
||||
|
||||
.toolbar-bar .toolbar-icon-devel:active::before,
|
||||
.toolbar-bar .toolbar-icon-devel.is-active::before {
|
||||
background-image: url(../icons/ffffff/cog.svg);
|
||||
}
|
||||
|
||||
.toolbar-horizontal .toolbar-horizontal-item-hidden {
|
||||
display: none;
|
||||
}
|
||||
25
web/modules/contrib/devel/devel.api.php
Normal file
25
web/modules/contrib/devel/devel.api.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks for the devel module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alter devel dumper information declared by other modules.
|
||||
*
|
||||
* @param array $info
|
||||
* Devel dumper information to alter.
|
||||
*/
|
||||
function hook_devel_dumper_info_alter(array &$info) {
|
||||
$info['default']['label'] = 'Altered label';
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
||||
14
web/modules/contrib/devel/devel.info.yml
Normal file
14
web/modules/contrib/devel/devel.info.yml
Normal file
@ -0,0 +1,14 @@
|
||||
type: module
|
||||
name: Devel
|
||||
description: 'Various blocks, pages, and functions for developers.'
|
||||
package: Development
|
||||
core_version_requirement: "^10.3 || ^11 || ^12"
|
||||
php: 8.1
|
||||
configure: devel.admin_settings
|
||||
tags:
|
||||
- developer
|
||||
|
||||
# Information added by Drupal.org packaging script on 2025-07-07
|
||||
version: '5.4.0'
|
||||
project: 'devel'
|
||||
datestamp: 1751916161
|
||||
49
web/modules/contrib/devel/devel.install
Normal file
49
web/modules/contrib/devel/devel.install
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the devel module.
|
||||
*/
|
||||
|
||||
use Drupal\devel\Plugin\Devel\Dumper\VarDumper;
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function devel_install() {
|
||||
$rich_dumper = NULL;
|
||||
if (VarDumper::checkRequirements()) {
|
||||
$rich_dumper = 'var_dumper';
|
||||
}
|
||||
if (isset($rich_dumper)) {
|
||||
\Drupal::configFactory()->getEditable('devel.settings')
|
||||
->set('devel_dumper', $rich_dumper)
|
||||
->save(TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function devel_requirements($phase) {
|
||||
$requirements = [];
|
||||
|
||||
if ($phase == 'runtime') {
|
||||
// To understand the reasons why this message is marked as info see
|
||||
// https://www.drupal.org/node/2834400.
|
||||
$requirements['devel'] = [
|
||||
'title' => t('Devel module enabled'),
|
||||
'description' => t("The Devel module provides access to internal debugging information; therefore it's recommended to disable this module on sites in production."),
|
||||
'severity' => REQUIREMENT_INFO,
|
||||
];
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_update_last_removed().
|
||||
*/
|
||||
function devel_update_last_removed(): int {
|
||||
return 8002;
|
||||
}
|
||||
14
web/modules/contrib/devel/devel.libraries.yml
Normal file
14
web/modules/contrib/devel/devel.libraries.yml
Normal file
@ -0,0 +1,14 @@
|
||||
devel:
|
||||
css:
|
||||
theme:
|
||||
css/devel.css: {}
|
||||
|
||||
devel-toolbar:
|
||||
css:
|
||||
component:
|
||||
css/devel.toolbar.css: {}
|
||||
|
||||
devel-table-filter:
|
||||
js: {}
|
||||
dependencies:
|
||||
- system/drupal.system.modules
|
||||
94
web/modules/contrib/devel/devel.links.menu.yml
Normal file
94
web/modules/contrib/devel/devel.links.menu.yml
Normal file
@ -0,0 +1,94 @@
|
||||
devel.admin_settings:
|
||||
title: 'Devel settings'
|
||||
description: 'Helper functions, pages, and blocks to assist Drupal developers. The devel blocks can be managed via the block administration page.'
|
||||
route_name: devel.admin_settings
|
||||
parent: 'system.admin_config_development'
|
||||
devel.admin_settings_link:
|
||||
title: 'Devel settings'
|
||||
description: 'Helper functions, pages, and blocks to assist Drupal developers. The devel blocks can be managed via the block administration page.'
|
||||
route_name: devel.admin_settings
|
||||
menu_name: devel
|
||||
devel.configs_list:
|
||||
title: 'Config editor'
|
||||
description: 'Edit configuration.'
|
||||
route_name: devel.configs_list
|
||||
menu_name: devel
|
||||
devel.reinstall:
|
||||
title: 'Reinstall Modules'
|
||||
route_name: devel.reinstall
|
||||
menu_name: devel
|
||||
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
|
||||
devel.menu_rebuild:
|
||||
title: 'Rebuild Menu'
|
||||
route_name: devel.menu_rebuild
|
||||
menu_name: devel
|
||||
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
|
||||
devel.state_system_page:
|
||||
title: 'State editor'
|
||||
description: 'Edit state system values.'
|
||||
route_name: devel.state_system_page
|
||||
menu_name: devel
|
||||
devel.theme_registry:
|
||||
title: 'Theme registry'
|
||||
route_name: devel.theme_registry
|
||||
menu_name: devel
|
||||
devel.entity_info_page:
|
||||
title: 'Entity Info'
|
||||
route_name: devel.entity_info_page
|
||||
# parent: 'system.admin_config_development'
|
||||
menu_name: devel
|
||||
devel.field_info_page:
|
||||
title: 'Field Info'
|
||||
route_name: devel.field_info_page
|
||||
# parent: 'system.admin_config_development'
|
||||
menu_name: devel
|
||||
devel.phpinfo:
|
||||
title: 'PHPinfo()'
|
||||
route_name: system.php
|
||||
menu_name: devel
|
||||
devel.session:
|
||||
title: 'View Session'
|
||||
route_name: devel.session
|
||||
menu_name: devel
|
||||
devel.elements_page:
|
||||
title: 'Element Info'
|
||||
route_name: devel.elements_page
|
||||
menu_name: devel
|
||||
devel.cache_clear:
|
||||
title: 'Cache clear'
|
||||
route_name: devel.cache_clear
|
||||
menu_name: devel
|
||||
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
|
||||
devel.run_cron:
|
||||
title: 'Run cron'
|
||||
route_name: devel.run_cron
|
||||
menu_name: devel
|
||||
class: \Drupal\devel\Plugin\Menu\DestinationMenuLink
|
||||
devel.switch_user:
|
||||
title: 'Switch user'
|
||||
route_name: devel.switch_user
|
||||
menu_name: devel
|
||||
|
||||
|
||||
# Container info
|
||||
devel.container_info.service:
|
||||
title: 'Container Info'
|
||||
route_name: devel.container_info.service
|
||||
menu_name: devel
|
||||
|
||||
# Routes info
|
||||
devel.route_info:
|
||||
title: 'Routes Info'
|
||||
route_name: devel.route_info
|
||||
menu_name: devel
|
||||
devel.route_info.item:
|
||||
title: 'Current route info'
|
||||
route_name: devel.route_info.item
|
||||
menu_name: devel
|
||||
class: \Drupal\devel\Plugin\Menu\RouteDetailMenuLink
|
||||
|
||||
# Event info
|
||||
devel.event_info:
|
||||
title: 'Events Info'
|
||||
route_name: devel.event_info
|
||||
menu_name: devel
|
||||
18
web/modules/contrib/devel/devel.links.task.yml
Normal file
18
web/modules/contrib/devel/devel.links.task.yml
Normal file
@ -0,0 +1,18 @@
|
||||
devel.entities:
|
||||
class: \Drupal\Core\Menu\LocalTaskDefault
|
||||
deriver: \Drupal\devel\Plugin\Derivative\DevelLocalTask
|
||||
devel.admin_settings:
|
||||
title: 'Settings'
|
||||
route_name: devel.admin_settings
|
||||
base_route: devel.admin_settings
|
||||
weight: 0
|
||||
|
||||
# Container info
|
||||
devel.container_info.service:
|
||||
title: 'Services'
|
||||
route_name: devel.container_info.service
|
||||
base_route: devel.container_info.service
|
||||
devel.container_info.parameter:
|
||||
title: 'Parameters'
|
||||
route_name: devel.container_info.parameter
|
||||
base_route: devel.container_info.service
|
||||
709
web/modules/contrib/devel/devel.module
Normal file
709
web/modules/contrib/devel/devel.module
Normal file
@ -0,0 +1,709 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* This module holds functions useful for Drupal development.
|
||||
*
|
||||
* Please contribute!
|
||||
*
|
||||
* Devel is allowed to use its own functions kpr(), dpm() and dpq() so disable
|
||||
* the coding standard which gives warnings for using these.
|
||||
* phpcs:disable Drupal.Functions.DiscouragedFunctions
|
||||
*/
|
||||
|
||||
define('DEVEL_ERROR_HANDLER_NONE', 0);
|
||||
define('DEVEL_ERROR_HANDLER_STANDARD', 1);
|
||||
define('DEVEL_ERROR_HANDLER_BACKTRACE_KINT', 2);
|
||||
define('DEVEL_ERROR_HANDLER_BACKTRACE_DPM', 4);
|
||||
|
||||
use Drupal\Core\Database\Query\AlterableInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Logger\RfcLogLevel;
|
||||
use Drupal\Core\Menu\LocalTaskDefault;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\Utility\Error;
|
||||
use Drupal\devel\EntityTypeInfo;
|
||||
use Drupal\devel\ToolbarHandler;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function devel_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.devel':
|
||||
$output = '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Devel module provides a suite of modules containing fun for module developers and themers. For more information, see the <a href=":url">online documentation for the Devel module</a>.', [':url' => 'https://www.drupal.org/docs/contributed-modules/devel']) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Inspecting Service Container') . '</dt>';
|
||||
$output .= '<dd>' . t('The module allows you to inspect Services and Parameters registered in the Service Container. You can see those informations on <a href=":url">Container info</a> page.', [':url' => Url::fromRoute('devel.container_info.service')->toString()]) . '</dd>';
|
||||
$output .= '<dt>' . t('Inspecting Routes') . '</dt>';
|
||||
$output .= '<dd>' . t('The module allows you to inspect routes information, gathering all routing data from <em>.routing.yml</em> files and from classes which subscribe to the route build/alter events. You can see those informations on <a href=":url">Routes info</a> page.', [':url' => Url::fromRoute('devel.route_info')->toString()]) . '</dd>';
|
||||
$output .= '<dt>' . t('Inspecting Events') . '</dt>';
|
||||
$output .= '<dd>' . t('The module allow you to inspect listeners registered in the event dispatcher. You can see those informations on <a href=":url">Events info</a> page.', [':url' => Url::fromRoute('devel.event_info')->toString()]) . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
|
||||
case 'devel.container_info.service':
|
||||
case 'devel.container_info.parameter':
|
||||
return '<p>' . t('Displays Services and Parameters registered in the Service Container. For more informations on the Service Container, see the <a href=":url">Symfony online documentation</a>.', [':url' => 'http://symfony.com/doc/current/service_container.html']) . '</p>';
|
||||
|
||||
case 'devel.route_info':
|
||||
return '<p>' . t('Displays registered routes for the site. For a complete overview of the routing system, see the <a href=":url">online documentation</a>.', [':url' => 'https://www.drupal.org/docs/drupal-apis/routing-system']) . '</p>';
|
||||
|
||||
case 'devel.event_info':
|
||||
return '<p>' . t('Displays events and listeners registered in the event dispatcher. For a complete overview of the event system, see the <a href=":url">Symfony online documentation</a>.', [':url' => 'http://symfony.com/doc/current/components/event_dispatcher.html']) . '</p>';
|
||||
|
||||
case 'devel.reinstall':
|
||||
$output = '<p>' . t('<strong>Warning</strong> - will delete your module tables and configuration.') . '</p>';
|
||||
$output .= '<p>' . t('Uninstall and then install the selected modules. <code>hook_uninstall()</code> and <code>hook_install()</code> will be executed and the schema version number will be set to the most recent update number.') . '</p>';
|
||||
return $output;
|
||||
|
||||
case 'devel/session':
|
||||
return '<p>' . t('Here are the contents of your <code>$_SESSION</code> variable.') . '</p>';
|
||||
|
||||
case 'devel.state_system_page':
|
||||
return '<p>' . t('This is a list of state variables and their values. For more information read online documentation of <a href=":documentation">State API in Drupal 8</a>.', [':documentation' => "https://www.drupal.org/docs/develop/drupal-apis/state-api"]) . '</p>';
|
||||
|
||||
case 'devel.layout_info':
|
||||
return '<p>' . t('Displays layouts available to the site. For a complete overview of the layout system, see the <a href=":url">Layout API documentation</a>.', [':url' => 'https://www.drupal.org/docs/drupal-apis/layout-api']) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_alter().
|
||||
*/
|
||||
function devel_entity_type_alter(array &$entity_types) {
|
||||
Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityTypeAlter($entity_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_operation().
|
||||
*/
|
||||
function devel_entity_operation(EntityInterface $entity) {
|
||||
return Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityOperation($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_toolbar().
|
||||
*/
|
||||
function devel_toolbar() {
|
||||
return Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(ToolbarHandler::class)
|
||||
->toolbar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu_links_discovered_alter().
|
||||
*/
|
||||
function devel_menu_links_discovered_alter(&$links) {
|
||||
// Conditionally add the Layouts info menu link.
|
||||
if (Drupal::moduleHandler()->moduleExists('layout_discovery')) {
|
||||
$links['devel.layout_info'] = [
|
||||
'title' => new TranslatableMarkup('Layouts Info'),
|
||||
'route_name' => 'devel.layout_info',
|
||||
'description' => new TranslatableMarkup('Overview of layouts available to the site.'),
|
||||
'menu_name' => 'devel',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_local_tasks_alter().
|
||||
*/
|
||||
function devel_local_tasks_alter(&$local_tasks) {
|
||||
if (Drupal::moduleHandler()->moduleExists('toolbar')) {
|
||||
$local_tasks['devel.toolbar.settings_form'] = [
|
||||
'title' => 'Toolbar Settings',
|
||||
'base_route' => 'devel.admin_settings',
|
||||
'route_name' => 'devel.toolbar.settings_form',
|
||||
'class' => LocalTaskDefault::class,
|
||||
'options' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets message.
|
||||
*/
|
||||
function devel_set_message($msg, ?string $type = NULL) {
|
||||
if (function_exists('drush_log')) {
|
||||
drush_log($msg, $type);
|
||||
}
|
||||
else {
|
||||
Drupal::messenger()->addMessage($msg, $type, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets error handlers.
|
||||
*/
|
||||
function devel_get_handlers() {
|
||||
$error_handlers = Drupal::config('devel.settings')->get('error_handlers');
|
||||
if (!empty($error_handlers)) {
|
||||
unset($error_handlers[DEVEL_ERROR_HANDLER_NONE]);
|
||||
}
|
||||
return $error_handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new error handler or restores the prior one.
|
||||
*/
|
||||
function devel_set_handler($handlers) {
|
||||
if (empty($handlers)) {
|
||||
restore_error_handler();
|
||||
}
|
||||
elseif (count($handlers) == 1 && isset($handlers[DEVEL_ERROR_HANDLER_STANDARD])) {
|
||||
// Do nothing.
|
||||
}
|
||||
else {
|
||||
set_error_handler('backtrace_error_handler');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays backtrace showing the route of calls to the current error.
|
||||
*
|
||||
* @param int $error_level
|
||||
* The level of the error raised.
|
||||
* @param string $message
|
||||
* The error message.
|
||||
* @param string|null $filename
|
||||
* (optional) The filename that the error was raised in.
|
||||
* @param int $line
|
||||
* (optional) The line number the error was raised at.
|
||||
* @param array $context
|
||||
* (optional) An array that points to the active symbol table at the point the
|
||||
* error occurred.
|
||||
*/
|
||||
function backtrace_error_handler($error_level, $message, ?string $filename = NULL, $line = NULL, ?array $context = NULL) {
|
||||
// Hide stack trace and parameters from unqualified users.
|
||||
if (!Drupal::currentUser()->hasPermission('access devel information')) {
|
||||
// Do what core does in bootstrap.inc and errors.inc.
|
||||
// (We need to duplicate the core code here rather than calling it
|
||||
// to avoid having the backtrace_error_handler() on top of the call stack.)
|
||||
if ($error_level & error_reporting()) {
|
||||
$types = drupal_error_levels();
|
||||
[$severity_msg, $severity_level] = $types[$error_level];
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$caller = Error::getLastCaller($backtrace);
|
||||
|
||||
// We treat recoverable errors as fatal.
|
||||
_drupal_log_error([
|
||||
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
|
||||
'@message' => $message,
|
||||
'%function' => $caller['function'],
|
||||
'%file' => $caller['file'],
|
||||
'%line' => $caller['line'],
|
||||
'severity_level' => $severity_level,
|
||||
'backtrace' => $backtrace,
|
||||
], $error_level == E_RECOVERABLE_ERROR);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't respond to the error if it was suppressed with a '@'.
|
||||
if (error_reporting() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't respond to warning caused by ourselves.
|
||||
if (preg_match('#Cannot modify header information - headers already sent by \\([^\\)]*[/\\\\]devel[/\\\\]#', $message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($error_level & error_reporting()) {
|
||||
// Only write each distinct NOTICE message once, as repeats do not give any
|
||||
// further information and can choke the page output.
|
||||
if ($error_level == E_NOTICE) {
|
||||
static $written = [];
|
||||
if (!empty($written[$line][$filename][$message])) {
|
||||
return;
|
||||
}
|
||||
$written[$line][$filename][$message] = TRUE;
|
||||
}
|
||||
|
||||
$types = drupal_error_levels();
|
||||
[$severity_msg, $severity_level] = $types[$error_level];
|
||||
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
$caller = Error::getLastCaller($backtrace);
|
||||
$variables = [
|
||||
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
|
||||
'@message' => $message,
|
||||
'%function' => $caller['function'],
|
||||
'%file' => $caller['file'],
|
||||
'%line' => $caller['line'],
|
||||
];
|
||||
$msg = t('%type: @message in %function (line %line of %file).', $variables);
|
||||
|
||||
// Show message if error_level is ERROR_REPORTING_DISPLAY_SOME or higher.
|
||||
// (This is Drupal's error_level, which is different from $error_level,
|
||||
// and we purposely ignore the difference between _SOME and _ALL,
|
||||
// see #970688!)
|
||||
if (Drupal::config('system.logging')->get('error_level') != 'hide') {
|
||||
$error_handlers = devel_get_handlers();
|
||||
if (!empty($error_handlers[DEVEL_ERROR_HANDLER_STANDARD])) {
|
||||
Drupal::messenger()
|
||||
->addMessage($msg, ($severity_level <= RfcLogLevel::NOTICE ? MessengerInterface::TYPE_ERROR : MessengerInterface::TYPE_WARNING), TRUE);
|
||||
}
|
||||
if (!empty($error_handlers[DEVEL_ERROR_HANDLER_BACKTRACE_KINT])) {
|
||||
$input = ddebug_backtrace(return: TRUE, pop: 1);
|
||||
print Drupal::service('devel.dumper')
|
||||
->dumpOrExport(input: $input, name: $msg);
|
||||
}
|
||||
if (!empty($error_handlers[DEVEL_ERROR_HANDLER_BACKTRACE_DPM])) {
|
||||
$input = ddebug_backtrace(return: TRUE, pop: 1);
|
||||
Drupal::service('devel.dumper')
|
||||
->message(input: $input, name: $msg, type: MessengerInterface::TYPE_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
Drupal::logger('php')->log($severity_level, $msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_page_attachments_alter().
|
||||
*/
|
||||
function devel_page_attachments_alter(array &$attachments): void {
|
||||
if (Drupal::currentUser()->hasPermission('access devel information')
|
||||
&& Drupal::config('devel.settings')->get('page_alter')) {
|
||||
Drupal::service('devel.dumper')->message(input: $attachments, name: 'attachments');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps information about a variable.
|
||||
*
|
||||
* Wrapper for DevelDumperManager::dump().
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
* @param string $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::dump()
|
||||
*/
|
||||
function devel_dump($input, ?string $name = NULL, $plugin_id = NULL) {
|
||||
Drupal::service('devel.dumper')->dump($input, $name, $plugin_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of a variable.
|
||||
*
|
||||
* Wrapper for DevelDumperManager::export().
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
* @param string $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @return string
|
||||
* String representation of a variable.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::export()
|
||||
*/
|
||||
function devel_export($input, ?string $name = NULL, $plugin_id = NULL) {
|
||||
return Drupal::service('devel.dumper')->export($input, $name, $plugin_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a message with a string representation of a variable.
|
||||
*
|
||||
* Wrapper for DevelDumperManager::message().
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
* @param string $type
|
||||
* (optional) The message's type. Defaults to 'status'.
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::message()
|
||||
*/
|
||||
function devel_message(mixed $input, ?string $name = NULL, string $type = MessengerInterface::TYPE_STATUS, ?string $plugin_id = NULL): void {
|
||||
Drupal::service('devel.dumper')
|
||||
->message(input: $input, name: $name, type: $type, plugin_id: $plugin_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a variable to a drupal_debug.txt in the site's temp directory.
|
||||
*
|
||||
* Wrapper for DevelDumperManager::debug().
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to log to the drupal_debug.txt log file.
|
||||
* @param string|null $name
|
||||
* (optional) If set, a label to output before $data in the log file.
|
||||
* @param string $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @return null|false
|
||||
* Empty if successful, FALSE if the log file could not be written.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::debug()
|
||||
*/
|
||||
function devel_debug($input, ?string $name = NULL, $plugin_id = NULL) {
|
||||
return Drupal::service('devel.dumper')->debug($input, $name, $plugin_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::dump().
|
||||
*
|
||||
* Calls the http://www.firephp.org/ fb() function if it is found.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::dump()
|
||||
*/
|
||||
function dfb() {
|
||||
$args = func_get_args();
|
||||
Drupal::service('devel.dumper')->dump($args, NULL, 'firephp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::dump().
|
||||
*
|
||||
* Calls dfb() to output a backtrace.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::dump()
|
||||
*/
|
||||
function dfbt($label) {
|
||||
Drupal::service('devel.dumper')->dump(FirePHP::TRACE, $label, 'firephp');
|
||||
}
|
||||
|
||||
if (!function_exists('ddm')) {
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::debug() to replace previous dd function.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::debug()
|
||||
*/
|
||||
function ddm($data, ?string $label = NULL) {
|
||||
return Drupal::service('devel.dumper')->debug($data, $label, 'default');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::message().
|
||||
*
|
||||
* Prints a variable to the 'message' area of the page.
|
||||
*
|
||||
* Uses Drupal\Core\Messenger\MessengerInterface::addMessage()
|
||||
*
|
||||
* @param mixed $input
|
||||
* An arbitrary value to output.
|
||||
* @param string|null $name
|
||||
* Optional name for identifying the output.
|
||||
* @param string $type
|
||||
* Optional message type see MessengerInterface, defaults to TYPE_STATUS.
|
||||
* @param bool $load_dependencies
|
||||
* Optional load dependencies if an entity is passed.
|
||||
*
|
||||
* @return mixed
|
||||
* The unaltered input value.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::message()
|
||||
*/
|
||||
function dpm(mixed $input, ?string $name = NULL, string $type = MessengerInterface::TYPE_STATUS, bool $load_dependencies = FALSE): mixed {
|
||||
Drupal::service('devel.dumper')
|
||||
->message(input: $input, name: $name, type: $type, load_references: $load_dependencies);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::message().
|
||||
*
|
||||
* Displays a Variable::export() variable to the 'message' area of the page.
|
||||
*
|
||||
* Uses Drupal\Core\Messenger\MessengerInterface::addMessage()
|
||||
*
|
||||
* @param mixed $input
|
||||
* An arbitrary value to output.
|
||||
* @param string|null $name
|
||||
* Optional name for identifying the output.
|
||||
* @param bool $load_dependencies
|
||||
* Optional load dependencies if an entity is passed.
|
||||
*
|
||||
* @return mixed
|
||||
* The unaltered input value.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::message()
|
||||
*/
|
||||
function dvm(mixed $input, ?string $name = NULL, bool $load_dependencies = FALSE): mixed {
|
||||
Drupal::service('devel.dumper')
|
||||
->message(input: $input, name: $name, plugin_id: 'drupal_variable', load_references: $load_dependencies);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias for dpm(), for historic reasons.
|
||||
*/
|
||||
function dsm($input, ?string $name = NULL, $load_dependencies = FALSE) {
|
||||
Drupal::service('devel.dumper')
|
||||
->message(input: $input, name: $name, load_references: $load_dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::dumpOrExport().
|
||||
*
|
||||
* An alias for the 'devel.dumper' service. Saves carpal tunnel syndrome.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::dumpOrExport()
|
||||
*/
|
||||
function dpr($input, $export = FALSE, ?string $name = NULL) {
|
||||
return Drupal::service('devel.dumper')
|
||||
->dumpOrExport(input: $input, name: $name, export: $export, plugin_id: 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for DevelDumperManager::dumpOrExport().
|
||||
*
|
||||
* Like dpr(), but uses Variable::export() instead.
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperManager::dumpOrExport()
|
||||
*/
|
||||
function dvr($input, $export = FALSE, ?string $name = NULL) {
|
||||
return Drupal::service('devel.dumper')
|
||||
->dumpOrExport(input: $input, name: $name, export: $export, plugin_id: 'drupal_variable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the arguments passed into the current function.
|
||||
*/
|
||||
function dargs($always = TRUE) {
|
||||
static $printed;
|
||||
if ($always || !$printed) {
|
||||
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
print Drupal::service('devel.dumper')->dumpOrExport(input: $bt[1]['args']);
|
||||
$printed = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a SQL string from a DBTNG Select object. Includes quoted arguments.
|
||||
*
|
||||
* @param object $query
|
||||
* An object that implements the SelectInterface interface.
|
||||
* @param bool $return
|
||||
* Whether to return the string. Default is FALSE, meaning to print it
|
||||
* and return $query instead.
|
||||
* @param string|null $name
|
||||
* Optional name for identifying the output.
|
||||
*
|
||||
* @return object|string
|
||||
* The $query object, or the query string if $return was TRUE.
|
||||
*/
|
||||
function dpq($query, $return = FALSE, ?string $name = NULL) {
|
||||
if (Drupal::currentUser()->hasPermission('access devel information')) {
|
||||
if (method_exists($query, 'preExecute')) {
|
||||
$query->preExecute();
|
||||
}
|
||||
$sql = (string) $query;
|
||||
$quoted = [];
|
||||
$database = Drupal::database();
|
||||
foreach ((array) $query->arguments() as $key => $val) {
|
||||
$quoted[$key] = is_null($val) ? 'NULL' : $database->quote($val);
|
||||
}
|
||||
$sql = strtr($sql, $quoted);
|
||||
if ($return) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
Drupal::service('devel.dumper')->message(input: $sql, name: $name);
|
||||
}
|
||||
return ($return ? NULL : $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a renderable array element to the screen using kprint_r().
|
||||
*
|
||||
* #pre_render and/or #post_render pass-through callback for kprint_r().
|
||||
*
|
||||
* @todo Investigate appending to #suffix.
|
||||
* @todo Investigate label derived from #id, #title, #name, and #theme.
|
||||
*/
|
||||
function devel_render() {
|
||||
$args = func_get_args();
|
||||
// #pre_render and #post_render pass the rendered $element as last argument.
|
||||
Drupal::service('devel.dumper')
|
||||
->dumpOrExport(input: end($args), export: FALSE);
|
||||
// #pre_render and #post_render expect the first argument to be returned.
|
||||
return reset($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the function call stack.
|
||||
*
|
||||
* @param bool $return
|
||||
* Pass TRUE to return the formatted backtrace rather than displaying it in
|
||||
* the browser via kprint_r().
|
||||
* @param int $pop
|
||||
* How many items to pop from the top of the stack; useful when calling from
|
||||
* an error handler.
|
||||
* @param int $options
|
||||
* Options (treated as a bit mask) to pass on to PHP's debug_backtrace().
|
||||
*
|
||||
* @return array|null
|
||||
* The formatted backtrace, if requested, or NULL.
|
||||
*
|
||||
* @see http://php.net/manual/en/function.debug-backtrace.php
|
||||
*/
|
||||
function ddebug_backtrace(bool $return = FALSE, int $pop = 0, int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT): ?array {
|
||||
if (Drupal::currentUser()
|
||||
->hasPermission('access devel information') === FALSE) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$backtrace = debug_backtrace($options);
|
||||
while ($pop-- > 0) {
|
||||
array_shift($backtrace);
|
||||
}
|
||||
$counter = count($backtrace);
|
||||
$path = $backtrace[$counter - 1]['file'];
|
||||
$path = substr($path, 0, strlen($path) - 10);
|
||||
$paths[$path] = strlen($path) + 1;
|
||||
$paths[DRUPAL_ROOT] = strlen(DRUPAL_ROOT) + 1;
|
||||
$nbsp = "\xC2\xA0";
|
||||
|
||||
// Show message if error_level is ERROR_REPORTING_DISPLAY_SOME or higher.
|
||||
// (This is Drupal's error_level, which is different from $error_level,
|
||||
// and we purposely ignore the difference between _SOME and _ALL,
|
||||
// see #970688!)
|
||||
if (Drupal::config('system.logging')
|
||||
->get('error_level') === ERROR_REPORTING_HIDE) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$nicetrace = [];
|
||||
while (!empty($backtrace)) {
|
||||
$call = [];
|
||||
if (isset($backtrace[0]['file'])) {
|
||||
$call['file'] = $backtrace[0]['file'];
|
||||
foreach ($paths as $path => $len) {
|
||||
if (strpos($backtrace[0]['file'], $path) === 0) {
|
||||
$call['file'] = substr($backtrace[0]['file'], $len);
|
||||
}
|
||||
}
|
||||
$call['file'] .= ':' . $backtrace[0]['line'];
|
||||
}
|
||||
|
||||
if (isset($backtrace[1])) {
|
||||
if (isset($backtrace[1]['class'])) {
|
||||
$function = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
|
||||
}
|
||||
else {
|
||||
$function = $backtrace[1]['function'] . '()';
|
||||
}
|
||||
|
||||
$backtrace[1] += ['args' => []];
|
||||
foreach ($backtrace[1]['args'] as $key => $value) {
|
||||
$call['args'][$key] = $value;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$function = 'main()';
|
||||
$requestStack = Drupal::service('request_stack');
|
||||
$call['args'] = $requestStack->getCurrentRequest()->query->all();
|
||||
}
|
||||
|
||||
$nicetrace[($counter <= 10 ? $nbsp : '') . --$counter . ': ' . $function] = $call;
|
||||
array_shift($backtrace);
|
||||
}
|
||||
|
||||
if ($return) {
|
||||
return $nicetrace;
|
||||
}
|
||||
|
||||
Drupal::service('devel.dumper')
|
||||
->dumpOrExport(input: $nicetrace, export: FALSE);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*
|
||||
* Adds mouse-over hints on the Permissions page to display
|
||||
* language-independent machine names and module base names.
|
||||
*
|
||||
* @see \Drupal\user\Form\UserPermissionsForm::buildForm()
|
||||
*/
|
||||
function devel_form_user_admin_permissions_alter(&$form, FormStateInterface $form_state) {
|
||||
if (Drupal::currentUser()->hasPermission('access devel information')
|
||||
&& Drupal::config('devel.settings')->get('raw_names')) {
|
||||
foreach (Element::children($form['permissions']) as $key) {
|
||||
if (isset($form['permissions'][$key][0])) {
|
||||
$form['permissions'][$key][0]['#wrapper_attributes']['title'] = $key;
|
||||
}
|
||||
elseif (isset($form['permissions'][$key]['description'])) {
|
||||
$form['permissions'][$key]['description']['#wrapper_attributes']['title'] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*
|
||||
* Adds mouse-over hints on the Modules page to display module base names.
|
||||
*
|
||||
* @see \Drupal\system\Form\ModulesListForm::buildForm()
|
||||
* @see theme_system_modules_details()
|
||||
*/
|
||||
function devel_form_system_modules_alter(&$form, FormStateInterface $form_state) {
|
||||
if (Drupal::currentUser()->hasPermission('access devel information')
|
||||
&& Drupal::config('devel.settings')->get('raw_names')
|
||||
&& isset($form['modules']) && is_array($form['modules'])) {
|
||||
foreach (Element::children($form['modules']) as $group) {
|
||||
if (is_array($form['modules'][$group])) {
|
||||
foreach (Element::children($form['modules'][$group]) as $key) {
|
||||
if (isset($form['modules'][$group][$key]['name']['#markup'])) {
|
||||
$form['modules'][$group][$key]['name']['#markup'] = '<span title="' . $key . '">' . $form['modules'][$group][$key]['name']['#markup'] . '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_query_TAG_alter().
|
||||
*
|
||||
* Makes debugging entity query much easier.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* @code
|
||||
* $query = \Drupal::entityQuery('node');
|
||||
* $query->condition('status', NODE_PUBLISHED);
|
||||
* $query->addTag('debug');
|
||||
* $query->execute();
|
||||
* @endcode
|
||||
*/
|
||||
function devel_query_debug_alter(AlterableInterface $query) {
|
||||
if (!$query->hasTag('debug-semaphore')) {
|
||||
$query->addTag('debug-semaphore');
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
dpq($query);
|
||||
}
|
||||
}
|
||||
9
web/modules/contrib/devel/devel.permissions.yml
Normal file
9
web/modules/contrib/devel/devel.permissions.yml
Normal file
@ -0,0 +1,9 @@
|
||||
access devel information:
|
||||
description: 'View developer output like variable printouts, query log, etc.'
|
||||
title: 'Access developer information'
|
||||
restrict access: TRUE
|
||||
|
||||
switch users:
|
||||
title: 'Switch users'
|
||||
description: 'Become any user on the site with just a click.'
|
||||
restrict access: TRUE
|
||||
298
web/modules/contrib/devel/devel.routing.yml
Normal file
298
web/modules/contrib/devel/devel.routing.yml
Normal file
@ -0,0 +1,298 @@
|
||||
devel.admin_settings:
|
||||
path: '/admin/config/development/devel'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\SettingsForm'
|
||||
_title: 'Devel settings'
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.toolbar.settings_form:
|
||||
path: '/admin/config/development/devel/toolbar'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\ToolbarSettingsForm'
|
||||
_title: 'Devel Toolbar Settings'
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
_module_dependencies: 'toolbar'
|
||||
|
||||
devel.reinstall:
|
||||
path: '/devel/reinstall'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\DevelReinstall'
|
||||
_title: 'Reinstall modules'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.menu_rebuild:
|
||||
path: '/devel/menu/reset'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\RouterRebuildConfirmForm'
|
||||
_title: 'Rebuild router'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.configs_list:
|
||||
path: '/devel/config/{filter}'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\ConfigsList'
|
||||
_title: 'Config editor'
|
||||
filter: ''
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.config_edit:
|
||||
path: '/devel/config/edit/{config_name}'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\ConfigEditor'
|
||||
_title: 'Edit configuration object: @config_name'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.config_delete:
|
||||
path: '/devel/config/delete/{config_name}'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\ConfigDeleteForm'
|
||||
_title: 'Delete configuration object: @config_name'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.state_system_page:
|
||||
path: '/devel/state'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\DevelController::stateSystemPage'
|
||||
_title: 'State editor'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.system_state_edit:
|
||||
path: '/devel/state/edit/{state_name}'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\SystemStateEdit'
|
||||
_title: 'Edit state variable: @state_name'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
|
||||
devel.theme_registry:
|
||||
path: '/devel/theme/registry'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\DevelController::themeRegistry'
|
||||
_title: 'Theme registry'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.field_info_page:
|
||||
path: '/devel/field/info'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\DevelController::fieldInfoPage'
|
||||
_title: 'Field info'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.session:
|
||||
path: '/devel/session'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\DevelController::session'
|
||||
_title: 'Session viewer'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.switch:
|
||||
path: '/devel/switch/{name}'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\SwitchUserController::switchUser'
|
||||
_title: 'Switch user'
|
||||
name: ''
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'switch users'
|
||||
_csrf_token: 'TRUE'
|
||||
|
||||
devel.switch_user:
|
||||
path: '/devel/switch-user'
|
||||
defaults:
|
||||
_form: '\Drupal\devel\Form\SwitchUserPageForm'
|
||||
_title: 'Switch user'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'switch users'
|
||||
|
||||
devel.cache_clear:
|
||||
path: '/devel/cache/clear'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\DevelController::cacheClear'
|
||||
_title: 'Clear cache'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
_csrf_token: 'TRUE'
|
||||
|
||||
devel.run_cron:
|
||||
path: '/devel/run-cron'
|
||||
defaults:
|
||||
_controller: '\Drupal\system\CronController::runManually'
|
||||
_title: 'Run cron'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'administer site configuration'
|
||||
_csrf_token: 'TRUE'
|
||||
|
||||
# Container info
|
||||
devel.container_info.service:
|
||||
path: '/devel/container/service'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\ContainerInfoController::serviceList'
|
||||
_title: 'Container services'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.container_info.service.detail:
|
||||
path: '/devel/container/service/{service_id}'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\ContainerInfoController::serviceDetail'
|
||||
_title: 'Service @service_id detail'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.container_info.parameter:
|
||||
path: '/devel/container/parameter'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\ContainerInfoController::parameterList'
|
||||
_title: 'Container parameters'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.container_info.parameter.detail:
|
||||
path: '/devel/container/parameter/{parameter_name}'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\ContainerInfoController::parameterDetail'
|
||||
_title: 'Parameter @parameter_name value'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
# Route info
|
||||
devel.route_info:
|
||||
path: '/devel/routes'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\RouteInfoController::routeList'
|
||||
_title: 'Routes'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.route_info.item:
|
||||
path: '/devel/routes/item'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\RouteInfoController::routeDetail'
|
||||
_title: 'Route detail'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
# Event info
|
||||
devel.event_info:
|
||||
path: '/devel/events'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\EventInfoController::eventList'
|
||||
_title: 'Events'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
# Layouts info
|
||||
devel.layout_info:
|
||||
path: '/devel/layouts'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\LayoutInfoController::layoutInfoPage'
|
||||
_title: 'Layouts'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
_module_dependencies: 'layout_discovery'
|
||||
|
||||
# Element info
|
||||
devel.elements_page:
|
||||
path: '/devel/elements'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\ElementInfoController::elementList'
|
||||
_title: 'Element Info'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.elements_page.detail:
|
||||
path: '/devel/elements/{element_name}'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\ElementInfoController::elementDetail'
|
||||
_title: 'Element @element_name'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
# Entity type info
|
||||
devel.entity_info_page:
|
||||
path: '/devel/entity/info'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\EntityTypeInfoController::entityTypeList'
|
||||
_title: 'Entity info'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.entity_info_page.detail:
|
||||
path: '/devel/entity/info/{entity_type_id}'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\EntityTypeInfoController::entityTypeDetail'
|
||||
_title: 'Entity type @entity_type_id'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
|
||||
devel.entity_info_page.fields:
|
||||
path: '/devel/entity/fields/{entity_type_id}'
|
||||
defaults:
|
||||
_controller: '\Drupal\devel\Controller\EntityTypeInfoController::entityTypeFields'
|
||||
_title: 'Entity fields @entity_type_id'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
requirements:
|
||||
_permission: 'access devel information'
|
||||
51
web/modules/contrib/devel/devel.services.yml
Normal file
51
web/modules/contrib/devel/devel.services.yml
Normal file
@ -0,0 +1,51 @@
|
||||
services:
|
||||
devel.error_subscriber:
|
||||
class: Drupal\devel\EventSubscriber\ErrorHandlerSubscriber
|
||||
arguments: ['@current_user']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
||||
devel.theme_rebuild_subscriber:
|
||||
class: Drupal\devel\EventSubscriber\ThemeInfoRebuildSubscriber
|
||||
arguments: ['@config.factory', '@current_user', '@theme_handler', '@messenger', '@string_translation', '@theme.registry']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
||||
devel.route_subscriber:
|
||||
class: Drupal\devel\Routing\RouteSubscriber
|
||||
arguments: ['@entity_type.manager', '@router.route_provider']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
||||
plugin.manager.devel_dumper:
|
||||
class: Drupal\devel\DevelDumperPluginManager
|
||||
arguments: []
|
||||
parent: default_plugin_manager
|
||||
|
||||
devel.dumper:
|
||||
class: Drupal\devel\DevelDumperManager
|
||||
arguments: ['@config.factory', '@current_user', '@plugin.manager.devel_dumper', '@entity_type.manager', '@messenger', '@string_translation']
|
||||
|
||||
devel.twig.debug_extension:
|
||||
class: Drupal\devel\Twig\Extension\Debug
|
||||
arguments: ['@devel.dumper']
|
||||
tags:
|
||||
- { name: twig.extension }
|
||||
|
||||
devel.switch_user_list_helper:
|
||||
class: Drupal\devel\SwitchUserListHelper
|
||||
arguments: ['@current_user', '@entity_type.manager', '@redirect.destination', '@string_translation']
|
||||
|
||||
logger.channel.devel:
|
||||
parent: logger.channel_base
|
||||
arguments: ['devel']
|
||||
|
||||
consolidation.site_alias:
|
||||
class: Consolidation\SiteAlias\SiteAliasManager
|
||||
|
||||
consolidation.site_process:
|
||||
class: Consolidation\SiteProcess\ProcessManager
|
||||
|
||||
devel.lazy_builders:
|
||||
class: Drupal\devel\DevelLazyBuilders
|
||||
arguments: ['@menu.link_tree', '@config.factory']
|
||||
59
web/modules/contrib/devel/devel_generate/README.md
Normal file
59
web/modules/contrib/devel/devel_generate/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
[[_TOC_]]
|
||||
|
||||
This module may be used to create entities that contain sample content. This is
|
||||
useful when showing off your site to a client, for example. Even if the content
|
||||
is not yet available, the site can show its look and feel and behavior.
|
||||
|
||||
The sample entities may be created via the Web or via the included Drush commands
|
||||
like `drush genc`.
|
||||
|
||||
#### Recommended Modules
|
||||
|
||||
- [Devel Images Provider](http://drupal.org/project/devel_image_provider) allows to configure external providers for images.
|
||||
|
||||
#### Custom plugins
|
||||
|
||||
This module creates the _DevelGenerate_ plugin type.
|
||||
|
||||
All you need to do to provide a new instance for DevelGenerate plugin type
|
||||
is to create your class extending `DevelGenerateBase` and following these steps:
|
||||
|
||||
1. Declare your plugin with annotations:
|
||||
````
|
||||
/**
|
||||
* Provides a ExampleDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "example",
|
||||
* label = @Translation("example"),
|
||||
* description = @Translation("Generate a given number of example elements."),
|
||||
* url = "example",
|
||||
* permission = "administer example",
|
||||
* settings = {
|
||||
* "num" = 50,
|
||||
* "kill" = FALSE,
|
||||
* "another_property" = "default_value"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
````
|
||||
1. Implement the `settingsForm` method to create a form using the properties
|
||||
from the annotations.
|
||||
1. Implement the `handleDrushParams` method. It should return an array of
|
||||
values.
|
||||
1. Implement the `generateElements` method. You can write here your business
|
||||
logic using the array of values.
|
||||
|
||||
#### Notes
|
||||
|
||||
- You can alter existing properties for every plugin by implementing
|
||||
`hook_devel_generate_info_alter`.
|
||||
- DevelGenerateBaseInterface details base wrapping methods that most
|
||||
DevelGenerate implementations will want to directly inherit from
|
||||
`Drupal\devel_generate\DevelGenerateBase`.
|
||||
- To give support for a new field type the field type base class should properly
|
||||
implement `\Drupal\Core\Field\FieldItemInterface::generateSampleValue()`.
|
||||
Devel Generate automatically uses the values returned by this method during the
|
||||
generate process for generating placeholder field values. For more information
|
||||
see: https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Field!FieldItemInterface.php/function/FieldItemInterface::generateSampleValue
|
||||
- For Drupal 10, the webprofiler module has broken out to its own project at https://www.drupal.org/project/webprofiler
|
||||
45
web/modules/contrib/devel/devel_generate/composer.json
Normal file
45
web/modules/contrib/devel/devel_generate/composer.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "drupal/devel_generate",
|
||||
"description": "Generate test users, nodes, menus, taxonomy terms...",
|
||||
"type": "drupal-module",
|
||||
"homepage": "http://drupal.org/project/devel",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Moshe Weitzman",
|
||||
"email": "weitzman@tejasa.com",
|
||||
"homepage": "https://github.com/weitzman",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Hans Salvisberg",
|
||||
"email": "drupal@salvisberg.com",
|
||||
"homepage": "https://www.drupal.org/u/salvis",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Luca Lusso",
|
||||
"homepage": "https://www.drupal.org/u/lussoluca",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Marco (willzyx)",
|
||||
"homepage": "https://www.drupal.org/u/willzyx",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "See contributors",
|
||||
"homepage": "https://www.drupal.org/node/3236/committers"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "http://drupal.org/project/devel",
|
||||
"irc": "irc://irc.freenode.org/drupal-contribute",
|
||||
"source": "http://cgit.drupalcode.org/devel"
|
||||
},
|
||||
"license": "GPL-2.0-or-later",
|
||||
"minimum-stability": "dev",
|
||||
"require": {},
|
||||
"suggest": {
|
||||
"drupal/realistic_dummy_content": "Generate realistic demo content with Devel's devel_generate module."
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides common batch functions for every DevelGeneratePlugin.
|
||||
*/
|
||||
|
||||
use Drupal\devel_generate\DevelGenerateBaseInterface;
|
||||
|
||||
/**
|
||||
* Calls the correct method responsible for handling a given batch operation.
|
||||
*/
|
||||
function devel_generate_operation(DevelGenerateBaseInterface $class, $method, $vars, &$context) {
|
||||
return $class->$method($vars, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard finish batch function.
|
||||
*/
|
||||
function devel_generate_batch_finished($success, $results, $operations) {
|
||||
|
||||
if ($success) {
|
||||
if (!empty($results['num_translations'])) {
|
||||
$message = t('Finished @num elements and @num_translations translations created successfully.', ['@num' => $results['num'], '@num_translations' => $results['num_translations']]);
|
||||
}
|
||||
else {
|
||||
$message = t('Finished @num elements created successfully.', ['@num' => $results['num']]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$message = t('Finished with an error.');
|
||||
}
|
||||
\Drupal::messenger()->addMessage($message);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
type: module
|
||||
name: 'Devel Generate'
|
||||
description: 'Generate test users, nodes, menus, taxonomy terms...'
|
||||
package: Development
|
||||
core_version_requirement: ">=10.0 <12.0.0-stable"
|
||||
tags:
|
||||
- developer
|
||||
|
||||
# Information added by Drupal.org packaging script on 2025-07-07
|
||||
version: '5.4.0'
|
||||
project: 'devel'
|
||||
datestamp: 1751916161
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Devel sub-module to for generating content, menus, taxonomy terms etc.
|
||||
*
|
||||
* See src/Plugin for specific details of each type that can be generated.
|
||||
*/
|
||||
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Implements hook_menu_links_discovered_alter().
|
||||
*/
|
||||
function devel_generate_menu_links_discovered_alter(array &$links): void {
|
||||
$devel_generate_plugins = \Drupal::service('plugin.manager.develgenerate')->getDefinitions();
|
||||
foreach ($devel_generate_plugins as $id => $plugin) {
|
||||
$label = $plugin['label'];
|
||||
$links['devel_generate.' . $id] = [
|
||||
'title' => new TranslatableMarkup("Generate @label", ['@label' => $label]),
|
||||
'parent' => 'devel_generate.admin_config_generate',
|
||||
'description' => $plugin['description'],
|
||||
'route_name' => 'devel_generate.' . $id,
|
||||
'provider' => 'devel_generate',
|
||||
];
|
||||
}
|
||||
|
||||
// Store the basic link info for repeated use. Each of the three actual links
|
||||
// require subtle variations on this.
|
||||
$basics = [
|
||||
'title' => new TranslatableMarkup('Generate'),
|
||||
'description' => new TranslatableMarkup('Generate realistic items (content, users, menus, etc) to assist your site development and testing.'),
|
||||
'route_name' => 'devel_generate.admin_config_generate',
|
||||
'provider' => 'devel_generate',
|
||||
];
|
||||
|
||||
// Define a separate group on admin/config page, so that 'Generate' has its
|
||||
// own block with all the generate links.
|
||||
$links['devel_generate.admin_config_generate'] = [
|
||||
'parent' => 'system.admin_config',
|
||||
// The main development group has weight -10 in system.links.menu.yml so use
|
||||
// -9 here as this block should be near but just after it on the page.
|
||||
'weight' => -9,
|
||||
] + $basics;
|
||||
|
||||
// Add a link in the main development group, to allow direct access to the
|
||||
// the Generate page and to make the back breadcrumb more useful.
|
||||
$links['devel_generate.generate'] = [
|
||||
'title' => new TranslatableMarkup('Devel generate'),
|
||||
'parent' => 'system.admin_config_development',
|
||||
] + $basics;
|
||||
|
||||
// Define a top-level link (with no parent) in the 'devel' menu. This also
|
||||
// means that it will be available in the devel admin toolbar.
|
||||
$links['devel_generate.generate2'] = ['menu_name' => 'devel'] + $basics;
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
administer devel_generate:
|
||||
title: 'Administer devel_generate'
|
||||
|
||||
permission_callbacks:
|
||||
- \Drupal\devel_generate\DevelGeneratePermissions::permissions
|
||||
@ -0,0 +1,2 @@
|
||||
route_callbacks:
|
||||
- '\Drupal\devel_generate\Routing\DevelGenerateRoutes::routes'
|
||||
@ -0,0 +1,10 @@
|
||||
services:
|
||||
plugin.manager.develgenerate:
|
||||
class: Drupal\devel_generate\DevelGeneratePluginManager
|
||||
parent: default_plugin_manager
|
||||
arguments: ['@entity_type.manager', '@messenger', '@language_manager', '@string_translation', '@entity_field.manager']
|
||||
Drupal\devel_generate\DevelGeneratePluginManager: '@plugin.manager.develgenerate'
|
||||
|
||||
logger.channel.devel_generate:
|
||||
parent: logger.channel_base
|
||||
arguments: ['devel_generate']
|
||||
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
use Drupal\Core\Annotation\Translation;
|
||||
|
||||
/**
|
||||
* Defines a DevelGenerate annotation object.
|
||||
*
|
||||
* DevelGenerate handle the bulk creation of entites.
|
||||
*
|
||||
* Additional annotation keys for DevelGenerate can be defined in
|
||||
* hook_devel_generate_info_alter().
|
||||
*
|
||||
* @Annotation
|
||||
*
|
||||
* @see \Drupal\devel_generate\DevelGeneratePluginManager
|
||||
* @see \Drupal\devel_generate\DevelGenerateBaseInterface
|
||||
*/
|
||||
class DevelGenerate extends Plugin {
|
||||
/**
|
||||
* The human-readable name of the DevelGenerate type.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public Translation $label;
|
||||
|
||||
/**
|
||||
* A short description of the DevelGenerate type.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public Translation $description;
|
||||
|
||||
/**
|
||||
* A url to access the plugin settings form.
|
||||
*/
|
||||
public string $url;
|
||||
|
||||
/**
|
||||
* The permission required to access the plugin settings form.
|
||||
*/
|
||||
public string $permission;
|
||||
|
||||
/**
|
||||
* The name of the DevelGenerate class.
|
||||
*
|
||||
* This is not provided manually, it will be added by the discovery mechanism.
|
||||
*/
|
||||
public string $class;
|
||||
|
||||
/**
|
||||
* An array of settings passed to the DevelGenerate settingsForm.
|
||||
*
|
||||
* The keys are the names of the settings and the values are the default
|
||||
* values for those settings.
|
||||
*/
|
||||
public array $settings = [];
|
||||
|
||||
/**
|
||||
* Modules that should be enabled in order to make the plugin discoverable.
|
||||
*/
|
||||
public array $dependencies = [];
|
||||
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\devel_generate\Attributes;
|
||||
|
||||
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
|
||||
|
||||
/**
|
||||
* Devel generate plugin details.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class Generator {
|
||||
|
||||
public function __construct(
|
||||
public string $id,
|
||||
) {}
|
||||
|
||||
public static function handle(\ReflectionAttribute $attribute, CommandInfo $commandInfo): void {
|
||||
$args = $attribute->getArguments();
|
||||
$commandInfo->addAnnotation('pluginId', $args['id']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate;
|
||||
|
||||
use Drupal\Component\Utility\Random;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use JetBrains\PhpStorm\Deprecated;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a base DevelGenerate plugin implementation.
|
||||
*/
|
||||
abstract class DevelGenerateBase extends PluginBase implements DevelGenerateBaseInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*/
|
||||
protected EntityTypeManagerInterface $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*/
|
||||
protected LanguageManagerInterface $languageManager;
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*/
|
||||
protected ModuleHandlerInterface $moduleHandler;
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*/
|
||||
protected EntityFieldManagerInterface $entityFieldManager;
|
||||
|
||||
/**
|
||||
* The plugin settings.
|
||||
*/
|
||||
protected array $settings = [];
|
||||
|
||||
/**
|
||||
* The random data generator.
|
||||
*/
|
||||
protected ?Random $random = NULL;
|
||||
|
||||
/**
|
||||
* Instantiates a new instance of this class.
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$instance = new static($configuration, $plugin_id, $plugin_definition);
|
||||
$instance->entityTypeManager = $container->get('entity_type.manager');
|
||||
$instance->languageManager = $container->get('language_manager');
|
||||
$instance->moduleHandler = $container->get('module_handler');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
$instance->entityFieldManager = $container->get('entity_field.manager');
|
||||
$instance->setMessenger($container->get('messenger'));
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSetting(string $key) {
|
||||
// Merge defaults if we have no value for the key.
|
||||
if (!array_key_exists($key, $this->settings)) {
|
||||
$this->settings = $this->getDefaultSettings();
|
||||
}
|
||||
|
||||
return $this->settings[$key] ?? NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSettings(): array {
|
||||
$definition = $this->getPluginDefinition();
|
||||
return $definition['settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSettings(): array {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
|
||||
// Validation is optional.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generate(array $values): void {
|
||||
$this->generateElements($values);
|
||||
$this->messenger()->addMessage('Generate process complete.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Business logic relating with each DevelGenerate plugin.
|
||||
*
|
||||
* @param array $values
|
||||
* The input values from the settings form.
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the fields on a given entity with sample values.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to be enriched with sample field values.
|
||||
* @param array $skip
|
||||
* A list of field names to avoid when populating.
|
||||
* @param array $base
|
||||
* A list of base field names to populate.
|
||||
*/
|
||||
public function populateFields(EntityInterface $entity, array $skip = [], array $base = []): void {
|
||||
if (!$entity->getEntityType()->entityClassImplements(FieldableEntityInterface::class)) {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
$instances = $this->entityFieldManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());
|
||||
$instances = array_diff_key($instances, array_flip($skip));
|
||||
foreach ($instances as $instance) {
|
||||
$field_storage = $instance->getFieldStorageDefinition();
|
||||
$field_name = $field_storage->getName();
|
||||
if ($field_storage->isBaseField() && !in_array($field_name, $base)) {
|
||||
// Skip base field unless specifically requested.
|
||||
continue;
|
||||
}
|
||||
|
||||
$max = $field_storage->getCardinality();
|
||||
$cardinality = $max;
|
||||
if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
|
||||
// Just an arbitrary number for 'unlimited'.
|
||||
$max = random_int(1, 3);
|
||||
}
|
||||
|
||||
$entity->$field_name->generateSampleItems($max);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function handleDrushParams($args) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a message for either drush or the web interface.
|
||||
*
|
||||
* @param string|\Drupal\Component\Render\MarkupInterface $msg
|
||||
* The message to display.
|
||||
* @param string $type
|
||||
* (optional) The message type, as defined in MessengerInterface. Defaults
|
||||
* to MessengerInterface::TYPE_STATUS.
|
||||
*/
|
||||
#[Deprecated(reason: 'Use the messenger trait directly.')]
|
||||
protected function setMessage($msg, string $type = MessengerInterface::TYPE_STATUS): void {
|
||||
$this->messenger()->addMessage($msg, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given param is a number.
|
||||
*
|
||||
* @param mixed $number
|
||||
* The parameter to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the parameter is a number, FALSE otherwise.
|
||||
*/
|
||||
public static function isNumber(mixed $number): bool {
|
||||
if ($number === NULL) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return is_numeric($number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the random data generator.
|
||||
*
|
||||
* @return \Drupal\Component\Utility\Random
|
||||
* The random data generator.
|
||||
*/
|
||||
protected function getRandom(): Random {
|
||||
if (!$this->random instanceof Random) {
|
||||
$this->random = new Random();
|
||||
}
|
||||
|
||||
return $this->random;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random sentence of specific length.
|
||||
*
|
||||
* Words are randomly selected with length from 2 up to the optional parameter
|
||||
* $max_word_length. The first word is capitalised. No ending period is added.
|
||||
*
|
||||
* @param int $sentence_length
|
||||
* The total length of the sentence, including the word-separating spaces.
|
||||
* @param int $max_word_length
|
||||
* (optional) Maximum length of each word. Defaults to 8.
|
||||
*
|
||||
* @return string
|
||||
* A sentence of the required length.
|
||||
*/
|
||||
protected function randomSentenceOfLength(int $sentence_length, int $max_word_length = 8): string {
|
||||
// Maximum word length cannot be longer than the sentence length.
|
||||
$max_word_length = min($sentence_length, $max_word_length);
|
||||
$words = [];
|
||||
$remainder = $sentence_length;
|
||||
do {
|
||||
// If near enough to the end then generate the exact length word to fit.
|
||||
// Otherwise, the remaining space cannot be filled with one word, so
|
||||
// choose a random length, short enough for a following word of at least
|
||||
// minimum length.
|
||||
$next_word = $remainder <= $max_word_length ? $remainder : mt_rand(2, min($max_word_length, $remainder - 3));
|
||||
|
||||
$words[] = $this->getRandom()->word($next_word);
|
||||
$remainder = $remainder - $next_word - 1;
|
||||
} while ($remainder > 0);
|
||||
|
||||
return ucfirst(implode(' ', $words));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the language and translation section of the form.
|
||||
*
|
||||
* This is used by both Content and Term generation.
|
||||
*
|
||||
* @param string $items
|
||||
* The name of the things that are being generated - 'nodes' or 'terms'.
|
||||
*
|
||||
* @return array
|
||||
* The language details section of the form.
|
||||
*/
|
||||
protected function getLanguageForm(string $items): array {
|
||||
// We always need a language, even if the language module is not installed.
|
||||
$options = [];
|
||||
$languages = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE);
|
||||
foreach ($languages as $langcode => $language) {
|
||||
$options[$langcode] = $language->getName();
|
||||
}
|
||||
|
||||
$language_module_exists = $this->moduleHandler->moduleExists('language');
|
||||
$translation_module_exists = $this->moduleHandler->moduleExists('content_translation');
|
||||
|
||||
$form['language'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Language'),
|
||||
'#open' => $language_module_exists,
|
||||
];
|
||||
$form['language']['add_language'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Select the primary language(s) for @items', ['@items' => $items]),
|
||||
'#multiple' => TRUE,
|
||||
'#description' => $language_module_exists ? '' : $this->t('Disabled - requires Language module'),
|
||||
'#options' => $options,
|
||||
'#default_value' => [
|
||||
$this->languageManager->getDefaultLanguage()->getId(),
|
||||
],
|
||||
'#disabled' => !$language_module_exists,
|
||||
];
|
||||
$form['language']['translate_language'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Select the language(s) for translated @items', ['@items' => $items]),
|
||||
'#multiple' => TRUE,
|
||||
'#description' => $translation_module_exists ? $this->t('Translated @items will be created for each language selected.', ['@items' => $items]) : $this->t('Disabled - requires Content Translation module.'),
|
||||
'#options' => $options,
|
||||
'#disabled' => !$translation_module_exists,
|
||||
];
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a language code.
|
||||
*
|
||||
* @param array $add_language
|
||||
* Optional array of language codes from which to select one at random.
|
||||
* If empty then return the site's default language.
|
||||
*
|
||||
* @return string
|
||||
* The language code to use.
|
||||
*/
|
||||
protected function getLangcode(array $add_language): string {
|
||||
if ($add_language === []) {
|
||||
return $this->languageManager->getDefaultLanguage()->getId();
|
||||
}
|
||||
|
||||
return $add_language[array_rand($add_language)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a csv string into an array of items.
|
||||
*
|
||||
* Borrowed from Drush.
|
||||
*
|
||||
* @param string|array|null $args
|
||||
* A simple csv string; e.g. 'a,b,c'
|
||||
* or a simple list of items; e.g. array('a','b','c')
|
||||
* or some combination; e.g. array('a,b','c') or array('a,','b,','c,').
|
||||
*/
|
||||
public static function csvToArray($args): array {
|
||||
if ($args === NULL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 1: implode(',',$args) converts from array('a,','b,','c,') to 'a,,b,,c,'
|
||||
// 2: explode(',', ...) converts to array('a','','b','','c','')
|
||||
// 3: array_filter(...) removes the empty items
|
||||
// 4: array_map(...) trims extra whitespace from each item
|
||||
// (handles csv strings with extra whitespace, e.g. 'a, b, c')
|
||||
//
|
||||
$args = is_array($args) ? implode(',', array_map('strval', $args)) : (string) $args;
|
||||
return array_map('trim', array_filter(explode(',', $args)));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Base interface definition for "DevelGenerate" plugins.
|
||||
*
|
||||
* This interface details base wrapping methods that most DevelGenerate
|
||||
* implementations will want to directly inherit from
|
||||
* Drupal\devel_generate\DevelGenerateBase.
|
||||
*
|
||||
* DevelGenerate implementation plugins should have their own settingsForm() and
|
||||
* generateElements() to achieve their own behaviour.
|
||||
*/
|
||||
interface DevelGenerateBaseInterface extends PluginInspectionInterface {
|
||||
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
/**
|
||||
* Returns the array of settings, including defaults for missing settings.
|
||||
*
|
||||
* @param string $key
|
||||
* The setting name.
|
||||
*
|
||||
* @return array|int|string|bool|null
|
||||
* The setting.
|
||||
*/
|
||||
public function getSetting(string $key);
|
||||
|
||||
/**
|
||||
* Returns the default settings for the plugin.
|
||||
*
|
||||
* @return array
|
||||
* The array of default setting values, keyed by setting names.
|
||||
*/
|
||||
public function getDefaultSettings(): array;
|
||||
|
||||
/**
|
||||
* Returns the current settings for the plugin.
|
||||
*
|
||||
* @return array
|
||||
* The array of current setting values, keyed by setting names.
|
||||
*/
|
||||
public function getSettings(): array;
|
||||
|
||||
/**
|
||||
* Returns the form for the plugin.
|
||||
*
|
||||
* @return array
|
||||
* The array of default setting values, keyed by setting names.
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array;
|
||||
|
||||
/**
|
||||
* Form validation handler.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state): void;
|
||||
|
||||
/**
|
||||
* Execute the instructions in common for all DevelGenerate plugin.
|
||||
*
|
||||
* @param array $values
|
||||
* The input values from the settings form.
|
||||
*/
|
||||
public function generate(array $values): void;
|
||||
|
||||
/**
|
||||
* Responsible for validating Drush params.
|
||||
*
|
||||
* @param array $args
|
||||
* The command arguments.
|
||||
* @param array $options
|
||||
* The commend options.
|
||||
*
|
||||
* @return array
|
||||
* An array of values ready to be used for generateElements().
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array;
|
||||
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate;
|
||||
|
||||
/**
|
||||
* DevelGenerateException extending Generic Plugin exception class.
|
||||
*/
|
||||
class DevelGenerateException extends \Exception {
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides dynamic permissions of the filter module.
|
||||
*/
|
||||
class DevelGeneratePermissions implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The plugin manager.
|
||||
*/
|
||||
protected DevelGeneratePluginManager $develGeneratePluginManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): self {
|
||||
$instance = new self();
|
||||
$instance->develGeneratePluginManager = $container->get('plugin.manager.develgenerate');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* A permissions' callback.
|
||||
*
|
||||
* @see devel_generate.permissions.yml
|
||||
*
|
||||
* @return array
|
||||
* An array of permissions for all plugins.
|
||||
*/
|
||||
public function permissions(): array {
|
||||
$permissions = [];
|
||||
$devel_generate_plugins = $this->develGeneratePluginManager->getDefinitions();
|
||||
foreach ($devel_generate_plugins as $plugin) {
|
||||
$permission = $plugin['permission'];
|
||||
$permissions[$permission] = [
|
||||
'title' => $this->t('@permission', ['@permission' => $permission]),
|
||||
];
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\devel_generate\Annotation\DevelGenerate;
|
||||
|
||||
/**
|
||||
* Plugin type manager for DevelGenerate plugins.
|
||||
*/
|
||||
class DevelGeneratePluginManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*/
|
||||
protected EntityTypeManagerInterface $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*/
|
||||
protected MessengerInterface $messenger;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*/
|
||||
protected LanguageManagerInterface $languageManager;
|
||||
|
||||
/**
|
||||
* The translation manager.
|
||||
*/
|
||||
protected TranslationInterface $stringTranslation;
|
||||
|
||||
/**
|
||||
* Constructs a DevelGeneratePluginManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation manager.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager
|
||||
* The entity field manager.
|
||||
*/
|
||||
public function __construct(
|
||||
\Traversable $namespaces,
|
||||
CacheBackendInterface $cache_backend,
|
||||
ModuleHandlerInterface $module_handler,
|
||||
EntityTypeManagerInterface $entity_type_manager,
|
||||
MessengerInterface $messenger,
|
||||
LanguageManagerInterface $language_manager,
|
||||
TranslationInterface $string_translation,
|
||||
protected EntityFieldManagerInterface $entityFieldManager,
|
||||
) {
|
||||
parent::__construct('Plugin/DevelGenerate', $namespaces, $module_handler, NULL, DevelGenerate::class);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->messenger = $messenger;
|
||||
$this->languageManager = $language_manager;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->alterInfo('devel_generate_info');
|
||||
$this->setCacheBackend($cache_backend, 'devel_generate_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function findDefinitions(): array {
|
||||
$definitions = [];
|
||||
foreach (parent::findDefinitions() as $plugin_id => $plugin_definition) {
|
||||
$plugin_available = TRUE;
|
||||
foreach ($plugin_definition['dependencies'] as $module_name) {
|
||||
// If a plugin defines module dependencies and at least one module is
|
||||
// not installed don't make this plugin available.
|
||||
if (!$this->moduleHandler->moduleExists($module_name)) {
|
||||
$plugin_available = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($plugin_available) {
|
||||
$definitions[$plugin_id] = $plugin_definition;
|
||||
}
|
||||
}
|
||||
|
||||
return $definitions;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Drush\Commands;
|
||||
|
||||
use Consolidation\AnnotatedCommand\CommandData;
|
||||
use Consolidation\AnnotatedCommand\Hooks\HookManager;
|
||||
use Drupal\devel_generate\Attributes\Generator;
|
||||
use Drupal\devel_generate\DevelGenerateBaseInterface;
|
||||
use Drupal\devel_generate\DevelGeneratePluginManager;
|
||||
use Drush\Attributes as CLI;
|
||||
use Drush\Commands\AutowireTrait;
|
||||
use Drush\Commands\DrushCommands;
|
||||
|
||||
/**
|
||||
* Provide Drush commands for all the core Devel Generate plugins.
|
||||
*
|
||||
* For commands that are parts of modules, Drush expects to find commandfiles in
|
||||
* __MODULE__/src/Drush/Commands, and the namespace is Drupal/__MODULE__/Drush/Commands.
|
||||
*/
|
||||
final class DevelGenerateCommands extends DrushCommands {
|
||||
|
||||
use AutowireTrait;
|
||||
|
||||
const USERS = 'devel-generate:users';
|
||||
|
||||
const TERMS = 'devel-generate:terms';
|
||||
|
||||
const VOCABS = 'devel-generate:vocabs';
|
||||
|
||||
const MENUS = 'devel-generate:menus';
|
||||
|
||||
const CONTENT = 'devel-generate:content';
|
||||
|
||||
const BLOCK_CONTENT = 'devel-generate:block-content';
|
||||
|
||||
const MEDIA = 'devel-generate:media';
|
||||
|
||||
/**
|
||||
* The plugin instance.
|
||||
*/
|
||||
private DevelGenerateBaseInterface $pluginInstance;
|
||||
|
||||
/**
|
||||
* The Generate plugin parameters.
|
||||
*/
|
||||
private array $parameters;
|
||||
|
||||
/**
|
||||
* DevelGenerateCommands constructor.
|
||||
*
|
||||
* @param \Drupal\devel_generate\DevelGeneratePluginManager $manager
|
||||
* The DevelGenerate plugin manager.
|
||||
*/
|
||||
public function __construct(protected DevelGeneratePluginManager $manager) {
|
||||
parent::__construct();
|
||||
$this->setManager($manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DevelGenerate plugin manager.
|
||||
*
|
||||
* @return \Drupal\devel_generate\DevelGeneratePluginManager
|
||||
* The DevelGenerate plugin manager.
|
||||
*/
|
||||
public function getManager(): DevelGeneratePluginManager {
|
||||
return $this->manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the DevelGenerate plugin manager.
|
||||
*
|
||||
* @param \Drupal\devel_generate\DevelGeneratePluginManager $manager
|
||||
* The DevelGenerate plugin manager.
|
||||
*/
|
||||
public function setManager(DevelGeneratePluginManager $manager): void {
|
||||
$this->manager = $manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DevelGenerate plugin instance.
|
||||
*
|
||||
* @return \Drupal\devel_generate\DevelGenerateBaseInterface
|
||||
* The DevelGenerate plugin instance.
|
||||
*/
|
||||
public function getPluginInstance(): DevelGenerateBaseInterface {
|
||||
return $this->pluginInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the DevelGenerate plugin instance.
|
||||
*
|
||||
* @param mixed $pluginInstance
|
||||
* The DevelGenerate plugin instance.
|
||||
*/
|
||||
public function setPluginInstance(mixed $pluginInstance): void {
|
||||
$this->pluginInstance = $pluginInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the DevelGenerate plugin parameters.
|
||||
*
|
||||
* @return array
|
||||
* The plugin parameters.
|
||||
*/
|
||||
public function getParameters(): array {
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the DevelGenerate plugin parameters.
|
||||
*
|
||||
* @param array $parameters
|
||||
* The plugin parameters.
|
||||
*/
|
||||
public function setParameters(array $parameters): void {
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create users.
|
||||
*/
|
||||
#[CLI\Command(name: self::USERS, aliases: ['genu', 'devel-generate-users'])]
|
||||
#[CLI\Argument(name: 'num', description: 'Number of users to generate.')]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete all users before generating new ones.')]
|
||||
#[CLI\Option(name: 'roles', description: 'A comma delimited list of role IDs for new users. Don\'t specify <info>authenticated</info>.')]
|
||||
#[CLI\Option(name: 'pass', description: 'Specify a password to be set for all generated users.')]
|
||||
#[Generator(id: 'user')]
|
||||
public function users(string|int $num = 50, array $options = ['kill' => FALSE, 'roles' => self::REQ]): void {
|
||||
// @todo pass $options to the plugins.
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create terms in specified vocabulary.
|
||||
*/
|
||||
#[CLI\Command(name: self::TERMS, aliases: ['gent', 'devel-generate-terms'])]
|
||||
#[CLI\Argument(name: 'num', description: 'Number of terms to generate.')]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete all terms in these vocabularies before generating new ones.')]
|
||||
#[CLI\Option(name: 'bundles', description: 'A comma-delimited list of machine names for the vocabularies where terms will be created.')]
|
||||
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
|
||||
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
|
||||
#[CLI\Option(name: 'translations', description: 'A comma-separated list of language codes for translations.')]
|
||||
#[CLI\Option(name: 'min-depth', description: 'The minimum depth of hierarchy for the new terms.')]
|
||||
#[CLI\Option(name: 'max-depth', description: 'The maximum depth of hierarchy for the new terms.')]
|
||||
#[Generator(id: 'term')]
|
||||
public function terms(?string $num = '50', array $options = ['kill' => FALSE, 'bundles' => self::REQ, 'feedback' => '1000', 'languages' => self::REQ, 'translations' => self::REQ, 'min-depth' => '1', 'max-depth' => '4']): void {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create vocabularies.
|
||||
*/
|
||||
#[CLI\Command(name: self::VOCABS, aliases: ['genv', 'devel-generate-vocabs'])]
|
||||
#[CLI\Argument(name: 'num', description: 'Number of vocabularies to generate.')]
|
||||
#[Generator(id: 'vocabulary')]
|
||||
#[CLI\ValidateModulesEnabled(modules: ['taxonomy'])]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete all vocabs before generating new ones.')]
|
||||
public function vocabs(?string $num = '1', array $options = ['kill' => FALSE]): void {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menus.
|
||||
*/
|
||||
#[CLI\Command(name: self::MENUS, aliases: ['genm', 'devel-generate-menus'])]
|
||||
#[CLI\Argument(name: 'number_menus', description: 'Number of menus to generate.')]
|
||||
#[CLI\Argument(name: 'number_links', description: 'Number of links to generate.')]
|
||||
#[CLI\Argument(name: 'max_depth', description: 'Max link depth.')]
|
||||
#[CLI\Argument(name: 'max_width', description: 'Max width of first level of links.')]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete any menus and menu links previously created by devel_generate before generating new ones.')]
|
||||
#[Generator(id: 'menu')]
|
||||
public function menus(?string $number_menus = '2', ?string $number_links = '50', ?string $max_depth = '3', string $max_width = '8', array $options = ['kill' => FALSE]): void {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create content.
|
||||
*/
|
||||
#[CLI\Command(name: self::CONTENT, aliases: ['genc', 'devel-generate-content'])]
|
||||
#[CLI\ValidateModulesEnabled(modules: ['node'])]
|
||||
#[CLI\Argument(name: 'num', description: 'Number of nodes to generate.')]
|
||||
#[CLI\Argument(name: 'max_comments', description: 'Maximum number of comments to generate.')]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete all content before generating new content.')]
|
||||
#[CLI\Option(name: 'bundles', description: 'A comma-delimited list of content types to create.')]
|
||||
#[CLI\Option(name: 'authors', description: 'A comma delimited list of authors ids. Defaults to all users.')]
|
||||
#[CLI\Option(name: 'roles', description: 'A comma delimited list of role machine names to filter the random selection of users. Defaults to all roles.')]
|
||||
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
|
||||
#[CLI\Option(name: 'skip-fields', description: 'A comma delimited list of fields to omit when generating random values')]
|
||||
#[CLI\Option(name: 'base-fields', description: 'A comma delimited list of base field names to populate')]
|
||||
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
|
||||
#[CLI\Option(name: 'translations', description: 'A comma-separated list of language codes for translations.')]
|
||||
#[CLI\Option(name: 'add-type-label', description: 'Add the content type label to the front of the node title')]
|
||||
#[Generator(id: 'content')]
|
||||
public function content(string $num = '50', ?string $max_comments = '0', array $options = ['kill' => FALSE, 'bundles' => 'page,article', 'authors' => self::REQ, 'roles' => self::REQ, 'feedback' => 1000, 'skip-fields' => self::REQ, 'base-fields' => self::REQ, 'languages' => self::REQ, 'translations' => self::REQ, 'add-type-label' => FALSE]): void {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Block content blocks.
|
||||
*/
|
||||
#[CLI\Command(name: self::BLOCK_CONTENT, aliases: ['genbc', 'devel-generate-block-content'])]
|
||||
#[CLI\ValidateModulesEnabled(modules: ['block_content'])]
|
||||
#[CLI\Argument(name: 'num', description: 'Number of blocks to generate.')]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete all block content before generating new.')]
|
||||
#[CLI\Option(name: 'block_types', description: 'A comma-delimited list of block content types to create.')]
|
||||
#[CLI\Option(name: 'authors', description: 'A comma delimited list of authors ids. Defaults to all users.')]
|
||||
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
|
||||
#[CLI\Option(name: 'skip-fields', description: 'A comma delimited list of fields to omit when generating random values')]
|
||||
#[CLI\Option(name: 'base-fields', description: 'A comma delimited list of base field names to populate')]
|
||||
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
|
||||
#[CLI\Option(name: 'translations', description: 'A comma-separated list of language codes for translations.')]
|
||||
#[CLI\Option(name: 'add-type-label', description: 'Add the block type label to the front of the node title')]
|
||||
#[CLI\Option(name: 'reusable', description: 'Create re-usable blocks. Disable for inline Layout Builder blocks, for example.')]
|
||||
#[Generator(id: 'block_content')]
|
||||
public function blockContent(?string $num = '50', array $options = ['kill' => FALSE, 'block_types' => 'basic', 'feedback' => 1000, 'skip-fields' => self::REQ, 'base-fields' => self::REQ, 'languages' => self::REQ, 'translations' => self::REQ, 'add-type-label' => FALSE, 'reusable' => TRUE]): void {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create media items.
|
||||
*/
|
||||
#[CLI\Command(name: self::MEDIA, aliases: ['genmd', 'devel-generate-media'])]
|
||||
#[CLI\Argument(name: 'num', description: 'Number of media to generate.')]
|
||||
#[CLI\Option(name: 'kill', description: 'Delete all media items before generating new.')]
|
||||
#[CLI\Option(name: 'media_types', description: 'A comma-delimited list of media types to create.')]
|
||||
#[CLI\Option(name: 'feedback', description: 'An integer representing interval for insertion rate logging.')]
|
||||
#[CLI\Option(name: 'skip-fields', description: 'A comma delimited list of fields to omit when generating random values')]
|
||||
#[CLI\Option(name: 'base-fields', description: 'A comma delimited list of base field names to populate')]
|
||||
#[CLI\Option(name: 'languages', description: 'A comma-separated list of language codes')]
|
||||
#[CLI\ValidateModulesEnabled(modules: ['media'])]
|
||||
#[Generator(id: 'media')]
|
||||
public function media(?string $num = '50', array $options = ['kill' => FALSE, 'media-types' => self::REQ, 'feedback' => 1000, 'skip-fields' => self::REQ, 'languages' => self::REQ, 'base-fields' => self::REQ]): void {
|
||||
$this->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* The standard drush validate hook.
|
||||
*
|
||||
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
|
||||
* The data sent from the drush command.
|
||||
*/
|
||||
#[CLI\Hook(HookManager::ARGUMENT_VALIDATOR)]
|
||||
public function validate(CommandData $commandData): void {
|
||||
$manager = $this->manager;
|
||||
$args = $commandData->input()->getArguments();
|
||||
// The command name is the first argument but we do not need this.
|
||||
array_shift($args);
|
||||
/** @var \Drupal\devel_generate\DevelGenerateBaseInterface $instance */
|
||||
$instance = $manager->createInstance($commandData->annotationData()->get('pluginId'), []);
|
||||
$this->setPluginInstance($instance);
|
||||
$parameters = $instance->validateDrushParams($args, $commandData->input()->getOptions());
|
||||
$this->setParameters($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for calling the plugin instance generate function.
|
||||
*/
|
||||
public function generate(): void {
|
||||
$instance = $this->pluginInstance;
|
||||
$instance->generate($this->parameters);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Form;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBaseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a form that allows privileged users to generate entities.
|
||||
*/
|
||||
class DevelGenerateForm extends FormBase {
|
||||
|
||||
/**
|
||||
* The manager to be used for instantiating plugins.
|
||||
*/
|
||||
protected PluginManagerInterface $develGenerateManager;
|
||||
|
||||
/**
|
||||
* Logger service.
|
||||
*/
|
||||
protected LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->develGenerateManager = $container->get('plugin.manager.develgenerate');
|
||||
$instance->messenger = $container->get('messenger');
|
||||
$instance->logger = $container->get('logger.channel.devel_generate');
|
||||
$instance->requestStack = $container->get('request_stack');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId(): string {
|
||||
return 'devel_generate_form_' . $this->getPluginIdFromRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the param _plugin_id for the current request.
|
||||
*
|
||||
* @see \Drupal\devel_generate\Routing\DevelGenerateRouteSubscriber
|
||||
*/
|
||||
protected function getPluginIdFromRequest() {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
return $request->get('_plugin_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a DevelGenerate plugin instance for a given plugin id.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
*
|
||||
* @return \Drupal\devel_generate\DevelGenerateBaseInterface
|
||||
* A DevelGenerate plugin instance.
|
||||
*/
|
||||
public function getPluginInstance(string $plugin_id): DevelGenerateBaseInterface {
|
||||
return $this->develGenerateManager->createInstance($plugin_id, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state): array {
|
||||
$plugin_id = $this->getPluginIdFromRequest();
|
||||
$instance = $this->getPluginInstance($plugin_id);
|
||||
$form = $instance->settingsForm($form, $form_state);
|
||||
$form['actions'] = ['#type' => 'actions'];
|
||||
$form['actions']['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Generate'),
|
||||
'#button_type' => 'primary',
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state): void {
|
||||
$plugin_id = $this->getPluginIdFromRequest();
|
||||
$instance = $this->getPluginInstance($plugin_id);
|
||||
$instance->settingsFormValidate($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state): void {
|
||||
try {
|
||||
$plugin_id = $this->getPluginIdFromRequest();
|
||||
$instance = $this->getPluginInstance($plugin_id);
|
||||
$instance->generate($form_state->getValues());
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->logger->error($this->t('Failed to generate elements due to "%error".', ['%error' => $e->getMessage()]));
|
||||
$this->messenger->addMessage($this->t('Failed to generate elements due to "%error".', ['%error' => $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,493 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Extension\ExtensionPathResolver;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\block_content\BlockContentInterface;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a BlockContentDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "block_content",
|
||||
* label = @Translation("Block Content"),
|
||||
* description = @Translation("Generate a given number of Block content blocks. Optionally delete current blocks."),
|
||||
* url = "block-content",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 50,
|
||||
* "kill" = FALSE,
|
||||
* "title_length" = 4,
|
||||
* "add_type_label" = FALSE,
|
||||
* "reusable" = TRUE
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class BlockContentDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The block content storage.
|
||||
*/
|
||||
protected EntityStorageInterface $blockContentStorage;
|
||||
|
||||
/**
|
||||
* The block content type storage.
|
||||
*/
|
||||
protected EntityStorageInterface $blockContentTypeStorage;
|
||||
|
||||
/**
|
||||
* The extension path resolver service.
|
||||
*/
|
||||
protected ExtensionPathResolver $extensionPathResolver;
|
||||
|
||||
/**
|
||||
* The entity type bundle info service.
|
||||
*/
|
||||
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*/
|
||||
protected ?ContentTranslationManagerInterface $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* The Drush batch flag.
|
||||
*/
|
||||
protected bool $drushBatch = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
|
||||
// @phpstan-ignore ternary.alwaysTrue (False positive)
|
||||
$content_translation_manager = $container->has('content_translation.manager') ? $container->get('content_translation.manager') : NULL;
|
||||
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->blockContentStorage = $entity_type_manager->getStorage('block_content');
|
||||
$instance->blockContentTypeStorage = $entity_type_manager->getStorage('block_content_type');
|
||||
$instance->extensionPathResolver = $container->get('extension.path.resolver');
|
||||
$instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
|
||||
$instance->contentTranslationManager = $content_translation_manager;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
/** @var \Drupal\block_content\BlockContentTypeInterface[] $blockTypes */
|
||||
$blockTypes = $this->blockContentTypeStorage->loadMultiple();
|
||||
$options = [];
|
||||
|
||||
foreach ($blockTypes as $type) {
|
||||
$options[$type->id()] = [
|
||||
'type' => [
|
||||
'label' => $type->label(),
|
||||
'description' => $type->getDescription(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$header = [
|
||||
'type' => $this->t('Block Content type'),
|
||||
'description' => $this->t('Description'),
|
||||
];
|
||||
|
||||
$form['block_types'] = [
|
||||
'#type' => 'tableselect',
|
||||
'#header' => $header,
|
||||
'#options' => $options,
|
||||
];
|
||||
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('<strong>Delete all content</strong> in these block types before generating new content.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
|
||||
$form['num'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('How many blocks would you like to generate?'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
|
||||
$form['title_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum number of words in block descriptions'),
|
||||
'#default_value' => $this->getSetting('title_length'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 1,
|
||||
'#max' => 255,
|
||||
];
|
||||
|
||||
$form['skip_fields'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Fields to leave empty'),
|
||||
'#description' => $this->t('Enter the field names as a comma-separated list. These will be skipped and have a default value in the generated content.'),
|
||||
'#default_value' => NULL,
|
||||
];
|
||||
$form['base_fields'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Base fields to populate'),
|
||||
'#description' => $this->t('Enter the field names as a comma-separated list. These will be populated.'),
|
||||
'#default_value' => NULL,
|
||||
];
|
||||
|
||||
$form['reusable'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Reusable blocks'),
|
||||
'#description' => $this->t('This will mark the blocks to be created as reusable.'),
|
||||
'#default_value' => $this->getSetting('reusable'),
|
||||
|
||||
];
|
||||
$form['add_type_label'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Prefix the title with the block type label.'),
|
||||
'#description' => $this->t('This will not count against the maximum number of title words specified above.'),
|
||||
'#default_value' => $this->getSetting('add_type_label'),
|
||||
];
|
||||
|
||||
// Add the language and translation options.
|
||||
$form += $this->getLanguageForm('blocks');
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
|
||||
if (array_filter($form_state->getValue('block_types')) === []) {
|
||||
$form_state->setErrorByName('block_types', $this->t('Please select at least one block type'));
|
||||
}
|
||||
|
||||
$skip_fields = is_null($form_state->getValue('skip_fields')) ? [] : self::csvToArray($form_state->getValue('skip_fields'));
|
||||
$base_fields = is_null($form_state->getValue('base_fields')) ? [] : self::csvToArray($form_state->getValue('base_fields'));
|
||||
$form_state->setValue('skip_fields', $skip_fields);
|
||||
$form_state->setValue('base_fields', $base_fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
$add_language = self::csvToArray($options['languages']);
|
||||
// Intersect with the enabled languages to make sure the language args
|
||||
// passed are actually enabled.
|
||||
$valid_languages = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
|
||||
$values['add_language'] = array_intersect($add_language, $valid_languages);
|
||||
|
||||
$translate_language = self::csvToArray($options['translations']);
|
||||
$values['translate_language'] = array_intersect($translate_language, $valid_languages);
|
||||
|
||||
$values['add_type_label'] = $options['add-type-label'];
|
||||
$values['kill'] = $options['kill'];
|
||||
$values['feedback'] = $options['feedback'];
|
||||
$values['skip_fields'] = is_null($options['skip-fields']) ? [] : self::csvToArray($options['skip-fields']);
|
||||
$values['base_fields'] = is_null($options['base-fields']) ? [] : self::csvToArray($options['base-fields']);
|
||||
$values['title_length'] = 6;
|
||||
$values['num'] = array_shift($args);
|
||||
$values['max_comments'] = array_shift($args);
|
||||
|
||||
$all_types = array_keys($this->blockContentGetBundles());
|
||||
$selected_types = self::csvToArray($options['block_types']);
|
||||
|
||||
if ($selected_types === []) {
|
||||
throw new \Exception(dt('No Block content types available'));
|
||||
}
|
||||
|
||||
$values['block_types'] = array_combine($selected_types, $selected_types);
|
||||
$block_types = array_filter($values['block_types']);
|
||||
|
||||
if (!empty($values['kill']) && $block_types === []) {
|
||||
throw new \Exception(dt('To delete content, please provide the Block content types (--bundles)'));
|
||||
}
|
||||
|
||||
// Checks for any missing block content types before generating blocks.
|
||||
if (array_diff($block_types, $all_types) !== []) {
|
||||
throw new \Exception(dt('One or more block content types have been entered that don\'t exist on this site'));
|
||||
}
|
||||
|
||||
if ($this->isBatch($values['num'])) {
|
||||
$this->drushBatch = TRUE;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
if ($this->isBatch($values['num'])) {
|
||||
$this->generateBatchContent($values);
|
||||
}
|
||||
else {
|
||||
$this->generateContent($values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content in batch mode.
|
||||
*
|
||||
* This method is used when the number of elements is 50 or more.
|
||||
*/
|
||||
private function generateBatchContent(array $values): void {
|
||||
$operations = [];
|
||||
|
||||
// Remove unselected block content types.
|
||||
$values['block_types'] = array_filter($values['block_types']);
|
||||
// If it is drushBatch then this operation is already run in the
|
||||
// self::validateDrushParams().
|
||||
// Add the kill operation.
|
||||
if ($values['kill']) {
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchContentKill', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Add the operations to create the blocks.
|
||||
for ($num = 0; $num < $values['num']; ++$num) {
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchContentAddBlock', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Set the batch.
|
||||
$batch = [
|
||||
'title' => $this->t('Generating Content'),
|
||||
'operations' => $operations,
|
||||
'finished' => 'devel_generate_batch_finished',
|
||||
'file' => $this->extensionPathResolver->getPath('module', 'devel_generate') . '/devel_generate.batch.inc',
|
||||
];
|
||||
|
||||
batch_set($batch);
|
||||
if ($this->drushBatch) {
|
||||
drush_backend_batch_process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch wrapper for calling ContentAddBlock.
|
||||
*/
|
||||
public function batchContentAddBlock(array $vars, array &$context): void {
|
||||
if (!isset($context['results']['num'])) {
|
||||
$context['results']['num'] = 0;
|
||||
}
|
||||
|
||||
if ($this->drushBatch) {
|
||||
++$context['results']['num'];
|
||||
$this->develGenerateContentAddBlock($vars);
|
||||
}
|
||||
else {
|
||||
$context['results'] = $vars;
|
||||
$this->develGenerateContentAddBlock($context['results']);
|
||||
}
|
||||
|
||||
if (!empty($vars['num_translations'])) {
|
||||
$context['results']['num_translations'] += $vars['num_translations'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch wrapper for calling ContentKill.
|
||||
*/
|
||||
public function batchContentKill(array $vars, array &$context): void {
|
||||
if ($this->drushBatch) {
|
||||
$this->contentKill($vars);
|
||||
}
|
||||
else {
|
||||
$context['results'] = $vars;
|
||||
$this->contentKill($context['results']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content when not in batch mode.
|
||||
*
|
||||
* This method is used when the number of elements is under 50.
|
||||
*/
|
||||
private function generateContent(array $values): void {
|
||||
$values['block_types'] = array_filter($values['block_types']);
|
||||
if (!empty($values['kill']) && $values['block_types']) {
|
||||
$this->contentKill($values);
|
||||
}
|
||||
|
||||
if (isset($values['block_types']) && $values['block_types'] !== []) {
|
||||
$start = time();
|
||||
$values['num_translations'] = 0;
|
||||
for ($i = 1; $i <= $values['num']; ++$i) {
|
||||
$this->develGenerateContentAddBlock($values);
|
||||
if (isset($values['feedback']) && $i % $values['feedback'] == 0) {
|
||||
$now = time();
|
||||
$options = [
|
||||
'@feedback' => $values['feedback'],
|
||||
'@rate' => ($values['feedback'] * 60) / ($now - $start),
|
||||
];
|
||||
$this->messenger->addStatus(dt('Completed @feedback blocks (@rate blocks/min)', $options));
|
||||
$start = $now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->setMessage($this->formatPlural($values['num'], 'Created 1 block', 'Created @count blocks'));
|
||||
if ($values['num_translations'] > 0) {
|
||||
$this->setMessage($this->formatPlural($values['num_translations'], 'Created 1 block translation', 'Created @count block translations'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one block. Used by both batch and non-batch code branches.
|
||||
*
|
||||
* @param array $results
|
||||
* Results information.
|
||||
*/
|
||||
protected function develGenerateContentAddBlock(array &$results): void {
|
||||
if (!isset($results['time_range'])) {
|
||||
$results['time_range'] = 0;
|
||||
}
|
||||
|
||||
$block_type = array_rand($results['block_types']);
|
||||
|
||||
// Add the block type label if required.
|
||||
$title_prefix = $results['add_type_label'] ? $this->blockContentTypeStorage->load($block_type)->label() . ' - ' : '';
|
||||
|
||||
$values = [
|
||||
'info' => $title_prefix . $this->getRandom()->sentences(mt_rand(1, $results['title_length']), TRUE),
|
||||
'type' => $block_type,
|
||||
// A flag to let hook_block_content_insert() implementations know that this is a generated block.
|
||||
'devel_generate' => $results,
|
||||
];
|
||||
|
||||
if (isset($results['add_language'])) {
|
||||
$values['langcode'] = $this->getLangcode($results['add_language']);
|
||||
}
|
||||
|
||||
if (isset($results['reusable'])) {
|
||||
$values['reusable'] = (int) $results['reusable'];
|
||||
}
|
||||
|
||||
/** @var \Drupal\block_content\BlockContentInterface $block */
|
||||
$block = $this->blockContentStorage->create($values);
|
||||
|
||||
// Populate non-skipped fields with sample values.
|
||||
$this->populateFields($block, $results['skip_fields'], $results['base_fields']);
|
||||
|
||||
// Remove the fields which are intended to have no value.
|
||||
foreach ($results['skip_fields'] as $field) {
|
||||
unset($block->$field);
|
||||
}
|
||||
|
||||
$block->save();
|
||||
|
||||
// Add translations.
|
||||
$this->develGenerateContentAddBlockTranslation($results, $block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create translation for the given block.
|
||||
*
|
||||
* @param array $results
|
||||
* Results array.
|
||||
* @param \Drupal\block_content\BlockContentInterface $block
|
||||
* Block to add translations to.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*/
|
||||
protected function develGenerateContentAddBlockTranslation(array &$results, BlockContentInterface $block): void {
|
||||
if (empty($results['translate_language'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($this->contentTranslationManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->contentTranslationManager->isEnabled('block_content', $block->bundle())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($block->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_SPECIFIED
|
||||
|| $block->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($results['num_translations'])) {
|
||||
$results['num_translations'] = 0;
|
||||
}
|
||||
|
||||
// Translate the block to each target language.
|
||||
$skip_languages = [
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
LanguageInterface::LANGCODE_NOT_APPLICABLE,
|
||||
$block->get('langcode')->getLangcode(),
|
||||
];
|
||||
foreach ($results['translate_language'] as $langcode) {
|
||||
if (in_array($langcode, $skip_languages)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$translation_block = $block->addTranslation($langcode);
|
||||
$translation_block->setInfo($block->label() . ' (' . $langcode . ')');
|
||||
$this->populateFields($translation_block);
|
||||
$translation_block->save();
|
||||
|
||||
++$results['num_translations'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all blocks of given block content types.
|
||||
*
|
||||
* @param array $values
|
||||
* The input values from the settings form.
|
||||
*/
|
||||
protected function contentKill(array $values): void {
|
||||
$bids = $this->blockContentStorage->getQuery()
|
||||
->condition('type', $values['block_types'], 'IN')
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
|
||||
if (!empty($bids)) {
|
||||
$blocks = $this->blockContentStorage->loadMultiple($bids);
|
||||
$this->blockContentStorage->delete($blocks);
|
||||
$this->setMessage($this->t('Deleted %count blocks.', ['%count' => count($bids)]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the content should be generated in batch mode.
|
||||
*/
|
||||
protected function isBatch($content_count): bool {
|
||||
return $content_count >= 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of available block content type names.
|
||||
*
|
||||
* This list can include types that are queued for addition or deletion.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of block content type labels,
|
||||
* keyed by the block content type name.
|
||||
*/
|
||||
public function blockContentGetBundles(): array {
|
||||
return array_map(static fn($bundle_info) => $bundle_info['label'], $this->entityTypeBundleInfo->getBundleInfo('block_content'));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,888 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\comment\CommentManagerInterface;
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Component\Render\FormattableMarkup;
|
||||
use Drupal\Component\Utility\Random;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Datetime\DateFormatterInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Extension\ExtensionPathResolver;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\UrlGeneratorInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\node\NodeStorageInterface;
|
||||
use Drupal\path_alias\PathAliasStorage;
|
||||
use Drupal\user\RoleStorageInterface;
|
||||
use Drupal\user\UserStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a ContentDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "content",
|
||||
* label = @Translation("content"),
|
||||
* description = @Translation("Generate a given number of content. Optionally delete current content."),
|
||||
* url = "content",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 50,
|
||||
* "kill" = FALSE,
|
||||
* "max_comments" = 0,
|
||||
* "title_length" = 4,
|
||||
* "add_type_label" = FALSE
|
||||
* },
|
||||
* dependencies = {
|
||||
* "node",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class ContentDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The node storage.
|
||||
*/
|
||||
protected NodeStorageInterface $nodeStorage;
|
||||
|
||||
/**
|
||||
* The node type storage.
|
||||
*/
|
||||
protected EntityStorageInterface $nodeTypeStorage;
|
||||
|
||||
/**
|
||||
* The user storage.
|
||||
*/
|
||||
protected UserStorageInterface $userStorage;
|
||||
|
||||
/**
|
||||
* The url generator service.
|
||||
*/
|
||||
protected UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
/**
|
||||
* The alias storage.
|
||||
*/
|
||||
protected PathAliasStorage $aliasStorage;
|
||||
|
||||
/**
|
||||
* The date formatter service.
|
||||
*/
|
||||
protected DateFormatterInterface $dateFormatter;
|
||||
|
||||
/**
|
||||
* Provides system time.
|
||||
*/
|
||||
protected TimeInterface $time;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*/
|
||||
protected Connection $database;
|
||||
|
||||
/**
|
||||
* The extension path resolver service.
|
||||
*/
|
||||
protected ExtensionPathResolver $extensionPathResolver;
|
||||
|
||||
/**
|
||||
* The role storage.
|
||||
*/
|
||||
protected RoleStorageInterface $roleStorage;
|
||||
|
||||
/**
|
||||
* The comment manager service.
|
||||
*/
|
||||
protected ?CommentManagerInterface $commentManager;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*/
|
||||
protected ?ContentTranslationManagerInterface $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* The Drush batch flag.
|
||||
*/
|
||||
protected bool $drushBatch = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
|
||||
// @phpstan-ignore ternary.alwaysTrue (False positive)
|
||||
$comment_manager = $container->has('comment.manager') ? $container->get('comment.manager') : NULL;
|
||||
|
||||
// @phpstan-ignore ternary.alwaysTrue (False positive)
|
||||
$content_translation_manager = $container->has('content_translation.manager') ? $container->get('content_translation.manager') : NULL;
|
||||
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->nodeTypeStorage = $entity_type_manager->getStorage('node_type');
|
||||
$instance->nodeStorage = $entity_type_manager->getStorage('node');
|
||||
$instance->userStorage = $entity_type_manager->getStorage('user');
|
||||
$instance->urlGenerator = $container->get('url_generator');
|
||||
$instance->aliasStorage = $entity_type_manager->getStorage('path_alias');
|
||||
$instance->dateFormatter = $container->get('date.formatter');
|
||||
$instance->time = $container->get('datetime.time');
|
||||
$instance->database = $container->get('database');
|
||||
$instance->extensionPathResolver = $container->get('extension.path.resolver');
|
||||
$instance->roleStorage = $entity_type_manager->getStorage('user_role');
|
||||
$instance->commentManager = $comment_manager;
|
||||
$instance->contentTranslationManager = $content_translation_manager;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
$types = $this->nodeTypeStorage->loadMultiple();
|
||||
|
||||
if (empty($types)) {
|
||||
$create_url = $this->urlGenerator->generateFromRoute('node.type_add');
|
||||
$this->messenger()->addMessage($this->t('You do not have any content types that can be generated. <a href=":create-type">Go create a new content type</a>', [':create-type' => $create_url]), 'error');
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$options[$type->id()] = [
|
||||
'type' => ['#markup' => $type->label()],
|
||||
];
|
||||
if ($this->commentManager instanceof CommentManagerInterface) {
|
||||
$comment_fields = $this->commentManager->getFields('node');
|
||||
$map = [$this->t('Hidden'), $this->t('Closed'), $this->t('Open')];
|
||||
|
||||
$fields = [];
|
||||
foreach ($comment_fields as $field_name => $info) {
|
||||
// Find all comment fields for the bundle.
|
||||
if (in_array($type->id(), $info['bundles'])) {
|
||||
$instance = FieldConfig::loadByName('node', $type->id(), $field_name);
|
||||
$default_value = $instance->getDefaultValueLiteral();
|
||||
$default_mode = reset($default_value);
|
||||
$fields[] = new FormattableMarkup('@field: @state', [
|
||||
'@field' => $instance->label(),
|
||||
'@state' => $map[$default_mode['status']],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Refactor display of comment fields.
|
||||
if ($fields !== []) {
|
||||
$options[$type->id()]['comments'] = [
|
||||
'data' => [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $fields,
|
||||
],
|
||||
];
|
||||
}
|
||||
else {
|
||||
$options[$type->id()]['comments'] = $this->t('No comment fields');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$header = [
|
||||
'type' => $this->t('Content type'),
|
||||
];
|
||||
if ($this->commentManager instanceof CommentManagerInterface) {
|
||||
$header['comments'] = [
|
||||
'data' => $this->t('Comments'),
|
||||
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
|
||||
];
|
||||
}
|
||||
|
||||
$form['node_types'] = [
|
||||
'#type' => 'tableselect',
|
||||
'#header' => $header,
|
||||
'#options' => $options,
|
||||
];
|
||||
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('<strong>Delete all content</strong> in these content types before generating new content.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
$form['num'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('How many nodes would you like to generate?'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
|
||||
$options = [1 => $this->t('Now')];
|
||||
foreach ([3600, 86400, 604800, 2592000, 31536000] as $interval) {
|
||||
$options[$interval] = $this->dateFormatter->formatInterval($interval, 1) . ' ' . $this->t('ago');
|
||||
}
|
||||
|
||||
$form['time_range'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('How far back in time should the nodes be dated?'),
|
||||
'#description' => $this->t('Node creation dates will be distributed randomly from the current time, back to the selected time.'),
|
||||
'#options' => $options,
|
||||
'#default_value' => 604800,
|
||||
];
|
||||
|
||||
$form['max_comments'] = [
|
||||
'#type' => $this->moduleHandler->moduleExists('comment') ? 'number' : 'value',
|
||||
'#title' => $this->t('Maximum number of comments per node.'),
|
||||
'#description' => $this->t('You must also enable comments for the content types you are generating. Note that some nodes will randomly receive zero comments. Some will receive the max.'),
|
||||
'#default_value' => $this->getSetting('max_comments'),
|
||||
'#min' => 0,
|
||||
'#access' => $this->moduleHandler->moduleExists('comment'),
|
||||
];
|
||||
$form['title_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum number of words in titles'),
|
||||
'#default_value' => $this->getSetting('title_length'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 1,
|
||||
'#max' => 255,
|
||||
];
|
||||
$form['skip_fields'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Fields to leave empty'),
|
||||
'#description' => $this->t('Enter the field names as a comma-separated list. These will be skipped and have a default value in the generated content.'),
|
||||
'#default_value' => NULL,
|
||||
];
|
||||
$form['base_fields'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Base fields to populate'),
|
||||
'#description' => $this->t('Enter the field names as a comma-separated list. These will be populated.'),
|
||||
'#default_value' => NULL,
|
||||
];
|
||||
$form['add_type_label'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Prefix the title with the content type label.'),
|
||||
'#description' => $this->t('This will not count against the maximum number of title words specified above.'),
|
||||
'#default_value' => $this->getSetting('add_type_label'),
|
||||
];
|
||||
$form['add_alias'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#disabled' => !$this->moduleHandler->moduleExists('path'),
|
||||
'#description' => $this->t('Requires path.module'),
|
||||
'#title' => $this->t('Add an url alias for each node.'),
|
||||
'#default_value' => FALSE,
|
||||
];
|
||||
$form['add_statistics'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Add statistics for each node (node_counter table).'),
|
||||
'#default_value' => TRUE,
|
||||
'#access' => $this->moduleHandler->moduleExists('statistics'),
|
||||
];
|
||||
|
||||
// Add the language and translation options.
|
||||
$form += $this->getLanguageForm('nodes');
|
||||
|
||||
// Add the user selection checkboxes.
|
||||
$author_header = [
|
||||
'id' => $this->t('User ID'),
|
||||
'user' => $this->t('Name'),
|
||||
'role' => $this->t('Role(s)'),
|
||||
];
|
||||
|
||||
$num_users = $this->database->select('users')
|
||||
->countQuery()
|
||||
->execute()
|
||||
->fetchField();
|
||||
$author_form_limit = 50;
|
||||
$query = $this->database->select('users', 'u')
|
||||
->fields('u', ['uid'])
|
||||
->range(0, $author_form_limit)
|
||||
->orderBy('uid');
|
||||
$uids = $query->execute()->fetchCol();
|
||||
|
||||
$author_rows = [];
|
||||
foreach ($uids as $uid) {
|
||||
/** @var \Drupal\user\UserInterface $user */
|
||||
$user = $this->userStorage->load($uid);
|
||||
$author_rows[$user->id()] = [
|
||||
'id' => ['#markup' => $user->id()],
|
||||
'user' => ['#markup' => $user->getAccountName()],
|
||||
'role' => ['#markup' => implode(", ", $user->getRoles())],
|
||||
];
|
||||
}
|
||||
|
||||
$form['authors-wrap'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Users'),
|
||||
'#open' => FALSE,
|
||||
'#description' => $this->t('Select users for randomly assigning as authors of the generated content.')
|
||||
. ($num_users > $author_form_limit ? ' ' . $this->t('The site has @num_users users, only the first @$author_form_limit are shown and selectable here.', ['@num_users' => $num_users, '@$author_form_limit' => $author_form_limit]) : ''),
|
||||
];
|
||||
|
||||
$form['authors-wrap']['authors'] = [
|
||||
'#type' => 'tableselect',
|
||||
'#header' => $author_header,
|
||||
'#options' => $author_rows,
|
||||
];
|
||||
|
||||
$role_rows = [];
|
||||
$roles = array_map(static fn($role): string => $role->label(), $this->roleStorage->loadMultiple());
|
||||
foreach ($roles as $role_id => $role_name) {
|
||||
$role_rows[$role_id] = [
|
||||
'id' => ['#markup' => $role_id],
|
||||
'role' => ['#markup' => $role_name],
|
||||
];
|
||||
}
|
||||
|
||||
$form['authors-wrap']['roles'] = [
|
||||
'#type' => 'tableselect',
|
||||
'#header' => [
|
||||
'id' => $this->t('Role ID'),
|
||||
'role' => $this->t('Role Description'),
|
||||
],
|
||||
'#options' => $role_rows,
|
||||
'#prefix' => $this->t('Specify the roles that randomly selected authors must have.'),
|
||||
'#suffix' => $this->t('You can select users and roles. Authors will be randomly selected that match at least one of the criteria. Leave <em>both</em> selections unchecked to use a random selection of @$author_form_limit users, including Anonymous.', ['@$author_form_limit' => $author_form_limit]),
|
||||
];
|
||||
|
||||
$form['#redirect'] = FALSE;
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
|
||||
if (array_filter($form_state->getValue('node_types')) === []) {
|
||||
$form_state->setErrorByName('node_types', $this->t('Please select at least one content type'));
|
||||
}
|
||||
|
||||
$skip_fields = is_null($form_state->getValue('skip_fields')) ? [] : self::csvToArray($form_state->getValue('skip_fields'));
|
||||
$base_fields = is_null($form_state->getValue('base_fields')) ? [] : self::csvToArray($form_state->getValue('base_fields'));
|
||||
$form_state->setValue('skip_fields', $skip_fields);
|
||||
$form_state->setValue('base_fields', $base_fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
if ($this->isBatch($values['num'], $values['max_comments'])) {
|
||||
$this->generateBatchContent($values);
|
||||
}
|
||||
else {
|
||||
$this->generateContent($values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content when not in batch mode.
|
||||
*
|
||||
* This method is used when the number of elements is under 50.
|
||||
*/
|
||||
private function generateContent(array $values): void {
|
||||
$values['node_types'] = array_filter($values['node_types']);
|
||||
if (!empty($values['kill']) && $values['node_types']) {
|
||||
$this->contentKill($values);
|
||||
}
|
||||
|
||||
if ($values['node_types'] !== []) {
|
||||
// Generate nodes.
|
||||
$this->develGenerateContentPreNode($values);
|
||||
$start = time();
|
||||
$values['num_translations'] = 0;
|
||||
for ($i = 1; $i <= $values['num']; ++$i) {
|
||||
$this->develGenerateContentAddNode($values);
|
||||
if (isset($values['feedback']) && $i % $values['feedback'] == 0) {
|
||||
$now = time();
|
||||
$options = [
|
||||
'@feedback' => $values['feedback'],
|
||||
'@rate' => ($values['feedback'] * 60) / ($now - $start),
|
||||
];
|
||||
$this->messenger->addStatus(dt('Completed @feedback nodes (@rate nodes/min)', $options));
|
||||
$start = $now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->messenger()->addMessage($this->formatPlural($values['num'], 'Created 1 node', 'Created @count nodes'));
|
||||
if ($values['num_translations'] > 0) {
|
||||
$this->messenger()->addMessage($this->formatPlural($values['num_translations'], 'Created 1 node translation', 'Created @count node translations'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate content in batch mode.
|
||||
*
|
||||
* This method is used when the number of elements is 50 or more.
|
||||
*/
|
||||
private function generateBatchContent(array $values): void {
|
||||
$operations = [];
|
||||
|
||||
// Remove unselected node types.
|
||||
$values['node_types'] = array_filter($values['node_types']);
|
||||
// If it is drushBatch then this operation is already run in the
|
||||
// self::validateDrushParams().
|
||||
if (!$this->drushBatch) {
|
||||
// Setup the batch operations and save the variables.
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchContentPreNode', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Add the kill operation.
|
||||
if ($values['kill']) {
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchContentKill', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Add the operations to create the nodes.
|
||||
for ($num = 0; $num < $values['num']; ++$num) {
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchContentAddNode', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Set the batch.
|
||||
$batch = [
|
||||
'title' => $this->t('Generating Content'),
|
||||
'operations' => $operations,
|
||||
'finished' => 'devel_generate_batch_finished',
|
||||
'file' => $this->extensionPathResolver->getPath('module', 'devel_generate') . '/devel_generate.batch.inc',
|
||||
];
|
||||
|
||||
batch_set($batch);
|
||||
if ($this->drushBatch) {
|
||||
drush_backend_batch_process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch wrapper for calling ContentPreNode.
|
||||
*/
|
||||
public function batchContentPreNode($vars, array &$context): void {
|
||||
$context['results'] = $vars;
|
||||
$context['results']['num'] = 0;
|
||||
$context['results']['num_translations'] = 0;
|
||||
$this->develGenerateContentPreNode($context['results']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch wrapper for calling ContentAddNode.
|
||||
*/
|
||||
public function batchContentAddNode(array $vars, array &$context): void {
|
||||
if ($this->drushBatch) {
|
||||
$this->develGenerateContentAddNode($vars);
|
||||
}
|
||||
else {
|
||||
$this->develGenerateContentAddNode($context['results']);
|
||||
}
|
||||
|
||||
if (!isset($context['results']['num'])) {
|
||||
$context['results']['num'] = 0;
|
||||
}
|
||||
|
||||
++$context['results']['num'];
|
||||
if (!empty($vars['num_translations'])) {
|
||||
$context['results']['num_translations'] += $vars['num_translations'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch wrapper for calling ContentKill.
|
||||
*/
|
||||
public function batchContentKill(array $vars, array &$context): void {
|
||||
if ($this->drushBatch) {
|
||||
$this->contentKill($vars);
|
||||
}
|
||||
else {
|
||||
$this->contentKill($context['results']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
$add_language = self::csvToArray($options['languages']);
|
||||
// Intersect with the enabled languages to make sure the language args
|
||||
// passed are actually enabled.
|
||||
$valid_languages = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
|
||||
$values['add_language'] = array_intersect($add_language, $valid_languages);
|
||||
|
||||
$translate_language = self::csvToArray($options['translations']);
|
||||
$values['translate_language'] = array_intersect($translate_language, $valid_languages);
|
||||
|
||||
$values['add_type_label'] = $options['add-type-label'];
|
||||
$values['kill'] = $options['kill'];
|
||||
$values['feedback'] = $options['feedback'];
|
||||
$values['skip_fields'] = is_null($options['skip-fields']) ? [] : self::csvToArray($options['skip-fields']);
|
||||
$values['base_fields'] = is_null($options['base-fields']) ? [] : self::csvToArray($options['base-fields']);
|
||||
$values['title_length'] = 6;
|
||||
$values['num'] = (int) trim(array_shift($args));
|
||||
$values['max_comments'] = (int) array_shift($args);
|
||||
// Do not use csvToArray for 'authors' because it removes '0' values.
|
||||
$values['authors'] = is_null($options['authors']) ? [] : explode(',', $options['authors']);
|
||||
$values['roles'] = self::csvToArray($options['roles']);
|
||||
|
||||
$all_types = array_keys(node_type_get_names());
|
||||
$default_types = array_intersect(['page', 'article'], $all_types);
|
||||
$selected_types = self::csvToArray($options['bundles'] ?: $default_types);
|
||||
|
||||
if ($selected_types === []) {
|
||||
throw new \Exception(dt('No content types available'));
|
||||
}
|
||||
|
||||
$values['node_types'] = array_combine($selected_types, $selected_types);
|
||||
$node_types = array_filter($values['node_types']);
|
||||
|
||||
if (!empty($values['kill']) && $node_types === []) {
|
||||
throw new \Exception(dt('To delete content, please provide the content types (--bundles)'));
|
||||
}
|
||||
|
||||
// Checks for any missing content types before generating nodes.
|
||||
if (array_diff($node_types, $all_types) !== []) {
|
||||
throw new \Exception(dt('One or more content types have been entered that don\'t exist on this site'));
|
||||
}
|
||||
|
||||
if ($this->isBatch((int) $values['num'], (int) $values['max_comments'])) {
|
||||
$this->drushBatch = TRUE;
|
||||
$this->develGenerateContentPreNode($values);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the content should be generated in batch mode.
|
||||
*/
|
||||
protected function isBatch(int $content_count, int $comment_count): bool {
|
||||
return $content_count >= 50 || $comment_count >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all nodes of given node types.
|
||||
*
|
||||
* @param array $values
|
||||
* The input values from the settings form.
|
||||
*/
|
||||
protected function contentKill(array $values): void {
|
||||
$nids = $this->nodeStorage->getQuery()
|
||||
->condition('type', $values['node_types'], 'IN')
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
|
||||
if (!empty($nids)) {
|
||||
$nodes = $this->nodeStorage->loadMultiple($nids);
|
||||
$this->nodeStorage->delete($nodes);
|
||||
$this->messenger()->addMessage($this->t('Deleted @count nodes.', ['@count' => count($nids)]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses $results before adding content.
|
||||
*
|
||||
* @param array $results
|
||||
* Results information.
|
||||
*/
|
||||
protected function develGenerateContentPreNode(array &$results): void {
|
||||
$authors = $results['authors'];
|
||||
// Remove non-selected users. !== 0 will leave the Anonymous user in if it
|
||||
// was selected on the form or entered in the drush parameters.
|
||||
$authors = array_filter($authors, static fn($k): bool => $k !== 0);
|
||||
// Likewise remove non-selected roles.
|
||||
$roles = $results['roles'];
|
||||
$roles = array_filter($roles, static fn($k): bool => $k !== 0);
|
||||
|
||||
// If specific roles have been selected then also add up to 50 users who
|
||||
// have one of these roles. There is no direct way randomise the selection
|
||||
// using entity queries, so we use a database query instead.
|
||||
if ($roles !== [] && !in_array('authenticated', $roles)) {
|
||||
$query = $this->database->select('user__roles', 'ur')
|
||||
->fields('ur', ['entity_id', 'roles_target_id'])
|
||||
->condition('roles_target_id', $roles, 'in')
|
||||
->range(0, 50)
|
||||
->orderRandom();
|
||||
$uids = array_unique($query->execute()->fetchCol());
|
||||
// If the 'anonymous' role is selected, then add '0' to the user ids. Also
|
||||
// do this if no users were specified and none were found with the role(s)
|
||||
// requested. This makes it clear that no users were found. It would be
|
||||
// worse to fall through and select completely random users who do not
|
||||
// have any of the roles requested.
|
||||
if (in_array('anonymous', $roles) || ($authors === [] && $uids === [])) {
|
||||
$uids[] = '0';
|
||||
}
|
||||
|
||||
$authors = array_unique(array_merge($authors, $uids));
|
||||
}
|
||||
|
||||
// If still no authors have been collected, or the 'authenticated' role was
|
||||
// requested then add a random set of users up to a maximum of 50.
|
||||
if ($authors === [] || in_array('authenticated', $roles)) {
|
||||
$query = $this->database->select('users', 'u')
|
||||
->fields('u', ['uid'])
|
||||
->range(0, 50)
|
||||
->orderRandom();
|
||||
$uids = $query->execute()->fetchCol();
|
||||
$authors = array_unique(array_merge($authors, $uids));
|
||||
}
|
||||
|
||||
$results['users'] = $authors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one node. Used by both batch and non-batch code branches.
|
||||
*
|
||||
* @param array $results
|
||||
* Results information.
|
||||
*/
|
||||
protected function develGenerateContentAddNode(array &$results): void {
|
||||
if (!isset($results['time_range'])) {
|
||||
$results['time_range'] = 0;
|
||||
}
|
||||
|
||||
$users = $results['users'];
|
||||
|
||||
$node_type = array_rand($results['node_types']);
|
||||
$uid = $users[array_rand($users)];
|
||||
|
||||
// Add the content type label if required.
|
||||
$title_prefix = $results['add_type_label'] ? $this->nodeTypeStorage->load($node_type)->label() . ' - ' : '';
|
||||
|
||||
$values = [
|
||||
'nid' => NULL,
|
||||
'type' => $node_type,
|
||||
'title' => $title_prefix . $this->getRandom()->sentences(mt_rand(1, $results['title_length']), TRUE),
|
||||
'uid' => $uid,
|
||||
'revision' => mt_rand(0, 1),
|
||||
'moderation_state' => 'published',
|
||||
'status' => TRUE,
|
||||
'promote' => mt_rand(0, 1),
|
||||
'created' => $this->time->getRequestTime() - mt_rand(0, $results['time_range']),
|
||||
// A flag to let hook_node_insert() implementations know that this is a
|
||||
// generated node.
|
||||
'devel_generate' => $results,
|
||||
];
|
||||
|
||||
if (isset($results['add_language'])) {
|
||||
$values['langcode'] = $this->getLangcode($results['add_language']);
|
||||
}
|
||||
|
||||
/** @var \Drupal\node\NodeInterface $node */
|
||||
$node = $this->nodeStorage->create($values);
|
||||
|
||||
// Populate non-skipped fields with sample values.
|
||||
$this->populateFields($node, $results['skip_fields'], $results['base_fields']);
|
||||
|
||||
// Remove the fields which are intended to have no value.
|
||||
foreach ($results['skip_fields'] as $field) {
|
||||
unset($node->$field);
|
||||
}
|
||||
|
||||
$node->save();
|
||||
$this->insertNodeData($node);
|
||||
|
||||
// Add url alias if required.
|
||||
if (!empty($results['add_alias'])) {
|
||||
$path_alias = $this->aliasStorage->create([
|
||||
'path' => '/node/' . $node->id(),
|
||||
'alias' => '/node-' . $node->id() . '-' . $node->bundle(),
|
||||
'langcode' => $values['langcode'] ?? LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
]);
|
||||
$path_alias->save();
|
||||
}
|
||||
|
||||
// Add translations.
|
||||
$this->develGenerateContentAddNodeTranslation($results, $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create translation for the given node.
|
||||
*
|
||||
* @param array $results
|
||||
* Results array.
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* Node to add translations to.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
*/
|
||||
protected function develGenerateContentAddNodeTranslation(array &$results, NodeInterface $node): void {
|
||||
if (empty($results['translate_language'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_null($this->contentTranslationManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->contentTranslationManager->isEnabled('node', $node->getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($node->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_SPECIFIED
|
||||
|| $node->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($results['num_translations'])) {
|
||||
$results['num_translations'] = 0;
|
||||
}
|
||||
|
||||
// Translate node to each target language.
|
||||
$skip_languages = [
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
LanguageInterface::LANGCODE_NOT_APPLICABLE,
|
||||
$node->get('langcode')->getLangcode(),
|
||||
];
|
||||
foreach ($results['translate_language'] as $langcode) {
|
||||
if (in_array($langcode, $skip_languages)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$translation_node = $node->addTranslation($langcode);
|
||||
$translation_node->setTitle($node->getTitle() . ' (' . $langcode . ')');
|
||||
$this->populateFields($translation_node);
|
||||
$translation_node->save();
|
||||
if ($translation_node->id() > 0 && !empty($results['add_alias'])) {
|
||||
$path_alias = $this->aliasStorage->create([
|
||||
'path' => '/node/' . $translation_node->id(),
|
||||
'alias' => '/node-' . $translation_node->id() . '-' . $translation_node->bundle() . '-' . $langcode,
|
||||
'langcode' => $langcode,
|
||||
]);
|
||||
$path_alias->save();
|
||||
}
|
||||
|
||||
++$results['num_translations'];
|
||||
}
|
||||
}
|
||||
|
||||
private function insertNodeData(NodeInterface $node): void {
|
||||
if (!isset($node->devel_generate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$results = $node->devel_generate;
|
||||
if (!empty($results['max_comments'])) {
|
||||
foreach ($node->getFieldDefinitions() as $field_name => $field_definition) {
|
||||
if ($field_definition->getType() !== 'comment') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($node->get($field_name)->getValue()[0]['status'] !== CommentItemInterface::OPEN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add comments for each comment field on entity.
|
||||
$this->addNodeComments($node, $field_definition, $results['users'], $results['max_comments'], $results['title_length']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($results['add_statistics'])) {
|
||||
$this->addNodeStatistics($node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create comments and add them to a node.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* Node to add comments to.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The field storage definition.
|
||||
* @param array $users
|
||||
* Array of users to assign comment authors.
|
||||
* @param int $max_comments
|
||||
* Max number of comments to generate per node.
|
||||
* @param int $title_length
|
||||
* Max length of the title of the comments.
|
||||
*/
|
||||
private function addNodeComments(NodeInterface $node, FieldDefinitionInterface $field_definition, array $users, int $max_comments, int $title_length = 8): void {
|
||||
$parents = [];
|
||||
$commentStorage = $this->entityTypeManager->getStorage('comment');
|
||||
$field_name = $field_definition->getName();
|
||||
$num_comments = mt_rand(0, $max_comments);
|
||||
for ($i = 1; $i <= $num_comments; ++$i) {
|
||||
$query = $commentStorage->getQuery();
|
||||
switch ($i % 3) {
|
||||
case 0:
|
||||
// No parent.
|
||||
case 1:
|
||||
// Top level parent.
|
||||
$parents = $query
|
||||
->condition('pid', 0)
|
||||
->condition('entity_id', $node->id())
|
||||
->condition('entity_type', 'node')
|
||||
->condition('field_name', $field_name)
|
||||
->range(0, 1)
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Non top level parent.
|
||||
$parents = $query
|
||||
->condition('pid', 0, '>')
|
||||
->condition('entity_id', $node->id())
|
||||
->condition('entity_type', 'node')
|
||||
->condition('field_name', $field_name)
|
||||
->range(0, 1)
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
break;
|
||||
}
|
||||
|
||||
$random = new Random();
|
||||
$stub = [
|
||||
'entity_type' => $node->getEntityTypeId(),
|
||||
'entity_id' => $node->id(),
|
||||
'field_name' => $field_name,
|
||||
'name' => 'devel generate',
|
||||
'mail' => 'devel_generate@example.com',
|
||||
'timestamp' => mt_rand($node->getCreatedTime(), $this->time->getRequestTime()),
|
||||
'subject' => substr($random->sentences(mt_rand(1, $title_length), TRUE), 0, 63),
|
||||
'uid' => $users[array_rand($users)],
|
||||
'langcode' => $node->language()->getId(),
|
||||
];
|
||||
if ($parents) {
|
||||
$stub['pid'] = current($parents);
|
||||
}
|
||||
|
||||
$comment = $commentStorage->create($stub);
|
||||
|
||||
// Populate all core fields.
|
||||
$this->populateFields($comment);
|
||||
$comment->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate statistics information for a node.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* A node object.
|
||||
*/
|
||||
private function addNodeStatistics(NodeInterface $node): void {
|
||||
if (!$this->moduleHandler->moduleExists('statistics')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statistic = [
|
||||
'nid' => $node->id(),
|
||||
'totalcount' => mt_rand(0, 500),
|
||||
'timestamp' => $this->time->getRequestTime() - mt_rand(0, $node->getCreatedTime()),
|
||||
];
|
||||
$statistic['daycount'] = mt_rand(0, $statistic['totalcount']);
|
||||
$this->database->insert('node_counter')->fields($statistic)->execute();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,534 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
|
||||
use Drupal\Core\Datetime\DateFormatterInterface;
|
||||
use Drupal\Core\Entity\ContentEntityStorageInterface;
|
||||
use Drupal\Core\Extension\ExtensionPathResolver;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\UrlGeneratorInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Drupal\user\UserStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a plugin that generates media entities.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "media",
|
||||
* label = @Translation("media"),
|
||||
* description = @Translation("Generate a given number of media entities."),
|
||||
* url = "media",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 50,
|
||||
* "kill" = FALSE,
|
||||
* "name_length" = 4,
|
||||
* },
|
||||
* dependencies = {
|
||||
* "media",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class MediaDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The media entity storage.
|
||||
*/
|
||||
protected ContentEntityStorageInterface $mediaStorage;
|
||||
|
||||
/**
|
||||
* The media type entity storage.
|
||||
*/
|
||||
protected ConfigEntityStorageInterface $mediaTypeStorage;
|
||||
|
||||
/**
|
||||
* The user entity storage.
|
||||
*/
|
||||
protected UserStorageInterface $userStorage;
|
||||
|
||||
/**
|
||||
* The url generator service.
|
||||
*/
|
||||
protected UrlGeneratorInterface $urlGenerator;
|
||||
|
||||
/**
|
||||
* The date formatter service.
|
||||
*/
|
||||
protected DateFormatterInterface $dateFormatter;
|
||||
|
||||
/**
|
||||
* The system time service.
|
||||
*/
|
||||
protected TimeInterface $time;
|
||||
|
||||
/**
|
||||
* The extension path resolver service.
|
||||
*/
|
||||
protected ExtensionPathResolver $extensionPathResolver;
|
||||
|
||||
/**
|
||||
* The Drush batch flag.
|
||||
*/
|
||||
protected bool $drushBatch = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->mediaStorage = $entity_type_manager->getStorage('media');
|
||||
$instance->mediaTypeStorage = $entity_type_manager->getStorage('media_type');
|
||||
$instance->userStorage = $entity_type_manager->getStorage('user');
|
||||
$instance->urlGenerator = $container->get('url_generator');
|
||||
$instance->dateFormatter = $container->get('date.formatter');
|
||||
$instance->time = $container->get('datetime.time');
|
||||
$instance->extensionPathResolver = $container->get('extension.path.resolver');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
$types = $this->mediaTypeStorage->loadMultiple();
|
||||
|
||||
if (empty($types)) {
|
||||
$create_url = $this->urlGenerator->generateFromRoute('entity.media_type.add_form');
|
||||
$this->setMessage($this->t('You do not have any media types that can be generated. <a href=":url">Go create a new media type</a>', [
|
||||
':url' => $create_url,
|
||||
]), MessengerInterface::TYPE_ERROR);
|
||||
return [];
|
||||
}
|
||||
|
||||
$options = [];
|
||||
foreach ($types as $type) {
|
||||
$options[$type->id()] = ['type' => ['#markup' => $type->label()]];
|
||||
}
|
||||
|
||||
$form['media_types'] = [
|
||||
'#type' => 'tableselect',
|
||||
'#header' => ['type' => $this->t('Media type')],
|
||||
'#options' => $options,
|
||||
];
|
||||
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('<strong>Delete all media</strong> in these types before generating new media.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
$form['num'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('How many media items would you like to generate?'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
|
||||
$options = [1 => $this->t('Now')];
|
||||
foreach ([3600, 86400, 604800, 2592000, 31536000] as $interval) {
|
||||
$options[$interval] = $this->dateFormatter->formatInterval($interval, 1) . ' ' . $this->t('ago');
|
||||
}
|
||||
|
||||
$form['time_range'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('How far back in time should the media be dated?'),
|
||||
'#description' => $this->t('Media creation dates will be distributed randomly from the current time, back to the selected time.'),
|
||||
'#options' => $options,
|
||||
'#default_value' => 604800,
|
||||
];
|
||||
|
||||
$form['name_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum number of words in names'),
|
||||
'#default_value' => $this->getSetting('name_length'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 1,
|
||||
'#max' => 255,
|
||||
];
|
||||
|
||||
$form['skip_fields'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Fields to leave empty'),
|
||||
'#description' => $this->t('Enter the field names as a comma-separated list. These will be skipped and have a default value in the generated content.'),
|
||||
'#default_value' => NULL,
|
||||
];
|
||||
$form['base_fields'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Base fields to populate'),
|
||||
'#description' => $this->t('Enter the field names as a comma-separated list. These will be populated.'),
|
||||
'#default_value' => NULL,
|
||||
];
|
||||
|
||||
$options = [];
|
||||
// We always need a language.
|
||||
$languages = $this->languageManager->getLanguages(LanguageInterface::STATE_ALL);
|
||||
foreach ($languages as $langcode => $language) {
|
||||
$options[$langcode] = $language->getName();
|
||||
}
|
||||
|
||||
$form['add_language'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Set language on media'),
|
||||
'#multiple' => TRUE,
|
||||
'#description' => $this->t('Requires locale.module'),
|
||||
'#options' => $options,
|
||||
'#default_value' => [
|
||||
$this->languageManager->getDefaultLanguage()->getId(),
|
||||
],
|
||||
];
|
||||
|
||||
$form['#redirect'] = FALSE;
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state): void {
|
||||
// Remove the media types not selected.
|
||||
$media_types = array_filter($form_state->getValue('media_types'));
|
||||
if ($media_types === []) {
|
||||
$form_state->setErrorByName('media_types', $this->t('Please select at least one media type'));
|
||||
}
|
||||
|
||||
// Store the normalized value back, in form state.
|
||||
$form_state->setValue('media_types', array_combine($media_types, $media_types));
|
||||
|
||||
$skip_fields = is_null($form_state->getValue('skip_fields')) ? [] : self::csvToArray($form_state->getValue('skip_fields'));
|
||||
$base_fields = is_null($form_state->getValue('base_fields')) ? [] : self::csvToArray($form_state->getValue('base_fields'));
|
||||
$form_state->setValue('skip_fields', $skip_fields);
|
||||
$form_state->setValue('base_fields', $base_fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
if ($this->isBatch((int) $values['num'])) {
|
||||
$this->generateBatchMedia($values);
|
||||
}
|
||||
else {
|
||||
$this->generateMedia($values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for creating media when number of elements is less than 50.
|
||||
*
|
||||
* @param array $values
|
||||
* Array of values submitted through a form.
|
||||
*
|
||||
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
|
||||
* Thrown if the storage handler couldn't be loaded.
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
|
||||
* Thrown if the entity type doesn't exist.
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* Thrown if the bundle does not exist or was needed but not specified.
|
||||
*/
|
||||
protected function generateMedia(array $values): void {
|
||||
if (!empty($values['kill']) && $values['media_types']) {
|
||||
$this->mediaKill($values);
|
||||
}
|
||||
|
||||
if (!empty($values['media_types'])) {
|
||||
// Generate media items.
|
||||
$this->preGenerate($values);
|
||||
$start = time();
|
||||
for ($i = 1; $i <= $values['num']; ++$i) {
|
||||
$this->createMediaItem($values);
|
||||
if (isset($values['feedback']) && $i % $values['feedback'] == 0) {
|
||||
$now = time();
|
||||
$this->messenger->addStatus(dt('Completed !feedback media items (!rate media/min)', [
|
||||
'!feedback' => $values['feedback'],
|
||||
'!rate' => ($values['feedback'] * 60) / ($now - $start),
|
||||
]));
|
||||
$start = $now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->setMessage($this->formatPlural($values['num'], '1 media item created.', 'Finished creating @count media items.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for creating media when number of elements is greater than 50.
|
||||
*
|
||||
* @param array $values
|
||||
* The input values from the settings form.
|
||||
*/
|
||||
protected function generateBatchMedia(array $values): void {
|
||||
$operations = [];
|
||||
|
||||
// Setup the batch operations and save the variables.
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchPreGenerate', $values],
|
||||
];
|
||||
|
||||
// Add the kill operation.
|
||||
if ($values['kill']) {
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchMediaKill', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Add the operations to create the media.
|
||||
for ($num = 0; $num < $values['num']; ++$num) {
|
||||
$operations[] = [
|
||||
'devel_generate_operation',
|
||||
[$this, 'batchCreateMediaItem', $values],
|
||||
];
|
||||
}
|
||||
|
||||
// Start the batch.
|
||||
$batch = [
|
||||
'title' => $this->t('Generating media items'),
|
||||
'operations' => $operations,
|
||||
'finished' => 'devel_generate_batch_finished',
|
||||
'file' => $this->extensionPathResolver->getPath('module', 'devel_generate') . '/devel_generate.batch.inc',
|
||||
];
|
||||
batch_set($batch);
|
||||
|
||||
if ($this->drushBatch) {
|
||||
drush_backend_batch_process();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a batch version of preGenerate().
|
||||
*
|
||||
* @param array $vars
|
||||
* The input values from the settings form.
|
||||
* @param iterable $context
|
||||
* Batch job context.
|
||||
*
|
||||
* @see self::preGenerate()
|
||||
*/
|
||||
public function batchPreGenerate(array $vars, iterable &$context): void {
|
||||
$context['results'] = $vars;
|
||||
$context['results']['num'] = 0;
|
||||
$this->preGenerate($context['results']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a batch version of createMediaItem().
|
||||
*
|
||||
* @param array $vars
|
||||
* The input values from the settings form.
|
||||
* @param iterable $context
|
||||
* Batch job context.
|
||||
*
|
||||
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
|
||||
* Thrown if the storage handler couldn't be loaded.
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
|
||||
* Thrown if the entity type doesn't exist.
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* Thrown if the bundle does not exist or was needed but not specified.
|
||||
*
|
||||
* @see self::createMediaItem()
|
||||
*/
|
||||
public function batchCreateMediaItem(array $vars, iterable &$context): void {
|
||||
if ($this->drushBatch) {
|
||||
$this->createMediaItem($vars);
|
||||
}
|
||||
else {
|
||||
$this->createMediaItem($context['results']);
|
||||
}
|
||||
|
||||
if (!isset($context['results']['num'])) {
|
||||
$context['results']['num'] = 0;
|
||||
}
|
||||
|
||||
++$context['results']['num'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a batch version of mediaKill().
|
||||
*
|
||||
* @param array $vars
|
||||
* The input values from the settings form.
|
||||
* @param iterable $context
|
||||
* Batch job context.
|
||||
*
|
||||
* @see self::mediaKill()
|
||||
*/
|
||||
public function batchMediaKill(array $vars, iterable &$context): void {
|
||||
if ($this->drushBatch) {
|
||||
$this->mediaKill($vars);
|
||||
}
|
||||
else {
|
||||
$this->mediaKill($context['results']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
$add_language = $options['languages'];
|
||||
if (!empty($add_language)) {
|
||||
$add_language = explode(',', str_replace(' ', '', $add_language));
|
||||
// Intersect with the enabled languages to make sure the language args
|
||||
// passed are actually enabled.
|
||||
$values['values']['add_language'] = array_intersect($add_language, array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL)));
|
||||
}
|
||||
|
||||
$values['kill'] = $options['kill'];
|
||||
$values['feedback'] = $options['feedback'];
|
||||
$values['name_length'] = 6;
|
||||
$values['num'] = (int) array_shift($args);
|
||||
|
||||
$values['skip_fields'] = is_null($options['skip-fields']) ? [] : self::csvToArray($options['skip-fields']);
|
||||
$values['base_fields'] = is_null($options['base-fields']) ? [] : self::csvToArray($options['base-fields']);
|
||||
|
||||
$all_media_types = array_values($this->mediaTypeStorage->getQuery()->accessCheck(FALSE)->execute());
|
||||
$requested_media_types = self::csvToArray($options['media-types'] ?: $all_media_types);
|
||||
|
||||
if ($requested_media_types === []) {
|
||||
throw new \Exception(dt('No media types available'));
|
||||
}
|
||||
|
||||
// Check for any missing media type.
|
||||
if (($invalid_media_types = array_diff($requested_media_types, $all_media_types)) !== []) {
|
||||
throw new \Exception("Requested media types don't exists: " . implode(', ', $invalid_media_types));
|
||||
}
|
||||
|
||||
$values['media_types'] = array_combine($requested_media_types, $requested_media_types);
|
||||
|
||||
if ($this->isBatch($values['num'])) {
|
||||
$this->drushBatch = TRUE;
|
||||
$this->preGenerate($values);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all media of given media media types.
|
||||
*
|
||||
* @param array $values
|
||||
* The input values from the settings form.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* Thrown if the media type does not exist.
|
||||
*/
|
||||
protected function mediaKill(array $values): void {
|
||||
$mids = $this->mediaStorage->getQuery()
|
||||
->condition('bundle', $values['media_types'], 'IN')
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
|
||||
if (!empty($mids)) {
|
||||
$media = $this->mediaStorage->loadMultiple($mids);
|
||||
$this->mediaStorage->delete($media);
|
||||
$this->setMessage($this->t('Deleted %count media items.', ['%count' => count($mids)]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Code to be run before generating items.
|
||||
*
|
||||
* Returns the same array passed in as parameter, but with an array of uids
|
||||
* for the key 'users'.
|
||||
*
|
||||
* @param array $results
|
||||
* The input values from the settings form.
|
||||
*/
|
||||
protected function preGenerate(array &$results): void {
|
||||
// Get user id.
|
||||
$users = array_values($this->userStorage->getQuery()
|
||||
->range(0, 50)
|
||||
->accessCheck(FALSE)
|
||||
->execute());
|
||||
$users = array_merge($users, ['0']);
|
||||
$results['users'] = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one media item. Used by both batch and non-batch code branches.
|
||||
*
|
||||
* @param array $results
|
||||
* The input values from the settings form.
|
||||
*
|
||||
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
|
||||
* Thrown if the storage handler couldn't be loaded.
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
|
||||
* Thrown if the entity type doesn't exist.
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* Thrown if the bundle does not exist or was needed but not specified.
|
||||
*/
|
||||
protected function createMediaItem(array &$results): void {
|
||||
if (!isset($results['time_range'])) {
|
||||
$results['time_range'] = 0;
|
||||
}
|
||||
|
||||
$media_type = array_rand($results['media_types']);
|
||||
$uid = $results['users'][array_rand($results['users'])];
|
||||
|
||||
$media = $this->mediaStorage->create([
|
||||
'bundle' => $media_type,
|
||||
'name' => $this->getRandom()->sentences(mt_rand(1, $results['name_length']), TRUE),
|
||||
'uid' => $uid,
|
||||
'revision' => mt_rand(0, 1),
|
||||
'status' => TRUE,
|
||||
'moderation_state' => 'published',
|
||||
'created' => $this->time->getRequestTime() - mt_rand(0, $results['time_range']),
|
||||
'langcode' => $this->getLangcode($results),
|
||||
// A flag to let hook implementations know that this is a generated item.
|
||||
'devel_generate' => $results,
|
||||
]);
|
||||
|
||||
// Populate all non-skipped fields with sample values.
|
||||
$this->populateFields($media, $results['skip_fields'], $results['base_fields']);
|
||||
|
||||
// Remove the fields which are intended to have no value.
|
||||
foreach ($results['skip_fields'] as $field) {
|
||||
unset($media->$field);
|
||||
}
|
||||
|
||||
$media->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine language based on $results.
|
||||
*
|
||||
* @param array $results
|
||||
* The input values from the settings form.
|
||||
*
|
||||
* @return string
|
||||
* The language code.
|
||||
*/
|
||||
protected function getLangcode(array $results): string {
|
||||
if (isset($results['add_language'])) {
|
||||
$langcodes = $results['add_language'];
|
||||
|
||||
return $langcodes[array_rand($langcodes)];
|
||||
}
|
||||
|
||||
return $this->languageManager->getDefaultLanguage()->getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if the media item generation will run in batch process.
|
||||
*
|
||||
* @param int $media_items_count
|
||||
* Number of media items to be generated.
|
||||
*
|
||||
* @return bool
|
||||
* If the process should be a batch process.
|
||||
*/
|
||||
protected function isBatch(int $media_items_count): bool {
|
||||
return $media_items_count >= 50;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,383 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Menu\MenuLinkTreeInterface;
|
||||
use Drupal\Core\Menu\MenuTreeParameters;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Drupal\menu_link_content\MenuLinkContentStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a MenuDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "menu",
|
||||
* label = @Translation("menus"),
|
||||
* description = @Translation("Generate a given number of menus and menu
|
||||
* links. Optionally delete current menus."), url = "menu", permission =
|
||||
* "administer devel_generate", settings = {
|
||||
* "num_menus" = 2,
|
||||
* "num_links" = 50,
|
||||
* "title_length" = 12,
|
||||
* "max_width" = 6,
|
||||
* "kill" = FALSE,
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class MenuDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The menu tree service.
|
||||
*/
|
||||
protected MenuLinkTreeInterface $menuLinkTree;
|
||||
|
||||
/**
|
||||
* The menu storage.
|
||||
*/
|
||||
protected EntityStorageInterface $menuStorage;
|
||||
|
||||
/**
|
||||
* The menu link storage.
|
||||
*/
|
||||
protected MenuLinkContentStorageInterface $menuLinkContentStorage;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*/
|
||||
protected Connection $database;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->menuLinkTree = $container->get('menu.link_tree');
|
||||
$instance->menuStorage = $entity_type_manager->getStorage('menu');
|
||||
$instance->menuLinkContentStorage = $entity_type_manager->getStorage('menu_link_content');
|
||||
$instance->database = $container->get('database');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
$menus = array_map(static fn($menu) => $menu->label(), $this->menuStorage->loadMultiple());
|
||||
asort($menus);
|
||||
$menus = ['__new-menu__' => $this->t('Create new menu(s)')] + $menus;
|
||||
$form['existing_menus'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Generate links for these menus'),
|
||||
'#options' => $menus,
|
||||
'#default_value' => ['__new-menu__'],
|
||||
'#required' => TRUE,
|
||||
];
|
||||
$form['num_menus'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Number of new menus to create'),
|
||||
'#default_value' => $this->getSetting('num_menus'),
|
||||
'#min' => 0,
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name="existing_menus[__new-menu__]"]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
$form['num_links'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Number of links to generate'),
|
||||
'#default_value' => $this->getSetting('num_links'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
$form['title_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum length for menu titles and menu links'),
|
||||
'#description' => $this->t('Text will be generated at random lengths up to this value. Enter a number between 2 and 128.'),
|
||||
'#default_value' => $this->getSetting('title_length'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 2,
|
||||
'#max' => 128,
|
||||
];
|
||||
$form['link_types'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Types of links to generate'),
|
||||
'#options' => [
|
||||
'node' => $this->t('Nodes'),
|
||||
'front' => $this->t('Front page'),
|
||||
'external' => $this->t('External'),
|
||||
],
|
||||
'#default_value' => ['node', 'front', 'external'],
|
||||
'#required' => TRUE,
|
||||
];
|
||||
$form['max_depth'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Maximum link depth'),
|
||||
'#options' => range(0, $this->menuLinkTree->maxDepth()),
|
||||
'#default_value' => floor($this->menuLinkTree->maxDepth() / 2),
|
||||
'#required' => TRUE,
|
||||
];
|
||||
unset($form['max_depth']['#options'][0]);
|
||||
$form['max_width'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum menu width'),
|
||||
'#default_value' => $this->getSetting('max_width'),
|
||||
'#description' => $this->t("Limit the width of the generated menu's first level of links to a certain number of items."),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Delete existing custom generated menus and menu links before generating new ones.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
// If the create new menus checkbox is off, set the number of menus to 0.
|
||||
if (!isset($values['existing_menus']['__new-menu__']) || !$values['existing_menus']['__new-menu__']) {
|
||||
$values['num_menus'] = 0;
|
||||
}
|
||||
else {
|
||||
// Unset the aux menu to avoid attach menu new items.
|
||||
unset($values['existing_menus']['__new-menu__']);
|
||||
}
|
||||
|
||||
// Delete custom menus.
|
||||
if ($values['kill']) {
|
||||
[$menus_deleted, $links_deleted] = $this->deleteMenus();
|
||||
$this->setMessage($this->t('Deleted @menus_deleted menu(s) and @links_deleted other link(s).',
|
||||
[
|
||||
'@menus_deleted' => $menus_deleted,
|
||||
'@links_deleted' => $links_deleted,
|
||||
]));
|
||||
}
|
||||
|
||||
// Generate new menus.
|
||||
$new_menus = $this->generateMenus($values['num_menus'], $values['title_length']);
|
||||
if ($new_menus !== []) {
|
||||
$this->setMessage($this->formatPlural(count($new_menus), 'Created the following 1 new menu: @menus', 'Created the following @count new menus: @menus',
|
||||
['@menus' => implode(', ', $new_menus)]));
|
||||
}
|
||||
|
||||
// Generate new menu links.
|
||||
$menus = $new_menus;
|
||||
if (isset($values['existing_menus'])) {
|
||||
$menus += $values['existing_menus'];
|
||||
}
|
||||
|
||||
$new_links = $this->generateLinks($values['num_links'], $menus, $values['title_length'], $values['link_types'], $values['max_depth'], $values['max_width']);
|
||||
$this->setMessage($this->formatPlural(count($new_links), 'Created 1 new menu link.', 'Created @count new menu links.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
$link_types = ['node', 'front', 'external'];
|
||||
$values = [
|
||||
'num_menus' => array_shift($args),
|
||||
'num_links' => array_shift($args),
|
||||
'kill' => $options['kill'],
|
||||
'link_types' => array_combine($link_types, $link_types),
|
||||
];
|
||||
|
||||
$max_depth = array_shift($args);
|
||||
$max_width = array_shift($args);
|
||||
$values['max_depth'] = $max_depth ?: 3;
|
||||
$values['max_width'] = $max_width ?: 8;
|
||||
$values['title_length'] = $this->getSetting('title_length');
|
||||
$values['existing_menus']['__new-menu__'] = TRUE;
|
||||
|
||||
if ($this->isNumber($values['num_menus']) == FALSE) {
|
||||
throw new \Exception(dt('Invalid number of menus'));
|
||||
}
|
||||
|
||||
if ($this->isNumber($values['num_links']) == FALSE) {
|
||||
throw new \Exception(dt('Invalid number of links'));
|
||||
}
|
||||
|
||||
if ($this->isNumber($values['max_depth']) == FALSE || $values['max_depth'] > 9 || $values['max_depth'] < 1) {
|
||||
throw new \Exception(dt('Invalid maximum link depth. Use a value between 1 and 9'));
|
||||
}
|
||||
|
||||
if ($this->isNumber($values['max_width']) == FALSE || $values['max_width'] < 1) {
|
||||
throw new \Exception(dt('Invalid maximum menu width. Use a positive numeric value.'));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes custom generated menus.
|
||||
*/
|
||||
protected function deleteMenus(): array {
|
||||
$menu_ids = [];
|
||||
if ($this->moduleHandler->moduleExists('menu_ui')) {
|
||||
$all = $this->menuStorage->loadMultiple();
|
||||
foreach ($all as $menu) {
|
||||
if (str_starts_with($menu->id(), 'devel-')) {
|
||||
$menu_ids[] = $menu->id();
|
||||
}
|
||||
}
|
||||
|
||||
if ($menu_ids !== []) {
|
||||
$menus = $this->menuStorage->loadMultiple($menu_ids);
|
||||
$this->menuStorage->delete($menus);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete menu links in other menus, but generated by devel.
|
||||
$link_ids = $this->menuLinkContentStorage->getQuery()
|
||||
->condition('menu_name', 'devel', '<>')
|
||||
->condition('link__options', '%' . $this->database->escapeLike('s:5:"devel";b:1') . '%', 'LIKE')
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
|
||||
if ($link_ids) {
|
||||
$links = $this->menuLinkContentStorage->loadMultiple($link_ids);
|
||||
$this->menuLinkContentStorage->delete($links);
|
||||
}
|
||||
|
||||
return [count($menu_ids), count($link_ids)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new menus.
|
||||
*
|
||||
* @param int $num_menus
|
||||
* Number of menus to create.
|
||||
* @param int $title_length
|
||||
* (optional) Maximum length of menu name.
|
||||
*
|
||||
* @return array
|
||||
* Array containing the generated menus.
|
||||
*/
|
||||
protected function generateMenus(int $num_menus, int $title_length = 12): array {
|
||||
$menus = [];
|
||||
|
||||
for ($i = 1; $i <= $num_menus; ++$i) {
|
||||
$name = $this->randomSentenceOfLength(mt_rand(2, $title_length));
|
||||
// Create a random string of random length for the menu id. The maximum
|
||||
// machine-name length is 32, so allowing for prefix 'devel-' we can have
|
||||
// up to 26 here. For safety avoid accidentally reusing the same id.
|
||||
do {
|
||||
$id = 'devel-' . $this->getRandom()->word(mt_rand(2, 26));
|
||||
} while (array_key_exists($id, $menus));
|
||||
|
||||
$menu = $this->menuStorage->create([
|
||||
'label' => $name,
|
||||
'id' => $id,
|
||||
'description' => $this->t('Description of @name', ['@name' => $name]),
|
||||
]);
|
||||
|
||||
$menu->save();
|
||||
$menus[$menu->id()] = $menu->label();
|
||||
}
|
||||
|
||||
return $menus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates menu links in a tree structure.
|
||||
*
|
||||
* @return array<int|string, string>
|
||||
* Array containing the titles of the generated menu links.
|
||||
*/
|
||||
protected function generateLinks(int $num_links, array $menus, int $title_length, array $link_types, int $max_depth, int $max_width): array {
|
||||
$links = [];
|
||||
$menus = array_keys(array_filter($menus));
|
||||
$link_types = array_keys(array_filter($link_types));
|
||||
|
||||
$nids = [];
|
||||
for ($i = 1; $i <= $num_links; ++$i) {
|
||||
// Pick a random menu.
|
||||
$menu_name = $menus[array_rand($menus)];
|
||||
// Build up our link.
|
||||
$link_title = $this->getRandom()->word(mt_rand(2, max(2, $title_length)));
|
||||
|
||||
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $menuLinkContent */
|
||||
$menuLinkContent = $this->menuLinkContentStorage->create([
|
||||
'menu_name' => $menu_name,
|
||||
'weight' => mt_rand(-50, 50),
|
||||
'title' => $link_title,
|
||||
'bundle' => 'menu_link_content',
|
||||
'description' => $this->t('Description of @title.', ['@title' => $link_title]),
|
||||
]);
|
||||
$link = $menuLinkContent->get('link');
|
||||
$options['devel'] = TRUE;
|
||||
$link->setValue(['options' => $options]);
|
||||
|
||||
// For the first $max_width items, make first level links, otherwise, get
|
||||
// a random parent menu depth.
|
||||
$max_link_depth = $i <= $max_width ? 0 : mt_rand(1, max(1, $max_depth - 1));
|
||||
|
||||
// Get a random parent link from the proper depth.
|
||||
for ($depth = $max_link_depth; $depth >= 0; --$depth) {
|
||||
$parameters = new MenuTreeParameters();
|
||||
$parameters->setMinDepth($depth);
|
||||
$parameters->setMaxDepth($depth);
|
||||
$tree = $this->menuLinkTree->load($menu_name, $parameters);
|
||||
if ($tree === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$menuLinkContent->set('parent', array_rand($tree));
|
||||
break;
|
||||
}
|
||||
|
||||
$link_type = array_rand($link_types);
|
||||
switch ($link_types[$link_type]) {
|
||||
case 'node':
|
||||
// Grab a random node ID.
|
||||
$select = $this->database->select('node_field_data', 'n')
|
||||
->fields('n', ['nid', 'title'])
|
||||
->condition('n.status', 1)
|
||||
->range(0, 1)
|
||||
->orderRandom();
|
||||
// Don't put a node into the menu twice.
|
||||
if (isset($nids[$menu_name])) {
|
||||
$select->condition('n.nid', $nids[$menu_name], 'NOT IN');
|
||||
}
|
||||
|
||||
$node = $select->execute()->fetchAssoc();
|
||||
if (isset($node['nid'])) {
|
||||
$nids[$menu_name][] = $node['nid'];
|
||||
$link->setValue(['uri' => 'entity:node/' . $node['nid']]);
|
||||
$menuLinkContent->set('title', $node['title']);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'external':
|
||||
$link->setValue(['uri' => 'https://www.example.com/']);
|
||||
break;
|
||||
|
||||
case 'front':
|
||||
$link->setValue(['uri' => 'internal:/<front>']);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$menuLinkContent->save();
|
||||
|
||||
$links[$menuLinkContent->id()] = $menuLinkContent->getTitle();
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,454 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\content_translation\ContentTranslationManagerInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Drupal\taxonomy\TermInterface;
|
||||
use Drupal\taxonomy\TermStorageInterface;
|
||||
use Drupal\taxonomy\VocabularyStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a TermDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "term",
|
||||
* label = @Translation("terms"),
|
||||
* description = @Translation("Generate a given number of terms. Optionally delete current terms."),
|
||||
* url = "term",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 10,
|
||||
* "title_length" = 12,
|
||||
* "minimum_depth" = 1,
|
||||
* "maximum_depth" = 4,
|
||||
* "kill" = FALSE,
|
||||
* },
|
||||
* dependencies = {
|
||||
* "taxonomy",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class TermDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The vocabulary storage.
|
||||
*/
|
||||
protected VocabularyStorageInterface $vocabularyStorage;
|
||||
|
||||
/**
|
||||
* The term storage.
|
||||
*/
|
||||
protected TermStorageInterface $termStorage;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*/
|
||||
protected Connection $database;
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*/
|
||||
protected ModuleHandlerInterface $moduleHandler;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*/
|
||||
protected LanguageManagerInterface $languageManager;
|
||||
|
||||
/**
|
||||
* The content translation manager.
|
||||
*/
|
||||
protected ?ContentTranslationManagerInterface $contentTranslationManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
|
||||
// @phpstan-ignore ternary.alwaysTrue (False positive)
|
||||
$content_translation_manager = $container->has('content_translation.manager') ? $container->get('content_translation.manager') : NULL;
|
||||
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->vocabularyStorage = $entity_type_manager->getStorage('taxonomy_vocabulary');
|
||||
$instance->termStorage = $entity_type_manager->getStorage('taxonomy_term');
|
||||
$instance->database = $container->get('database');
|
||||
$instance->contentTranslationManager = $content_translation_manager;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
$options = [];
|
||||
foreach ($this->vocabularyStorage->loadMultiple() as $vocabulary) {
|
||||
$options[$vocabulary->id()] = $vocabulary->label();
|
||||
}
|
||||
|
||||
// Sort by vocabulary label.
|
||||
asort($options);
|
||||
// Set default to 'tags' only if it exists as a vocabulary.
|
||||
$default_vids = array_key_exists('tags', $options) ? 'tags' : '';
|
||||
$form['vids'] = [
|
||||
'#type' => 'select',
|
||||
'#multiple' => TRUE,
|
||||
'#title' => $this->t('Vocabularies'),
|
||||
'#required' => TRUE,
|
||||
'#default_value' => $default_vids,
|
||||
'#options' => $options,
|
||||
'#description' => $this->t('Restrict terms to these vocabularies.'),
|
||||
];
|
||||
$form['num'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Number of terms'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
$form['title_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum number of characters in term names'),
|
||||
'#default_value' => $this->getSetting('title_length'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 2,
|
||||
'#max' => 255,
|
||||
];
|
||||
$form['minimum_depth'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Minimum depth for new terms in the vocabulary hierarchy'),
|
||||
'#description' => $this->t('Enter a value from 1 to 20.'),
|
||||
'#default_value' => $this->getSetting('minimum_depth'),
|
||||
'#min' => 1,
|
||||
'#max' => 20,
|
||||
];
|
||||
$form['maximum_depth'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum depth for new terms in the vocabulary hierarchy'),
|
||||
'#description' => $this->t('Enter a value from 1 to 20.'),
|
||||
'#default_value' => $this->getSetting('maximum_depth'),
|
||||
'#min' => 1,
|
||||
'#max' => 20,
|
||||
];
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Delete existing terms in specified vocabularies before generating new terms.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
|
||||
// Add the language and translation options.
|
||||
$form += $this->getLanguageForm('terms');
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
$new_terms = $this->generateTerms($values);
|
||||
if (!empty($new_terms['terms'])) {
|
||||
$this->setMessage($this->formatPlural($new_terms['terms'], 'Created 1 new term', 'Created @count new terms'));
|
||||
|
||||
// Helper function to format the number of terms and the list of terms.
|
||||
$format_terms_func = function (array $data, $level) {
|
||||
if ($data['total'] > 10) {
|
||||
$data['terms'][] = '...';
|
||||
}
|
||||
|
||||
return $this->formatPlural($data['total'],
|
||||
'1 new term at level @level (@terms)',
|
||||
'@count new terms at level @level (@terms)',
|
||||
['@level' => $level, '@terms' => implode(',', $data['terms'])]);
|
||||
};
|
||||
|
||||
foreach ($new_terms['vocabs'] as $vid => $vlabel) {
|
||||
if (array_key_exists($vid, $new_terms)) {
|
||||
ksort($new_terms[$vid]);
|
||||
$termlist = implode(', ', array_map($format_terms_func, $new_terms[$vid], array_keys($new_terms[$vid])));
|
||||
$this->setMessage($this->t('In vocabulary @vlabel: @termlist', ['@vlabel' => $vlabel, '@termlist' => $termlist]));
|
||||
}
|
||||
else {
|
||||
$this->setMessage($this->t('In vocabulary @vlabel: No terms created', ['@vlabel' => $vlabel]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($new_terms['terms_translations'] > 0) {
|
||||
$this->setMessage($this->formatPlural($new_terms['terms_translations'], 'Created 1 term translation', 'Created @count term translations'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all terms of given vocabularies.
|
||||
*
|
||||
* @param array $vids
|
||||
* Array of vocabulary ids.
|
||||
*
|
||||
* @return int
|
||||
* The number of terms deleted.
|
||||
*/
|
||||
protected function deleteVocabularyTerms(array $vids): int {
|
||||
$tids = $this->vocabularyStorage->getToplevelTids($vids);
|
||||
$terms = $this->termStorage->loadMultiple($tids);
|
||||
$total_deleted = 0;
|
||||
foreach ($vids as $vid) {
|
||||
$total_deleted += count($this->termStorage->loadTree($vid));
|
||||
}
|
||||
|
||||
$this->termStorage->delete($terms);
|
||||
return $total_deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates taxonomy terms for a list of given vocabularies.
|
||||
*
|
||||
* @param array $parameters
|
||||
* The input parameters from the settings form or drush command.
|
||||
*
|
||||
* @return array
|
||||
* Information about the created terms.
|
||||
*/
|
||||
protected function generateTerms(array $parameters): array {
|
||||
$info = [
|
||||
'terms' => 0,
|
||||
'terms_translations' => 0,
|
||||
];
|
||||
$min_depth = $parameters['minimum_depth'];
|
||||
$max_depth = $parameters['maximum_depth'];
|
||||
|
||||
// $parameters['vids'] from the UI has keys of the vocab ids. From drush
|
||||
// the array is keyed 0,1,2. Therefore create $vocabs which has keys of the
|
||||
// vocab ids, so it can be used with array_rand().
|
||||
$vocabs = array_combine($parameters['vids'], $parameters['vids']);
|
||||
|
||||
// Delete terms from the vocabularies we are creating new terms in.
|
||||
if ($parameters['kill']) {
|
||||
$deleted = $this->deleteVocabularyTerms($vocabs);
|
||||
$this->setMessage($this->formatPlural($deleted, 'Deleted 1 existing term', 'Deleted @count existing terms'));
|
||||
if ($min_depth != 1) {
|
||||
$this->setMessage($this->t('Minimum depth changed from @min_depth to 1 because all terms were deleted', ['@min_depth' => $min_depth]));
|
||||
$min_depth = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Build an array of potential parents for the new terms. These will be
|
||||
// terms in the vocabularies we are creating in, which have a depth of one
|
||||
// less than the minimum for new terms up to one less than the maximum.
|
||||
$all_parents = [];
|
||||
foreach ($parameters['vids'] as $vid) {
|
||||
$info['vocabs'][$vid] = $this->vocabularyStorage->load($vid)->label();
|
||||
// Initialise the nested array for this vocabulary.
|
||||
$all_parents[$vid] = ['top_level' => [], 'lower_levels' => []];
|
||||
$ids = [];
|
||||
for ($depth = 1; $depth < $max_depth; ++$depth) {
|
||||
$query = $this->termStorage->getQuery()->accessCheck(FALSE)->condition('vid', $vid);
|
||||
if ($depth == 1) {
|
||||
// For the top level the parent id must be zero.
|
||||
$query->condition('parent', 0);
|
||||
}
|
||||
else {
|
||||
// For lower levels use the $ids array obtained in the previous loop.
|
||||
$query->condition('parent', $ids, 'IN');
|
||||
}
|
||||
|
||||
$ids = $query->execute();
|
||||
|
||||
if (empty($ids)) {
|
||||
// Reached the end, no more parents to be found.
|
||||
break;
|
||||
}
|
||||
|
||||
// Store these terms as parents if they are within the depth range for
|
||||
// new terms.
|
||||
if ($depth == $min_depth - 1) {
|
||||
$all_parents[$vid]['top_level'] = array_fill_keys($ids, $depth);
|
||||
}
|
||||
elseif ($depth >= $min_depth) {
|
||||
$all_parents[$vid]['lower_levels'] += array_fill_keys($ids, $depth);
|
||||
}
|
||||
}
|
||||
|
||||
// No top-level parents will have been found above when the minimum depth
|
||||
// is 1 so add a record for that data here.
|
||||
if ($min_depth == 1) {
|
||||
$all_parents[$vid]['top_level'] = [0 => 0];
|
||||
}
|
||||
elseif (empty($all_parents[$vid]['top_level'])) {
|
||||
// No parents for required minimum level so cannot use this vocabulary.
|
||||
unset($vocabs[$vid]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($vocabs === []) {
|
||||
// There are no available parents at the required depth in any vocabulary,
|
||||
// so we cannot create any new terms.
|
||||
throw new \Exception(sprintf('Invalid minimum depth %s because there are no terms in any vocabulary at depth %s', $min_depth, $min_depth - 1));
|
||||
}
|
||||
|
||||
// Insert new data:
|
||||
for ($i = 1; $i <= $parameters['num']; ++$i) {
|
||||
// Select a vocabulary at random.
|
||||
$vid = array_rand($vocabs);
|
||||
|
||||
// Set the group to use to select a random parent from. Using < 50 means
|
||||
// on average half of the new terms will be top_level. Also if no terms
|
||||
// exist yet in 'lower_levels' then we have to use 'top_level'.
|
||||
$group = (mt_rand(0, 100) < 50 || empty($all_parents[$vid]['lower_levels'])) ? 'top_level' : 'lower_levels';
|
||||
$parent = array_rand($all_parents[$vid][$group]);
|
||||
$depth = $all_parents[$vid][$group][$parent] + 1;
|
||||
$name = $this->getRandom()->word(mt_rand(2, $parameters['title_length']));
|
||||
|
||||
$values = [
|
||||
'name' => $name,
|
||||
'description' => 'Description of ' . $name . ' (depth ' . $depth . ')',
|
||||
'format' => filter_fallback_format(),
|
||||
'weight' => mt_rand(0, 10),
|
||||
'vid' => $vid,
|
||||
'parent' => [$parent],
|
||||
// Give hook implementations access to the parameters used for generation.
|
||||
'devel_generate' => $parameters,
|
||||
];
|
||||
if (isset($parameters['add_language'])) {
|
||||
$values['langcode'] = $this->getLangcode($parameters['add_language']);
|
||||
}
|
||||
|
||||
/** @var \Drupal\taxonomy\TermInterface $term */
|
||||
$term = $this->termStorage->create($values);
|
||||
|
||||
// Populate all fields with sample values.
|
||||
$this->populateFields($term);
|
||||
$term->save();
|
||||
|
||||
// Add translations.
|
||||
if (isset($parameters['translate_language']) && !empty($parameters['translate_language'])) {
|
||||
$info['terms_translations'] += $this->generateTermTranslation($parameters['translate_language'], $term);
|
||||
}
|
||||
|
||||
// If the depth of the new term is less than the maximum depth then it can
|
||||
// also be saved as a potential parent for the subsequent new terms.
|
||||
if ($depth < $max_depth) {
|
||||
$all_parents[$vid]['lower_levels'] += [$term->id() => $depth];
|
||||
}
|
||||
|
||||
// Store data about the newly generated term.
|
||||
++$info['terms'];
|
||||
@$info[$vid][$depth]['total']++;
|
||||
// List only the first 10 new terms at each vocab/level.
|
||||
if (!isset($info[$vid][$depth]['terms']) || count($info[$vid][$depth]['terms']) < 10) {
|
||||
$info[$vid][$depth]['terms'][] = $term->label();
|
||||
}
|
||||
|
||||
unset($term);
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create translation for the given term.
|
||||
*
|
||||
* @param array $translate_language
|
||||
* Potential translate languages array.
|
||||
* @param \Drupal\taxonomy\TermInterface $term
|
||||
* Term to add translations to.
|
||||
*
|
||||
* @return int
|
||||
* Number of translations added.
|
||||
*/
|
||||
protected function generateTermTranslation(array $translate_language, TermInterface $term): int {
|
||||
if (is_null($this->contentTranslationManager)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!$this->contentTranslationManager->isEnabled('taxonomy_term', $term->bundle())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($term->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_SPECIFIED
|
||||
|| $term->get('langcode')->getLangcode() === LanguageInterface::LANGCODE_NOT_APPLICABLE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$num_translations = 0;
|
||||
// Translate term to each target language.
|
||||
$skip_languages = [
|
||||
LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
LanguageInterface::LANGCODE_NOT_APPLICABLE,
|
||||
$term->get('langcode')->getLangcode(),
|
||||
];
|
||||
foreach ($translate_language as $langcode) {
|
||||
if (in_array($langcode, $skip_languages)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$translation_term = $term->addTranslation($langcode);
|
||||
$translation_term->setName($term->getName() . ' (' . $langcode . ')');
|
||||
$this->populateFields($translation_term);
|
||||
$translation_term->save();
|
||||
++$num_translations;
|
||||
}
|
||||
|
||||
return $num_translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
// Get default settings from the annotated command definition.
|
||||
$defaultSettings = $this->getDefaultSettings();
|
||||
|
||||
$bundles = self::csvToarray($options['bundles']);
|
||||
if (count($bundles) < 1) {
|
||||
throw new \Exception(dt('Please provide a vocabulary machine name (--bundles).'));
|
||||
}
|
||||
|
||||
foreach ($bundles as $bundle) {
|
||||
// Verify that each bundle is a valid vocabulary id.
|
||||
if (!$this->vocabularyStorage->load($bundle)) {
|
||||
throw new \Exception(dt('Invalid vocabulary machine name: @name', ['@name' => $bundle]));
|
||||
}
|
||||
}
|
||||
|
||||
$number = array_shift($args) ?: $defaultSettings['num'];
|
||||
if (!$this->isNumber($number)) {
|
||||
throw new \Exception(dt('Invalid number of terms: @num', ['@num' => $number]));
|
||||
}
|
||||
|
||||
$minimum_depth = $options['min-depth'] ?? $defaultSettings['minimum_depth'];
|
||||
$maximum_depth = $options['max-depth'] ?? $defaultSettings['maximum_depth'];
|
||||
if ($minimum_depth < 1 || $minimum_depth > 20 || $maximum_depth < 1 || $maximum_depth > 20 || $minimum_depth > $maximum_depth) {
|
||||
throw new \Exception(dt('The depth values must be in the range 1 to 20 and min-depth cannot be larger than max-depth (values given: min-depth @min, max-depth @max)', ['@min' => $minimum_depth, '@max' => $maximum_depth]));
|
||||
}
|
||||
|
||||
$values = [
|
||||
'num' => $number,
|
||||
'kill' => $options['kill'],
|
||||
'title_length' => 12,
|
||||
'vids' => $bundles,
|
||||
'minimum_depth' => $minimum_depth,
|
||||
'maximum_depth' => $maximum_depth,
|
||||
];
|
||||
$add_language = self::csvToArray($options['languages']);
|
||||
// Intersect with the enabled languages to make sure the language args
|
||||
// passed are actually enabled.
|
||||
$valid_languages = array_keys($this->languageManager->getLanguages(LanguageInterface::STATE_ALL));
|
||||
$values['add_language'] = array_intersect($add_language, $valid_languages);
|
||||
|
||||
$translate_language = self::csvToArray($options['translations']);
|
||||
$values['translate_language'] = array_intersect($translate_language, $valid_languages);
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Datetime\DateFormatterInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Drupal\user\RoleStorageInterface;
|
||||
use Drupal\user\UserStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a UserDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "user",
|
||||
* label = @Translation("users"),
|
||||
* description = @Translation("Generate a given number of users. Optionally delete current users."),
|
||||
* url = "user",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 50,
|
||||
* "kill" = FALSE,
|
||||
* "pass" = ""
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class UserDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The user storage.
|
||||
*/
|
||||
protected UserStorageInterface $userStorage;
|
||||
|
||||
/**
|
||||
* The date formatter service.
|
||||
*/
|
||||
protected DateFormatterInterface $dateFormatter;
|
||||
|
||||
/**
|
||||
* Provides system time.
|
||||
*/
|
||||
protected TimeInterface $time;
|
||||
|
||||
/**
|
||||
* The role storage.
|
||||
*/
|
||||
protected RoleStorageInterface $roleStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->userStorage = $entity_type_manager->getStorage('user');
|
||||
$instance->dateFormatter = $container->get('date.formatter');
|
||||
$instance->time = $container->get('datetime.time');
|
||||
$instance->roleStorage = $entity_type_manager->getStorage('user_role');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
$form['num'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('How many users would you like to generate?'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Delete all users (except user id 1) before generating new users.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
|
||||
$roles = array_map(static fn($role): string => $role->label(), $this->roleStorage->loadMultiple());
|
||||
unset($roles[AccountInterface::AUTHENTICATED_ROLE], $roles[AccountInterface::ANONYMOUS_ROLE]);
|
||||
$form['roles'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Which roles should the users receive?'),
|
||||
'#description' => $this->t('Users always receive the <em>authenticated user</em> role.'),
|
||||
'#options' => $roles,
|
||||
];
|
||||
|
||||
$form['pass'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Password to be set'),
|
||||
'#default_value' => $this->getSetting('pass'),
|
||||
'#size' => 32,
|
||||
'#description' => $this->t('Leave this field empty if you do not need to set a password'),
|
||||
];
|
||||
|
||||
$options = [1 => $this->t('Now')];
|
||||
foreach ([3600, 86400, 604800, 2592000, 31536000] as $interval) {
|
||||
$options[$interval] = $this->dateFormatter->formatInterval($interval, 1) . ' ' . $this->t('ago');
|
||||
}
|
||||
|
||||
$form['time_range'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('How old should user accounts be?'),
|
||||
'#description' => $this->t('User ages will be distributed randomly from the current time, back to the selected time.'),
|
||||
'#options' => $options,
|
||||
'#default_value' => 604800,
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
$num = $values['num'];
|
||||
$kill = $values['kill'];
|
||||
$pass = $values['pass'];
|
||||
$age = $values['time_range'];
|
||||
$roles = array_filter($values['roles']);
|
||||
|
||||
if ($kill) {
|
||||
$uids = $this->userStorage->getQuery()
|
||||
->condition('uid', 1, '>')
|
||||
->accessCheck(FALSE)
|
||||
->execute();
|
||||
$users = $this->userStorage->loadMultiple($uids);
|
||||
$this->userStorage->delete($users);
|
||||
|
||||
$this->setMessage($this->formatPlural(count($uids), '1 user deleted', '@count users deleted.'));
|
||||
}
|
||||
|
||||
if ($num > 0) {
|
||||
$names = [];
|
||||
while (count($names) < $num) {
|
||||
$name = $this->getRandom()->word(mt_rand(6, 12));
|
||||
$names[$name] = '';
|
||||
}
|
||||
|
||||
if ($roles === []) {
|
||||
$roles = [AccountInterface::AUTHENTICATED_ROLE];
|
||||
}
|
||||
|
||||
foreach (array_keys($names) as $name) {
|
||||
/** @var \Drupal\user\UserInterface $account */
|
||||
$account = $this->userStorage->create([
|
||||
'uid' => NULL,
|
||||
'name' => $name,
|
||||
'pass' => $pass,
|
||||
'mail' => $name . '@example.com',
|
||||
'status' => 1,
|
||||
'created' => $this->time->getRequestTime() - mt_rand(0, $age),
|
||||
'roles' => array_values($roles),
|
||||
// A flag to let hook_user_* know that this is a generated user.
|
||||
'devel_generate' => TRUE,
|
||||
]);
|
||||
|
||||
// Populate all fields with sample values.
|
||||
$this->populateFields($account);
|
||||
$account->save();
|
||||
}
|
||||
}
|
||||
|
||||
$this->setMessage($this->t('@num_users created.',
|
||||
['@num_users' => $this->formatPlural($num, '1 user', '@count users')]));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
return [
|
||||
'num' => array_shift($args),
|
||||
'time_range' => 0,
|
||||
'roles' => self::csvToArray($options['roles']),
|
||||
'kill' => $options['kill'],
|
||||
'pass' => $options['pass'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Drupal\taxonomy\VocabularyStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a VocabularyDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "vocabulary",
|
||||
* label = @Translation("vocabularies"),
|
||||
* description = @Translation("Generate a given number of vocabularies. Optionally delete current vocabularies."),
|
||||
* url = "vocabs",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 1,
|
||||
* "title_length" = 12,
|
||||
* "kill" = FALSE
|
||||
* },
|
||||
* dependencies = {
|
||||
* "taxonomy",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class VocabularyDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The vocabulary storage.
|
||||
*/
|
||||
protected VocabularyStorageInterface $vocabularyStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->vocabularyStorage = $entity_type_manager->getStorage('taxonomy_vocabulary');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
$form['num'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Number of vocabularies?'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 0,
|
||||
];
|
||||
$form['title_length'] = [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum number of characters in vocabulary names'),
|
||||
'#default_value' => $this->getSetting('title_length'),
|
||||
'#required' => TRUE,
|
||||
'#min' => 2,
|
||||
'#max' => 255,
|
||||
];
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Delete existing vocabularies before generating new ones.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
if ($values['kill']) {
|
||||
$this->deleteVocabularies();
|
||||
$this->setMessage($this->t('Deleted existing vocabularies.'));
|
||||
}
|
||||
|
||||
$new_vocs = $this->generateVocabularies($values['num'], $values['title_length']);
|
||||
if ($new_vocs !== []) {
|
||||
$this->setMessage($this->t('Created the following new vocabularies: @vocs', ['@vocs' => implode(', ', $new_vocs)]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all vocabularies.
|
||||
*/
|
||||
protected function deleteVocabularies(): void {
|
||||
$vocabularies = $this->vocabularyStorage->loadMultiple();
|
||||
$this->vocabularyStorage->delete($vocabularies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates vocabularies.
|
||||
*
|
||||
* @param int $records
|
||||
* Number of vocabularies to create.
|
||||
* @param int $maxlength
|
||||
* (optional) Maximum length for vocabulary name.
|
||||
*
|
||||
* @return array
|
||||
* Array containing the generated vocabularies id.
|
||||
*/
|
||||
protected function generateVocabularies(int $records, int $maxlength = 12): array {
|
||||
$vocabularies = [];
|
||||
|
||||
// Insert new data:
|
||||
for ($i = 1; $i <= $records; ++$i) {
|
||||
$name = $this->getRandom()->word(mt_rand(2, $maxlength));
|
||||
|
||||
$vocabulary = $this->vocabularyStorage->create([
|
||||
'name' => $name,
|
||||
'vid' => mb_strtolower($name),
|
||||
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
|
||||
'description' => 'Description of ' . $name,
|
||||
'hierarchy' => 1,
|
||||
'weight' => mt_rand(0, 10),
|
||||
'multiple' => 1,
|
||||
'required' => 0,
|
||||
'relations' => 1,
|
||||
]);
|
||||
|
||||
// Populate all fields with sample values.
|
||||
$this->populateFields($vocabulary);
|
||||
$vocabulary->save();
|
||||
|
||||
$vocabularies[] = $vocabulary->id();
|
||||
unset($vocabulary);
|
||||
}
|
||||
|
||||
return $vocabularies;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
$values = [
|
||||
'num' => array_shift($args),
|
||||
'kill' => $options['kill'],
|
||||
'title_length' => 12,
|
||||
];
|
||||
|
||||
if ($this->isNumber($values['num']) == FALSE) {
|
||||
throw new \Exception(dt('Invalid number of vocabularies: @num.', ['@num' => $values['num']]));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate\Routing;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\devel_generate\Form\DevelGenerateForm;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Provides dynamic routes for devel_generate.
|
||||
*/
|
||||
class DevelGenerateRoutes implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The manager to be used for instantiating plugins.
|
||||
*/
|
||||
protected PluginManagerInterface $develGenerateManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): self {
|
||||
$instance = new self();
|
||||
$instance->develGenerateManager = $container->get('plugin.manager.develgenerate');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define routes for all devel_generate plugins.
|
||||
*/
|
||||
public function routes(): array {
|
||||
$devel_generate_plugins = $this->develGenerateManager->getDefinitions();
|
||||
|
||||
$routes = [];
|
||||
foreach ($devel_generate_plugins as $id => $plugin) {
|
||||
$label = $plugin['label'];
|
||||
$type_url_str = str_replace('_', '-', $plugin['url']);
|
||||
$routes['devel_generate.' . $id] = new Route(
|
||||
'admin/config/development/generate/' . $type_url_str,
|
||||
[
|
||||
'_form' => DevelGenerateForm::class,
|
||||
'_title' => 'Generate ' . $label,
|
||||
'_plugin_id' => $id,
|
||||
],
|
||||
[
|
||||
'_permission' => $plugin['permission'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Add the route for the 'Generate' admin group on the admin/config page.
|
||||
// This also provides the page for all devel_generate links.
|
||||
$routes['devel_generate.admin_config_generate'] = new Route(
|
||||
'/admin/config/development/generate',
|
||||
[
|
||||
'_controller' => '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage',
|
||||
'_title' => 'Generate',
|
||||
],
|
||||
[
|
||||
'_permission' => 'administer devel_generate',
|
||||
]
|
||||
);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
name: 'Devel Generate Example'
|
||||
type: module
|
||||
description: 'Create an example of a Devel Generate plugin type for testing purposes.'
|
||||
package: Testing
|
||||
configure: admin/config/development/generate
|
||||
tags:
|
||||
- developer
|
||||
|
||||
# Information added by Drupal.org packaging script on 2025-07-07
|
||||
version: '5.4.0'
|
||||
project: 'devel'
|
||||
datestamp: 1751916161
|
||||
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel_generate_example\Plugin\DevelGenerate;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\devel_generate\DevelGenerateBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a ExampleDevelGenerate plugin.
|
||||
*
|
||||
* @DevelGenerate(
|
||||
* id = "devel_generate_example",
|
||||
* label = "Example",
|
||||
* description = "Generate a given number of examples.",
|
||||
* url = "devel_generate_example",
|
||||
* permission = "administer devel_generate",
|
||||
* settings = {
|
||||
* "num" = 50,
|
||||
* "kill" = FALSE
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ExampleDevelGenerate extends DevelGenerateBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Provides system time.
|
||||
*/
|
||||
protected TimeInterface $time;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
|
||||
$instance->time = $container->get('datetime.time');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state): array {
|
||||
|
||||
$form['num'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('How many examples would you like to generate?'),
|
||||
'#default_value' => $this->getSetting('num'),
|
||||
'#size' => 10,
|
||||
];
|
||||
|
||||
$form['kill'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Delete all examples before generating new examples.'),
|
||||
'#default_value' => $this->getSetting('kill'),
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function generateElements(array $values): void {
|
||||
$num = $values['num'];
|
||||
$kill = $values['kill'];
|
||||
|
||||
if ($kill) {
|
||||
$this->setMessage($this->t('Old examples have been deleted.'));
|
||||
}
|
||||
|
||||
// Creating user in order to demonstrate
|
||||
// how to override default business login generation.
|
||||
$edit = [
|
||||
'uid' => NULL,
|
||||
'name' => 'example_devel_generate',
|
||||
'pass' => '',
|
||||
'mail' => 'example_devel_generate@example.com',
|
||||
'status' => 1,
|
||||
'created' => $this->time->getRequestTime(),
|
||||
'roles' => '',
|
||||
// A flag to let hook_user_* know that this is a generated user.
|
||||
'devel_generate' => TRUE,
|
||||
];
|
||||
|
||||
$account = user_load_by_name('example_devel_generate');
|
||||
if (!$account) {
|
||||
$account = $this->entityTypeManager->getStorage('user')->create($edit);
|
||||
}
|
||||
|
||||
// Populate all fields with sample values.
|
||||
$this->populateFields($account);
|
||||
|
||||
$account->save();
|
||||
|
||||
$this->setMessage($this->t('@num_examples created.', [
|
||||
'@num_examples' => $this->formatPlural($num, '1 example', '@count examples'),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateDrushParams(array $args, array $options = []): array {
|
||||
return [
|
||||
'num' => $options['num'],
|
||||
'kill' => $options['kill'],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
name: 'Devel Generate Fields'
|
||||
type: module
|
||||
description: 'Alter in a base field for testing purposes.'
|
||||
package: Testing
|
||||
tags:
|
||||
- developer
|
||||
|
||||
# Information added by Drupal.org packaging script on 2025-07-07
|
||||
version: '5.4.0'
|
||||
project: 'devel'
|
||||
datestamp: 1751916161
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test module for field population.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Implements hook_entity_base_field_info_alter().
|
||||
*/
|
||||
function devel_generate_fields_entity_base_field_info_alter(array &$fields, EntityTypeInterface $entity_type): void {
|
||||
if (in_array($entity_type->id(), ['node', 'media'])) {
|
||||
$fields['phish'] = BaseFieldDefinition::create('string')
|
||||
->setName('phish')
|
||||
->setLabel(new TranslatableMarkup('Phish music'));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,439 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\devel_generate\Functional;
|
||||
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
|
||||
/**
|
||||
* Tests the logic to generate data.
|
||||
*
|
||||
* @group devel_generate
|
||||
*/
|
||||
class DevelGenerateBrowserTest extends DevelGenerateBrowserTestBase {
|
||||
|
||||
use MediaTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests generating users.
|
||||
*/
|
||||
public function testDevelGenerateUsers(): void {
|
||||
$this->drupalGet('admin/config/development/generate/user');
|
||||
$edit = [
|
||||
'num' => 4,
|
||||
];
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('4 users created.');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that if no content types are selected an error message is shown.
|
||||
*/
|
||||
public function testDevelGenerateContent(): void {
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$edit = [
|
||||
'num' => 4,
|
||||
'title_length' => 4,
|
||||
];
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Please select at least one content type');
|
||||
|
||||
// Create a node in order to test the Delete content checkbox.
|
||||
$this->drupalCreateNode(['type' => 'article']);
|
||||
|
||||
// Generate articles with comments and aliases.
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$edit = [
|
||||
'num' => 4,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'time_range' => 604800,
|
||||
'max_comments' => 3,
|
||||
'title_length' => 4,
|
||||
'add_alias' => 1,
|
||||
];
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Deleted 1 node');
|
||||
$this->assertSession()->pageTextContains('Created 4 nodes');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
$this->assertSession()->pageTextNotContains('translations');
|
||||
|
||||
// Tests that nodes have been created in the generation process.
|
||||
$nodes = Node::loadMultiple();
|
||||
$this->assertEquals(4, count($nodes), 'Nodes generated successfully.');
|
||||
|
||||
// Tests url alias for the generated nodes.
|
||||
foreach ($nodes as $node) {
|
||||
$alias = 'node-' . $node->id() . '-' . $node->bundle();
|
||||
$this->drupalGet($alias);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains($node->getTitle());
|
||||
}
|
||||
|
||||
// Generate articles with translations.
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$edit = [
|
||||
'num' => 3,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'add_language[]' => ['en'],
|
||||
'translate_language[]' => ['de', 'ca'],
|
||||
'add_alias' => TRUE,
|
||||
];
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Deleted 4 nodes');
|
||||
$this->assertSession()->pageTextContains('Created 3 nodes');
|
||||
// Two translations for each node makes six.
|
||||
$this->assertSession()->pageTextContains('Created 6 node translations');
|
||||
$articles = \Drupal::entityQuery('node')->accessCheck(FALSE)->execute();
|
||||
$this->assertCount(3, $articles);
|
||||
$node = Node::load(end($articles));
|
||||
$this->assertTrue($node->hasTranslation('de'));
|
||||
$this->assertTrue($node->hasTranslation('ca'));
|
||||
$this->assertFalse($node->hasTranslation('fr'));
|
||||
|
||||
// Check url alias for each of the translations.
|
||||
foreach (Node::loadMultiple($articles) as $node) {
|
||||
foreach (['de', 'ca'] as $langcode) {
|
||||
$translation_node = $node->getTranslation($langcode);
|
||||
$alias = 'node-' . $translation_node->id() . '-' . $translation_node->bundle() . '-' . $langcode;
|
||||
$this->drupalGet($langcode . '/' . $alias);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains($translation_node->getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
// Create article to make sure it is not deleted when only killing pages.
|
||||
$article = $this->drupalCreateNode(['type' => 'article', 'title' => 'Alive']);
|
||||
// The 'page' content type is not enabled for translation.
|
||||
$edit = [
|
||||
'num' => 2,
|
||||
'kill' => TRUE,
|
||||
'node_types[page]' => TRUE,
|
||||
'add_language[]' => ['en'],
|
||||
'translate_language[]' => ['fr'],
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextNotContains('Deleted');
|
||||
$this->assertSession()->pageTextContains('Created 2 nodes');
|
||||
$this->assertSession()->pageTextNotContains('node translations');
|
||||
// Check that 'kill' has not deleted the article.
|
||||
$this->assertNotEmpty(Node::load($article->id()));
|
||||
$pages = \Drupal::entityQuery('node')->condition('type', 'page')->accessCheck(FALSE)->execute();
|
||||
$this->assertCount(2, $pages);
|
||||
$node = Node::load(end($pages));
|
||||
$this->assertFalse($node->hasTranslation('fr'));
|
||||
|
||||
// Create articles with add-type-label option.
|
||||
$edit = [
|
||||
'num' => 5,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'add_type_label' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Created 5 nodes');
|
||||
$this->assertSession()->pageTextContains('Generate process complete');
|
||||
|
||||
// Count the articles created in the generation process.
|
||||
$nodes = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->execute();
|
||||
$this->assertCount(5, $nodes);
|
||||
|
||||
// Load the final node and verify that the title starts with the label.
|
||||
$node = Node::load(end($nodes));
|
||||
$this->assertEquals('Article - ', substr($node->title->value, 0, 10));
|
||||
|
||||
// Test creating content with specified authors. First create 15 more users
|
||||
// making 18 in total, to make the test much stronger.
|
||||
for ($i = 0; $i < 15; ++$i) {
|
||||
$this->drupalCreateUser();
|
||||
}
|
||||
|
||||
$edit = [
|
||||
'num' => 10,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'authors[3]' => TRUE,
|
||||
'authors[4]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
|
||||
// Display the full content list for information and debug only.
|
||||
$this->drupalGet('admin/content');
|
||||
|
||||
// Count all the articles by user 3 and 4 and by others. We count the two
|
||||
// users nodes separately to ensure that there are some by each user.
|
||||
$nodes_by_user_3 = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->condition('uid', ['3'], 'IN')->execute();
|
||||
$nodes_by_user_4 = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->condition('uid', ['4'], 'IN')->execute();
|
||||
$nodes_by_others = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->condition('uid', ['3', '4'], 'NOT IN')->execute();
|
||||
|
||||
// If the user option was not working correctly and users were assigned at
|
||||
// random, then the chance that these assertions will correctly detect the
|
||||
// error is 1 - (2/18 ** 10) = 99.99%.
|
||||
$this->assertEquals(10, count($nodes_by_user_3) + count($nodes_by_user_4));
|
||||
$this->assertCount(0, $nodes_by_others);
|
||||
|
||||
// If the user option is coded correctly the chance of either of these
|
||||
// assertions giving a false failure is 1/2 ** 10 = 0.097%.
|
||||
$this->assertGreaterThan(0, count($nodes_by_user_3));
|
||||
$this->assertGreaterThan(0, count($nodes_by_user_4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating terms.
|
||||
*/
|
||||
public function testDevelGenerateTerms(): void {
|
||||
// Generate terms.
|
||||
$edit = [
|
||||
'vids[]' => $this->vocabulary->id(),
|
||||
'num' => 5,
|
||||
'title_length' => 12,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/term');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Created 5 new terms');
|
||||
$this->assertSession()->pageTextContains('In vocabulary ' . $this->vocabulary->label());
|
||||
$this->assertSession()->pageTextNotContains('translations');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
$this->assertCount(5, \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->execute());
|
||||
|
||||
// Generate terms with translations.
|
||||
$edit = [
|
||||
'vids[]' => $this->vocabulary->id(),
|
||||
'num' => 3,
|
||||
'add_language[]' => ['en'],
|
||||
'translate_language[]' => ['ca'],
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/term');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextNotContains('Deleted');
|
||||
$this->assertSession()->pageTextContains('Created 3 new terms');
|
||||
$this->assertSession()->pageTextContains('Created 3 term translations');
|
||||
// Not using 'kill' so there should be 8 terms.
|
||||
$terms = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->execute();
|
||||
$this->assertCount(8, $terms);
|
||||
// Check the translations created (and not created).
|
||||
$term = Term::load(end($terms));
|
||||
$this->assertTrue($term->hasTranslation('ca'));
|
||||
$this->assertFalse($term->hasTranslation('de'));
|
||||
$this->assertFalse($term->hasTranslation('fr'));
|
||||
|
||||
// Generate terms in vocabulary 2 only.
|
||||
$edit = [
|
||||
'vids[]' => $this->vocabulary2->id(),
|
||||
'num' => 4,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/term');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Created 4 new terms');
|
||||
$this->assertSession()->pageTextNotContains('In vocabulary ' . $this->vocabulary->label());
|
||||
$this->assertSession()->pageTextContains('In vocabulary ' . $this->vocabulary2->label());
|
||||
// Check the term count in each vocabulary.
|
||||
$terms1 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary->id())->execute();
|
||||
$this->assertCount(8, $terms1);
|
||||
$terms2 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary2->id())->execute();
|
||||
$this->assertCount(4, $terms2);
|
||||
|
||||
// Generate in vocabulary 2 with 'kill' to remove the existing vocab2 terms.
|
||||
$edit = [
|
||||
'vids[]' => $this->vocabulary2->id(),
|
||||
'num' => 6,
|
||||
'kill' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/term');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Deleted 4 existing terms');
|
||||
$this->assertSession()->pageTextContains('Created 6 new terms');
|
||||
// Check the term count in vocabulary 1 has not changed.
|
||||
$terms1 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary->id())->execute();
|
||||
$this->assertCount(8, $terms1);
|
||||
// Check the term count in vocabulary 2 is just from the second call.
|
||||
$terms2 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary2->id())->execute();
|
||||
$this->assertCount(6, $terms2);
|
||||
|
||||
// Generate in both vocabularies and specify minimum and maximum depth.
|
||||
$edit = [
|
||||
'vids[]' => [$this->vocabulary->id(), $this->vocabulary2->id()],
|
||||
'num' => 9,
|
||||
'minimum_depth' => 2,
|
||||
'maximum_depth' => 6,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/term');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Created 9 new terms');
|
||||
// Check the total term count is 8 + 6 + 9 = 23.
|
||||
$terms1 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary->id())->execute();
|
||||
$terms2 = \Drupal::entityQuery('taxonomy_term')->accessCheck(FALSE)->condition('vid', $this->vocabulary2->id())->execute();
|
||||
$this->assertCount(23, $terms1 + $terms2);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating vocabularies.
|
||||
*/
|
||||
public function testDevelGenerateVocabs(): void {
|
||||
$edit = [
|
||||
'num' => 5,
|
||||
'title_length' => 12,
|
||||
'kill' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/vocabs');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Created the following new vocabularies: ');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating menus.
|
||||
*
|
||||
* @todo Add test coverage to check:
|
||||
* - title_length is not exceeded.
|
||||
* - max_depth and max_width work as designed.
|
||||
* - generating links in existing menus, and then deleting them with kill.
|
||||
* - using specific link_types settings only create those links.
|
||||
*/
|
||||
public function testDevelGenerateMenus(): void {
|
||||
$edit = [
|
||||
'num_menus' => 5,
|
||||
'num_links' => 7,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/menu');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Created the following 5 new menus: ');
|
||||
$this->assertSession()->pageTextContains('Created 7 new menu links');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
|
||||
// Use big numbers for menus and links, but short text, to test for clashes.
|
||||
// Also verify the kill option.
|
||||
$edit = [
|
||||
'num_menus' => 160,
|
||||
'num_links' => 380,
|
||||
'title_length' => 3,
|
||||
'kill' => 1,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/menu');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Deleted 5 menu(s) and 0 other link(s).');
|
||||
$this->assertSession()->pageTextContains('Created the following 160 new menus: ');
|
||||
$this->assertSession()->pageTextContains('Created 380 new menu links');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating media.
|
||||
*/
|
||||
public function testDevelGenerateMedia(): void {
|
||||
// As the 'media' plugin has a dependency on 'media' module, the plugin is
|
||||
// not generating a route to the plugin form.
|
||||
$this->drupalGet('admin/config/development/generate/media');
|
||||
$this->assertSession()->statusCodeEquals(404);
|
||||
// Enable the module and retry.
|
||||
\Drupal::service('module_installer')->install(['media']);
|
||||
$this->getSession()->reload();
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->assertSession()->pageTextContains('Generate media');
|
||||
|
||||
// Create two media types.
|
||||
$media_type1 = $this->createMediaType('image');
|
||||
$media_type2 = $this->createMediaType('audio_file');
|
||||
|
||||
// Creating media items (non-batch mode).
|
||||
$edit = [
|
||||
'num' => 5,
|
||||
'name_length' => 12,
|
||||
sprintf('media_types[%s]', $media_type1->id()) => 1,
|
||||
sprintf('media_types[%s]', $media_type2->id()) => 1,
|
||||
'base_fields' => 'phish',
|
||||
'kill' => 1,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/media');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Finished creating 5 media items.');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
$medias = \Drupal::entityQuery('media')->accessCheck(FALSE)->execute();
|
||||
$this->assertCount(5, $medias);
|
||||
$media = Media::load(end($medias));
|
||||
$this->assertNotEmpty($media->get('phish')->getString());
|
||||
|
||||
// Creating media items (batch mode).
|
||||
$edit = [
|
||||
'num' => 56,
|
||||
'name_length' => 6,
|
||||
sprintf('media_types[%s]', $media_type1->id()) => 1,
|
||||
sprintf('media_types[%s]', $media_type2->id()) => 1,
|
||||
'base_fields' => 'phish',
|
||||
'kill' => 1,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/media');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Finished 56 elements created successfully.');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
$this->assertCount(56, \Drupal::entityQuery('media')->accessCheck(FALSE)->execute());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating content in batch mode.
|
||||
*/
|
||||
public function testDevelGenerateBatchContent(): void {
|
||||
// For 50 or more nodes, the processing will be done via batch.
|
||||
$edit = [
|
||||
'num' => 55,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'node_types[page]' => TRUE,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertSession()->pageTextContains('Finished 55 elements created successfully.');
|
||||
$this->assertSession()->pageTextContains('Generate process complete.');
|
||||
|
||||
// Tests that the expected number of nodes have been created.
|
||||
$count = count(Node::loadMultiple());
|
||||
$this->assertEquals(55, $count, sprintf('The expected total number of nodes is %s, found %s', 55, $count));
|
||||
|
||||
// Create nodes with translations via batch.
|
||||
$edit = [
|
||||
'num' => 52,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'node_types[page]' => TRUE,
|
||||
'add_language[]' => ['en'],
|
||||
'translate_language[]' => ['de', 'ca'],
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
$this->assertCount(52, \Drupal::entityQuery('node')->accessCheck(FALSE)->execute());
|
||||
// Only articles will have translations so get that number.
|
||||
$articles = \Drupal::entityQuery('node')->accessCheck(FALSE)->condition('type', 'article')->execute();
|
||||
$this->assertSession()->pageTextContains(sprintf('Finished 52 elements and %s translations created successfully.', 2 * count($articles)));
|
||||
|
||||
// Generate only articles.
|
||||
$edit = [
|
||||
'num' => 60,
|
||||
'kill' => TRUE,
|
||||
'node_types[article]' => TRUE,
|
||||
'node_types[page]' => FALSE,
|
||||
];
|
||||
$this->drupalGet('admin/config/development/generate/content');
|
||||
$this->submitForm($edit, 'Generate');
|
||||
|
||||
// Tests that all the created nodes were of the node type selected.
|
||||
$nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
|
||||
$type = 'article';
|
||||
$count = $nodeStorage->getQuery()
|
||||
->condition('type', $type)
|
||||
->accessCheck(FALSE)
|
||||
->count()
|
||||
->execute();
|
||||
$this->assertEquals(60, $count, sprintf('The expected number of %s is %s, found %s', $type, 60, $count));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\devel_generate\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\devel_generate\Traits\DevelGenerateSetupTrait;
|
||||
|
||||
/**
|
||||
* Base class for devel_generate functional browser tests.
|
||||
*
|
||||
* DevelGenerateCommandsTest should not extend this class so that it can remain
|
||||
* independent and be used as a cut-and-paste example for other developers.
|
||||
*/
|
||||
abstract class DevelGenerateBrowserTestBase extends BrowserTestBase {
|
||||
|
||||
use DevelGenerateSetupTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected static $modules = [
|
||||
'content_translation',
|
||||
'devel',
|
||||
'devel_generate',
|
||||
'devel_generate_fields',
|
||||
'language',
|
||||
'menu_ui',
|
||||
'node',
|
||||
'comment',
|
||||
'taxonomy',
|
||||
'path',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Prepares the testing environment.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->setUpData();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\devel_generate\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\devel_generate\Traits\DevelGenerateSetupTrait;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\comment\Entity\Comment;
|
||||
use Drupal\devel_generate\Drush\Commands\DevelGenerateCommands;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\menu_link_content\Entity\MenuLinkContent;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\system\Entity\Menu;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drush\TestTraits\DrushTestTrait;
|
||||
|
||||
/**
|
||||
* Test class for the Devel Generate drush commands.
|
||||
*
|
||||
* Note: Drush must be in the Composer project.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\devel_generate\Drush\Commands\DevelGenerateCommands
|
||||
* @group devel_generate
|
||||
*/
|
||||
class DevelGenerateCommandsTest extends BrowserTestBase {
|
||||
|
||||
use DrushTestTrait;
|
||||
use DevelGenerateSetupTrait;
|
||||
use MediaTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'comment',
|
||||
'content_translation',
|
||||
'devel',
|
||||
'devel_generate',
|
||||
'devel_generate_fields',
|
||||
'language',
|
||||
'media',
|
||||
'menu_ui',
|
||||
'node',
|
||||
'path',
|
||||
'taxonomy',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Prepares the testing environment.
|
||||
*/
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->setUpData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating users.
|
||||
*/
|
||||
public function testDrushGenerateUsers(): void {
|
||||
// Make sure users get created, and with correct roles.
|
||||
$this->drush(DevelGenerateCommands::USERS, ['55'], [
|
||||
'kill' => NULL,
|
||||
'roles' => 'administrator',
|
||||
]);
|
||||
$user = User::load(55);
|
||||
$this->assertTrue($user->hasRole('administrator'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating terms.
|
||||
*/
|
||||
public function testDrushGenerateTerms(): void {
|
||||
// Make sure terms get created, and with correct vocab.
|
||||
$this->drush(DevelGenerateCommands::TERMS, ['55'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => $this->vocabulary->id(),
|
||||
]);
|
||||
$term = Term::load(55);
|
||||
$this->assertEquals($this->vocabulary->id(), $term->bundle());
|
||||
|
||||
// Make sure terms get created, with proper language.
|
||||
$this->drush(DevelGenerateCommands::TERMS, ['10'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => $this->vocabulary->id(),
|
||||
'languages' => 'fr',
|
||||
]);
|
||||
$term = Term::load(60);
|
||||
$this->assertEquals('fr', $term->language()->getId());
|
||||
|
||||
// Make sure terms gets created, with proper translation.
|
||||
$this->drush(DevelGenerateCommands::TERMS, ['10'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => $this->vocabulary->id(),
|
||||
'languages' => 'fr',
|
||||
'translations' => 'de',
|
||||
]);
|
||||
$term = Term::load(70);
|
||||
$this->assertTrue($term->hasTranslation('de'));
|
||||
$this->assertTrue($term->hasTranslation('fr'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating vocabularies.
|
||||
*/
|
||||
public function testDrushGenerateVocabs(): void {
|
||||
// Make sure vocabs get created.
|
||||
$this->drush(DevelGenerateCommands::VOCABS, ['5'], ['kill' => NULL]);
|
||||
$vocabs = Vocabulary::loadMultiple();
|
||||
$this->assertGreaterThan(4, count($vocabs));
|
||||
$vocab = array_pop($vocabs);
|
||||
$this->assertNotEmpty($vocab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating menus.
|
||||
*/
|
||||
public function testDrushGenerateMenus(): void {
|
||||
$generatedMenu = NULL;
|
||||
|
||||
// Make sure menus, and with correct properties.
|
||||
$this->drush(DevelGenerateCommands::MENUS, ['1', '5'], ['kill' => NULL]);
|
||||
$menus = Menu::loadMultiple();
|
||||
foreach ($menus as $menu) {
|
||||
if (str_contains($menu->id(), 'devel-')) {
|
||||
// We have a menu that we created.
|
||||
$generatedMenu = $menu;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$link = MenuLinkContent::load(5);
|
||||
|
||||
$this->assertNotNull($generatedMenu, 'Generated menu successfully.');
|
||||
$this->assertNotNull($link, 'Generated link successfully.');
|
||||
$this->assertEquals($generatedMenu->id(), $link->getMenuName(), 'Generated menu ID matches link menu name.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating content.
|
||||
*/
|
||||
public function testDrushGenerateContent(): void {
|
||||
// Generate content using the minimum parameters.
|
||||
$this->drush(DevelGenerateCommands::CONTENT, ['21']);
|
||||
$node = Node::load(21);
|
||||
$this->assertNotEmpty($node);
|
||||
|
||||
// Make sure articles get comments. Only one third of articles will have
|
||||
// comment status 'open' and therefore the ability to receive a comment.
|
||||
// However, generating 30 articles will give the likelihood of test failure
|
||||
// (i.e. no article gets a comment) as 2/3 ^ 30 = 0.00052% or 1 in 191751.
|
||||
$this->drush(DevelGenerateCommands::CONTENT, ['30', '9'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => 'article',
|
||||
]);
|
||||
$comment = Comment::load(1);
|
||||
$this->assertNotEmpty($comment);
|
||||
|
||||
// Generate content with a higher number that triggers batch running.
|
||||
$this->drush(DevelGenerateCommands::CONTENT, ['55'], ['kill' => NULL]);
|
||||
$nodes = \Drupal::entityQuery('node')->accessCheck(FALSE)->execute();
|
||||
$this->assertCount(55, $nodes);
|
||||
$messages = $this->getErrorOutput();
|
||||
$this->assertStringContainsStringIgnoringCase('Finished 55 elements created successfully.', $messages, 'devel-generate-content batch ending message not found');
|
||||
|
||||
// Generate specified language. Verify base field is populated.
|
||||
$this->drush(DevelGenerateCommands::CONTENT, ['10'], [
|
||||
'kill' => NULL,
|
||||
'languages' => 'fr',
|
||||
'base-fields' => 'phish',
|
||||
]);
|
||||
$nodes = \Drupal::entityQuery('node')->accessCheck(FALSE)->execute();
|
||||
$node = Node::load(end($nodes));
|
||||
$this->assertEquals('fr', $node->language()->getId());
|
||||
$this->assertNotEmpty($node->get('phish')->getString());
|
||||
|
||||
// Generate content with translations.
|
||||
$this->drush(DevelGenerateCommands::CONTENT, ['18'], [
|
||||
'kill' => NULL,
|
||||
'languages' => 'fr',
|
||||
'translations' => 'de',
|
||||
]);
|
||||
// Only articles are enabled for translations.
|
||||
$articles = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'article')
|
||||
->execute();
|
||||
$pages = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'page')
|
||||
->execute();
|
||||
$this->assertCount(18, $articles + $pages);
|
||||
// Check that the last article has 'de' and 'fr' but no 'ca' translation.
|
||||
$node = Node::load(end($articles));
|
||||
$this->assertTrue($node->hasTranslation('de'));
|
||||
$this->assertTrue($node->hasTranslation('fr'));
|
||||
$this->assertFalse($node->hasTranslation('ca'));
|
||||
|
||||
// Generate just page content with option --add-type-label.
|
||||
// Note: Use the -v verbose option to get the ending message shown when not
|
||||
// generating enough to trigger batch mode.
|
||||
// @todo Remove -v when the messages are shown for both run types.
|
||||
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['9'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => 'page',
|
||||
'add-type-label' => NULL,
|
||||
]);
|
||||
// Count the page nodes.
|
||||
$nodes = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'page')
|
||||
->execute();
|
||||
$this->assertCount(9, $nodes);
|
||||
$messages = $this->getErrorOutput();
|
||||
$this->assertStringContainsStringIgnoringCase('Created 9 nodes', $messages, 'batch end message not found');
|
||||
// Load the final node and verify that the title starts with the label.
|
||||
$node = Node::load(end($nodes));
|
||||
$this->assertEquals('Basic Page - ', substr($node->title->value, 0, 13));
|
||||
|
||||
// Generate articles with a specified users.
|
||||
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['10'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => 'article',
|
||||
'authors' => '2',
|
||||
]);
|
||||
// Count the nodes assigned to user 2. We have two other users (0 and 1) so
|
||||
// if the code was broken and users were assigned randomly the chance that
|
||||
// this fauly would be detected is 1 - (1/3 ** 10) = 99.998%.
|
||||
$nodes = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'article')
|
||||
->condition('uid', ['2'], 'IN')
|
||||
->execute();
|
||||
$this->assertCount(10, $nodes);
|
||||
|
||||
// Generate page content using the 'roles' option to select authors based
|
||||
// on the roles that the user has. For this we need a new user with a
|
||||
// distinct role.
|
||||
$userA = $this->drupalCreateUser(['access content']);
|
||||
$roleA = $userA->getRoles()[1];
|
||||
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['8'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => 'page',
|
||||
'roles' => $roleA,
|
||||
]);
|
||||
// Count the number of nodes assigned to User A. There are three other users
|
||||
// so if the code was broken and authors assigned randomly, the chance that
|
||||
// this test would detect the fault is 1 - (1/4 ^ 8) = 99.998%.
|
||||
$nodesA = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'page')
|
||||
->condition('uid', $userA->id())
|
||||
->execute();
|
||||
$this->assertCount(8, $nodesA, 'User A should have all the generated content');
|
||||
|
||||
// Repeat the above using two roles and two users.
|
||||
$userB = $this->drupalCreateUser(['create page content']);
|
||||
$roleB = $userB->getRoles()[1];
|
||||
$this->drush(DevelGenerateCommands::CONTENT . ' -v', ['20'], [
|
||||
'kill' => NULL,
|
||||
'bundles' => 'page',
|
||||
'roles' => sprintf('%s, %s', $roleA, $roleB),
|
||||
]);
|
||||
// Count the nodes assigned to users A and B. There are three other users
|
||||
// so if the code was broken and users were assigned randomly the chance
|
||||
// that the test would detect the fault is 1 - (2/5 ^ 20) = 99.999%.
|
||||
$nodesA = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'page')
|
||||
->condition('uid', $userA->id())
|
||||
->execute();
|
||||
$nodesB = \Drupal::entityQuery('node')
|
||||
->accessCheck(FALSE)
|
||||
->condition('type', 'page')
|
||||
->condition('uid', $userB->id())
|
||||
->execute();
|
||||
$this->assertGreaterThan(0, count($nodesA), 'User A should have some content');
|
||||
$this->assertGreaterThan(0, count($nodesB), 'User B should have some content');
|
||||
$this->assertCount(20, $nodesA + $nodesB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests generating media.
|
||||
*/
|
||||
public function testDrushGenerateMedia(): void {
|
||||
// Create two media types.
|
||||
$media_type1 = $this->createMediaType('image');
|
||||
$media_type2 = $this->createMediaType('audio_file');
|
||||
// Make sure media items gets created with batch process.
|
||||
$this->drush(DevelGenerateCommands::MEDIA, ['53'], [
|
||||
'kill' => NULL,
|
||||
'base-fields' => 'phish',
|
||||
]);
|
||||
$this->assertCount(53, \Drupal::entityQuery('media')
|
||||
->accessCheck(FALSE)
|
||||
->execute());
|
||||
$messages = $this->getErrorOutput();
|
||||
$this->assertStringContainsStringIgnoringCase('Finished 53 elements created successfully.', $messages, 'devel-generate-media batch ending message not found');
|
||||
$medias = \Drupal::entityQuery('media')->accessCheck(FALSE)->execute();
|
||||
$media = Media::load(end($medias));
|
||||
// Verify that base field populates.
|
||||
$this->assertNotEmpty($media->get('phish')->getString());
|
||||
|
||||
// Test also with a non-batch process. We're testing also --kill here.
|
||||
$this->drush(DevelGenerateCommands::MEDIA, ['7'], [
|
||||
'media-types' => $media_type1->id() . ',' . $media_type2->id(),
|
||||
'kill' => NULL,
|
||||
]);
|
||||
$this->assertCount(7, \Drupal::entityQuery('media')
|
||||
->accessCheck(FALSE)
|
||||
->execute());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\devel_generate\Traits;
|
||||
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\Language\Language;
|
||||
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
|
||||
/**
|
||||
* Provides methods to assist Devel Generate testing.
|
||||
*
|
||||
* Referenced in DevelGenerateBrowserTestBase and DevelGenerateCommandsTest.
|
||||
*/
|
||||
trait DevelGenerateSetupTrait {
|
||||
|
||||
use CommentTestTrait;
|
||||
use EntityReferenceFieldCreationTrait;
|
||||
|
||||
/**
|
||||
* Vocabulary for testing generation of terms.
|
||||
*
|
||||
* @var \Drupal\taxonomy\VocabularyInterface
|
||||
*/
|
||||
protected $vocabulary;
|
||||
|
||||
/**
|
||||
* Second vocabulary for testing generation of terms.
|
||||
*
|
||||
* @var \Drupal\taxonomy\VocabularyInterface
|
||||
*/
|
||||
protected $vocabulary2;
|
||||
|
||||
/**
|
||||
* General set-up for all tests.
|
||||
*/
|
||||
public function setUpData(): void {
|
||||
// Create user with devel_generate permissions and access to admin/content.
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'administer devel_generate',
|
||||
'access devel information',
|
||||
'access content overview',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
$entity_type_manager = $this->container->get('entity_type.manager');
|
||||
// Create Basic page and Article node types.
|
||||
if ($this->profile != 'standard') {
|
||||
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic Page']);
|
||||
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
|
||||
$this->addDefaultCommentField('node', 'article');
|
||||
}
|
||||
|
||||
// Enable translation for article content type (but not for page).
|
||||
\Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE);
|
||||
// Create languages for generated translations.
|
||||
ConfigurableLanguage::createFromLangcode('ca')->save();
|
||||
ConfigurableLanguage::createFromLangcode('de')->save();
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
|
||||
// Creating a vocabulary to associate taxonomy terms generated.
|
||||
$this->vocabulary = Vocabulary::create([
|
||||
'name' => 'Vocab 1 ' . $this->randomString(15),
|
||||
'description' => $this->randomMachineName(),
|
||||
'vid' => 'vocab_1_' . mb_strtolower($this->randomMachineName()),
|
||||
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
|
||||
]);
|
||||
$this->vocabulary->save();
|
||||
// Enable translation for terms in this vocabulary.
|
||||
\Drupal::service('content_translation.manager')->setEnabled('taxonomy_term', $this->vocabulary->id(), TRUE);
|
||||
|
||||
// Creates a field of an entity reference field storage on article.
|
||||
$field_name = 'taxonomy_' . $this->vocabulary->id();
|
||||
|
||||
$handler_settings = [
|
||||
'target_bundles' => [
|
||||
$this->vocabulary->id() => $this->vocabulary->id(),
|
||||
],
|
||||
'auto_create' => TRUE,
|
||||
];
|
||||
$this->createEntityReferenceField('node', 'article', $field_name, '', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
|
||||
$entity_type_manager->getStorage('entity_form_display')
|
||||
->load('node.article.default')
|
||||
->setComponent($field_name, [
|
||||
'type' => 'options_select',
|
||||
])
|
||||
->save();
|
||||
|
||||
$entity_type_manager->getStorage('entity_view_display')
|
||||
->load('node.article.default')
|
||||
->setComponent($field_name, [
|
||||
'type' => 'entity_reference_label',
|
||||
])
|
||||
->save();
|
||||
|
||||
// Create the second vocabulary.
|
||||
$this->vocabulary2 = Vocabulary::create([
|
||||
'name' => 'Vocab 2 ' . $this->randomString(15),
|
||||
'vid' => 'vocab_2_' . mb_strtolower($this->randomMachineName()),
|
||||
'langcode' => Language::LANGCODE_NOT_SPECIFIED,
|
||||
]);
|
||||
$this->vocabulary2->save();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\devel_generate\Unit;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManager;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\devel_generate\DevelGeneratePluginManager;
|
||||
use Drupal\devel_generate_example\Plugin\DevelGenerate\ExampleDevelGenerate;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\devel_generate\DevelGeneratePluginManager
|
||||
* @group devel_generate
|
||||
*/
|
||||
class DevelGenerateManagerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The plugin discovery.
|
||||
*/
|
||||
protected MockObject|DiscoveryInterface $discovery;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Mock the plugin discovery.
|
||||
$this->discovery = $this->createMock(DiscoveryInterface::class);
|
||||
$this->discovery->expects($this->any())
|
||||
->method('getDefinitions')
|
||||
->willReturnCallback(function (): array {
|
||||
return $this->getMockDefinitions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating an instance of the DevelGenerateManager.
|
||||
*/
|
||||
public function testCreateInstance(): void {
|
||||
$namespaces = new \ArrayObject(['Drupal\devel_generate_example' => realpath(__DIR__ . '/../../../modules/devel_generate_example/lib')]);
|
||||
$cache_backend = $this->createMock(CacheBackendInterface::class);
|
||||
$module_handler = $this->createMock(ModuleHandlerInterface::class);
|
||||
$entity_type_manager = $this->createMock(EntityTypeManager::class);
|
||||
$messenger = $this->createMock(MessengerInterface::class);
|
||||
$language_manager = $this->createMock(LanguageManagerInterface::class);
|
||||
$string_translation = $this->createMock(TranslationInterface::class);
|
||||
$entityFieldManager = $this->createMock(EntityFieldManagerInterface::class);
|
||||
|
||||
$manager = new DevelGeneratePluginManager(
|
||||
$namespaces,
|
||||
$cache_backend,
|
||||
$module_handler,
|
||||
$entity_type_manager,
|
||||
$messenger,
|
||||
$language_manager,
|
||||
$string_translation,
|
||||
$entityFieldManager,
|
||||
);
|
||||
|
||||
// Use reflection to set the protected discovery property.
|
||||
$reflection = new \ReflectionClass($manager);
|
||||
$property = $reflection->getProperty('discovery');
|
||||
$property->setValue($manager, $this->discovery);
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$time = $this->createMock(TimeInterface::class);
|
||||
$container->set('entity_type.manager', $entity_type_manager);
|
||||
$container->set('messenger', $messenger);
|
||||
$container->set('language_manager', $language_manager);
|
||||
$container->set('module_handler', $module_handler);
|
||||
$container->set('string_translation', $string_translation);
|
||||
$container->set('entity_field.manager', $entityFieldManager);
|
||||
$container->set('datetime.time', $time);
|
||||
\Drupal::setContainer($container);
|
||||
|
||||
$example_instance = $manager->createInstance('devel_generate_example');
|
||||
$plugin_def = $example_instance->getPluginDefinition();
|
||||
|
||||
$this->assertInstanceOf(ExampleDevelGenerate::class, $example_instance);
|
||||
$this->assertArrayHasKey('url', $plugin_def);
|
||||
$this->assertTrue($plugin_def['url'] == 'devel_generate_example');
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function to return mock definitions.
|
||||
*
|
||||
* @return array
|
||||
* The mock of devel generate plugin definitions.
|
||||
*/
|
||||
public function getMockDefinitions(): array {
|
||||
return [
|
||||
'devel_generate_example' => [
|
||||
'id' => 'devel_generate_example',
|
||||
'class' => ExampleDevelGenerate::class,
|
||||
'url' => 'devel_generate_example',
|
||||
'dependencies' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
1
web/modules/contrib/devel/icons/bebebe/cog.svg
Normal file
1
web/modules/contrib/devel/icons/bebebe/cog.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#bebebe" d="M15.176 9.041c.045-.327.076-.658.076-.998 0-.36-.035-.71-.086-1.056l-2.275-.293c-.115-.426-.283-.827-.498-1.201l1.396-1.808c-.416-.551-.906-1.039-1.459-1.452l-1.807 1.391c-.373-.215-.774-.383-1.2-.499l-.292-2.252c-.338-.048-.677-.081-1.029-.081s-.694.033-1.032.082l-.291 2.251c-.426.116-.826.284-1.2.499l-1.805-1.391c-.552.413-1.044.901-1.459 1.452l1.395 1.808c-.215.374-.383.774-.499 1.2l-2.276.294c-.05.346-.085.696-.085 1.056 0 .34.031.671.077.998l2.285.295c.115.426.284.826.499 1.2l-1.417 1.836c.411.55.896 1.038 1.443 1.452l1.842-1.42c.374.215.774.383 1.2.498l.298 2.311c.337.047.677.08 1.025.08s.688-.033 1.021-.08l.299-2.311c.426-.115.826-.283 1.201-.498l1.842 1.42c.547-.414 1.031-.902 1.443-1.452l-1.416-1.837c.215-.373.383-.773.498-1.199l2.286-.295zm-7.174 1.514c-1.406 0-2.543-1.137-2.543-2.541 0-1.402 1.137-2.541 2.543-2.541 1.402 0 2.541 1.138 2.541 2.541 0 1.404-1.139 2.541-2.541 2.541z"/></svg>
|
||||
|
After Width: | Height: | Size: 998 B |
1
web/modules/contrib/devel/icons/ffffff/cog.svg
Normal file
1
web/modules/contrib/devel/icons/ffffff/cog.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ffffff" d="M15.176 9.041c.045-.327.076-.658.076-.998 0-.36-.035-.71-.086-1.056l-2.275-.293c-.115-.426-.283-.827-.498-1.201l1.396-1.808c-.416-.551-.906-1.039-1.459-1.452l-1.807 1.391c-.373-.215-.774-.383-1.2-.499l-.292-2.252c-.338-.048-.677-.081-1.029-.081s-.694.033-1.032.082l-.291 2.251c-.426.116-.826.284-1.2.499l-1.805-1.391c-.552.413-1.044.901-1.459 1.452l1.395 1.808c-.215.374-.383.774-.499 1.2l-2.276.294c-.05.346-.085.696-.085 1.056 0 .34.031.671.077.998l2.285.295c.115.426.284.826.499 1.2l-1.417 1.836c.411.55.896 1.038 1.443 1.452l1.842-1.42c.374.215.774.383 1.2.498l.298 2.311c.337.047.677.08 1.025.08s.688-.033 1.021-.08l.299-2.311c.426-.115.826-.283 1.201-.498l1.842 1.42c.547-.414 1.031-.902 1.443-1.452l-1.416-1.837c.215-.373.383-.773.498-1.199l2.286-.295zm-7.174 1.514c-1.406 0-2.543-1.137-2.543-2.541 0-1.402 1.137-2.541 2.543-2.541 1.402 0 2.541 1.138 2.541 2.541 0 1.404-1.139 2.541-2.541 2.541z"/></svg>
|
||||
|
After Width: | Height: | Size: 998 B |
BIN
web/modules/contrib/devel/icons/folder.png
Normal file
BIN
web/modules/contrib/devel/icons/folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
web/modules/contrib/devel/logo.png
Normal file
BIN
web/modules/contrib/devel/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
73
web/modules/contrib/devel/phpcs.xml.dist
Normal file
73
web/modules/contrib/devel/phpcs.xml.dist
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="Module">
|
||||
<file>.</file>
|
||||
<arg name="extensions" value="php,module,inc,install,test,profile,theme,css,info,txt,md,yml"/>
|
||||
<config name="drupal_core_version" value="8"/>
|
||||
|
||||
<!-- Initially include all Drupal and DrupalPractice sniffs. -->
|
||||
<rule ref="vendor/drupal/coder/coder_sniffer/Drupal"/>
|
||||
<rule ref="vendor/drupal/coder/coder_sniffer/DrupalPractice"/>
|
||||
|
||||
<!-- Use 's' to print the full sniff name in the report. -->
|
||||
<!-- A '-' is prefixed to each of these, so s becomes -s, etc. -->
|
||||
<arg value="s"/>
|
||||
<arg value="-colors"/>
|
||||
<arg name='report-width' value='120'/>
|
||||
|
||||
<!-- Ignore all files that match these patterns. They are matched against -->
|
||||
<!-- the full file path and there is an implied wildcard at each end. -->
|
||||
<!-- Periods must be escaped using \. -->
|
||||
<exclude-pattern>_ignore</exclude-pattern>
|
||||
<exclude-pattern>\.patch</exclude-pattern>
|
||||
<exclude-pattern>interdif</exclude-pattern>
|
||||
<exclude-pattern>\.ddev</exclude-pattern>
|
||||
|
||||
<!-- Examples for how you disable rules you do not like. -->
|
||||
<!-- Exclude a sniff from running on specific files. -->
|
||||
<rule ref="Drupal.Files.TxtFileLineLength.TooLong">
|
||||
<!-- Exclude .md files from the line limit rule. -->
|
||||
<exclude-pattern>\.md</exclude-pattern>
|
||||
</rule>
|
||||
<rule ref="Drupal.Commenting.DocComment.ParamNotFirst">
|
||||
<!-- Drush commands are most readable with @command at top. -->
|
||||
<exclude-pattern>Commands\.php</exclude-pattern>
|
||||
</rule>
|
||||
<!-- Devel is allowed to use its own debug functions, but instead of disabling
|
||||
the rule Drupal.Functions.DiscouragedFunctions globally, it is done just
|
||||
in the places required. -->
|
||||
|
||||
<!-- Ignore specific sniffs in all files. The following are either not
|
||||
relevant for Devel or we have decided not to adhere to them anyway. -->
|
||||
<!-- Method declarations should be exempt from the long line limit. -->
|
||||
<rule ref="Drupal.Arrays.Array.LongLineDeclaration"><severity>0</severity></rule>
|
||||
<!-- We prefer typed properties, see https://stitcher.io/blog/typed-properties-in-php-74 -->
|
||||
<rule ref="Drupal.Commenting.VariableComment.Missing"><severity>0</severity></rule>
|
||||
<!-- Devel debug functions do not need to start with 'devel_' -->
|
||||
<rule ref="Drupal.NamingConventions.ValidFunctionName.InvalidPrefix"><severity>0</severity></rule>
|
||||
<!-- Fixing this makes the code look worse. Constants already start with DEVEL_ -->
|
||||
<rule ref="DrupalPractice.Constants.GlobalDefine.GlobalConstant"><severity>0</severity></rule>
|
||||
<!-- This module is allowed to use debug code -->
|
||||
<rule ref="MySource.Debug.DebugCode.Found"><severity>0</severity></rule>
|
||||
<!-- Commented out code triggers this. Long lived, commented out code is useful. https://gitlab.com/drupalspoons/devel/-/jobs/3439809881 -->
|
||||
<rule ref="Drupal.WhiteSpace.ScopeIndent.Incorrect"><severity>0</severity></rule>
|
||||
<!-- Some classes have self-explanatory names -->
|
||||
<rule ref="Drupal.Commenting.ClassComment.Missing"><severity>0</severity></rule>
|
||||
<!-- Some functions have self-explanatory names -->
|
||||
<rule ref="Drupal.Commenting.FunctionComment.Missing"><severity>0</severity></rule>
|
||||
<!-- Commented out code triggers this. Long lived, commented out code is useful. https://gitlab.com/drupalspoons/devel/-/jobs/3439809881 -->
|
||||
<rule ref="Drupal.Commenting.InlineComment.SpacingBefore"><severity>0</severity></rule>
|
||||
<!-- Commented out code triggers this. Long lived, commented out code is useful. https://gitlab.com/drupalspoons/devel/-/jobs/3439809881 -->
|
||||
<rule ref="Drupal.Commenting.InlineComment.InvalidEndChar"><severity>0</severity></rule>
|
||||
<!-- Use a real code editor -->
|
||||
<rule ref="Drupal.Files.LineLength.TooLong"><severity>0</severity></rule>
|
||||
|
||||
<!-- While work is ongoing to fix coding standards faults, and to determine
|
||||
which rules and sniffs we want to ignore, the following is a list of all
|
||||
the currently failing sniffs and they are set to be ignored. The phpcs
|
||||
job can now be allowed to fail for any new coding standard fault
|
||||
introduced. When the codebase no longer has any messages for a particular
|
||||
sniff below, it should be removed from this list.
|
||||
-->
|
||||
<rule ref="DrupalPractice.FunctionCalls.InsecureUnserialize.InsecureUnserialize"><severity>0</severity></rule>
|
||||
|
||||
</ruleset>
|
||||
4
web/modules/contrib/devel/phpstan.neon
Normal file
4
web/modules/contrib/devel/phpstan.neon
Normal file
@ -0,0 +1,4 @@
|
||||
includes:
|
||||
- phar://phpstan.phar/conf/bleedingEdge.neon
|
||||
parameters:
|
||||
level: 5
|
||||
37
web/modules/contrib/devel/src/Annotation/DevelDumper.php
Normal file
37
web/modules/contrib/devel/src/Annotation/DevelDumper.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines a DevelDumper annotation object.
|
||||
*
|
||||
* @Annotation
|
||||
*
|
||||
* @see \Drupal\devel\DevelDumperPluginManager
|
||||
* @see \Drupal\devel\DevelDumperInterface
|
||||
* @see \Drupal\devel\DevelDumperBase
|
||||
* @see plugin_api
|
||||
*/
|
||||
class DevelDumper extends Plugin {
|
||||
|
||||
/**
|
||||
* The human-readable name of the DevelDumper type.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* A short description of the DevelDumper type.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public $description;
|
||||
|
||||
}
|
||||
@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\DrupalKernelInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\devel\DevelDumperManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Provides route responses for the container info pages.
|
||||
*/
|
||||
class ContainerInfoController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The drupal kernel.
|
||||
*/
|
||||
protected DrupalKernelInterface $kernel;
|
||||
|
||||
/**
|
||||
* The dumper manager service.
|
||||
*/
|
||||
protected DevelDumperManagerInterface $dumper;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->kernel = $container->get('kernel');
|
||||
$instance->dumper = $container->get('devel.dumper');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services overview page.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function serviceList(): array {
|
||||
$headers = [
|
||||
$this->t('ID'),
|
||||
$this->t('Class'),
|
||||
$this->t('Alias'),
|
||||
$this->t('Operations'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
if ($cached_definitions = $this->kernel->getCachedContainerDefinition()) {
|
||||
foreach ($cached_definitions['services'] as $service_id => $definition) {
|
||||
$service = unserialize($definition);
|
||||
|
||||
$row['id'] = [
|
||||
'data' => $service_id,
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['class'] = [
|
||||
'data' => $service['class'] ?? '',
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['alias'] = [
|
||||
'data' => array_search($service_id, $cached_definitions['aliases'], TRUE) ?: '',
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['operations']['data'] = [
|
||||
'#type' => 'operations',
|
||||
'#links' => [
|
||||
'devel' => [
|
||||
'title' => $this->t('Devel'),
|
||||
'url' => Url::fromRoute('devel.container_info.service.detail', ['service_id' => $service_id]),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'minHeight' => 500,
|
||||
]),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$rows[$service_id] = $row;
|
||||
}
|
||||
|
||||
ksort($rows);
|
||||
}
|
||||
|
||||
$output['services'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter service id, alias or class'),
|
||||
'#filter_description' => $this->t('Enter a part of the service id, service alias or class to filter by.'),
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No services found.'),
|
||||
'#sticky' => TRUE,
|
||||
'#attributes' => [
|
||||
'class' => ['devel-service-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a render array representation of the service.
|
||||
*
|
||||
* @param string $service_id
|
||||
* The ID of the service to retrieve.
|
||||
*
|
||||
* @return array
|
||||
* A render array containing the service detail.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* If the requested service is not defined.
|
||||
*/
|
||||
public function serviceDetail(string $service_id): array {
|
||||
$container = $this->kernel->getContainer();
|
||||
/** @var object|null $instance */
|
||||
$instance = $container->get($service_id, ContainerInterface::NULL_ON_INVALID_REFERENCE);
|
||||
if ($instance === NULL) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$output = [];
|
||||
|
||||
// Tries to retrieve the service definition from the kernel's cached
|
||||
// container definition.
|
||||
$cached_definitions = $this->kernel->getCachedContainerDefinition();
|
||||
if ($cached_definitions && isset($cached_definitions['services'][$service_id])) {
|
||||
$definition = unserialize($cached_definitions['services'][$service_id]);
|
||||
|
||||
// If the service has an alias add it to the definition.
|
||||
if ($alias = array_search($service_id, $cached_definitions['aliases'], TRUE)) {
|
||||
$definition['alias'] = $alias;
|
||||
}
|
||||
|
||||
$output['definition'] = $this->dumper->exportAsRenderable($definition, $this->t('Computed Definition'));
|
||||
}
|
||||
|
||||
$output['instance'] = $this->dumper->exportAsRenderable($instance, $this->t('Instance'));
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the parameters overview page.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function parameterList(): array {
|
||||
$headers = [
|
||||
$this->t('Name'),
|
||||
$this->t('Operations'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
if ($cached_definitions = $this->kernel->getCachedContainerDefinition()) {
|
||||
foreach ($cached_definitions['parameters'] as $parameter_name => $definition) {
|
||||
$row['name'] = [
|
||||
'data' => $parameter_name,
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['operations']['data'] = [
|
||||
'#type' => 'operations',
|
||||
'#links' => [
|
||||
'devel' => [
|
||||
'title' => $this->t('Devel'),
|
||||
'url' => Url::fromRoute('devel.container_info.parameter.detail', ['parameter_name' => $parameter_name]),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'minHeight' => 500,
|
||||
]),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$rows[$parameter_name] = $row;
|
||||
}
|
||||
|
||||
ksort($rows);
|
||||
}
|
||||
|
||||
$output['parameters'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter parameter name'),
|
||||
'#filter_description' => $this->t('Enter a part of the parameter name to filter by.'),
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No parameters found.'),
|
||||
'#sticky' => TRUE,
|
||||
'#attributes' => [
|
||||
'class' => ['devel-parameter-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a render array representation of the parameter value.
|
||||
*
|
||||
* @param string $parameter_name
|
||||
* The name of the parameter to retrieve.
|
||||
*
|
||||
* @return array
|
||||
* A render array containing the parameter value.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* If the requested parameter is not defined.
|
||||
*/
|
||||
public function parameterDetail(string $parameter_name): array {
|
||||
$container = $this->kernel->getContainer();
|
||||
try {
|
||||
$parameter = $container->getParameter($parameter_name);
|
||||
}
|
||||
catch (ParameterNotFoundException) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $this->dumper->exportAsRenderable($parameter);
|
||||
}
|
||||
|
||||
}
|
||||
208
web/modules/contrib/devel/src/Controller/DevelController.php
Normal file
208
web/modules/contrib/devel/src/Controller/DevelController.php
Normal file
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\Field\FormatterPluginManager;
|
||||
use Drupal\Core\Field\WidgetPluginManager;
|
||||
use Drupal\Core\Theme\Registry;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\devel\DevelDumperManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Returns responses for devel module routes.
|
||||
*/
|
||||
class DevelController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The dumper service.
|
||||
*/
|
||||
protected DevelDumperManagerInterface $dumper;
|
||||
|
||||
/**
|
||||
* The entity type bundle info service.
|
||||
*/
|
||||
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The field type plugin manager service.
|
||||
*/
|
||||
protected FieldTypePluginManagerInterface $fieldTypeManager;
|
||||
|
||||
/**
|
||||
* The field formatter plugin manager.
|
||||
*/
|
||||
protected FormatterPluginManager $formatterPluginManager;
|
||||
|
||||
/**
|
||||
* The field widget plugin manager.
|
||||
*/
|
||||
protected WidgetPluginManager $widgetPluginManager;
|
||||
|
||||
/**
|
||||
* The theme registry.
|
||||
*/
|
||||
protected Registry $themeRegistry;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->dumper = $container->get('devel.dumper');
|
||||
$instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
|
||||
$instance->fieldTypeManager = $container->get('plugin.manager.field.field_type');
|
||||
$instance->formatterPluginManager = $container->get('plugin.manager.field.formatter');
|
||||
$instance->widgetPluginManager = $container->get('plugin.manager.field.widget');
|
||||
$instance->currentUser = $container->get('current_user');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
$instance->themeRegistry = $container->get('theme.registry');
|
||||
$instance->entityTypeManager = $container->get('entity_type.manager');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all caches, then redirects to the previous page.
|
||||
*/
|
||||
public function cacheClear() {
|
||||
drupal_flush_all_caches();
|
||||
|
||||
// @todo Use DI for messenger once https://www.drupal.org/project/drupal/issues/2940148 is resolved.
|
||||
$this->messenger()->addMessage($this->t('Cache cleared.'));
|
||||
|
||||
return $this->redirect('<front>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme registry.
|
||||
*
|
||||
* @return array
|
||||
* The complete theme registry as renderable.
|
||||
*/
|
||||
public function themeRegistry(): array {
|
||||
$hooks = $this->themeRegistry->get();
|
||||
ksort($hooks);
|
||||
return $this->dumper->exportAsRenderable($hooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the fields info overview page.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function fieldInfoPage() {
|
||||
$fields = $this->entityTypeManager->getStorage('field_storage_config')
|
||||
->loadMultiple();
|
||||
ksort($fields);
|
||||
$output['fields'] = $this->dumper->exportAsRenderable($fields, $this->t('Fields'));
|
||||
|
||||
$field_instances = $this->entityTypeManager->getStorage('field_config')
|
||||
->loadMultiple();
|
||||
ksort($field_instances);
|
||||
$output['instances'] = $this->dumper->exportAsRenderable($field_instances, $this->t('Instances'));
|
||||
|
||||
$bundles = $this->entityTypeBundleInfo->getAllBundleInfo();
|
||||
ksort($bundles);
|
||||
$output['bundles'] = $this->dumper->exportAsRenderable($bundles, $this->t('Bundles'));
|
||||
|
||||
$field_types = $this->fieldTypeManager->getUiDefinitions();
|
||||
ksort($field_types);
|
||||
$output['field_types'] = $this->dumper->exportAsRenderable($field_types, $this->t('Field types'));
|
||||
|
||||
$formatter_types = $this->formatterPluginManager->getDefinitions();
|
||||
ksort($formatter_types);
|
||||
$output['formatter_types'] = $this->dumper->exportAsRenderable($formatter_types, $this->t('Formatter types'));
|
||||
|
||||
$widget_types = $this->widgetPluginManager->getDefinitions();
|
||||
ksort($widget_types);
|
||||
$output['widget_types'] = $this->dumper->exportAsRenderable($widget_types, $this->t('Widget types'));
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the state variable overview page.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function stateSystemPage(): array {
|
||||
$can_edit = $this->currentUser->hasPermission('administer site configuration');
|
||||
|
||||
$header = [
|
||||
'name' => $this->t('Name'),
|
||||
'value' => $this->t('Value'),
|
||||
];
|
||||
|
||||
if ($can_edit) {
|
||||
$header['edit'] = $this->t('Operations');
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
// State class doesn't have getAll method so we get all states from the
|
||||
// KeyValueStorage.
|
||||
foreach ($this->keyValue('state')->getAll() as $state_name => $state) {
|
||||
$rows[$state_name] = [
|
||||
'name' => [
|
||||
'data' => $state_name,
|
||||
'class' => 'table-filter-text-source',
|
||||
],
|
||||
'value' => [
|
||||
'data' => $this->dumper->export($state),
|
||||
],
|
||||
];
|
||||
|
||||
if ($can_edit) {
|
||||
$operations['edit'] = [
|
||||
'title' => $this->t('Edit'),
|
||||
'url' => Url::fromRoute('devel.system_state_edit', ['state_name' => $state_name]),
|
||||
];
|
||||
$rows[$state_name]['edit'] = [
|
||||
'data' => ['#type' => 'operations', '#links' => $operations],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$output['states'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter state name'),
|
||||
'#filter_title' => $this->t('Enter a part of the state name to filter by.'),
|
||||
'#header' => $header,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No state variables found.'),
|
||||
'#attributes' => [
|
||||
'class' => ['devel-state-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the session overview page.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function session() {
|
||||
$output['description'] = [
|
||||
'#markup' => '<p>' . $this->t('Here are the contents of your $_SESSION variable.') . '</p>',
|
||||
];
|
||||
$output['session'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => [$this->t('Session name'), $this->t('Session ID')],
|
||||
'#rows' => [[session_name(), session_id()]],
|
||||
'#empty' => $this->t('No session available.'),
|
||||
];
|
||||
$output['data'] = $this->dumper->exportAsRenderable($_SESSION);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Render\ElementInfoManagerInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\devel\DevelDumperManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Provides route responses for the element info page.
|
||||
*/
|
||||
class ElementInfoController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Element info manager service.
|
||||
*/
|
||||
protected ElementInfoManagerInterface $elementInfo;
|
||||
|
||||
/**
|
||||
* The dumper service.
|
||||
*/
|
||||
protected DevelDumperManagerInterface $dumper;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->elementInfo = $container->get('element_info');
|
||||
$instance->dumper = $container->get('devel.dumper');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the element overview page.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function elementList(): array {
|
||||
$headers = [
|
||||
$this->t('Name'),
|
||||
$this->t('Provider'),
|
||||
$this->t('Class'),
|
||||
$this->t('Operations'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($this->elementInfo->getDefinitions() as $element_type => $definition) {
|
||||
$row['name'] = [
|
||||
'data' => $element_type,
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['provider'] = [
|
||||
'data' => $definition['provider'],
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['class'] = [
|
||||
'data' => $definition['class'],
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['operations']['data'] = [
|
||||
'#type' => 'operations',
|
||||
'#links' => [
|
||||
'devel' => [
|
||||
'title' => $this->t('Devel'),
|
||||
'url' => Url::fromRoute('devel.elements_page.detail', ['element_name' => $element_type]),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'minHeight' => 500,
|
||||
]),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$rows[$element_type] = $row;
|
||||
}
|
||||
|
||||
ksort($rows);
|
||||
|
||||
$output['elements'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter element id, provider or class'),
|
||||
'#filter_description' => $this->t('Enter a part of the element id, provider or class to filter by.'),
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No elements found.'),
|
||||
'#sticky' => TRUE,
|
||||
'#attributes' => [
|
||||
'class' => ['devel-element-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a render array representation of the element.
|
||||
*
|
||||
* @param string $element_name
|
||||
* The name of the element to retrieve.
|
||||
*
|
||||
* @return array
|
||||
* A render array containing the element.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* If the requested element is not defined.
|
||||
*/
|
||||
public function elementDetail($element_name): array {
|
||||
if (!$element = $this->elementInfo->getDefinition($element_name, FALSE)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$element += $this->elementInfo->getInfo($element_name);
|
||||
return $this->dumper->exportAsRenderable($element, $element_name);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\TranslationManager;
|
||||
use Drupal\devel\DevelDumperManagerInterface;
|
||||
use Drupal\path_alias\PathAliasStorage;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Controller for devel entity debug.
|
||||
*
|
||||
* @see \Drupal\devel\Routing\RouteSubscriber
|
||||
* @see \Drupal\devel\Plugin\Derivative\DevelLocalTask
|
||||
*/
|
||||
class EntityDebugController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The dumper service.
|
||||
*/
|
||||
protected DevelDumperManagerInterface $dumper;
|
||||
|
||||
/**
|
||||
* The translation manager.
|
||||
*/
|
||||
protected TranslationManager $translationManager;
|
||||
|
||||
/**
|
||||
* The alias storage.
|
||||
*/
|
||||
protected PathAliasStorage $aliasStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$entityTypeManager = $container->get('entity_type.manager');
|
||||
$instance = parent::create($container);
|
||||
$instance->dumper = $container->get('devel.dumper');
|
||||
$instance->entityTypeManager = $entityTypeManager;
|
||||
$instance->translationManager = $container->get('string_translation');
|
||||
$instance->aliasStorage = $entityTypeManager->getStorage('path_alias');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entity type definition of the current entity.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* A RouteMatch object.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function entityTypeDefinition(RouteMatchInterface $route_match): array {
|
||||
$entity = $this->getEntityFromRouteMatch($route_match);
|
||||
if (!$entity instanceof EntityInterface) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->dumper->exportAsRenderable($entity->getEntityType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the loaded structure of the current entity.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* A RouteMatch object.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function entityLoad(RouteMatchInterface $route_match): array {
|
||||
$output = [];
|
||||
|
||||
$entity = $this->getEntityWithFieldDefinitions($route_match);
|
||||
|
||||
if ($entity instanceof EntityInterface) {
|
||||
// Field definitions are lazy loaded and are populated only when needed.
|
||||
// By calling ::getFieldDefinitions() we are sure that field definitions
|
||||
// are populated and available in the dump output.
|
||||
// @see https://www.drupal.org/node/2311557
|
||||
if ($entity instanceof FieldableEntityInterface) {
|
||||
$entity->getFieldDefinitions();
|
||||
}
|
||||
|
||||
$output = $this->dumper->exportAsRenderable($entity);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the loaded structure of the current entity with references.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* A RouteMatch object.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function entityLoadWithReferences(RouteMatchInterface $route_match): array {
|
||||
$entity = $this->getEntityWithFieldDefinitions($route_match);
|
||||
if (!$entity instanceof EntityInterface) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->dumper->exportAsRenderable($entity, NULL, NULL, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the render structure of the current entity.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* A RouteMatch object.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function entityRender(RouteMatchInterface $route_match): array {
|
||||
$output = [];
|
||||
|
||||
$entity = $this->getEntityFromRouteMatch($route_match);
|
||||
|
||||
if ($entity instanceof EntityInterface) {
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$view_hook = $entity_type_id . '_view';
|
||||
|
||||
$build = [];
|
||||
// If module implements own {entity_type}_view() hook use it, otherwise
|
||||
// fallback to the entity view builder if available.
|
||||
if (function_exists($view_hook)) {
|
||||
$build = $view_hook($entity);
|
||||
}
|
||||
elseif ($this->entityTypeManager->hasHandler($entity_type_id, 'view_builder')) {
|
||||
$build = $this->entityTypeManager->getViewBuilder($entity_type_id)->view($entity);
|
||||
}
|
||||
|
||||
$output = $this->dumper->exportAsRenderable($build);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return definitions for any related path aliases.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* A RouteMatch object.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function pathAliases(RouteMatchInterface $route_match): array {
|
||||
$entity = $this->getEntityFromRouteMatch($route_match);
|
||||
if ($entity === NULL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = sprintf('/%s/%s', $entity->getEntityTypeId(), $entity->id());
|
||||
$aliases = $this->aliasStorage->loadByProperties(['path' => $path]);
|
||||
$aliasCount = count($aliases);
|
||||
if ($aliasCount > 0) {
|
||||
$message = $this->translationManager->formatPlural(
|
||||
$aliasCount,
|
||||
'Found 1 alias with path "@path."',
|
||||
'Found @count aliases with path "@path".',
|
||||
['@path' => $path]
|
||||
);
|
||||
}
|
||||
else {
|
||||
$message = $this->t('Found no aliases with path "@path".', ['@path' => $path]);
|
||||
}
|
||||
|
||||
$build['header'] = [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'p',
|
||||
'#value' => $message,
|
||||
];
|
||||
|
||||
// Add alias dump to the response.
|
||||
$build['aliases'] = [];
|
||||
foreach ($aliases as $alias) {
|
||||
$build['aliases'][] = $this->dumper->exportAsRenderable($alias);
|
||||
}
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves entity from route match.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The route match.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface|null
|
||||
* The entity object as determined from the passed-in route match.
|
||||
*/
|
||||
protected function getEntityFromRouteMatch(RouteMatchInterface $route_match) {
|
||||
$parameter_name = $route_match->getRouteObject()->getOption('_devel_entity_type_id');
|
||||
|
||||
return $route_match->getParameter($parameter_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an entity with field definitions from the given route match.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The route match.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface|null
|
||||
* The entity object with field definitions as determined from the
|
||||
* passed-in route match.
|
||||
*/
|
||||
protected function getEntityWithFieldDefinitions(RouteMatchInterface $route_match): ?EntityInterface {
|
||||
$entity = $this->getEntityFromRouteMatch($route_match);
|
||||
if (!$entity instanceof EntityInterface) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Field definitions are lazy loaded and are populated only when needed.
|
||||
// By calling ::getFieldDefinitions() we are sure that field definitions
|
||||
// are populated and available in the dump output.
|
||||
// @see https://www.drupal.org/node/2311557
|
||||
if ($entity instanceof FieldableEntityInterface) {
|
||||
$entity->getFieldDefinitions();
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\devel\DevelDumperManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Provides route responses for the entity types info page.
|
||||
*/
|
||||
class EntityTypeInfoController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The dumper service.
|
||||
*/
|
||||
protected DevelDumperManagerInterface $dumper;
|
||||
|
||||
/**
|
||||
* The installed entity definition repository service.
|
||||
*/
|
||||
protected EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->dumper = $container->get('devel.dumper');
|
||||
$instance->entityLastInstalledSchemaRepository = $container->get('entity.last_installed_schema.repository');
|
||||
$instance->entityTypeManager = $container->get('entity_type.manager');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the entity types overview page.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function entityTypeList(): array {
|
||||
$headers = [
|
||||
$this->t('ID'),
|
||||
$this->t('Name'),
|
||||
$this->t('Provider'),
|
||||
$this->t('Class'),
|
||||
$this->t('Operations'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
$row['id'] = [
|
||||
'data' => $entity_type->id(),
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['name'] = [
|
||||
'data' => $entity_type->getLabel(),
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['provider'] = [
|
||||
'data' => $entity_type->getProvider(),
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['class'] = [
|
||||
'data' => $entity_type->getClass(),
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['operations']['data'] = [
|
||||
'#type' => 'operations',
|
||||
'#links' => [
|
||||
'devel' => [
|
||||
'title' => $this->t('Devel'),
|
||||
'url' => Url::fromRoute('devel.entity_info_page.detail', ['entity_type_id' => $entity_type_id]),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'minHeight' => 500,
|
||||
]),
|
||||
],
|
||||
],
|
||||
'fields' => [
|
||||
'title' => $this->t('Fields'),
|
||||
'url' => Url::fromRoute('devel.entity_info_page.fields', ['entity_type_id' => $entity_type_id]),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'minHeight' => 500,
|
||||
]),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$rows[$entity_type_id] = $row;
|
||||
}
|
||||
|
||||
ksort($rows);
|
||||
|
||||
$output['entities'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter entity type id, provider or class'),
|
||||
'#filter_description' => $this->t('Enter a part of the entity type id, provider or class to filter by.'),
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No entity types found.'),
|
||||
'#sticky' => TRUE,
|
||||
'#attributes' => [
|
||||
'class' => ['devel-entity-type-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a render array representation of the entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The name of the entity type to retrieve.
|
||||
*
|
||||
* @return array
|
||||
* A render array containing the entity type.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* If the requested entity type is not defined.
|
||||
*/
|
||||
public function entityTypeDetail($entity_type_id): array {
|
||||
if (!$entity_type = $this->entityTypeManager->getDefinition($entity_type_id, FALSE)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $this->dumper->exportAsRenderable($entity_type, $entity_type_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a render array representation of the entity type field definitions.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The name of the entity type to retrieve.
|
||||
*
|
||||
* @return array
|
||||
* A render array containing the entity type field definitions.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* If the requested entity type is not defined.
|
||||
*/
|
||||
public function entityTypeFields($entity_type_id): array {
|
||||
if (!$this->entityTypeManager->getDefinition($entity_type_id, FALSE)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$field_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id);
|
||||
return $this->dumper->exportAsRenderable($field_storage_definitions, $entity_type_id);
|
||||
}
|
||||
|
||||
}
|
||||
112
web/modules/contrib/devel/src/Controller/EventInfoController.php
Normal file
112
web/modules/contrib/devel/src/Controller/EventInfoController.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Provides route responses for the event info page.
|
||||
*/
|
||||
class EventInfoController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Event dispatcher service.
|
||||
*/
|
||||
protected EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->eventDispatcher = $container->get('event_dispatcher');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the events overview page.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function eventList(): array {
|
||||
$headers = [
|
||||
'name' => [
|
||||
'data' => $this->t('Event Name'),
|
||||
'class' => 'visually-hidden',
|
||||
],
|
||||
'callable' => $this->t('Callable'),
|
||||
'priority' => $this->t('Priority'),
|
||||
];
|
||||
|
||||
$event_listeners = $this->eventDispatcher->getListeners();
|
||||
ksort($event_listeners);
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($event_listeners as $event_name => $listeners) {
|
||||
|
||||
$rows[][] = [
|
||||
'data' => $event_name,
|
||||
'class' => ['devel-event-name-header'],
|
||||
'filter' => TRUE,
|
||||
'colspan' => '3',
|
||||
'header' => TRUE,
|
||||
];
|
||||
|
||||
foreach ($listeners as $listener) {
|
||||
$row['name'] = [
|
||||
'data' => $event_name,
|
||||
'class' => ['visually-hidden'],
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['class'] = [
|
||||
'data' => $this->resolveCallableName($listener),
|
||||
];
|
||||
$row['priority'] = [
|
||||
'data' => $this->eventDispatcher->getListenerPriority($event_name, $listener),
|
||||
];
|
||||
$rows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$output['events'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter event name'),
|
||||
'#filter_description' => $this->t('Enter a part of the event name to filter by.'),
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No events found.'),
|
||||
'#attributes' => [
|
||||
'class' => ['devel-event-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for resolve callable name.
|
||||
*
|
||||
* @param mixed $callable
|
||||
* The for which resolve the name. Can be either the name of a function
|
||||
* stored in a string variable, or an object and the name of a method
|
||||
* within the object.
|
||||
*
|
||||
* @return string
|
||||
* The resolved callable name or an empty string.
|
||||
*/
|
||||
protected function resolveCallableName(mixed $callable) {
|
||||
if (is_callable($callable, TRUE, $callable_name)) {
|
||||
return $callable_name;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Layout\LayoutPluginManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Returns response for Layout Info route.
|
||||
*/
|
||||
class LayoutInfoController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The Layout Plugin Manager.
|
||||
*/
|
||||
protected LayoutPluginManagerInterface $layoutPluginManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->layoutPluginManager = $container->get('plugin.manager.core.layout');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Layout Info page.
|
||||
*
|
||||
* @return array
|
||||
* Array of page elements to render.
|
||||
*/
|
||||
public function layoutInfoPage(): array {
|
||||
$headers = [
|
||||
$this->t('Icon'),
|
||||
$this->t('Label'),
|
||||
$this->t('Description'),
|
||||
$this->t('Category'),
|
||||
$this->t('Regions'),
|
||||
$this->t('Provider'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($this->layoutPluginManager->getDefinitions() as $layout) {
|
||||
$rows[] = [
|
||||
'icon' => ['data' => $layout->getIcon()],
|
||||
'label' => $layout->getLabel(),
|
||||
'description' => $layout->getDescription(),
|
||||
'category' => $layout->getCategory(),
|
||||
'regions' => implode(', ', $layout->getRegionLabels()),
|
||||
'provider' => $layout->getProvider(),
|
||||
];
|
||||
}
|
||||
|
||||
$output['layouts'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No layouts available.'),
|
||||
'#attributes' => [
|
||||
'class' => ['devel-layout-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
||||
176
web/modules/contrib/devel/src/Controller/RouteInfoController.php
Normal file
176
web/modules/contrib/devel/src/Controller/RouteInfoController.php
Normal file
@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\devel\DevelDumperManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* Provides route responses for the route info pages.
|
||||
*/
|
||||
class RouteInfoController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The route provider.
|
||||
*/
|
||||
protected RouteProviderInterface $routeProvider;
|
||||
|
||||
/**
|
||||
* The router service.
|
||||
*/
|
||||
protected RouterInterface $router;
|
||||
|
||||
/**
|
||||
* The dumper service.
|
||||
*/
|
||||
protected DevelDumperManagerInterface $dumper;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->routeProvider = $container->get('router.route_provider');
|
||||
$instance->router = $container->get('router.no_access_checks');
|
||||
$instance->dumper = $container->get('devel.dumper');
|
||||
$instance->messenger = $container->get('messenger');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the routes overview page.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function routeList(): array {
|
||||
$headers = [
|
||||
$this->t('Route Name'),
|
||||
$this->t('Path'),
|
||||
$this->t('Allowed Methods'),
|
||||
$this->t('Operations'),
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ($this->routeProvider->getAllRoutes() as $route_name => $route) {
|
||||
$row['name'] = [
|
||||
'data' => $route_name,
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['path'] = [
|
||||
'data' => $route->getPath(),
|
||||
'filter' => TRUE,
|
||||
];
|
||||
$row['methods']['data'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $route->getMethods(),
|
||||
'#empty' => $this->t('ANY'),
|
||||
'#context' => ['list_style' => 'comma-list'],
|
||||
];
|
||||
|
||||
// We cannot resolve routes with dynamic parameters from route path. For
|
||||
// these routes we pass the route name.
|
||||
// @see ::routeItem()
|
||||
if (str_contains($route->getPath(), '{')) {
|
||||
$parameters = ['query' => ['route_name' => $route_name]];
|
||||
}
|
||||
else {
|
||||
$parameters = ['query' => ['path' => $route->getPath()]];
|
||||
}
|
||||
|
||||
$row['operations']['data'] = [
|
||||
'#type' => 'operations',
|
||||
'#links' => [
|
||||
'devel' => [
|
||||
'title' => $this->t('Devel'),
|
||||
'url' => Url::fromRoute('devel.route_info.item', [], $parameters),
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 700,
|
||||
'minHeight' => 500,
|
||||
]),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
$output['routes'] = [
|
||||
'#type' => 'devel_table_filter',
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Enter route name or path'),
|
||||
'#filter_description' => $this->t('Enter a part of the route name or path to filter by.'),
|
||||
'#header' => $headers,
|
||||
'#rows' => $rows,
|
||||
'#empty' => $this->t('No routes found.'),
|
||||
'#sticky' => TRUE,
|
||||
'#attributes' => [
|
||||
'class' => ['devel-route-list'],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a render array representation of the route object.
|
||||
*
|
||||
* The method tries to resolve the route from the 'path' or the 'route_name'
|
||||
* query string value if available. If no route is retrieved from the query
|
||||
* string parameters it fallbacks to the current route.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The route match.
|
||||
*
|
||||
* @return array
|
||||
* A render array as expected by the renderer.
|
||||
*/
|
||||
public function routeDetail(Request $request, RouteMatchInterface $route_match): array {
|
||||
$route = NULL;
|
||||
|
||||
// Get the route object from the path query string if available.
|
||||
if ($path = $request->query->get('path')) {
|
||||
try {
|
||||
$route = $this->router->match($path);
|
||||
}
|
||||
catch (\Exception) {
|
||||
$this->messenger->addWarning($this->t("Unable to load route for url '%url'", ['%url' => $path]));
|
||||
}
|
||||
}
|
||||
|
||||
// Get the route object from the route name query string if available and
|
||||
// the route is not retrieved by path.
|
||||
if ($route === NULL && $route_name = $request->query->get('route_name')) {
|
||||
try {
|
||||
$route = $this->routeProvider->getRouteByName($route_name);
|
||||
}
|
||||
catch (\Exception) {
|
||||
$this->messenger->addWarning($this->t("Unable to load route '%name'", ['%name' => $route_name]));
|
||||
}
|
||||
}
|
||||
|
||||
// No route retrieved from path or name specified, get the current route.
|
||||
if ($route === NULL) {
|
||||
$route = $route_match->getRouteObject();
|
||||
}
|
||||
|
||||
return $this->dumper->exportAsRenderable($route);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\Core\Session\SessionManagerInterface;
|
||||
use Drupal\user\UserStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* Controller for switch to another user account.
|
||||
*/
|
||||
class SwitchUserController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*/
|
||||
protected AccountProxyInterface $account;
|
||||
|
||||
/**
|
||||
* The user storage.
|
||||
*/
|
||||
protected UserStorageInterface $userStorage;
|
||||
|
||||
/**
|
||||
* The session manager service.
|
||||
*/
|
||||
protected SessionManagerInterface $sessionManager;
|
||||
|
||||
/**
|
||||
* The session.
|
||||
*/
|
||||
protected Session $session;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): static {
|
||||
$instance = parent::create($container);
|
||||
$instance->account = $container->get('current_user');
|
||||
$instance->userStorage = $container->get('entity_type.manager')->getStorage('user');
|
||||
$instance->moduleHandler = $container->get('module_handler');
|
||||
$instance->sessionManager = $container->get('session_manager');
|
||||
$instance->session = $container->get('session');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a different user.
|
||||
*
|
||||
* We don't call session_save_session() because we really want to change
|
||||
* users. Usually unsafe!
|
||||
*
|
||||
* @param string|null $name
|
||||
* The username to switch to, or NULL to log out.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* A redirect response object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||
*/
|
||||
public function switchUser(?string $name = NULL) {
|
||||
if (empty($name) || !($account = $this->userStorage->loadByProperties(['name' => $name]))) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
||||
$account = reset($account);
|
||||
|
||||
// Call logout hooks when switching from original user.
|
||||
$this->moduleHandler->invokeAll('user_logout', [$this->account]);
|
||||
|
||||
// Regenerate the session ID to prevent against session fixation attacks.
|
||||
$this->sessionManager->regenerate();
|
||||
|
||||
// Based off masquarade module as:
|
||||
// https://www.drupal.org/node/218104 doesn't stick and instead only
|
||||
// keeps context until redirect.
|
||||
$this->account->setAccount($account);
|
||||
$this->session->set('uid', $account->id());
|
||||
|
||||
// Call all login hooks when switching to masquerading user.
|
||||
$this->moduleHandler->invokeAll('user_login', [$account]);
|
||||
|
||||
return $this->redirect('<front>');
|
||||
}
|
||||
|
||||
}
|
||||
88
web/modules/contrib/devel/src/DevelDumperBase.php
Normal file
88
web/modules/contrib/devel/src/DevelDumperBase.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Component\Render\MarkupInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\devel\Render\FilteredMarkup;
|
||||
use Drupal\devel\Twig\Extension\Debug;
|
||||
|
||||
/**
|
||||
* Defines a base devel dumper implementation.
|
||||
*
|
||||
* @see \Drupal\devel\Annotation\DevelDumper
|
||||
* @see \Drupal\devel\DevelDumperInterface
|
||||
* @see \Drupal\devel\DevelDumperPluginManager
|
||||
* @see plugin_api
|
||||
*/
|
||||
abstract class DevelDumperBase extends PluginBase implements DevelDumperInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dump($input, ?string $name = NULL): void {
|
||||
echo (string) $this->export($input, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exportAsRenderable($input, ?string $name = NULL): array {
|
||||
return ['#markup' => $this->export($input, $name)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for \Drupal\Core\Render\Markup::create().
|
||||
*
|
||||
* @param mixed $input
|
||||
* The input to mark as a safe string.
|
||||
*
|
||||
* @return \Drupal\Component\Render\MarkupInterface|string
|
||||
* The unaltered input value.
|
||||
*/
|
||||
protected function setSafeMarkup(mixed $input): MarkupInterface|string {
|
||||
return FilteredMarkup::create($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of internal functions.
|
||||
*
|
||||
* The list returned from this method can be used to exclude internal
|
||||
* functions from the backtrace output.
|
||||
*
|
||||
* @return array
|
||||
* An array of internal functions.
|
||||
*/
|
||||
protected function getInternalFunctions(): array {
|
||||
$class_name = static::class;
|
||||
$manager_class_name = DevelDumperManager::class;
|
||||
|
||||
return [
|
||||
[$class_name, 'dump'],
|
||||
[$class_name, 'export'],
|
||||
[$manager_class_name, 'dump'],
|
||||
[$manager_class_name, 'export'],
|
||||
[$manager_class_name, 'exportAsRenderable'],
|
||||
[$manager_class_name, 'message'],
|
||||
[Debug::class, 'dump'],
|
||||
'devel_export',
|
||||
'devel_message',
|
||||
'devel_debug',
|
||||
'dpm',
|
||||
'dvm',
|
||||
'dsm',
|
||||
'dpr',
|
||||
'dvr',
|
||||
'kpr',
|
||||
'dargs',
|
||||
'dcp',
|
||||
'dfb',
|
||||
'dfbt',
|
||||
'dpq',
|
||||
'ddebug_backtrace',
|
||||
'kdevel_print_object',
|
||||
'backtrace_error_handler',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
61
web/modules/contrib/devel/src/DevelDumperInterface.php
Normal file
61
web/modules/contrib/devel/src/DevelDumperInterface.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Component\Render\MarkupInterface;
|
||||
|
||||
/**
|
||||
* Base interface definition for DevelDumper plugins.
|
||||
*
|
||||
* @see \Drupal\devel\Annotation\DevelDumper
|
||||
* @see \Drupal\devel\DevelDumperPluginManager
|
||||
* @see \Drupal\devel\DevelDumperBase
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface DevelDumperInterface {
|
||||
|
||||
/**
|
||||
* Dumps information about a variable.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
*/
|
||||
public function dump(mixed $input, ?string $name = NULL);
|
||||
|
||||
/**
|
||||
* Returns a string representation of a variable.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to export.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
*
|
||||
* @return \Drupal\Component\Render\MarkupInterface|string
|
||||
* String representation of a variable.
|
||||
*/
|
||||
public function export(mixed $input, ?string $name = NULL): MarkupInterface|string;
|
||||
|
||||
/**
|
||||
* Returns a string representation of a variable wrapped in a render array.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to export.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
*
|
||||
* @return array
|
||||
* String representation of a variable wrapped in a render array.
|
||||
*/
|
||||
public function exportAsRenderable(mixed $input, ?string $name = NULL): array;
|
||||
|
||||
/**
|
||||
* Checks if requirements for this plugin are satisfied.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE is requirements are satisfied, FALSE otherwise.
|
||||
*/
|
||||
public static function checkRequirements(): bool;
|
||||
|
||||
}
|
||||
277
web/modules/contrib/devel/src/DevelDumperManager.php
Normal file
277
web/modules/contrib/devel/src/DevelDumperManager.php
Normal file
@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
|
||||
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
|
||||
use Drupal\Component\Render\MarkupInterface;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Config\ImmutableConfig;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Entity\TranslatableInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
|
||||
/**
|
||||
* Manager class for DevelDumper.
|
||||
*/
|
||||
class DevelDumperManager implements DevelDumperManagerInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The devel config.
|
||||
*/
|
||||
protected ImmutableConfig $config;
|
||||
|
||||
/**
|
||||
* The current account.
|
||||
*/
|
||||
protected AccountProxyInterface $account;
|
||||
|
||||
/**
|
||||
* The devel dumper plugin manager.
|
||||
*/
|
||||
protected DevelDumperPluginManagerInterface $dumperManager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*/
|
||||
protected EntityTypeManagerInterface $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The messenger.
|
||||
*/
|
||||
protected MessengerInterface $messenger;
|
||||
|
||||
/**
|
||||
* Constructs a DevelDumperPluginManager object.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\Core\Session\AccountProxyInterface $account
|
||||
* The current account.
|
||||
* @param \Drupal\devel\DevelDumperPluginManagerInterface $dumper_manager
|
||||
* The devel dumper plugin manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation manager.
|
||||
*/
|
||||
public function __construct(
|
||||
ConfigFactoryInterface $config_factory,
|
||||
AccountProxyInterface $account,
|
||||
DevelDumperPluginManagerInterface $dumper_manager,
|
||||
EntityTypeManagerInterface $entityTypeManager,
|
||||
MessengerInterface $messenger,
|
||||
TranslationInterface $string_translation,
|
||||
) {
|
||||
$this->config = $config_factory->get('devel.settings');
|
||||
$this->account = $account;
|
||||
$this->dumperManager = $dumper_manager;
|
||||
$this->entityTypeManager = $entityTypeManager;
|
||||
$this->messenger = $messenger;
|
||||
$this->stringTranslation = $string_translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instances a new dumper plugin.
|
||||
*
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @return \Drupal\devel\DevelDumperInterface
|
||||
* Returns the devel dumper plugin instance.
|
||||
*/
|
||||
protected function createInstance(?string $plugin_id = NULL) {
|
||||
if (!$plugin_id || !$this->dumperManager->isPluginSupported($plugin_id)) {
|
||||
$plugin_id = $this->config->get('devel_dumper');
|
||||
}
|
||||
|
||||
return $this->dumperManager->createInstance($plugin_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dump($input, ?string $name = NULL, $plugin_id = NULL): void {
|
||||
if ($this->hasAccessToDevelInformation()) {
|
||||
$this->createInstance($plugin_id)->dump($input, $name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function export(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL, bool $load_references = FALSE): MarkupInterface|string {
|
||||
if (!$this->hasAccessToDevelInformation()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($load_references && $input instanceof EntityInterface) {
|
||||
$input = $this->entityToArrayWithReferences($input);
|
||||
}
|
||||
|
||||
return $this->createInstance($plugin_id)->export($input, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function message($input, ?string $name = NULL, $type = MessengerInterface::TYPE_STATUS, ?string $plugin_id = NULL, $load_references = FALSE): void {
|
||||
if ($this->hasAccessToDevelInformation()) {
|
||||
$output = $this->export($input, $name, $plugin_id, $load_references);
|
||||
$this->messenger->addMessage($output, $type, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function debug($input, ?string $name = NULL, ?string $plugin_id = NULL) {
|
||||
$output = $this->createInstance($plugin_id)->export($input, $name) . "\n";
|
||||
// The temp directory does vary across multiple simpletest instances.
|
||||
$file = $this->config->get('debug_logfile');
|
||||
if (empty($file)) {
|
||||
$file = 'temporary://drupal_debug.txt';
|
||||
}
|
||||
|
||||
if (file_put_contents($file, $output, FILE_APPEND) === FALSE && $this->hasAccessToDevelInformation()) {
|
||||
$this->messenger->addError($this->t('Devel was unable to write to %file.', ['%file' => $file]));
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dumpOrExport($input, ?string $name = NULL, $export = TRUE, ?string $plugin_id = NULL) {
|
||||
if ($this->hasAccessToDevelInformation()) {
|
||||
$dumper = $this->createInstance($plugin_id);
|
||||
if ($export) {
|
||||
return $dumper->export($input, $name);
|
||||
}
|
||||
|
||||
$dumper->dump($input, $name);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exportAsRenderable($input, ?string $name = NULL, $plugin_id = NULL, $load_references = FALSE): array {
|
||||
if ($this->hasAccessToDevelInformation()) {
|
||||
if ($load_references && $input instanceof EntityInterface) {
|
||||
$input = $this->entityToArrayWithReferences($input);
|
||||
}
|
||||
|
||||
return $this->createInstance($plugin_id)->exportAsRenderable($input, $name);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user has access to devel information.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the user has the permission, FALSE otherwise.
|
||||
*/
|
||||
protected function hasAccessToDevelInformation(): bool {
|
||||
return $this->account->hasPermission('access devel information');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given entity to an array with referenced entities loaded.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The target entity.
|
||||
* @param int $depth
|
||||
* Internal. Track the recursion.
|
||||
* @param array $array_path
|
||||
* Internal. Track where we first say this entity.
|
||||
*
|
||||
* @return mixed[]
|
||||
* An array of field names and deep values.
|
||||
*/
|
||||
protected function entityToArrayWithReferences(EntityInterface $entity, int $depth = 0, array $array_path = []) {
|
||||
// Note that we've now seen this entity.
|
||||
$seen = &drupal_static(__FUNCTION__);
|
||||
$seen_key = $entity->getEntityTypeId() . '-' . $entity->id();
|
||||
if (!isset($seen[$seen_key])) {
|
||||
$seen[$seen_key] = $array_path;
|
||||
}
|
||||
|
||||
$array = $entity->toArray();
|
||||
|
||||
// Prevent out of memory and too deep traversing.
|
||||
if ($depth > 20) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
if (!$entity instanceof FieldableEntityInterface) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
foreach ($array as $field => &$value) {
|
||||
if (is_array($value)) {
|
||||
$fieldDefinition = $entity->getFieldDefinition($field);
|
||||
$target_type = $fieldDefinition->getSetting('target_type');
|
||||
if (!$target_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$storage = $this->entityTypeManager->getStorage($target_type);
|
||||
}
|
||||
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($value as $delta => &$item) {
|
||||
if (is_array($item)) {
|
||||
$referenced_entity = NULL;
|
||||
if (isset($item['target_id'])) {
|
||||
$referenced_entity = $storage->load($item['target_id']);
|
||||
}
|
||||
elseif (isset($item['target_revision_id'])) {
|
||||
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
|
||||
$referenced_entity = $storage->loadRevision($item['target_revision_id']);
|
||||
}
|
||||
|
||||
$langcode = $entity->language()->getId();
|
||||
if ($referenced_entity instanceof TranslatableInterface
|
||||
&& $referenced_entity->hasTranslation($langcode)) {
|
||||
$referenced_entity = $referenced_entity->getTranslation($langcode);
|
||||
}
|
||||
|
||||
if (empty($referenced_entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen_id = $referenced_entity->getEntityTypeId() . '-' . $referenced_entity->id();
|
||||
if (isset($seen[$seen_id])) {
|
||||
$item['message'] = 'Recursion detected.';
|
||||
$item['array_path'] = implode('.', $seen[$seen_id]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$item['entity'] = $this->entityToArrayWithReferences($referenced_entity, $depth++, array_merge($array_path, [$field, $delta, 'entity']));
|
||||
$item['bundle'] = $referenced_entity->bundle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
}
|
||||
115
web/modules/contrib/devel/src/DevelDumperManagerInterface.php
Normal file
115
web/modules/contrib/devel/src/DevelDumperManagerInterface.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Component\Render\MarkupInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
|
||||
/**
|
||||
* Interface for DevelDumper manager.
|
||||
*
|
||||
* @package Drupal\devel
|
||||
*/
|
||||
interface DevelDumperManagerInterface {
|
||||
|
||||
/**
|
||||
* Dumps information about a variable.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*/
|
||||
public function dump(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL);
|
||||
|
||||
/**
|
||||
* Returns a string representation of a variable.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable.
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
* @param bool $load_references
|
||||
* If the input is an entity, load the referenced entities.
|
||||
*
|
||||
* @return \Drupal\Component\Render\MarkupInterface|string
|
||||
* String representation of a variable.
|
||||
*/
|
||||
public function export(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL, bool $load_references = FALSE): MarkupInterface|string;
|
||||
|
||||
/**
|
||||
* Sets a message with a string representation of a variable.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* The label to output before variable.
|
||||
* @param string $type
|
||||
* (optional) The message's type. Defaults to
|
||||
* MessengerInterface::TYPE_STATUS.
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID. Defaults to NULL.
|
||||
* @param bool $load_references
|
||||
* (optional) If the input is an entity, load the referenced entities.
|
||||
* Defaults to FALSE.
|
||||
*/
|
||||
public function message(mixed $input, ?string $name = NULL, $type = MessengerInterface::TYPE_STATUS, ?string $plugin_id = NULL, $load_references = FALSE);
|
||||
|
||||
/**
|
||||
* Logs a variable to a drupal_debug.txt in the site's temp directory.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to log to the drupal_debug.txt log file.
|
||||
* @param string|null $name
|
||||
* (optional) If set, a label to output before $data in the log file.
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @return void|false
|
||||
* Empty if successful, FALSE if the log file could not be written.
|
||||
*
|
||||
* @see dd()
|
||||
* @see http://drupal.org/node/314112
|
||||
*/
|
||||
public function debug(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL);
|
||||
|
||||
/**
|
||||
* Wrapper for ::dump() and ::export().
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to dump.
|
||||
* @param string|null $name
|
||||
* (optional) The label to output before variable, defaults to NULL.
|
||||
* @param bool $export
|
||||
* (optional) Whether return string representation of a variable.
|
||||
* @param string|null $plugin_id
|
||||
* (optional) The plugin ID, defaults to NULL.
|
||||
*
|
||||
* @return string|null
|
||||
* String representation of a variable if $export is set to TRUE,
|
||||
* NULL otherwise.
|
||||
*/
|
||||
public function dumpOrExport(mixed $input, ?string $name = NULL, $export = TRUE, ?string $plugin_id = NULL);
|
||||
|
||||
/**
|
||||
* Returns a render array representation of a variable.
|
||||
*
|
||||
* @param mixed $input
|
||||
* The variable to export.
|
||||
* @param string|null $name
|
||||
* The label to output before variable.
|
||||
* @param string|null $plugin_id
|
||||
* The plugin ID.
|
||||
* @param bool $load_references
|
||||
* If the input is an entity, also load the referenced entities.
|
||||
*
|
||||
* @return array
|
||||
* String representation of a variable wrapped in a render array.
|
||||
*/
|
||||
public function exportAsRenderable(mixed $input, ?string $name = NULL, ?string $plugin_id = NULL, $load_references = FALSE): array;
|
||||
|
||||
}
|
||||
72
web/modules/contrib/devel/src/DevelDumperPluginManager.php
Normal file
72
web/modules/contrib/devel/src/DevelDumperPluginManager.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\devel\Annotation\DevelDumper;
|
||||
|
||||
/**
|
||||
* Plugin type manager for Devel Dumper plugins.
|
||||
*
|
||||
* @see \Drupal\devel\Annotation\DevelDumper
|
||||
* @see \Drupal\devel\DevelDumperInterface
|
||||
* @see \Drupal\devel\DevelDumperBase
|
||||
* @see plugin_api
|
||||
*/
|
||||
class DevelDumperPluginManager extends DefaultPluginManager implements DevelDumperPluginManagerInterface {
|
||||
|
||||
/**
|
||||
* Constructs a DevelDumperPluginManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/Devel/Dumper', $namespaces, $module_handler, DevelDumperInterface::class, DevelDumper::class);
|
||||
$this->setCacheBackend($cache_backend, 'devel_dumper_plugins');
|
||||
$this->alterInfo('devel_dumper_info');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function processDefinition(&$definition, $plugin_id): void {
|
||||
parent::processDefinition($definition, $plugin_id);
|
||||
|
||||
$definition['supported'] = (bool) call_user_func([$definition['class'], 'checkRequirements']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPluginSupported($plugin_id): bool {
|
||||
$definition = $this->getDefinition($plugin_id, FALSE);
|
||||
return $definition && $definition['supported'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createInstance($plugin_id, array $configuration = []) {
|
||||
if (!$this->isPluginSupported($plugin_id)) {
|
||||
$plugin_id = $this->getFallbackPluginId($plugin_id);
|
||||
}
|
||||
|
||||
return parent::createInstance($plugin_id, $configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFallbackPluginId($plugin_id, array $configuration = []): string {
|
||||
return 'var_dumper';
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Component\Plugin\FallbackPluginManagerInterface;
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
|
||||
/**
|
||||
* Interface for DevelDumper plugin manager.
|
||||
*/
|
||||
interface DevelDumperPluginManagerInterface extends PluginManagerInterface, FallbackPluginManagerInterface {
|
||||
|
||||
/**
|
||||
* Checks if plugin has a definition and is supported.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* The ID of the plugin to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the plugin is supported, FALSE otherwise.
|
||||
*/
|
||||
public function isPluginSupported($plugin_id): bool;
|
||||
|
||||
}
|
||||
106
web/modules/contrib/devel/src/DevelLazyBuilders.php
Normal file
106
web/modules/contrib/devel/src/DevelLazyBuilders.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Config\ImmutableConfig;
|
||||
use Drupal\Core\Menu\MenuLinkTreeInterface;
|
||||
use Drupal\Core\Menu\MenuTreeParameters;
|
||||
use Drupal\Core\Security\TrustedCallbackInterface;
|
||||
|
||||
/**
|
||||
* Lazy builders for the devel module.
|
||||
*/
|
||||
class DevelLazyBuilders implements TrustedCallbackInterface {
|
||||
|
||||
/**
|
||||
* The menu link tree service.
|
||||
*/
|
||||
protected MenuLinkTreeInterface $menuLinkTree;
|
||||
|
||||
/**
|
||||
* The devel toolbar config.
|
||||
*/
|
||||
protected ImmutableConfig $config;
|
||||
|
||||
/**
|
||||
* Constructs a new ShortcutLazyBuilders object.
|
||||
*
|
||||
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_link_tree
|
||||
* The menu link tree service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory.
|
||||
*/
|
||||
public function __construct(
|
||||
MenuLinkTreeInterface $menu_link_tree,
|
||||
ConfigFactoryInterface $config_factory,
|
||||
) {
|
||||
$this->menuLinkTree = $menu_link_tree;
|
||||
$this->config = $config_factory->get('devel.toolbar.settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function trustedCallbacks(): array {
|
||||
return ['renderMenu'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy builder callback for the devel menu toolbar.
|
||||
*
|
||||
* @return array
|
||||
* The renderable array rapresentation of the devel menu.
|
||||
*/
|
||||
public function renderMenu(): array {
|
||||
$parameters = new MenuTreeParameters();
|
||||
$parameters->onlyEnabledLinks()->setTopLevelOnly();
|
||||
|
||||
$tree = $this->menuLinkTree->load('devel', $parameters);
|
||||
$manipulators = [
|
||||
['callable' => 'menu.default_tree_manipulators:checkAccess'],
|
||||
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
|
||||
[
|
||||
'callable' => function (array $tree): array {
|
||||
return $this->processTree($tree);
|
||||
},
|
||||
],
|
||||
];
|
||||
$tree = $this->menuLinkTree->transform($tree, $manipulators);
|
||||
|
||||
$build = $this->menuLinkTree->build($tree);
|
||||
$build['#attributes']['class'] = ['toolbar-menu'];
|
||||
|
||||
CacheableMetadata::createFromRenderArray($build)
|
||||
->addCacheableDependency($this->config)
|
||||
->applyTo($build);
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds toolbar-specific attributes to the menu link tree.
|
||||
*
|
||||
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
|
||||
* The menu link tree to manipulate.
|
||||
*
|
||||
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
|
||||
* The manipulated menu link tree.
|
||||
*/
|
||||
public function processTree(array $tree): array {
|
||||
$visible_items = $this->config->get('toolbar_items') ?: [];
|
||||
|
||||
foreach ($tree as $element) {
|
||||
$plugin_id = $element->link->getPluginId();
|
||||
if (!in_array($plugin_id, $visible_items)) {
|
||||
// Add a class that allow to hide the non prioritized menu items when
|
||||
// the toolbar has horizontal orientation.
|
||||
$element->options['attributes']['class'][] = 'toolbar-horizontal-item-hidden';
|
||||
}
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
}
|
||||
304
web/modules/contrib/devel/src/Drush/Commands/DevelCommands.php
Normal file
304
web/modules/contrib/devel/src/Drush/Commands/DevelCommands.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Drush\Commands;
|
||||
|
||||
use Consolidation\AnnotatedCommand\Hooks\HookManager;
|
||||
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
|
||||
use Consolidation\SiteAlias\SiteAliasManagerInterface;
|
||||
use Consolidation\SiteProcess\Util\Escape;
|
||||
use Drupal\Component\Uuid\Php;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Utility\Token;
|
||||
use Drush\Attributes as CLI;
|
||||
use Drush\Commands\AutowireTrait;
|
||||
use Drush\Commands\DrushCommands;
|
||||
use Drush\Commands\pm\PmCommands;
|
||||
use Drush\Drush;
|
||||
use Drush\Exceptions\UserAbortException;
|
||||
use Drush\Exec\ExecTrait;
|
||||
use Drush\Utils\StringUtils;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Input\Input;
|
||||
use Symfony\Component\Console\Output\Output;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
final class DevelCommands extends DrushCommands {
|
||||
|
||||
use AutowireTrait;
|
||||
use ExecTrait;
|
||||
|
||||
const REINSTALL = 'devel:reinstall';
|
||||
|
||||
const HOOK = 'devel:hook';
|
||||
|
||||
const EVENT = 'devel:event';
|
||||
|
||||
const TOKEN = 'devel:token';
|
||||
|
||||
const UUID = 'devel:uuid';
|
||||
|
||||
const SERVICES = 'devel:services';
|
||||
|
||||
/**
|
||||
* Constructs a new DevelCommands object.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Token $token,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected ModuleHandlerInterface $moduleHandler,
|
||||
private readonly SiteAliasManagerInterface $siteAliasManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the module handler.
|
||||
*
|
||||
* @return \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
* The moduleHandler.
|
||||
*/
|
||||
public function getModuleHandler(): ModuleHandlerInterface {
|
||||
return $this->moduleHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the event dispatcher.
|
||||
*
|
||||
* @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
|
||||
* The eventDispatcher.
|
||||
*/
|
||||
public function getEventDispatcher(): EventDispatcherInterface {
|
||||
return $this->eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the container.
|
||||
*
|
||||
* @return \Drupal\Component\DependencyInjection\ContainerInterface
|
||||
* The container.
|
||||
*/
|
||||
public function getContainer(): ContainerInterface {
|
||||
return Drush::getContainer()->get('service_container');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the token.
|
||||
*
|
||||
* @return \Drupal\Core\Utility\Token
|
||||
* The token.
|
||||
*/
|
||||
public function getToken(): Token {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall, and Install modules.
|
||||
*/
|
||||
#[CLI\Command(name: self::REINSTALL, aliases: ['dre', 'devel-reinstall'])]
|
||||
#[CLI\Argument(name: 'modules', description: 'A comma-separated list of module names.')]
|
||||
public function reinstall($modules): void {
|
||||
/** @var \Drush\SiteAlias\ProcessManager $process_manager */
|
||||
$process_manager = $this->processManager();
|
||||
|
||||
$modules = StringUtils::csvToArray($modules);
|
||||
$modules_str = implode(',', $modules);
|
||||
$process = $process_manager->drush($this->siteAliasManager->getSelf(), PmCommands::UNINSTALL, [$modules_str]);
|
||||
$process->mustRun();
|
||||
$process = $process_manager->drush($this->siteAliasManager->getSelf(), PmCommands::INSTALL, [$modules_str]);
|
||||
$process->mustRun();
|
||||
}
|
||||
|
||||
/**
|
||||
* List implementations of a given hook and optionally edit one.
|
||||
*/
|
||||
#[CLI\Command(name: self::HOOK, aliases: ['fnh', 'fn-hook', 'hook', 'devel-hook'])]
|
||||
#[CLI\Argument(name: 'hook', description: 'The name of the hook to explore.')]
|
||||
#[CLI\Argument(name: 'implementation', description: 'The name of the implementation to edit. Usually omitted')]
|
||||
#[CLI\Usage(name: 'devel:hook cron', description: 'List implementations of hook_cron().')]
|
||||
#[CLI\OptionsetGetEditor()]
|
||||
public function hook(string $hook, string $implementation): void {
|
||||
// Get implementations in the .install files as well.
|
||||
include_once DRUPAL_ROOT . '/core/includes/install.inc';
|
||||
drupal_load_updates();
|
||||
$info = $this->codeLocate($implementation . ('_' . $hook));
|
||||
$exec = self::getEditor('');
|
||||
$cmd = sprintf($exec, Escape::shellArg($info['file']));
|
||||
$process = $this->processManager()->shell($cmd);
|
||||
$process->setTty(TRUE);
|
||||
$process->mustRun();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user to select a hook implementation.
|
||||
*/
|
||||
#[CLI\Hook(type: HookManager::INTERACT, target: self::HOOK)]
|
||||
public function hookInteract(Input $input, Output $output): void {
|
||||
$hook_implementations = [];
|
||||
if (!$input->getArgument('implementation')) {
|
||||
foreach (array_keys($this->moduleHandler->getModuleList()) as $key) {
|
||||
if ($this->moduleHandler->hasImplementations($input->getArgument('hook'), [$key])) {
|
||||
$hook_implementations[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hook_implementations !== []) {
|
||||
if (!$choice = $this->io()->select('Enter the number of the hook implementation you wish to view.', array_combine($hook_implementations, $hook_implementations))) {
|
||||
throw new UserAbortException();
|
||||
}
|
||||
|
||||
$input->setArgument('implementation', $choice);
|
||||
}
|
||||
else {
|
||||
throw new \Exception(dt('No implementations'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List implementations of a given event and optionally edit one.
|
||||
*/
|
||||
#[CLI\Command(name: self::EVENT, aliases: ['fne', 'fn-event', 'event'])]
|
||||
#[CLI\Argument(name: 'event', description: 'The name of the event to explore. If omitted, a list of events is shown.')]
|
||||
#[CLI\Argument(name: 'implementation', description: 'The name of the implementation to show. Usually omitted.')]
|
||||
#[CLI\Usage(name: 'drush devel:event', description: 'Pick a Kernel event, then pick an implementation, and then view its source code')]
|
||||
#[CLI\Usage(name: 'devel-event kernel.terminate', description: 'Pick a terminate subscribers implementation and view its source code.')]
|
||||
public function event($event, $implementation): void {
|
||||
$info = $this->codeLocate($implementation);
|
||||
$exec = self::getEditor('');
|
||||
$cmd = sprintf($exec, Escape::shellArg($info['file']));
|
||||
$process = $this->processManager()->shell($cmd);
|
||||
$process->setTty(TRUE);
|
||||
$process->mustRun();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user to select an event and the event's implementation.
|
||||
*/
|
||||
#[CLI\Hook(type: HookManager::INTERACT, target: self::EVENT)]
|
||||
public function interactEvent(Input $input, Output $output): void {
|
||||
$event = $input->getArgument('event');
|
||||
if (!$event) {
|
||||
// @todo Expand this list.
|
||||
$events = [
|
||||
'kernel.controller',
|
||||
'kernel.exception',
|
||||
'kernel.request',
|
||||
'kernel.response',
|
||||
'kernel.terminate',
|
||||
'kernel.view',
|
||||
];
|
||||
$events = array_combine($events, $events);
|
||||
if (!$event = $this->io()->select('Enter the event you wish to explore.', $events)) {
|
||||
throw new UserAbortException();
|
||||
}
|
||||
|
||||
$input->setArgument('event', $event);
|
||||
}
|
||||
|
||||
/** @var \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher */
|
||||
$event_dispatcher = $this->eventDispatcher;
|
||||
if ($implementations = $event_dispatcher->getListeners($event)) {
|
||||
$choices = [];
|
||||
foreach ($implementations as $implementation) {
|
||||
$callable = $implementation[0]::class . '::' . $implementation[1];
|
||||
$choices[$callable] = $callable;
|
||||
}
|
||||
|
||||
if (!$choice = $this->io()->select('Enter the number of the implementation you wish to view.', $choices)) {
|
||||
throw new UserAbortException();
|
||||
}
|
||||
|
||||
$input->setArgument('implementation', $choice);
|
||||
}
|
||||
else {
|
||||
throw new \Exception(dt('No implementations.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available tokens.
|
||||
*/
|
||||
#[CLI\Command(name: self::TOKEN, aliases: ['token', 'devel-token'])]
|
||||
#[CLI\FieldLabels(labels: ['group' => 'Group', 'token' => 'Token', 'name' => 'Name'])]
|
||||
#[CLI\DefaultTableFields(fields: ['group', 'token', 'name'])]
|
||||
public function token($options = ['format' => 'table']): RowsOfFields {
|
||||
$rows = [];
|
||||
$all = $this->token->getInfo();
|
||||
foreach ($all['tokens'] as $group => $tokens) {
|
||||
foreach ($tokens as $key => $token) {
|
||||
$rows[] = [
|
||||
'group' => $group,
|
||||
'token' => $key,
|
||||
'name' => $token['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return new RowsOfFields($rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Universally Unique Identifier (UUID).
|
||||
*/
|
||||
#[CLI\Command(name: self::UUID, aliases: ['uuid', 'devel-uuid'])]
|
||||
public function uuid(): string {
|
||||
$uuid = new Php();
|
||||
return $uuid->generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source code line for specified function or method.
|
||||
*/
|
||||
public function codeLocate($function_name): array {
|
||||
// Get implementations in the .install files as well.
|
||||
include_once DRUPAL_ROOT . '/core/includes/install.inc';
|
||||
drupal_load_updates();
|
||||
|
||||
if (!str_contains($function_name, '::')) {
|
||||
if (!function_exists($function_name)) {
|
||||
throw new \Exception(dt('Function not found'));
|
||||
}
|
||||
|
||||
$reflect = new \ReflectionFunction($function_name);
|
||||
}
|
||||
else {
|
||||
[$class, $method] = explode('::', $function_name);
|
||||
if (!method_exists($class, $method)) {
|
||||
throw new \Exception(dt('Method not found'));
|
||||
}
|
||||
|
||||
$reflect = new \ReflectionMethod($class, $method);
|
||||
}
|
||||
|
||||
return [
|
||||
'file' => $reflect->getFileName(),
|
||||
'startline' => $reflect->getStartLine(),
|
||||
'endline' => $reflect->getEndLine(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of available container services.
|
||||
*/
|
||||
#[CLI\Command(name: self::SERVICES, aliases: ['devel-container-services', 'dcs', 'devel-services'])]
|
||||
#[CLI\Argument(name: 'prefix', description: 'Optional prefix to filter the service list by.')]
|
||||
#[CLI\Usage(name: 'drush devel-services', description: 'Gets a list of all available container services')]
|
||||
#[CLI\Usage(name: 'drush dcs plugin.manager', description: 'Get all services containing "plugin.manager"')]
|
||||
public function services(?string $prefix = NULL, array $options = ['format' => 'yaml']): array {
|
||||
$container = $this->getContainer();
|
||||
$services = $container->getServiceIds();
|
||||
|
||||
// If there is a prefix, try to find matches.
|
||||
if (isset($prefix)) {
|
||||
$services = preg_grep(sprintf('/%s/', $prefix), $services);
|
||||
}
|
||||
|
||||
if (empty($services)) {
|
||||
throw new \Exception(dt('No container services found.'));
|
||||
}
|
||||
|
||||
sort($services);
|
||||
return $services;
|
||||
}
|
||||
|
||||
}
|
||||
131
web/modules/contrib/devel/src/Element/ClientSideFilterTable.php
Normal file
131
web/modules/contrib/devel/src/Element/ClientSideFilterTable.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\Element;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Render\Element\RenderElementBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a render element for filterable table data.
|
||||
*
|
||||
* Usage example:
|
||||
*
|
||||
* @code
|
||||
* $build['item'] = [
|
||||
* '#type' => 'devel_table_filter',
|
||||
* '#filter_label' => $this->t('Search'),
|
||||
* '#filter_placeholder' => $this->t('Enter element name.'),
|
||||
* '#filter_description' => $this->t('Enter a part of name to filter by.'),
|
||||
* '#header' => $headers,
|
||||
* '#rows' => $rows,
|
||||
* '#empty' => $this->t('No element found.'),
|
||||
* ];
|
||||
* @endcode
|
||||
*
|
||||
* @RenderElement("devel_table_filter")
|
||||
*/
|
||||
class ClientSideFilterTable extends RenderElementBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
// phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
|
||||
final public function __construct(array $configuration, string $plugin_id, string|array $plugin_definition) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
|
||||
$instance = new static($configuration, $plugin_id, $plugin_definition);
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo(): array {
|
||||
$class = static::class;
|
||||
return [
|
||||
'#filter_label' => $this->t('Search'),
|
||||
'#filter_placeholder' => $this->t('Search'),
|
||||
'#filter_description' => $this->t('Search'),
|
||||
'#header' => [],
|
||||
'#rows' => [],
|
||||
'#empty' => '',
|
||||
'#sticky' => FALSE,
|
||||
'#responsive' => TRUE,
|
||||
'#attributes' => [],
|
||||
'#pre_render' => [
|
||||
[$class, 'preRenderTable'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-render callback: Assemble render array for the filterable table.
|
||||
*
|
||||
* @param array $element
|
||||
* An associative array containing the properties of the element.
|
||||
*
|
||||
* @return array
|
||||
* The $element with prepared render array ready for rendering.
|
||||
*/
|
||||
public static function preRenderTable(array $element): array {
|
||||
$build['#attached']['library'][] = 'devel/devel-table-filter';
|
||||
$identifier = Html::getUniqueId('js-devel-table-filter');
|
||||
|
||||
$build['filters'] = [
|
||||
'#type' => 'container',
|
||||
'#weight' => -1,
|
||||
'#attributes' => [
|
||||
'class' => ['table-filter', 'js-show'],
|
||||
],
|
||||
];
|
||||
|
||||
$build['filters']['name'] = [
|
||||
'#type' => 'search',
|
||||
'#size' => 30,
|
||||
'#title' => $element['#filter_label'],
|
||||
'#placeholder' => $element['#filter_placeholder'],
|
||||
'#attributes' => [
|
||||
'class' => ['table-filter-text'],
|
||||
'data-table' => '.' . $identifier,
|
||||
'autocomplete' => 'off',
|
||||
'title' => $element['#filter_description'],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($element['#rows'] as &$row) {
|
||||
foreach ($row as &$cell) {
|
||||
if (!isset($cell['data'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($cell['filter'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cell['class'][] = 'table-filter-text-source';
|
||||
}
|
||||
}
|
||||
|
||||
$build['table'] = [
|
||||
'#type' => 'table',
|
||||
'#header' => $element['#header'],
|
||||
'#rows' => $element['#rows'],
|
||||
'#empty' => $element['#empty'],
|
||||
'#sticky' => $element['#sticky'],
|
||||
'#responsive' => $element['#responsive'],
|
||||
'#attributes' => $element['#attributes'],
|
||||
];
|
||||
|
||||
$build['table']['#attributes']['class'][] = $identifier;
|
||||
$build['table']['#attributes']['class'][] = 'devel-table-filter';
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
165
web/modules/contrib/devel/src/EntityTypeInfo.php
Normal file
165
web/modules/contrib/devel/src/EntityTypeInfo.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Manipulates entity type information.
|
||||
*
|
||||
* This class contains primarily bridged hooks for compile-time or
|
||||
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
|
||||
*/
|
||||
class EntityTypeInfo implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*/
|
||||
protected AccountInterface $currentUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container): self {
|
||||
$instance = new self();
|
||||
$instance->currentUser = $container->get('current_user');
|
||||
$instance->stringTranslation = $container->get('string_translation');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds devel links to appropriate entity types.
|
||||
*
|
||||
* This is an alter hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
|
||||
* The master entity type list to alter.
|
||||
*
|
||||
* @see hook_entity_type_alter()
|
||||
*/
|
||||
public function entityTypeAlter(array &$entity_types): void {
|
||||
foreach ($entity_types as $entity_type_id => $entity_type) {
|
||||
// Make devel-load and devel-load-with-references subtasks. The edit-form
|
||||
// template is used to extract and set additional parameters dynamically.
|
||||
// If there is no 'edit-form' template then still create the link using
|
||||
// 'entity_type_id/{entity_type_id}' as the link. This allows devel info
|
||||
// to be viewed for any entity, even if the url has to be typed manually.
|
||||
// @see https://gitlab.com/drupalspoons/devel/-/issues/377
|
||||
$entity_link = $entity_type->getLinkTemplate('edit-form') ?: $entity_type_id . sprintf('/{%s}', $entity_type_id);
|
||||
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-load', '/devel/' . $entity_type_id);
|
||||
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-load-with-references', '/devel/load-with-references/' . $entity_type_id);
|
||||
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-path-alias', '/devel/path-alias/' . $entity_type_id);
|
||||
|
||||
// Create the devel-render subtask.
|
||||
if ($entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical')) {
|
||||
// We use canonical template to extract and set additional parameters
|
||||
// dynamically.
|
||||
$entity_link = $entity_type->getLinkTemplate('canonical');
|
||||
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-render', '/devel/render/' . $entity_type_id);
|
||||
}
|
||||
|
||||
// Create the devel-definition subtask.
|
||||
if ($entity_type->hasLinkTemplate('devel-render') || $entity_type->hasLinkTemplate('devel-load')) {
|
||||
// We use canonical or edit-form template to extract and set additional
|
||||
// parameters dynamically.
|
||||
$entity_link = $entity_type->getLinkTemplate('edit-form');
|
||||
if (empty($entity_link)) {
|
||||
$entity_link = $entity_type->getLinkTemplate('canonical');
|
||||
}
|
||||
|
||||
$this->setEntityTypeLinkTemplate($entity_type, $entity_link, 'devel-definition', '/devel/definition/' . $entity_type_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets entity type link template.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* Entity type.
|
||||
* @param string $entity_link
|
||||
* Entity link.
|
||||
* @param string $devel_link_key
|
||||
* Devel link key.
|
||||
* @param string $base_path
|
||||
* Base path for devel link key.
|
||||
*/
|
||||
protected function setEntityTypeLinkTemplate(EntityTypeInterface $entity_type, $entity_link, $devel_link_key, string $base_path) {
|
||||
// Extract all route parameters from the given template and set them to
|
||||
// the current template.
|
||||
// Some entity templates can contain not only entity id,
|
||||
// for example /user/{user}/documents/{document}
|
||||
// /group/{group}/content/{group_content}
|
||||
// We use canonical or edit-form templates to get these parameters and set
|
||||
// them for devel entity link templates.
|
||||
$path_parts = $this->getPathParts($entity_link);
|
||||
$entity_type->setLinkTemplate($devel_link_key, $base_path . $path_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path parts.
|
||||
*
|
||||
* @param string $entity_path
|
||||
* Entity path.
|
||||
*
|
||||
* @return string
|
||||
* Path parts.
|
||||
*/
|
||||
protected function getPathParts($entity_path): string {
|
||||
$path = '';
|
||||
if (preg_match_all('/{\w*}/', $entity_path, $matches)) {
|
||||
foreach ($matches[0] as $match) {
|
||||
$path .= '/' . $match;
|
||||
}
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds devel operations on entity that supports it.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity on which to define an operation.
|
||||
*
|
||||
* @return array
|
||||
* An array of operation definitions.
|
||||
*
|
||||
* @see hook_entity_operation()
|
||||
*/
|
||||
public function entityOperation(EntityInterface $entity): array {
|
||||
$operations = $parameters = [];
|
||||
if ($this->currentUser->hasPermission('access devel information')) {
|
||||
if ($entity->hasLinkTemplate('canonical')) {
|
||||
$parameters = $entity->toUrl('canonical')->getRouteParameters();
|
||||
}
|
||||
if ($entity->hasLinkTemplate('devel-load')) {
|
||||
$url = $entity->toUrl('devel-load');
|
||||
$operations['devel'] = [
|
||||
'title' => $this->t('Devel'),
|
||||
'weight' => 100,
|
||||
'url' => $parameters ? $url->setRouteParameters($parameters) : $url,
|
||||
];
|
||||
}
|
||||
elseif ($entity->hasLinkTemplate('devel-render')) {
|
||||
$url = $entity->toUrl('devel-render');
|
||||
$operations['devel'] = [
|
||||
'title' => $this->t('Devel'),
|
||||
'weight' => 100,
|
||||
'url' => $parameters ? $url->setRouteParameters($parameters) : $url,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\devel\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Listener for handling PHP errors.
|
||||
*/
|
||||
class ErrorHandlerSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*/
|
||||
protected AccountProxyInterface $account;
|
||||
|
||||
/**
|
||||
* ErrorHandlerSubscriber constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountProxyInterface $account
|
||||
* The current user.
|
||||
*/
|
||||
public function __construct(AccountProxyInterface $account) {
|
||||
$this->account = $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register devel error handler.
|
||||
*
|
||||
* @param \Symfony\Component\HttpKernel\Event\RequestEvent|null $event
|
||||
* The event to process.
|
||||
*/
|
||||
public function registerErrorHandler(?RequestEvent $event = NULL): void {
|
||||
if (!$this->account->hasPermission('access devel information')) {
|
||||
return;
|
||||
}
|
||||
|
||||
devel_set_handler(devel_get_handlers());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents(): array {
|
||||
// Runs as soon as possible in the request but after
|
||||
// AuthenticationSubscriber (priority 300) because you need to access to
|
||||
// the current user for determine whether register the devel error handler
|
||||
// or not.
|
||||
$events[KernelEvents::REQUEST][] = ['registerErrorHandler', 256];
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user