first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
name: ddev-drupal-contrib
repository: ddev/ddev-drupal-contrib
version: 1.0.0-rc18
install_date: "2024-06-20T20:54:56+02:00"
project_files:
- commands/web/eslint
- commands/web/expand-composer-json
- commands/web/nightwatch
- commands/web/phpcbf
- commands/web/phpcs
- commands/web/phpunit
- commands/web/poser
- commands/web/stylelint
- commands/web/symlink-project
- config.contrib.yaml
global_files: []
removal_actions: []

View File

@@ -0,0 +1,20 @@
#!/bin/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
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/modules/custom/$DDEV_SITENAME" || exit
"$DDEV_COMPOSER_ROOT/$DDEV_DOCROOT/core/node_modules/.bin/eslint" --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 yarn --cwd $DDEV_DOCROOT/core install'"
exit 1
fi

View File

@@ -0,0 +1,14 @@
#!/bin/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
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
php expand_composer_json.php "$DDEV_SITENAME"
rm -f expand_composer_json.php

View File

@@ -0,0 +1,10 @@
#!/bin/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
yarn --cwd "$DDEV_DOCROOT/core" test:nightwatch "$DDEV_COMPOSER_ROOT/$DDEV_DOCROOT/modules/custom/" "$@"

View File

@@ -0,0 +1,15 @@
#!/bin/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
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/scripts/phpcs.xml.dist
phpcbf -s --report-full --report-summary --report-source web/modules/custom "$@"

View File

@@ -0,0 +1,15 @@
#!/bin/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
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 web/modules/custom --ignore=*/.ddev/* "$@"

View File

@@ -0,0 +1,22 @@
#!/bin/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
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 --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" --bootstrap $PWD/$DDEV_DOCROOT/core/tests/bootstrap.php $DDEV_DOCROOT/modules/custom "$@"
fi

View File

@@ -0,0 +1,15 @@
#!/bin/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"
## ExecRaw: true
export COMPOSER=composer.contrib.json
.ddev/commands/web/expand-composer-json "$DDEV_PROJECT_NAME"
composer install
rm composer.contrib.json composer.contrib.lock
yarn --cwd $DDEV_DOCROOT/core install
touch $DDEV_DOCROOT/core/.env

View File

@@ -0,0 +1,15 @@
#!/bin/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
if yarn --cwd "$DDEV_DOCROOT/core" stylelint --version >/dev/null ; then
yarn --color --cwd "$DDEV_DOCROOT/core" --config ./.stylelintrc.json stylelint ../modules/custom/**/*.css "$@"
else
echo "stylelint is not available. You may need to 'ddev yarn --cwd $DDEV_DOCROOT/core install'"
exit 1
fi

View File

@@ -0,0 +1,18 @@
#!/bin/bash
#ddev-generated
## Command provided by https://github.com/ddev/ddev-drupal-contrib
## Description: Symlink all root files/dirs into web.modules/custom/[PROJECT_NAME]
## Usage: symlink-project [flags] [args]
## Example: "ddev symlink-project"
## ExecRaw: true
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

View File

@@ -0,0 +1,17 @@
#ddev-generated
## Command provided by https://github.com/ddev/ddev-drupal-contrib
web_environment:
# If desired, override to a different version of Drupal core in via the project's DDEV config
- DRUPAL_CORE=^10
- SIMPLETEST_DB=mysql://db:db@db/db
- SIMPLETEST_BASE_URL=http://web
- BROWSERTEST_OUTPUT_DIRECTORY=/tmp
- BROWSERTEST_OUTPUT_BASE_URL=${DDEV_PRIMARY_URL}
hooks:
post-start:
- exec-host: |
if [[ -f vendor/autoload.php ]]; then
ddev symlink-project
else
exit 0
fi

View 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

36
modules/devel/.gitignore vendored Normal file
View 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/

View File

@@ -0,0 +1,38 @@
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:
OPT_IN_TEST_MAX_PHP: 1
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
modules/devel/CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
/src/Drush/Commands/ @weitzman

339
modules/devel/LICENSE.txt Normal file
View 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.

44
modules/devel/README.md Normal file
View File

@@ -0,0 +1,44 @@
[[_TOC_]]
#### Introduction
Devel module contains helper functions and pages for Drupal developers and
inquisitive admins:
- A block and toolbar for quickly accessing devel pages
- A menu tab added to entities to give access to internal entity 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.

View File

@@ -0,0 +1,39 @@
{
"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": "^12.5.1 || ^13@beta",
"firephp/firephp-core": "^0.5.3",
"kint-php/kint": "^5.1"
},
"suggest": {
"kint-php/kint": "Kint provides an informative display of arrays/objects. Useful for debugging and developing."
},
"conflict": {
"kint-php/kint": "<3",
"drush/drush": "<12.5.1"
},
"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
}
}
}

View 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;
}

View 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;
}

View 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".
*/

View File

@@ -0,0 +1,14 @@
type: module
name: Devel
description: 'Various blocks, pages, and functions for developers.'
package: Development
core_version_requirement: "^10 || ^11 || ^12"
php: 8.1
configure: devel.admin_settings
tags:
- developer
# Information added by Drupal.org packaging script on 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -0,0 +1,53 @@
<?php
/**
* @file
* Install, update and uninstall functions for the devel module.
*/
use Drupal\devel\Plugin\Devel\Dumper\Kint;
use Drupal\devel\Plugin\Devel\Dumper\VarDumper;
/**
* Implements hook_install().
*/
function devel_install() {
$rich_dumper = NULL;
if (VarDumper::checkRequirements()) {
$rich_dumper = 'var_dumper';
}
elseif (Kint::checkRequirements()) {
$rich_dumper = 'kint';
}
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;
}

View File

@@ -0,0 +1,17 @@
devel:
version: 0
css:
theme:
css/devel.css: {}
devel-toolbar:
version: VERSION
css:
component:
css/devel.toolbar.css: {}
devel-table-filter:
version: VERSION
js: {}
dependencies:
- system/drupal.system.modules

View 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

View 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

776
modules/devel/devel.module Normal file
View File

@@ -0,0 +1,776 @@
<?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, $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 $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, $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 $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, $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 $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, $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 $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, $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, $label = NULL) {
return Drupal::service('devel.dumper')->debug($data, $label, 'default');
}
}
if (!function_exists('kint')) {
/**
* Prints passed argument(s) using Kint debug tool.
*
* Wrapper for DevelDumperManager::dump().
*
* @see \Drupal\devel\DevelDumperManager::dump()
*/
function kint() {
$args = func_get_args();
if (count($args) == 1) {
// Pass a single argument directly, which works for any plug-in.
$args = $args[0];
$name = NULL;
}
else {
// Pass an array marked with a special name. The kint plug-in expands the
// arguments and prints each separately.
$name = '__ARGS__';
}
Drupal::service('devel.dumper')->dump($args, $name, 'kint');
}
}
if (!function_exists('ksm')) {
/**
* Prints passed argument(s) to the 'message' area of the page.
*
* Wrapper for DevelDumperManager::message().
*
* @see \Drupal\devel\DevelDumperManager::message()
*/
function ksm(): void {
$args = func_get_args();
if (count($args) == 1) {
// Pass a single argument directly, which works for any plug-in.
$args = $args[0];
$name = NULL;
}
else {
// Pass an array marked with a special name. The kint plug-in expands the
// arguments and prints each separately.
$name = '__ARGS__';
}
Drupal::service('devel.dumper')
->message(input: $args, name: $name, plugin_id: 'kint');
}
}
/**
* 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, $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, $name = NULL) {
return Drupal::service('devel.dumper')
->dumpOrExport(input: $input, name: $name, export: $export, plugin_id: 'default');
}
/**
* Wrapper for DevelDumperManager::dumpOrExport().
*
* An alias for devel_dump(). Saves carpal tunnel syndrome.
*
* @see \Drupal\devel\DevelDumperManager::dumpOrExport()
*/
function kpr($input, $export = FALSE, $name = NULL) {
return Drupal::service('devel.dumper')
->dumpOrExport(input: $input, name: $name, export: $export);
}
/**
* Wrapper for DevelDumperManager::dumpOrExport().
*
* Like dpr(), but uses Variable::export() instead.
*
* @see \Drupal\devel\DevelDumperManager::dumpOrExport()
*/
function dvr($input, $export = FALSE, $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 $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, $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);
}
}

View 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

View 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'

View 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']

View 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

View 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."
}
}

View File

@@ -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);
}

View File

@@ -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 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
administer devel_generate:
title: 'Administer devel_generate'
permission_callbacks:
- \Drupal\devel_generate\DevelGeneratePermissions::permissions

View File

@@ -0,0 +1,2 @@
route_callbacks:
- '\Drupal\devel_generate\Routing\DevelGenerateRoutes::routes'

View File

@@ -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']

View File

@@ -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 = [];
}

View File

@@ -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']);
}
}

View File

@@ -0,0 +1,347 @@
<?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 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->messenger = $container->get('messenger');
$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');
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->setMessage('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.
*/
protected function setMessage($msg, string $type = MessengerInterface::TYPE_STATUS): void {
if (function_exists('drush_log')) {
$msg = strip_tags($msg);
drush_log($msg);
}
else {
$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)));
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Drupal\devel_generate;
/**
* DevelGenerateException extending Generic Plugin exception class.
*/
class DevelGenerateException extends \Exception {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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(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(int $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(int $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(int $number_menus = 2, int $number_links = 50, int $max_depth = 3, int $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(int $num = 50, int $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(int $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(int $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);
}
}

View File

@@ -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()]));
}
}
}

View File

@@ -0,0 +1,493 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\block_content\BlockContentInterface;
use Drupal\content_translation\ContentTranslationManagerInterface;
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\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'));
}
}

View File

@@ -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->setMessage($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->setMessage($this->formatPlural($values['num'], 'Created 1 node', 'Created @count nodes'));
if ($values['num_translations'] > 0) {
$this->setMessage($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'] = array_shift($args);
$values['max_comments'] = 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($values['num'], $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->setMessage($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 ($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();
}
}

View File

@@ -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($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'] = 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;
}
}

View File

@@ -0,0 +1,384 @@
<?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'],
'pipe' => $options['pipe'],
'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;
}
}

View File

@@ -0,0 +1,454 @@
<?php
namespace Drupal\devel_generate\Plugin\DevelGenerate;
use Drupal\content_translation\ContentTranslationManagerInterface;
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\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;
}
}

View File

@@ -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'],
];
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -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'],
];
}
}

View File

@@ -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 2024-06-26
version: '5.2.1+50-dev'
project: 'devel'
datestamp: 1719414589

View File

@@ -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'));
}
}

View File

@@ -0,0 +1,439 @@
<?php
namespace Drupal\Tests\devel_generate\Functional;
use Drupal\media\Entity\Media;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
/**
* 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));
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace Drupal\Tests\devel_generate\Functional;
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\Tests\BrowserTestBase;
use Drupal\Tests\devel_generate\Traits\DevelGenerateSetupTrait;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
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());
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\Tests\devel_generate\Traits;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\Language;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* 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();
}
}

View File

@@ -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\devel_generate\DevelGeneratePluginManager;
use Drupal\devel_generate_example\Plugin\DevelGenerate\ExampleDevelGenerate;
use Drupal\Tests\UnitTestCase;
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' => [],
],
];
}
}

View File

@@ -0,0 +1,16 @@
# See docs at https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing-for-projects
build:
assessment:
validate_codebase: { }
# Remove all validation and standards checking from drupal.org CI as this
# is now covered by jobs running on Drupalspoons.
# https://gitlab.com/drupalspoons/devel/-/pipelines
testing:
run_tests.standard:
types: 'PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
suppress-deprecations: true
run_tests.js:
types: 'PHPUnit-FunctionalJavascript'
suppress-deprecations: true
concurrency: 1
halt-on-fail: false

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
modules/devel/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View 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>

View File

@@ -0,0 +1,4 @@
includes:
- phar://phpstan.phar/conf/bleedingEdge.neon
parameters:
level: 5

View 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;
}

View File

@@ -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);
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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 '';
}
}

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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 $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($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>');
}
}

View File

@@ -0,0 +1,87 @@
<?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, $name = NULL): void {
echo (string) $this->export($input, $name);
}
/**
* {@inheritdoc}
*/
public function exportAsRenderable($input, $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'],
'dpm',
'dvm',
'dsm',
'dpr',
'dvr',
'kpr',
'dargs',
'dcp',
'dfb',
'dfbt',
'dpq',
'kint',
'ksm',
'ddebug_backtrace',
'kdevel_print_object',
'backtrace_error_handler',
];
}
}

View 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 $name
* (optional) The label to output before variable, defaults to NULL.
*/
public function dump(mixed $input, $name = NULL);
/**
* Returns a string representation of a variable.
*
* @param mixed $input
* The variable to export.
* @param ?string $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 $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, $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;
}

View 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 $plugin_id
* (optional) The plugin ID, defaults to NULL.
*
* @return \Drupal\devel\DevelDumperInterface
* Returns the devel dumper plugin instance.
*/
protected function createInstance($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, $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, $name = NULL, $type = MessengerInterface::TYPE_STATUS, $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, $name = NULL, $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, $name = NULL, $export = TRUE, $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, $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;
}
}

View 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 $name
* (optional) The label to output before variable, defaults to NULL.
* @param string $plugin_id
* (optional) The plugin ID, defaults to NULL.
*/
public function dump(mixed $input, $name = NULL, $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 $name
* The label to output before variable.
* @param string $type
* (optional) The message's type. Defaults to
* MessengerInterface::TYPE_STATUS.
* @param string $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, $name = NULL, $type = MessengerInterface::TYPE_STATUS, $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 $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 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, $name = NULL, $plugin_id = NULL);
/**
* Wrapper for ::dump() and ::export().
*
* @param mixed $input
* The variable to dump.
* @param string $name
* (optional) The label to output before variable, defaults to NULL.
* @param bool $export
* (optional) Whether return string representation of a variable.
* @param string $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, $name = NULL, $export = TRUE, $plugin_id = NULL);
/**
* Returns a render array representation of a variable.
*
* @param mixed $input
* The variable to export.
* @param string $name
* The label to output before variable.
* @param string $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, $name = NULL, $plugin_id = NULL, $load_references = FALSE): array;
}

View 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';
}
}

View File

@@ -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;
}

View 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;
}
}

View 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 __DIR__ . '/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()->choice('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()->choice('Enter the event you wish to explore.', $events)) {
throw new UserAbortException();
}
$input->setArgument('event', $event);
}
/** @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $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()->choice('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 __DIR__ . '/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($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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Drupal\devel\EventSubscriber;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Theme\Registry;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Subscriber for force the system to rebuild the theme registry.
*/
class ThemeInfoRebuildSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Internal flag for handle user notification.
*/
protected string $notificationFlag = 'devel.rebuild_theme_warning';
/**
* The devel config.
*/
protected Config $config;
/**
* The current user.
*/
protected AccountProxyInterface $account;
/**
* The theme handler.
*/
protected ThemeHandlerInterface $themeHandler;
/**
* The messenger.
*/
protected MessengerInterface $messenger;
/**
* The theme registry.
*/
protected Registry $themeRegistry;
/**
* Constructs a ThemeInfoRebuildSubscriber object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config
* The config factory.
* @param \Drupal\Core\Session\AccountProxyInterface $account
* The current user.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
* @param \Drupal\Core\Theme\Registry $theme_registry
* The theme registry.
*/
public function __construct(
ConfigFactoryInterface $config,
AccountProxyInterface $account,
ThemeHandlerInterface $theme_handler,
MessengerInterface $messenger,
TranslationInterface $string_translation,
Registry $theme_registry,
) {
$this->config = $config->get('devel.settings');
$this->account = $account;
$this->themeHandler = $theme_handler;
$this->messenger = $messenger;
$this->stringTranslation = $string_translation;
$this->themeRegistry = $theme_registry;
}
/**
* Forces the system to rebuild the theme registry.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to process.
*/
public function rebuildThemeInfo(RequestEvent $event): void {
if ($this->config->get('rebuild_theme')) {
// Update the theme registry.
$this->themeRegistry->reset();
// Refresh theme data.
$this->themeHandler->refreshInfo();
// Resets the internal state of the theme handler and clear the 'system
// list' cache; this allow to properly register, if needed, PSR-4
// namespaces for theme extensions after refreshing the info data.
$this->themeHandler->reset();
// Notify the user that the theme info are rebuilt on every request.
$this->triggerWarningIfNeeded($event->getRequest());
}
}
/**
* Notifies the user that the theme info are rebuilt on every request.
*
* The warning message is shown only to users with adequate permissions and
* only once per session.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*/
protected function triggerWarningIfNeeded(Request $request) {
if ($this->account->hasPermission('access devel information')) {
$session = $request->getSession();
if (!$session->has($this->notificationFlag)) {
$session->set($this->notificationFlag, TRUE);
$message = $this->t('The theme information is being rebuilt on every request. Remember to <a href=":url">turn off</a> this feature on production websites.', [':url' => Url::fromRoute('devel.admin_settings')->toString()]);
$this->messenger->addWarning($message);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Set high priority value to start as early as possible.
$events[KernelEvents::REQUEST][] = ['rebuildThemeInfo', 256];
return $events;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Form\ConfirmFormHelper;
use Drupal\Core\Form\ConfirmFormInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit config variable form.
*/
class ConfigDeleteForm extends FormBase implements ConfirmFormInterface {
/**
* Logger service.
*/
protected LoggerInterface $logger;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->messenger = $container->get('messenger');
$instance->logger = $container->get('logger.channel.devel');
$instance->configFactory = $container->get('config.factory');
$instance->requestStack = $container->get('request_stack');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_config_system_delete_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $config_name = ''): array {
$config = $this->configFactory->get($config_name);
if ($config->isNew()) {
$this->messenger->addError($this->t('Config @name does not exist in the system.', ['@name' => $config_name]));
return $form;
}
$form['#title'] = $this->getQuestion();
$form['#attributes']['class'][] = 'confirmation';
$form['description'] = ['#markup' => $this->getDescription()];
$form[$this->getFormName()] = ['#type' => 'hidden', '#value' => 1];
// By default, render the form using theme_confirm_form().
if (!isset($form['#theme'])) {
$form['#theme'] = 'confirm_form';
}
$form['name'] = [
'#type' => 'value',
'#value' => $config_name,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#submit' => [
function (array &$form, FormStateInterface $form_state): void {
$this->submitForm($form, $form_state);
},
],
];
$form['actions']['cancel'] = ConfirmFormHelper::buildCancelLink($this, $this->requestStack->getCurrentRequest());
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$config_name = $form_state->getValue('name');
try {
$this->configFactory->getEditable($config_name)->delete();
$this->messenger->addStatus($this->t('Configuration variable %variable was successfully deleted.', ['%variable' => $config_name]));
$this->logger->info('Configuration variable %variable was successfully deleted.', ['%variable' => $config_name]);
$form_state->setRedirectUrl($this->getCancelUrl());
}
catch (\Exception $e) {
$this->messenger->addError($e->getMessage());
$this->logger->error('Error deleting configuration variable %variable : %error.', [
'%variable' => $config_name,
'%error' => $e->getMessage(),
]);
}
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return Url::fromRoute('devel.configs_list');
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete this configuration?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Confirm');
}
/**
* {@inheritdoc}
*/
public function getCancelText() {
return $this->t('Cancel');
}
/**
* {@inheritdoc}
*/
public function getFormName(): string {
return 'confirm';
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Edit config variable form.
*/
class ConfigEditor extends FormBase {
/**
* Logger service.
*/
protected LoggerInterface $logger;
/**
* The dumper service.
*/
protected DevelDumperManagerInterface $dumper;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->messenger = $container->get('messenger');
$instance->logger = $container->get('logger.channel.devel');
$instance->configFactory = $container->get('config.factory');
$instance->requestStack = $container->get('request_stack');
$instance->stringTranslation = $container->get('string_translation');
$instance->dumper = $container->get('devel.dumper');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_config_system_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $config_name = ''): array {
$config = $this->configFactory->get($config_name);
if ($config->isNew()) {
$this->messenger->addError($this->t('Config @name does not exist in the system.', ['@name' => $config_name]));
return $form;
}
$data = $config->getOriginal();
if (empty($data)) {
$this->messenger->addWarning($this->t('Config @name exists but has no data.', ['@name' => $config_name]));
return $form;
}
try {
$output = Yaml::encode($data);
}
catch (InvalidDataTypeException $e) {
$this->messenger->addError($this->t('Invalid data detected for @name : %error', [
'@name' => $config_name,
'%error' => $e->getMessage(),
]));
return $form;
}
$form['current'] = [
'#type' => 'details',
'#title' => $this->t('Current value for %variable', ['%variable' => $config_name]),
'#attributes' => ['class' => ['container-inline']],
];
$form['current']['value'] = [
'#type' => 'item',
'#markup' => $this->dumper->dumpOrExport(input: $output, plugin_id: 'default'),
];
$form['name'] = [
'#type' => 'value',
'#value' => $config_name,
];
$form['new'] = [
'#type' => 'textarea',
'#title' => $this->t('New value'),
'#default_value' => $output,
'#rows' => 24,
'#required' => TRUE,
];
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
$form['actions']['cancel'] = [
'#type' => 'link',
'#title' => $this->t('Cancel'),
'#url' => $this->buildCancelLinkUrl(),
];
$form['actions']['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
'#url' => Url::fromRoute('devel.config_delete', ['config_name' => $config_name]),
'#attributes' => [
'class' => ['button', 'button--danger'],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$value = $form_state->getValue('new');
// Try to parse the new provided value.
try {
$parsed_value = Yaml::decode($value);
// Config::setData needs array for the new configuration and
// a simple string is valid YAML for any reason.
if (is_array($parsed_value)) {
$form_state->setValue('parsed_value', $parsed_value);
}
else {
$form_state->setErrorByName('new', $this->t('Invalid input'));
}
}
catch (InvalidDataTypeException $e) {
$form_state->setErrorByName('new', $this->t('Invalid input: %error', ['%error' => $e->getMessage()]));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
try {
$this->configFactory->getEditable($values['name'])
->setData($values['parsed_value'])
->save();
$this->messenger->addMessage($this->t('Configuration variable %variable was successfully saved.', ['%variable' => $values['name']]));
$this->logger->info('Configuration variable %variable was successfully saved.', ['%variable' => $values['name']]);
$form_state->setRedirectUrl(Url::fromRoute('devel.configs_list'));
}
catch (\Exception $e) {
$this->messenger->addError($e->getMessage());
$this->logger->error('Error saving configuration variable %variable : %error.', [
'%variable' => $values['name'],
'%error' => $e->getMessage(),
]);
}
}
/**
* Builds the cancel link url for the form.
*
* @return \Drupal\Core\Url
* Cancel url
*/
private function buildCancelLinkUrl(): Url {
$query = $this->requestStack->getCurrentRequest()->query;
if ($query->has('destination')) {
$options = UrlHelper::parse($query->get('destination'));
return Url::fromUserInput('/' . ltrim($options['path'], '/'), $options);
}
return Url::fromRoute('devel.configs_list');
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form that displays all the config variables to edit them.
*/
class ConfigsList extends FormBase {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->configFactory = $container->get('config.factory');
$instance->redirectDestination = $container->get('redirect.destination');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_config_system_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $filter = ''): array {
$form['filter'] = [
'#type' => 'details',
'#title' => $this->t('Filter variables'),
'#attributes' => ['class' => ['container-inline']],
'#open' => isset($filter) && trim($filter) !== '',
];
$form['filter']['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Variable name'),
'#title_display' => 'invisible',
'#default_value' => $filter,
];
$form['filter']['actions'] = ['#type' => 'actions'];
$form['filter']['actions']['show'] = [
'#type' => 'submit',
'#value' => $this->t('Filter'),
];
$header = [
'name' => ['data' => $this->t('Name')],
'edit' => ['data' => $this->t('Operations')],
];
$rows = [];
$destination = $this->redirectDestination->getAsArray();
// List all the variables filtered if any filter was provided.
$names = $this->configFactory->listAll($filter);
foreach ($names as $config_name) {
$operations['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('devel.config_edit', ['config_name' => $config_name]),
'query' => $destination,
];
$rows[] = [
'name' => $config_name,
'operation' => [
'data' => [
'#type' => 'operations',
'#links' => $operations,
],
],
];
}
$form['variables'] = [
'#type' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No variables found'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$filter = $form_state->getValue('name');
$form_state->setRedirectUrl(Url::FromRoute('devel.configs_list', ['filter' => Html::escape($filter)]));
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Update\UpdateHookRegistry;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Display a dropdown of installed modules with the option to reinstall them.
*/
class DevelReinstall extends FormBase {
/**
* The module installer.
*/
protected ModuleInstallerInterface $moduleInstaller;
/**
* The module extension list.
*/
protected ModuleExtensionList $moduleExtensionList;
/**
* The update hook registry service.
*/
protected UpdateHookRegistry $updateHookRegistry;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->moduleInstaller = $container->get('module_installer');
$instance->moduleExtensionList = $container->get('extension.list.module');
$instance->updateHookRegistry = $container->get('update.update_hook_registry');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_reinstall_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Get a list of all available modules.
$modules = $this->moduleExtensionList->reset()->getList();
$uninstallable = array_filter($modules, fn($module): bool => empty($modules[$module->getName()]->info['required'])
&& $this->updateHookRegistry->getInstalledVersion($module->getName()) > UpdateHookRegistry::SCHEMA_UNINSTALLED
&& $module->getName() !== 'devel');
$form['filters'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['table-filter', 'js-show'],
],
];
$form['filters']['text'] = [
'#type' => 'search',
'#title' => $this->t('Search'),
'#size' => 30,
'#placeholder' => $this->t('Enter module name'),
'#attributes' => [
'class' => ['table-filter-text'],
'data-table' => '#devel-reinstall-form',
'autocomplete' => 'off',
'title' => $this->t('Enter a part of the module name or description to filter by.'),
],
];
// Only build the rest of the form if there are any modules available to
// uninstall.
if ($uninstallable === []) {
return $form;
}
$header = [
'name' => $this->t('Name'),
'description' => $this->t('Description'),
];
$rows = [];
foreach ($uninstallable as $module) {
$name = $module->info['name'] ?: $module->getName();
$rows[$module->getName()] = [
'name' => [
'data' => [
'#type' => 'inline_template',
'#template' => '<label class="module-name table-filter-text-source">{{ module_name }}</label>',
'#context' => ['module_name' => $name],
],
],
'description' => [
'data' => $module->info['description'],
'class' => ['description'],
],
];
}
$form['reinstall'] = [
'#type' => 'tableselect',
'#header' => $header,
'#options' => $rows,
'#js_select' => FALSE,
'#empty' => $this->t('No modules are available to uninstall.'),
];
$form['#attached']['library'][] = 'system/drupal.system.modules';
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Reinstall'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
// Form submitted, but no modules selected.
if (array_filter($form_state->getValue('reinstall')) === []) {
$form_state->setErrorByName('reinstall', $this->t('No modules selected.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
try {
$modules = $form_state->getValue('reinstall');
$reinstall = array_keys(array_filter($modules));
$this->moduleInstaller->uninstall($reinstall, FALSE);
$this->moduleInstaller->install($reinstall, FALSE);
// @todo Revisit usage of DI once https://www.drupal.org/project/drupal/issues/2940148 is resolved.
$this->messenger()->addMessage($this->t('Uninstalled and installed: %names.', ['%names' => implode(', ', $reinstall)]));
}
catch (\Exception $e) {
$this->messenger()->addError($this->t('Unable to reinstall modules. Error: %error.', ['%error' => $e->getMessage()]));
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides confirmation form for rebuilding the routes.
*/
class RouterRebuildConfirmForm extends ConfirmFormBase {
/**
* The route builder service.
*/
protected RouteBuilderInterface $routeBuilder;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->routeBuilder = $container->get('router.builder');
$instance->messenger = $container->get('messenger');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_menu_rebuild';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to rebuild the router?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl(): Url {
return new Url('<front>');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Rebuilds the routes information gathering all routing data from .routing.yml files and from classes which subscribe to the route build events. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Rebuild');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->routeBuilder->rebuild();
$this->messenger->addMessage($this->t('The router has been rebuilt.'));
$form_state->setRedirect('<front>');
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Config\Config;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\devel\DevelDumperPluginManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a form that configures devel settings.
*/
class SettingsForm extends ConfigFormBase {
protected DevelDumperPluginManagerInterface $dumperManager;
/**
* The 'devel.settings' config object.
*/
protected Config $config;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->dumperManager = $container->get('plugin.manager.devel_dumper');
$instance->config = $container->get('config.factory')->getEditable('devel.settings');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_admin_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return [
'devel.settings',
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL): array {
$current_url = Url::createFromRequest($request);
$form['page_alter'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display $attachments array'),
'#default_value' => $this->config->get('page_alter'),
'#description' => $this->t('Display $attachments array from <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21theme.api.php/function/hook_page_attachments_alter/10">hook_page_attachments_alter()</a> in the messages area of each page.'),
];
$form['raw_names'] = [
'#type' => 'checkbox',
'#title' => $this->t('Display machine names of permissions and modules'),
'#default_value' => $this->config->get('raw_names'),
'#description' => $this->t('Display the language-independent machine names of the permissions in mouse-over hints on the <a href=":permissions_url">Permissions</a> page and the module base file names on the Permissions and <a href=":modules_url">Modules</a> pages.', [
':permissions_url' => Url::fromRoute('user.admin_permissions')->toString(),
':modules_url' => Url::fromRoute('system.modules_list')->toString(),
]),
];
$form['rebuild_theme'] = [
'#type' => 'checkbox',
'#title' => $this->t('Rebuild the theme registry on every page load'),
'#description' => $this->t('New templates, theme overrides, and changes to the theme.info.yml need the theme registry to be rebuilt in order to appear on the site.'),
'#default_value' => $this->config->get('rebuild_theme'),
];
$error_handlers = devel_get_handlers();
$form['error_handlers'] = [
'#type' => 'select',
'#title' => $this->t('Error handlers'),
'#options' => [
DEVEL_ERROR_HANDLER_NONE => $this->t('None'),
DEVEL_ERROR_HANDLER_STANDARD => $this->t('Standard Drupal'),
DEVEL_ERROR_HANDLER_BACKTRACE_DPM => $this->t('Backtrace in the message area'),
DEVEL_ERROR_HANDLER_BACKTRACE_KINT => $this->t('Backtrace above the rendered page'),
],
'#multiple' => TRUE,
'#default_value' => empty($error_handlers) ? DEVEL_ERROR_HANDLER_NONE : $error_handlers,
'#description' => [
[
'#markup' => $this->t('Select the error handler(s) to use, in case you <a href=":choose">choose to show errors on screen</a>.', [':choose' => Url::fromRoute('system.logging_settings')->toString()]),
],
[
'#theme' => 'item_list',
'#items' => [
$this->t('<em>None</em> is a good option when stepping through the site in your debugger.'),
$this->t('<em>Standard Drupal</em> does not display all the information that is often needed to resolve an issue.'),
$this->t('<em>Backtrace</em> displays nice debug information when any type of error is noticed, but only to users with the %perm permission.', ['%perm' => $this->t('Access developer information')]),
],
],
[
'#markup' => $this->t('Depending on the situation, the theme, the size of the call stack and the arguments, etc., some handlers may not display their messages, or display them on the subsequent page. Select <em>Standard Drupal</em> <strong>and</strong> <em>Backtrace above the rendered page</em> to maximize your chances of not missing any messages.') . '<br />' .
$this->t('Demonstrate the current error handler(s):') . ' ' .
Link::fromTextAndUrl('notice', $current_url->setOption('query', ['demo' => 'notice']))->toString() . ', ' .
Link::fromTextAndUrl('notice+warning', $current_url->setOption('query', ['demo' => 'warning']))->toString() . ', ' .
Link::fromTextAndUrl('notice+warning+error', $current_url->setOption('query', ['demo' => 'error']))->toString() . ' (' .
$this->t('The presentation of the @error is determined by PHP.', ['@error' => 'error']) . ')',
],
],
];
$form['error_handlers']['#size'] = count($form['error_handlers']['#options']);
if ($request->query->has('demo')) {
if ($request->getMethod() === 'GET') {
$this->demonstrateErrorHandlers($request->query->get('demo'));
}
$request->query->remove('demo');
}
$dumper = $this->config->get('devel_dumper');
$default = $this->dumperManager->isPluginSupported($dumper) ? $dumper : $this->dumperManager->getFallbackPluginId('');
$form['dumper'] = [
'#type' => 'radios',
'#title' => $this->t('Variables Dumper'),
'#options' => [],
'#default_value' => $default,
'#description' => $this->t('Select the debugging tool used for formatting and displaying the variables inspected through the debug functions of Devel. <strong>NOTE</strong>: Some of these plugins require external libraries for to be enabled. Learn how install external libraries with <a href=":url">Composer</a>.', [
':url' => 'https://www.drupal.org/node/2404989',
]),
];
foreach ($this->dumperManager->getDefinitions() as $id => $definition) {
$form['dumper']['#options'][$id] = $definition['label'];
$supported = $this->dumperManager->isPluginSupported($id);
$form['dumper'][$id]['#disabled'] = !$supported;
$form['dumper'][$id]['#description'] = [
'#type' => 'inline_template',
'#template' => '{{ description }}{% if not supported %}<div><small>{% trans %}<strong>Not available</strong>. You may need to install external dependencies for use this plugin.{% endtrans %}</small></div>{% endif %}',
'#context' => [
'description' => $definition['description'],
'supported' => $supported,
],
];
}
// Allow custom debug filename for use in DevelDumperManager::debug()
$default_file = $this->config->get('debug_logfile') ?: 'temporary://drupal_debug.txt';
$form['debug_logfile'] = [
'#type' => 'textfield',
'#title' => $this->t('Debug Log File'),
'#description' => $this->t('This is the log file that Devel functions such as ddm() write to. Use temporary:// to represent your systems temporary directory. Save with a blank filename to revert to the default.'),
'#default_value' => $default_file,
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$values = $form_state->getValues();
$this->config
->set('page_alter', $values['page_alter'])
->set('raw_names', $values['raw_names'])
->set('error_handlers', $values['error_handlers'])
->set('rebuild_theme', $values['rebuild_theme'])
->set('devel_dumper', $values['dumper'])
->set('debug_logfile', $values['debug_logfile'] ?: 'temporary://drupal_debug.txt')
->save();
parent::submitForm($form, $form_state);
}
/**
* Demonstrates the capabilities of the error handler.
*
* @param string $severity
* The severity level for which demonstrate the error handler capabilities.
*/
protected function demonstrateErrorHandlers(string $severity): void {
switch ($severity) {
case 'notice':
trigger_error('This is an example notice', E_USER_NOTICE);
break;
case 'warning':
trigger_error('This is an example notice', E_USER_NOTICE);
trigger_error('This is an example warning', E_USER_WARNING);
break;
case 'error':
trigger_error('This is an example notice', E_USER_NOTICE);
trigger_error('This is an example warning', E_USER_WARNING);
trigger_error('This is an example error', E_USER_ERROR);
}
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\devel\Form;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Define a form to allow the user to switch and become another user.
*/
class SwitchUserForm extends FormBase {
/**
* The csrf token generator.
*/
protected CsrfTokenGenerator $csrfToken;
/**
* The user storage.
*/
protected UserStorageInterface $userStorage;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->csrfToken = $container->get('csrf_token');
$instance->userStorage = $container->get('entity_type.manager')->getStorage('user');
$instance->stringTranslation = $container->get('string_translation');
return $instance;
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'devel_switchuser_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['autocomplete'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['container-inline'],
],
];
$form['autocomplete']['userid'] = [
'#type' => 'entity_autocomplete',
'#title' => $this->t('Username'),
'#placeholder' => $this->t('Enter username'),
'#target_type' => 'user',
'#selection_settings' => [
'include_anonymous' => FALSE,
],
'#process_default_value' => FALSE,
'#title_display' => 'invisible',
'#required' => TRUE,
'#size' => '28',
];
$form['autocomplete']['actions'] = ['#type' => 'actions'];
$form['autocomplete']['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Switch'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
$userId = $form_state->getValue('userid');
if ($userId === NULL) {
$form_state->setErrorByName('userid', $this->t('Username not found'));
return;
}
/** @var \Drupal\user\UserInterface|null $account */
$account = $this->userStorage->load($userId);
if ($account === NULL) {
$form_state->setErrorByName('userid', $this->t('Username not found'));
}
else {
$form_state->setValue('username', $account->getAccountName());
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// We cannot rely on automatic token creation, since the csrf seed changes
// after the redirect and the generated token is not more valid.
// @todo find another way to do this.
$url = Url::fromRoute('devel.switch', ['name' => $form_state->getValue('username')]);
$url->setOption('query', ['token' => $this->csrfToken->get($url->getInternalPath())]);
$form_state->setRedirectUrl($url);
}
}

Some files were not shown because too many files have changed in this diff Show More