Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

297
.ddev/config.yaml Normal file
View File

@ -0,0 +1,297 @@
name: drupal11-ddev
type: drupal11
docroot: web
php_version: "8.3"
webserver_type: nginx-fpm
xdebug_enabled: false
additional_hostnames: []
additional_fqdns: []
database:
type: mariadb
version: "10.11"
use_dns_when_possible: true
composer_version: "2"
web_environment: []
corepack_enable: true
# Key features of DDEV's config.yaml:
# name: <projectname> # Name of the project, automatically provides
# http://projectname.ddev.site and https://projectname.ddev.site
# If the name is omitted, the project will take the name of the enclosing directory,
# which is useful if you want to have a copy of the project side by side with this one.
# type: <projecttype> # backdrop, cakephp, craftcms, drupal, drupal6, drupal7, drupal8, drupal9, drupal10, drupal11, generic, laravel, magento, magento2, php, shopware6, silverstripe, symfony, typo3, wordpress
# See https://docs.ddev.com/en/stable/users/quickstart/ for more
# information on the different project types
# docroot: <relative_path> # Relative path to the directory containing index.php.
# php_version: "8.3" # PHP version to use, "5.6" through "8.4"
# You can explicitly specify the webimage but this
# is not recommended, as the images are often closely tied to DDEV's' behavior,
# so this can break upgrades.
# webimage: <docker_image>
# Its unusual to change this option, and we dont recommend it without Docker experience and a good reason.
# Typically, this means additions to the existing web image using a .ddev/web-build/Dockerfile.*
# database:
# type: <dbtype> # mysql, mariadb, postgres
# version: <version> # database version, like "10.11" or "8.0"
# MariaDB versions can be 5.5-10.8, 10.11, 11.4, 11.8
# MySQL versions can be 5.5-8.0, 8.4
# PostgreSQL versions can be 9-17
# router_http_port: <port> # Port to be used for http (defaults to global configuration, usually 80)
# router_https_port: <port> # Port for https (defaults to global configuration, usually 443)
# xdebug_enabled: false # Set to true to enable Xdebug and "ddev start" or "ddev restart"
# Note that for most people the commands
# "ddev xdebug" to enable Xdebug and "ddev xdebug off" to disable it work better,
# as leaving Xdebug enabled all the time is a big performance hit.
# xhgui_http_port: "8143"
# xhgui_https_port: "8142"
# The XHGui ports can be changed from the default 8143 and 8142
# Very rarely used
# host_xhgui_port: "8142"
# Can be used to change the host binding port of the XHGui
# application. Rarely used; only when port conflict and
# bind_all_ports is used (normally with router disabled)
# xhprof_mode: [prepend|xhgui|global]
# Set to "xhgui" to enable XHGui features
# "xhgui" will become default in a future major release
# webserver_type: nginx-fpm, apache-fpm, generic
# timezone: Europe/Berlin
# If timezone is unset, DDEV will attempt to derive it from the host system timezone
# using the $TZ environment variable or the /etc/localtime symlink.
# This is the timezone used in the containers and by PHP;
# it can be set to any valid timezone,
# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# For example Europe/Dublin or MST7MDT
# composer_root: <relative_path>
# Relative path to the Composer root directory from the project root. This is
# the directory which contains the composer.json and where all Composer related
# commands are executed.
# composer_version: "2"
# You can set it to "" or "2" (default) for Composer v2 or "1" for Composer v1
# to use the latest major version available at the time your container is built.
# It is also possible to use each other Composer version channel. This includes:
# - 2.2 (latest Composer LTS version)
# - stable
# - preview
# - snapshot
# Alternatively, an explicit Composer version may be specified, for example "2.2.18".
# To reinstall Composer after the image was built, run "ddev debug rebuild".
# nodejs_version: "22"
# change from the default system Node.js version to any other version.
# See https://docs.ddev.com/en/stable/users/configuration/config/#nodejs_version for more information
# and https://www.npmjs.com/package/n#specifying-nodejs-versions for the full documentation,
# Note that using of 'ddev nvm' is discouraged because "nodejs_version" is much easier to use,
# can specify any version, and is more robust than using 'nvm'.
# corepack_enable: false
# Change to 'true' to 'corepack enable' and gain access to latest versions of yarn/pnpm
# additional_hostnames:
# - somename
# - someothername
# would provide http and https URLs for "somename.ddev.site"
# and "someothername.ddev.site".
# additional_fqdns:
# - example.com
# - sub1.example.com
# would provide http and https URLs for "example.com" and "sub1.example.com"
# Please take care with this because it can cause great confusion.
# upload_dirs: "custom/upload/dir"
#
# upload_dirs:
# - custom/upload/dir
# - ../private
#
# would set the destination paths for ddev import-files to <docroot>/custom/upload/dir
# When Mutagen is enabled this path is bind-mounted so that all the files
# in the upload_dirs don't have to be synced into Mutagen.
# disable_upload_dirs_warning: false
# If true, turns off the normal warning that says
# "You have Mutagen enabled and your 'php' project type doesn't have upload_dirs set"
# ddev_version_constraint: ""
# Example:
# ddev_version_constraint: ">= 1.24.8"
# This will enforce that the running ddev version is within this constraint.
# See https://github.com/Masterminds/semver#checking-version-constraints for
# supported constraint formats
# working_dir:
# web: /var/www/html
# db: /home
# would set the default working directory for the web and db services.
# These values specify the destination directory for ddev ssh and the
# directory in which commands passed into ddev exec are run.
# omit_containers: [db, ddev-ssh-agent]
# Currently only these containers are supported. Some containers can also be
# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit
# the "db" container, several standard features of DDEV that access the
# database container will be unusable. In the global configuration it is also
# possible to omit ddev-router, but not here.
# performance_mode: "global"
# DDEV offers performance optimization strategies to improve the filesystem
# performance depending on your host system. Should be configured globally.
#
# If set, will override the global config. Possible values are:
# - "global": uses the value from the global config.
# - "none": disables performance optimization for this project.
# - "mutagen": enables Mutagen for this project.
# - "nfs": enables NFS for this project.
#
# See https://docs.ddev.com/en/stable/users/install/performance/#nfs
# See https://docs.ddev.com/en/stable/users/install/performance/#mutagen
# fail_on_hook_fail: False
# Decide whether 'ddev start' should be interrupted by a failing hook
# host_https_port: "59002"
# The host port binding for https can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
# host_webserver_port: "59001"
# The host port binding for the ddev-webserver can be explicitly specified. It is
# dynamic unless otherwise specified.
# This is not used by most people, most people use the *router* instead
# of the localhost port.
# host_db_port: "59002"
# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic
# unless explicitly specified.
# mailpit_http_port: "8025"
# mailpit_https_port: "8026"
# The Mailpit ports can be changed from the default 8025 and 8026
# host_mailpit_port: "8025"
# The mailpit port is not normally bound on the host at all, instead being routed
# through ddev-router, but it can be bound directly to localhost if specified here.
# webimage_extra_packages: [php7.4-tidy, php-bcmath]
# Extra Debian packages that are needed in the webimage can be added here
# dbimage_extra_packages: [telnet,netcat]
# Extra Debian packages that are needed in the dbimage can be added here
# use_dns_when_possible: true
# If the host has internet access and the domain configured can
# successfully be looked up, DNS will be used for hostname resolution
# instead of editing /etc/hosts
# Defaults to true
# project_tld: ddev.site
# The top-level domain used for project URLs
# The default "ddev.site" allows DNS lookup via a wildcard
# If you prefer you can change this to "ddev.local" to preserve
# pre-v1.9 behavior.
# ngrok_args: --basic-auth username:pass1234
# Provide extra flags to the "ngrok http" command, see
# https://ngrok.com/docs/agent/config/v3/#agent-configuration or run "ngrok http -h"
# disable_settings_management: false
# If true, DDEV will not create CMS-specific settings files like
# Drupal's settings.php/settings.ddev.php or TYPO3's additional.php
# In this case the user must provide all such settings.
# You can inject environment variables into the web container with:
# web_environment:
# - SOMEENV=somevalue
# - SOMEOTHERENV=someothervalue
# no_project_mount: false
# (Experimental) If true, DDEV will not mount the project into the web container;
# the user is responsible for mounting it manually or via a script.
# This is to enable experimentation with alternate file mounting strategies.
# For advanced users only!
# bind_all_interfaces: false
# If true, host ports will be bound on all network interfaces,
# not the localhost interface only. This means that ports
# will be available on the local network if the host firewall
# allows it.
# default_container_timeout: 120
# The default time that DDEV waits for all containers to become ready can be increased from
# the default 120. This helps in importing huge databases, for example.
#web_extra_exposed_ports:
#- name: nodejs
# container_port: 3000
# http_port: 2999
# https_port: 3000
#- name: something
# container_port: 4000
# https_port: 4000
# http_port: 3999
# Allows a set of extra ports to be exposed via ddev-router
# Fill in all three fields even if you dont intend to use the https_port!
# If you dont add https_port, then it defaults to 0 and ddev-router will fail to start.
#
# The port behavior on the ddev-webserver must be arranged separately, for example
# using web_extra_daemons.
# For example, with a web app on port 3000 inside the container, this config would
# expose that web app on https://<project>.ddev.site:9999 and http://<project>.ddev.site:9998
# web_extra_exposed_ports:
# - name: myapp
# container_port: 3000
# http_port: 9998
# https_port: 9999
#web_extra_daemons:
#- name: "http-1"
# command: "/var/www/html/node_modules/.bin/http-server -p 3000"
# directory: /var/www/html
#- name: "http-2"
# command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000"
# directory: /var/www/html
# override_config: false
# By default, config.*.yaml files are *merged* into the configuration
# But this means that some things can't be overridden
# For example, if you have 'use_dns_when_possible: true'' you can't override it with a merge
# and you can't erase existing hooks or all environment variables.
# However, with "override_config: true" in a particular config.*.yaml file,
# 'use_dns_when_possible: false' can override the existing values, and
# hooks:
# post-start: []
# or
# web_environment: []
# or
# additional_hostnames: []
# can have their intended affect. 'override_config' affects only behavior of the
# config.*.yaml file it exists in.
# Many DDEV commands can be extended to run tasks before or after the
# DDEV command is executed, for example "post-start", "post-import-db",
# "pre-composer", "post-composer"
# See https://docs.ddev.com/en/stable/users/extend/custom-commands/ for more
# information on the commands that can be extended and the tasks you can define
# for them. Example:
#hooks:
# post-import-db:
# - exec: drush sql:sanitize
# - exec: drush updatedb
# - exec: drush cache:rebuild

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# Drupal editor configuration normalization
# @see http://editorconfig.org/
# This is the top-most .editorconfig file; do not search in parent directories.
root = true
# All files.
[*]
end_of_line = LF
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[composer.{json,lock}]
indent_size = 4

65
.gitattributes vendored Normal file
View File

@ -0,0 +1,65 @@
# Drupal git normalization
# @see https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
# @see https://www.drupal.org/node/1542048
# Normally these settings would be done with macro attributes for improved
# readability and easier maintenance. However macros can only be defined at the
# repository root directory. Drupal avoids making any assumptions about where it
# is installed.
# Define text file attributes.
# - Treat them as text.
# - Ensure no CRLF line-endings, neither on checkout nor on checkin.
# - Detect whitespace errors.
# - Exposed by default in `git diff --color` on the CLI.
# - Validate with `git diff --check`.
# - Deny applying with `git apply --whitespace=error-all`.
# - Fix automatically with `git apply --whitespace=fix`.
*.config text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.css text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.dist text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.engine text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.html text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=html
*.inc text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.install text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.js text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.json text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.lock text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.map text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.md text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.module text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.php text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.po text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.profile text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.script text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.sh text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.sql text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.svg text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.theme text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2 diff=php linguist-language=php
*.twig text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.txt text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.xml text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
*.yml text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tab-in-indent,tabwidth=2
# PHPStan's baseline uses tabs instead of spaces.
core/.phpstan-baseline.php text eol=lf whitespace=blank-at-eol,-blank-at-eof,-space-before-tab,tabwidth=2 diff=php linguist-language=php
# Define binary file attributes.
# - Do not treat them as text.
# - Include binary diff in patches instead of "binary files differ."
*.avif -text diff
*.eot -text diff
*.exe -text diff
*.gif -text diff
*.gz -text diff
*.ico -text diff
*.jpeg -text diff
*.jpg -text diff
*.otf -text diff
*.phar -text diff
*.png -text diff
*.svgz -text diff
*.ttf -text diff
*.woff -text diff
*.woff2 -text diff

106
composer.json Normal file
View File

@ -0,0 +1,106 @@
{
"name": "drupal/recommended-project",
"description": "Project template for Drupal projects with a relocated document root",
"type": "project",
"license": "GPL-2.0-or-later",
"homepage": "https://www.drupal.org/project/drupal",
"support": {
"docs": "https://www.drupal.org/docs/user_guide/en/index.html",
"chat": "https://www.drupal.org/node/314178"
},
"repositories": [
{
"type": "composer",
"url": "https://packages.drupal.org/8"
}
],
"require": {
"composer/installers": "^2.3",
"drupal/core-composer-scaffold": "^11.2",
"drupal/core-project-message": "^11.2",
"drupal/core-recipe-unpack": "^11.2",
"drupal/core-recommended": "^11.2",
"drush/drush": "^13.6"
},
"conflict": {
"drupal/drupal": "*"
},
"minimum-stability": "stable",
"prefer-stable": true,
"config": {
"allow-plugins": {
"composer/installers": true,
"drupal/core-composer-scaffold": true,
"drupal/core-recipe-unpack": true,
"drupal/core-project-message": true,
"phpstan/extension-installer": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"php-http/discovery": true,
"php-tuf/composer-integration": true
},
"sort-packages": true
},
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "web/"
}
},
"installer-paths": {
"web/core": [
"type:drupal-core"
],
"web/libraries/{$name}": [
"type:drupal-library"
],
"web/modules/contrib/{$name}": [
"type:drupal-module"
],
"web/profiles/contrib/{$name}": [
"type:drupal-profile"
],
"web/themes/contrib/{$name}": [
"type:drupal-theme"
],
"drush/Commands/contrib/{$name}": [
"type:drupal-drush"
],
"web/modules/custom/{$name}": [
"type:drupal-custom-module"
],
"web/profiles/custom/{$name}": [
"type:drupal-custom-profile"
],
"web/themes/custom/{$name}": [
"type:drupal-custom-theme"
],
"recipes/{$name}": [
"type:drupal-recipe"
]
},
"drupal-core-project-message": {
"include-keys": [
"homepage",
"support"
],
"post-create-project-cmd-message": [
"<bg=blue;fg=white> </>",
"<bg=blue;fg=white> Congratulations, youve installed the Drupal codebase </>",
"<bg=blue;fg=white> from the drupal/recommended-project template! </>",
"<bg=blue;fg=white> </>",
"",
"<bg=yellow;fg=black>Next steps</>:",
" * Install the site: https://www.drupal.org/docs/installing-drupal",
" * Read the user guide: https://www.drupal.org/docs/user_guide/en/index.html",
" * Get support: https://www.drupal.org/support",
" * Get involved with the Drupal community:",
" https://www.drupal.org/getting-involved",
" * Remove the plugin that prints this message:",
" composer remove drupal/core-project-message"
]
}
},
"require-dev": {
"drupal/devel": "^5.4"
}
}

6582
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
recipes/README.txt Normal file
View File

@ -0,0 +1,9 @@
Recipes allow the automation of Drupal module and theme installation and
configuration.
WHAT TO PLACE IN THIS DIRECTORY?
--------------------------------
Placing downloaded and custom recipes in this directory separates downloaded and
custom recipes from Drupal core's recipes. This allows Drupal core to be updated
without overwriting these files.

19
vendor/asm89/stack-cors/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2013-2017 Alexander <iam.asm89@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

85
vendor/asm89/stack-cors/README.md vendored Normal file
View File

@ -0,0 +1,85 @@
# Stack/Cors
Library and middleware enabling cross-origin resource sharing for your
http-{foundation,kernel} using application. It attempts to implement the
[W3C Recommendation] for cross-origin resource sharing.
[W3C Recommendation]: http://www.w3.org/TR/cors/
Build status: ![.github/workflows/run-tests.yml](https://github.com/asm89/stack-cors/workflows/.github/workflows/run-tests.yml/badge.svg)
## Installation
Require `asm89/stack-cors` using composer.
## Usage
This package can be used as a library or as [stack middleware].
[stack middleware]: http://stackphp.com/
### Options
| Option | Description | Default value |
|------------------------|------------------------------------------------------------|---------------|
| `allowedMethods` | Matches the request method. | `[]` |
| `allowedOrigins` | Matches the request origin. | `[]` |
| `allowedOriginsPatterns` | Matches the request origin with `preg_match`. | `[]` |
| `allowedHeaders` | Sets the Access-Control-Allow-Headers response header. | `[]` |
| `exposedHeaders` | Sets the Access-Control-Expose-Headers response header. | `false` |
| `maxAge` | Sets the Access-Control-Max-Age response header.<br/>Set to `null` to omit the header/use browser default. | `0` |
| `supportsCredentials` | Sets the Access-Control-Allow-Credentials header. | `false` |
The _allowedMethods_ and _allowedHeaders_ options are case-insensitive.
You don't need to provide both _allowedOrigins_ and _allowedOriginsPatterns_. If one of the strings passed matches, it is considered a valid origin.
If `['*']` is provided to _allowedMethods_, _allowedOrigins_ or _allowedHeaders_ all methods / origins / headers are allowed.
If _supportsCredentials_ is `true`, you must [explicitly set](https://fetch.spec.whatwg.org/#cors-protocol-and-credentials) `allowedHeaders` for any headers which are not CORS safelisted.
### Example: using the library
```php
<?php
use Asm89\Stack\CorsService;
$cors = new CorsService([
'allowedHeaders' => ['x-allowed-header', 'x-other-allowed-header'],
'allowedMethods' => ['DELETE', 'GET', 'POST', 'PUT'],
'allowedOrigins' => ['http://localhost'],
'allowedOriginsPatterns' => ['/localhost:\d/'],
'exposedHeaders' => false,
'maxAge' => 600,
'supportsCredentials' => true,
]);
$cors->addActualRequestHeaders(Response $response, $origin);
$cors->handlePreflightRequest(Request $request);
$cors->isActualRequestAllowed(Request $request);
$cors->isCorsRequest(Request $request);
$cors->isPreflightRequest(Request $request);
```
## Example: using the stack middleware
```php
<?php
use Asm89\Stack\Cors;
$app = new Cors($app, [
// you can use ['*'] to allow any headers
'allowedHeaders' => ['x-allowed-header', 'x-other-allowed-header'],
// you can use ['*'] to allow any methods
'allowedMethods' => ['DELETE', 'GET', 'POST', 'PUT'],
// you can use ['*'] to allow requests from any origin
'allowedOrigins' => ['localhost'],
// you can enter regexes that are matched to the origin request header
'allowedOriginsPatterns' => ['/localhost:\d/'],
'exposedHeaders' => false,
'maxAge' => 600,
'supportsCredentials' => false,
]);
```

45
vendor/asm89/stack-cors/composer.json vendored Normal file
View File

@ -0,0 +1,45 @@
{
"name": "asm89/stack-cors",
"description": "Cross-origin resource sharing library and stack middleware",
"keywords": ["stack", "cors"],
"homepage": "https://github.com/asm89/stack-cors",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Alexander",
"email": "iam.asm89@gmail.com"
}
],
"require": {
"php": "^7.3|^8.0",
"symfony/http-foundation": "^5.3|^6|^7",
"symfony/http-kernel": "^5.3|^6|^7"
},
"require-dev": {
"phpunit/phpunit": "^9",
"squizlabs/php_codesniffer": "^3.5"
},
"autoload": {
"psr-4": {
"Asm89\\Stack\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Asm89\\Stack\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"check-style": "phpcs -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src",
"fix-style": "phpcbf -p --standard=PSR12 --exclude=Generic.Files.LineLength --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src"
},
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
},
"minimum-stability": "beta",
"prefer-stable": true
}

61
vendor/asm89/stack-cors/src/Cors.php vendored Normal file
View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of asm89/stack-cors.
*
* (c) Alexander <iam.asm89@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Asm89\Stack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
class Cors implements HttpKernelInterface
{
/**
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
private $app;
/**
* @var \Asm89\Stack\CorsService
*/
private $cors;
private $defaultOptions = [
'allowedHeaders' => [],
'allowedMethods' => [],
'allowedOrigins' => [],
'allowedOriginsPatterns' => [],
'exposedHeaders' => [],
'maxAge' => 0,
'supportsCredentials' => false,
];
public function __construct(HttpKernelInterface $app, array $options = [])
{
$this->app = $app;
$this->cors = new CorsService(array_merge($this->defaultOptions, $options));
}
public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
{
if ($this->cors->isPreflightRequest($request)) {
$response = $this->cors->handlePreflightRequest($request);
return $this->cors->varyHeader($response, 'Access-Control-Request-Method');
}
$response = $this->app->handle($request, $type, $catch);
if ($request->getMethod() === 'OPTIONS') {
$this->cors->varyHeader($response, 'Access-Control-Request-Method');
}
return $this->cors->addActualRequestHeaders($response, $request);
}
}

View File

@ -0,0 +1,228 @@
<?php
/*
* This file is part of asm89/stack-cors.
*
* (c) Alexander <iam.asm89@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Asm89\Stack;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CorsService
{
private $options;
public function __construct(array $options = [])
{
$this->options = $this->normalizeOptions($options);
}
private function normalizeOptions(array $options = []): array
{
$options += [
'allowedOrigins' => [],
'allowedOriginsPatterns' => [],
'supportsCredentials' => false,
'allowedHeaders' => [],
'exposedHeaders' => [],
'allowedMethods' => [],
'maxAge' => 0,
];
// normalize array('*') to true
if (in_array('*', $options['allowedOrigins'])) {
$options['allowedOrigins'] = true;
}
if (in_array('*', $options['allowedHeaders'])) {
$options['allowedHeaders'] = true;
} else {
$options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']);
}
if (in_array('*', $options['allowedMethods'])) {
$options['allowedMethods'] = true;
} else {
$options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']);
}
return $options;
}
/**
* @deprecated use isOriginAllowed
*/
public function isActualRequestAllowed(Request $request): bool
{
return $this->isOriginAllowed($request);
}
public function isCorsRequest(Request $request): bool
{
return $request->headers->has('Origin');
}
public function isPreflightRequest(Request $request): bool
{
return $request->getMethod() === 'OPTIONS' && $request->headers->has('Access-Control-Request-Method');
}
public function handlePreflightRequest(Request $request): Response
{
$response = new Response();
$response->setStatusCode(204);
return $this->addPreflightRequestHeaders($response, $request);
}
public function addPreflightRequestHeaders(Response $response, Request $request): Response
{
$this->configureAllowedOrigin($response, $request);
if ($response->headers->has('Access-Control-Allow-Origin')) {
$this->configureAllowCredentials($response, $request);
$this->configureAllowedMethods($response, $request);
$this->configureAllowedHeaders($response, $request);
$this->configureMaxAge($response, $request);
}
return $response;
}
public function isOriginAllowed(Request $request): bool
{
if ($this->options['allowedOrigins'] === true) {
return true;
}
if (!$request->headers->has('Origin')) {
return false;
}
$origin = $request->headers->get('Origin');
if (in_array($origin, $this->options['allowedOrigins'])) {
return true;
}
foreach ($this->options['allowedOriginsPatterns'] as $pattern) {
if (preg_match($pattern, $origin)) {
return true;
}
}
return false;
}
public function addActualRequestHeaders(Response $response, Request $request): Response
{
$this->configureAllowedOrigin($response, $request);
if ($response->headers->has('Access-Control-Allow-Origin')) {
$this->configureAllowCredentials($response, $request);
$this->configureExposedHeaders($response, $request);
}
return $response;
}
private function configureAllowedOrigin(Response $response, Request $request)
{
if ($this->options['allowedOrigins'] === true && !$this->options['supportsCredentials']) {
// Safe+cacheable, allow everything
$response->headers->set('Access-Control-Allow-Origin', '*');
} elseif ($this->isSingleOriginAllowed()) {
// Single origins can be safely set
$response->headers->set('Access-Control-Allow-Origin', array_values($this->options['allowedOrigins'])[0]);
} else {
// For dynamic headers, set the requested Origin header when set and allowed
if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) {
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
}
$this->varyHeader($response, 'Origin');
}
}
private function isSingleOriginAllowed(): bool
{
if ($this->options['allowedOrigins'] === true || !empty($this->options['allowedOriginsPatterns'])) {
return false;
}
return count($this->options['allowedOrigins']) === 1;
}
private function configureAllowedMethods(Response $response, Request $request)
{
if ($this->options['allowedMethods'] === true) {
$allowMethods = strtoupper($request->headers->get('Access-Control-Request-Method'));
$this->varyHeader($response, 'Access-Control-Request-Method');
} else {
$allowMethods = implode(', ', $this->options['allowedMethods']);
}
$response->headers->set('Access-Control-Allow-Methods', $allowMethods);
}
private function configureAllowedHeaders(Response $response, Request $request)
{
if ($this->options['allowedHeaders'] === true) {
$allowHeaders = $request->headers->get('Access-Control-Request-Headers');
$this->varyHeader($response, 'Access-Control-Request-Headers');
} else {
$allowHeaders = implode(', ', $this->options['allowedHeaders']);
}
$response->headers->set('Access-Control-Allow-Headers', $allowHeaders);
}
private function configureAllowCredentials(Response $response, Request $request)
{
if ($this->options['supportsCredentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
}
private function configureExposedHeaders(Response $response, Request $request)
{
if ($this->options['exposedHeaders']) {
$response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders']));
}
}
private function configureMaxAge(Response $response, Request $request)
{
if ($this->options['maxAge'] !== null) {
$response->headers->set('Access-Control-Max-Age', (int) $this->options['maxAge']);
}
}
public function varyHeader(Response $response, $header): Response
{
$vary = $response->getVary();
if (!in_array($header, $vary, true)) {
if (count($response->headers->all('Vary')) === 1) {
$response->setVary($response->headers->get('Vary') . ', ' . $header, true);
} else {
$response->setVary($header, false);
}
}
return $response;
}
private function isSameHost(Request $request): bool
{
return $request->headers->get('Origin') === $request->getSchemeAndHttpHost();
}
}

22
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit9d559ab354f63dc7e9a3fc1a03ad7d46::getLoader();

119
vendor/bin/dcg vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../chi-teck/drupal-code-generator/bin/dcg)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/chi-teck/drupal-code-generator/bin/dcg');
}
}
return include __DIR__ . '/..'.'/chi-teck/drupal-code-generator/bin/dcg';

37
vendor/bin/drush vendored Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env sh
# Support bash to support `source` with fallback on $0 if this does not run with bash
# https://stackoverflow.com/a/35006505/6512
selfArg="$BASH_SOURCE"
if [ -z "$selfArg" ]; then
selfArg="$0"
fi
self=$(realpath "$selfArg" 2> /dev/null)
if [ -z "$self" ]; then
self="$selfArg"
fi
dir=$(cd "${self%[/\\]*}" > /dev/null; cd '../drush/drush' && pwd)
if [ -d /proc/cygdrive ]; then
case $(which php) in
$(readlink -n /proc/cygdrive)/*)
# We are in Cygwin using Windows php, so the path must be translated
dir=$(cygpath -m "$dir");
;;
esac
fi
export COMPOSER_RUNTIME_BIN_DIR="$(cd "${self%[/\\]*}" > /dev/null; pwd)"
# If bash is sourcing this file, we have to source the target as well
bashSource="$BASH_SOURCE"
if [ -n "$bashSource" ]; then
if [ "$bashSource" != "$0" ]; then
source "${dir}/drush" "$@"
return
fi
fi
exec "${dir}/drush" "$@"

119
vendor/bin/drush.php vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../drush/drush/drush.php)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/drush/drush/drush.php');
}
}
return include __DIR__ . '/..'.'/drush/drush/drush.php';

119
vendor/bin/patch-type-declarations vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/error-handler/Resources/bin/patch-type-declarations)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations');
}
}
return include __DIR__ . '/..'.'/symfony/error-handler/Resources/bin/patch-type-declarations';

119
vendor/bin/php-parse vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../nikic/php-parser/bin/php-parse)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse');
}
}
return include __DIR__ . '/..'.'/nikic/php-parser/bin/php-parse';

119
vendor/bin/psysh vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../psy/psysh/bin/psysh)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/psy/psysh/bin/psysh');
}
}
return include __DIR__ . '/..'.'/psy/psysh/bin/psysh';

119
vendor/bin/robo vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../consolidation/robo/robo)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/consolidation/robo/robo');
}
}
return include __DIR__ . '/..'.'/consolidation/robo/robo';

119
vendor/bin/var-dump-server vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/var-dumper/Resources/bin/var-dump-server)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
}
}
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';

119
vendor/bin/yaml-cli vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../grasmash/yaml-cli/bin/yaml-cli)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/grasmash/yaml-cli/bin/yaml-cli');
}
}
return include __DIR__ . '/..'.'/grasmash/yaml-cli/bin/yaml-cli';

119
vendor/bin/yaml-lint vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../symfony/yaml/Resources/bin/yaml-lint)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint');
}
}
return include __DIR__ . '/..'.'/symfony/yaml/Resources/bin/yaml-lint';

View File

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
time: "22:00"
open-pull-requests-limit: 10

View File

@ -0,0 +1,71 @@
name: Tests
on:
push:
branches: [ 3.x, 4.x ]
pull_request:
branches: [ 3.x, 4.x ]
workflow_dispatch:
env:
DCG_TMP_DIR: /dev/shm
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php:
- "8.3"
drupal:
- "11.x"
dependency-mode:
- prefer-stable
- prefer-lowest
steps:
- uses: actions/checkout@v3
- name: Upgrade sqlite3
run: |
wget https://www.sqlite.org/2024/sqlite-autoconf-3450300.tar.gz -O /tmp/sqlite.tar.gz
tar -xzf /tmp/sqlite.tar.gz -C /tmp
cd /tmp/sqlite-autoconf-3450300
./configure CFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA=1" --prefix=/usr/local
make && sudo make install
sudo ldconfig
- name: Install PHP with extensions
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
ini-values: zend.assertions=1
tools: composer:v2
- name: Install Symfony CLI
run: |
echo 'deb [trusted=yes] https://repo.symfony.com/apt/ /' | sudo tee /etc/apt/sources.list.d/symfony-cli.list
sudo apt update && sudo apt install symfony-cli
- name: Install dependencies
run: composer install --ansi
- name: Update dependencies
run: composer update --${{ matrix.dependency-mode }} --ansi
- name: Run lint
run: ./scripts/lint.sh
- name: Run unit tests
run: ./scripts/unit-tests.sh
- name: Run functional tests
env:
DCG_DRUPAL_VERSION: ${{ matrix.drupal }}
run: ./scripts/functional-tests.sh
- name: Run SUT tests
env:
DCG_DRUPAL_VERSION: ${{ matrix.drupal }}
run: ./scripts/sut-tests.sh

View File

@ -0,0 +1,63 @@
<?php /** @noinspection ALL */
namespace PHPSTORM_META {
use DrupalCodeGenerator\Helper\Dumper\DumperInterface;
override(
\Symfony\Component\Console\Helper\HelperSet::get(0),
map([
'service_info' => \DrupalCodeGenerator\Helper\Drupal\ServiceInfo::class,
'module_info' => \DrupalCodeGenerator\Helper\Drupal\ModuleInfo::class,
'theme_info' => \DrupalCodeGenerator\Helper\Drupal\ThemeInfo::class,
'hook_info' => \DrupalCodeGenerator\Helper\Drupal\HookInfo::class,
'route_info' => \DrupalCodeGenerator\Helper\Drupal\RouteInfo::class,
'permission_info' => \DrupalCodeGenerator\Helper\Drupal\PermissionInfo::class,
'config_info' => \DrupalCodeGenerator\Helper\Drupal\ConfigInfo::class,
'dry_dumper' => \DrupalCodeGenerator\Helper\Dumper\DumperInterface::class,
'filesytem_dumper' => \DrupalCodeGenerator\Helper\Dumper\DumperInterface::class,
'renderer' => \DrupalCodeGenerator\Helper\Renderer\RendererInterface::class,
'question' => \DrupalCodeGenerator\Helper\QuestionHelper::class,
'assets_table_printer' => \DrupalCodeGenerator\Helper\Printer\PrinterInterface::class,
'assets_list_printer' => \DrupalCodeGenerator\Helper\Printer\PrinterInterface::class,
]),
);
override(
\Symfony\Component\Console\Command\Command::getHelper(0),
map([
'service_info' => \DrupalCodeGenerator\Helper\Drupal\ServiceInfo::class,
'module_info' => \DrupalCodeGenerator\Helper\Drupal\ModuleInfo::class,
'theme_info' => \DrupalCodeGenerator\Helper\Drupal\ThemeInfo::class,
'hook_info' => \DrupalCodeGenerator\Helper\Drupal\HookInfo::class,
'route_info' => \DrupalCodeGenerator\Helper\Drupal\RouteInfo::class,
'permission_info' => \DrupalCodeGenerator\Helper\Drupal\PermissionInfo::class,
'config_info' => \DrupalCodeGenerator\Helper\Drupal\ConfigInfo::class,
'dry_dumper' => \DrupalCodeGenerator\Helper\Dumper\DumperInterface::class,
'filesytem_dumper' => \DrupalCodeGenerator\Helper\Dumper\DumperInterface::class,
'renderer' => \DrupalCodeGenerator\Helper\Renderer\RendererInterface::class,
'question' => \DrupalCodeGenerator\Helper\QuestionHelper::class,
'assets_table_printer' => \DrupalCodeGenerator\Helper\Printer\PrinterInterface::class,
'assets_list_printer' => \DrupalCodeGenerator\Helper\Printer\PrinterInterface::class,
])
);
override(
\Symfony\Component\DependencyInjection\ContainerInterface::get(0),
map([
'class_resolver' => \Drupal\Core\DependencyInjection\ClassResolverInterface::class,
'entity_field.manager' => \Drupal\Core\Entity\EntityFieldManagerInterface::class,
'entity_type.bundle.info' => \Drupal\Core\Entity\EntityTypeBundleInfoInterface::class,
'entity_type.manager' => \Drupal\Core\Entity\EntityTypeManagerInterface::class,
'event_dispatcher' => \Symfony\Contracts\EventDispatcher\EventDispatcherInterface::class,
'extension.list.module' => \Drupal\Core\Extension\ModuleExtensionList::class,
'kernel' => \Drupal\Core\DrupalKernelInterface::class,
'library.discovery' => \Drupal\Core\Asset\LibraryDiscovery::class,
'module_handler' => \Drupal\Core\Extension\ModuleHandlerInterface::class,
'theme_handler' => \Drupal\Core\Extension\ThemeHandlerInterface::class,
])
);
override(\Drupal\Core\Routing\RouteProviderInterface::getAllRoutes(), map(['' => \ArrayIterator::class]));
}

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.

View File

@ -0,0 +1,40 @@
# Drupal Code Generator
[![Tests](https://github.com/Chi-teck/drupal-code-generator/workflows/Tests/badge.svg)](https://github.com/Chi-teck/drupal-code-generator/actions?query=workflow%3ATests)
[![Total Downloads](https://poser.pugx.org/chi-teck/drupal-code-generator/downloads)](//packagist.org/packages/chi-teck/drupal-code-generator)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg?style=flat)](https://php.net/)
A command line code generator for Drupal.
## Installation
```
composer require chi-teck/drupal-code-generator --dev
```
Optionally, generate shell completions for DCG executable.
```
./vendor/bin/dcg completion bash >> ~/.bash_completion
```
## Usage
```shell
# Display navigation.
./vendor/bin/dcg
# Call generator directly.
./vendor/bin/dcg plugin:field:widget
# Generate code non-interactively.
./vendor/bin/dcg config-form -a Example -a example -a SettingsForm -a No
```
## Compatibility
DCG|PHP|Symfony|Twig|Drupal|Drush
:-:|:-:|:-:|:-:|:-:|:-:
1|7.1 - 7.4|3, 4|1, 2|7, 8|9, 10
2|7.4+|4, 5|2, 3|7, 9|11
3|8.1+|6|3|10|12
4|8.3+|7|3|11|13
## License
GNU General Public License, version 2 or later.

38
vendor/chi-teck/drupal-code-generator/bin/dcg vendored Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\BootstrapHandler;
$dcg_exit = static function(string $message): never {
\fwrite(\STDERR, "\e[0;41m [ERROR] $message \e[0m" . \PHP_EOL);
exit(1);
};
// __DIR__ cannot be used here as it contains a path with symlinks resolved.
$dcg_path = \str_starts_with($argv[0], '/') ?
$argv[0] : \getcwd() . '/' . $argv[0];
// DCG is installed as Composer package.
$autoload_dir = \dirname($dcg_path) . '/..';
if (!\file_exists($autoload_dir . '/autoload.php')) {
// DCG is installed as Composer project.
$autoload_dir = $autoload_dir . '/vendor';
}
if (!\file_exists($autoload_dir . '/autoload.php')) {
$dcg_exit('Could not locate class loader.');
}
$class_loader = require $autoload_dir . '/autoload.php';
$bootstrap_handler = new BootstrapHandler($class_loader);
try {
$container = $bootstrap_handler->bootstrap();
}
catch (\Exception $exception) {
$dcg_exit($exception->getMessage());
}
Application::create($container)->run();

View File

@ -0,0 +1,54 @@
{
"name": "chi-teck/drupal-code-generator",
"description": "Drupal code generator",
"license": "GPL-2.0-or-later",
"bin": [
"bin/dcg"
],
"autoload": {
"psr-4": {
"DrupalCodeGenerator\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"DrupalCodeGenerator\\Tests\\Unit\\": "tests/unit",
"DrupalCodeGenerator\\Tests\\Functional\\": "tests/functional"
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"require": {
"php": ">=8.3.0",
"ext-json": "*",
"psr/event-dispatcher": "^1.0",
"psr/log": "^3.0",
"symfony/console": "^7.1",
"symfony/dependency-injection": "^7.1",
"symfony/filesystem": "^7.1",
"symfony/string": "^7.0",
"twig/twig": "^3.4"
},
"require-dev": {
"ext-simplexml": "*",
"chi-teck/drupal-coder-extension": "^2.0.0-rc2",
"drupal/coder": "8.3.24",
"drupal/core": "11.x-dev",
"phpspec/prophecy-phpunit": "^2.2",
"phpunit/phpunit": "^10.5",
"squizlabs/php_codesniffer": "^3.9",
"symfony/var-dumper": "^7.1",
"symfony/yaml": "^7.1",
"vimeo/psalm": "^5.24.0"
},
"conflict": {
"squizlabs/php_codesniffer": "<3.6",
"nikic/php-parser": "<4.17"
},
"minimum-stability": "RC",
"prefer-stable": true
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types = 1);
/** @var \Composer\Autoload\ClassLoader $autoloader */
$autoloader = require __DIR__ . '/vendor/autoload.php';
$modules = [
'comment',
'file',
'node',
'taxonomy',
'user',
];
foreach ($modules as $module) {
$autoloader->addPsr4(
\sprintf('Drupal\%s\\', $module),
\sprintf('vendor/drupal/core/modules/%s/src', $module),
);
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0"?>
<psalm
checkForThrowsInGlobalScope="true"
ensureArrayStringOffsetsExist="true"
findUnusedPsalmSuppress="true"
findUnusedBaselineEntry="true"
findUnusedCode="false"
sealAllMethods="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorLevel="2"
autoloader="psalm-autoloader.php"
>
<projectFiles>
<directory name="src"/>
<ignoreFiles>
<!-- Psalm reports issues about vendors by some reason. -->
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<PossiblyNullArgument>
<errorLevel type="suppress">
<referencedFunction name="/__construct/"/>
</errorLevel>
</PossiblyNullArgument>
<!-- @see https://github.com/vimeo/psalm/issues/8765 -->
<PossiblyUndefinedVariable errorLevel="suppress"/>
<PropertyNotSetInConstructor errorLevel="suppress"/>
<RiskyTruthyFalsyComparison errorLevel="suppress"/>
</issueHandlers>
</psalm>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator;
use Composer\InstalledVersions;
use DrupalCodeGenerator\Command\Navigation;
use DrupalCodeGenerator\Event\GeneratorInfo;
use DrupalCodeGenerator\Event\GeneratorInfoAlter;
use DrupalCodeGenerator\Helper\Drupal\ConfigInfo;
use DrupalCodeGenerator\Helper\Drupal\HookInfo;
use DrupalCodeGenerator\Helper\Drupal\ModuleInfo;
use DrupalCodeGenerator\Helper\Drupal\PermissionInfo;
use DrupalCodeGenerator\Helper\Drupal\RouteInfo;
use DrupalCodeGenerator\Helper\Drupal\ServiceInfo;
use DrupalCodeGenerator\Helper\Drupal\ThemeInfo;
use DrupalCodeGenerator\Helper\Dumper\DryDumper;
use DrupalCodeGenerator\Helper\Dumper\FileSystemDumper;
use DrupalCodeGenerator\Helper\Printer\ListPrinter;
use DrupalCodeGenerator\Helper\Printer\TablePrinter;
use DrupalCodeGenerator\Helper\QuestionHelper;
use DrupalCodeGenerator\Helper\Renderer\TwigRenderer;
use DrupalCodeGenerator\Twig\TwigEnvironment;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem as SymfonyFileSystem;
use Twig\Loader\FilesystemLoader as TemplateLoader;
/**
* DCG console application.
*
* @psalm-suppress DeprecatedInterface
* @psalm-suppress DeprecatedTrait
*
* @todo Use Drupal replacement for ContainerAwareInterface when it's available.
* @see https://www.drupal.org/project/drupal/issues/3397522
*/
final class Application extends BaseApplication implements EventDispatcherInterface {
/**
* Path to DCG root directory.
*/
public const string ROOT = __DIR__ . '/..';
/**
* DCG version.
*
* @deprecated Use \DrupalCodeGenerator\Application->getVersion() instead.
*/
public const string VERSION = 'unknown';
/**
* DCG API version.
*/
public const int API = 3;
/**
* Path to templates directory.
*/
public const string TEMPLATE_PATH = self::ROOT . '/templates';
/**
* {@selfdoc}
*/
private ContainerInterface $container;
/**
* Creates the application.
*
* @psalm-suppress ArgumentTypeCoercion
*/
public static function create(ContainerInterface $container): self {
$application = new self(
'Drupal Code Generator',
InstalledVersions::getPrettyVersion('chi-teck/drupal-code-generator'),
);
$application->container = $container;
$file_system = new SymfonyFileSystem();
$template_loader = new TemplateLoader();
$template_loader->addPath(self::TEMPLATE_PATH . '/_lib', 'lib');
$application->setHelperSet(
new HelperSet([
new QuestionHelper(),
new DryDumper($file_system),
new FileSystemDumper($file_system),
new TwigRenderer(new TwigEnvironment($template_loader)),
new ListPrinter(),
new TablePrinter(),
new ModuleInfo($container->get('module_handler'), $container->get('extension.list.module')),
new ThemeInfo($container->get('theme_handler')),
new ServiceInfo($container),
new HookInfo($container->get('module_handler')),
new RouteInfo($container->get('router.route_provider')),
new ConfigInfo($container->get('config.factory')),
new PermissionInfo($container->get('user.permissions')),
]),
);
$generator_factory = new GeneratorFactory(
$container->get('class_resolver'),
);
$core_generators = $generator_factory->getGenerators();
$user_generators = [];
$application->dispatch(new GeneratorInfo($user_generators));
$all_generators = \array_merge($core_generators, $user_generators);
$application->addCommands(
$application->dispatch(new GeneratorInfoAlter($all_generators))->generators,
);
$application->add(new Navigation());
$application->setDefaultCommand('navigation');
/** @var \DrupalCodeGenerator\Application $application */
$application = $application->dispatch($application);
return $application;
}
/**
* Returns Drupal container.
*/
public function getContainer(): ContainerInterface {
return $this->container;
}
/**
* {@inheritdoc}
*
* @template T as object
* @psalm-param T $event
* @psalm-return T
*
* @todo Remove this once Symfony drops support for event-dispatcher-contracts v2.
* @see \Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch()
* @psalm-suppress UnusedPsalmSuppress
* @psalm-suppress InvalidReturnType
* @psalm-suppress InvalidReturnStatement
*/
public function dispatch(object $event): object {
return $this->container->get('event_dispatcher')->dispatch($event);
}
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Asset\Resolver\PreserveResolver;
use DrupalCodeGenerator\Asset\Resolver\ReplaceResolver;
use DrupalCodeGenerator\Asset\Resolver\ResolverDefinition;
use DrupalCodeGenerator\Asset\Resolver\ResolverInterface;
use DrupalCodeGenerator\InputOutput\IO;
use DrupalCodeGenerator\Utils;
/**
* Base class for assets.
*/
abstract class Asset implements \Stringable {
/**
* Indicates that the asset can be updated but never created.
*/
private bool $virtual = FALSE;
/**
* Asset mode.
*
* @psalm-var int<0, 511>
*/
private int $mode = 0444;
/**
* Template variables.
*
* @psalm-var array<string, mixed>
*/
private array $vars = [];
/**
* Content resolver.
*/
protected ?ResolverInterface $resolver = NULL;
/**
* Resolver definition.
*/
protected ResolverDefinition $resolverDefinition;
/**
* Asset constructor.
*/
public function __construct(protected readonly string $path) {
// @todo Test this.
match (TRUE) {
$this instanceof Directory,
$this instanceof File,
$this instanceof Symlink => NULL,
default => throw new \LogicException(\sprintf('%s class is internal for extension.', self::class)),
};
$this->resolverDefinition = new ResolverDefinition(ReplaceResolver::class);
}
/**
* Getter for the asset path.
*/
final public function getPath(): string {
return $this->replaceTokens($this->path);
}
/**
* Getter for the asset mode.
*
* @psalm-return int<0, 511>
*/
final public function getMode(): int {
return $this->mode;
}
/**
* Getter for the asset vars.
*
* @psalm-return array<string, mixed>
*/
final public function getVars(): array {
return $this->vars;
}
/**
* Checks if the asset is virtual.
*
* Virtual assets should not cause creating new directories, files or symlinks
* on file system. They meant to be used by resolvers to update existing
* objects.
*/
final public function isVirtual(): bool {
return $this->virtual;
}
/**
* Returns the asset resolver.
*/
public function getResolver(IO $io): ResolverInterface {
return $this->resolver ?? $this->resolverDefinition->createResolver($io);
}
/**
* Setter for asset mode.
*
* @psalm-param int<0, 511> $mode
*/
final public function mode(int $mode): static {
/** @psalm-suppress DocblockTypeContradiction */
if ($mode < 0000 || $mode > 0777) {
throw new \InvalidArgumentException('Incorrect mode value.');
}
$this->mode = $mode;
return $this;
}
/**
* Setter for the asset vars.
*
* @psalm-param array<string, mixed> $vars
*/
final public function vars(array $vars): static {
$this->vars = $vars;
return $this;
}
/**
* Makes the asset "virtual".
*/
final public function setVirtual(bool $virtual): static {
$this->virtual = $virtual;
return $this;
}
/**
* Indicates that existing asset should be replaced.
*/
final public function replaceIfExists(): static {
$this->resolverDefinition = new ResolverDefinition(ReplaceResolver::class);
return $this;
}
/**
* Indicates that existing asset should be preserved.
*/
final public function preserveIfExists(): static {
$this->resolverDefinition = new ResolverDefinition(PreserveResolver::class);
return $this;
}
/**
* Setter for asset resolver.
*/
final public function resolver(ResolverInterface $resolver): static {
$this->resolver = $resolver;
return $this;
}
/**
* Implements the magic __toString() method.
*/
final public function __toString(): string {
return $this->getPath();
}
/**
* Replaces all tokens in a given string with appropriate values.
*/
final protected function replaceTokens(string $input): string {
return Utils::replaceTokens($input, $this->vars);
}
}

View File

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
/**
* Asset collection.
*
* @template-implements \ArrayAccess<string,\DrupalCodeGenerator\Asset\Asset>
* @template-implements \IteratorAggregate<string,\DrupalCodeGenerator\Asset\Asset>
*/
final class AssetCollection implements \ArrayAccess, \IteratorAggregate, \Countable, \Stringable {
/**
* AssetCollection constructor.
*
* @param \DrupalCodeGenerator\Asset\Asset[] $assets
* Assets.
*/
public function __construct(private array $assets = []) {}
/**
* Creates a directory asset.
*/
public function addDirectory(string $path): Directory {
$directory = new Directory($path);
$this->assets[] = $directory;
return $directory;
}
/**
* Creates a file asset.
*/
public function addFile(string $path, ?string $template = NULL): File {
$file = new File($path);
if ($template) {
$file->template($template);
}
$this->assets[] = $file;
return $file;
}
/**
* Creates a symlink asset.
*
* @noinspection PhpUnused
*/
public function addSymlink(string $path, string $target): Symlink {
$symlink = new Symlink($path, $target);
$this->assets[] = $symlink;
return $symlink;
}
/**
* Adds an asset for configuration schema file.
*/
public function addSchemaFile(string $path = 'config/schema/{machine_name}.schema.yml'): File {
return $this->addFile($path)
->appendIfExists();
}
/**
* Adds an asset for service file.
*/
public function addServicesFile(string $path = '{machine_name}.services.yml'): File {
return $this->addFile($path)
->appendIfExists(1);
}
/**
* {@inheritdoc}
*
* @psalm-param \DrupalCodeGenerator\Asset\Asset $value
*/
public function offsetSet(mixed $offset, mixed $value): void {
match (TRUE) {
$value instanceof Directory,
$value instanceof File,
$value instanceof Symlink => NULL,
default => throw new \InvalidArgumentException('Unsupported asset type.'),
};
if ($offset === NULL) {
$this->assets[] = $value;
}
else {
$this->assets[$offset] = $value;
}
}
/**
* {@inheritdoc}
*/
public function offsetGet(mixed $offset): ?Asset {
return $this->assets[$offset] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function offsetUnset(mixed $offset): void {
unset($this->assets[$offset]);
}
/**
* {@inheritdoc}
*/
public function offsetExists(mixed $offset): bool {
return isset($this->assets[$offset]);
}
/**
* {@inheritdoc}
*/
public function getIterator(): \ArrayIterator {
return new \ArrayIterator($this->assets);
}
/**
* {@inheritdoc}
*
* @psalm-return int<0, max>
*/
public function count(): int {
return \count($this->assets);
}
/**
* Returns a collection of directory assets.
*/
public function getDirectories(): self {
return $this->getFiltered(
static fn (Asset $asset): bool => $asset instanceof Directory,
);
}
/**
* Returns a collection of file assets.
*/
public function getFiles(): self {
return $this->getFiltered(
static fn (Asset $asset): bool => $asset instanceof File,
);
}
/**
* Returns a collection of symlink assets.
*/
public function getSymlinks(): self {
return $this->getFiltered(
static fn (Asset $asset): bool => $asset instanceof Symlink,
);
}
/**
* Returns a collection of sorted assets.
*/
public function getSorted(): self {
$sorter = static function (Asset $a, Asset $b): int {
$name_a = (string) $a;
$name_b = (string) $b;
// Top level assets should go first.
$result = \strcasecmp(\dirname($name_a), \dirname($name_b));
if ($result === 0) {
$result = \strcasecmp($name_a, $name_b);
}
return $result;
};
$assets = $this->assets;
\usort($assets, $sorter);
return new self($assets);
}
/**
* Filters the asset collection.
*/
public function getFiltered(callable $filter): self {
$iterator = new \CallbackFilterIterator($this->getIterator(), $filter);
$assets = \iterator_to_array($iterator);
$str_keys = \array_filter(\array_keys($assets), 'is_string');
// Reindex if it's not an associative array.
return new self(\count($str_keys) > 0 ? $assets : \array_values($assets));
}
/**
* {@inheritdoc}
*/
public function __toString(): string {
$output = '';
foreach ($this->getSorted() as $asset) {
$output .= '• ' . $asset . \PHP_EOL;
}
return $output;
}
}

View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
// @todo Is it still needed?
\class_alias(AssetCollection::class, '\DrupalCodeGenerator\Asset\Assets');

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Asset\Resolver\PreserveResolver;
use DrupalCodeGenerator\Asset\Resolver\ResolverDefinition;
/**
* Simple data structure to represent a directory being created.
*/
final class Directory extends Asset {
/**
* {@inheritdoc}
*/
public function __construct(string $path) {
parent::__construct($path);
$this->mode(0755);
// Recreating existing directories makes no sense.
$this->resolverDefinition = new ResolverDefinition(PreserveResolver::class);
}
/**
* Named constructor.
*/
public static function create(string $path): self {
return new self($path);
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Asset\Resolver\AppendResolver;
use DrupalCodeGenerator\Asset\Resolver\PrependResolver;
use DrupalCodeGenerator\Asset\Resolver\ResolverDefinition;
use DrupalCodeGenerator\Helper\Renderer\RendererInterface;
/**
* A data structure to represent a file being generated.
*/
final class File extends Asset implements RenderableInterface {
/**
* Asset content.
*/
private string $content = '';
/**
* Template to render main content.
*/
private ?string $template = NULL;
/**
* The template string to render.
*/
private ?string $inlineTemplate = NULL;
/**
* {@inheritdoc}
*/
public function __construct(string $path) {
parent::__construct($path);
$this->mode(0644);
}
/**
* Named constructor.
*/
public static function create(string $path): self {
return new self($path);
}
/**
* Returns the asset content.
*/
public function getContent(): string {
return $this->content;
}
/**
* Sets the asset content.
*/
public function content(string $content): self {
$this->content = $content;
return $this;
}
/**
* Sets the asset template.
*
* Templates with 'twig' extension are processed with Twig template engine.
*/
public function template(string $template): self {
if ($this->inlineTemplate) {
throw new \LogicException('A file cannot have both inline and regular templates.');
}
$this->template = $template;
return $this;
}
/**
* Returns the asset inline template.
*/
public function inlineTemplate(string $inline_template): self {
if ($this->template) {
throw new \LogicException('A file cannot have both inline and regular templates.');
}
$this->inlineTemplate = $inline_template;
return $this;
}
/**
* Sets the "prepend" resolver.
*/
public function prependIfExists(): self {
$this->resolverDefinition = new ResolverDefinition(PrependResolver::class);
return $this;
}
/**
* Sets the "append" resolver.
*
* @psalm-param int<0, max> $header_size
*/
public function appendIfExists(int $header_size = 0): self {
$this->resolverDefinition = new ResolverDefinition(AppendResolver::class, $header_size);
return $this;
}
/**
* {@inheritdoc}
*/
public function render(RendererInterface $renderer): void {
if ($this->inlineTemplate) {
$content = $renderer->renderInline($this->inlineTemplate, $this->getVars());
$this->content($content);
}
elseif ($this->template) {
$template = $this->replaceTokens($this->template);
$content = $renderer->render($template, $this->getVars());
$this->content($content);
}
// It's OK that the file has no templates as consumers may set rendered
// content directly through `content()` method.
}
/**
* Checks if the asset is a PHP script.
*/
public function isPhp(): bool {
return \in_array(
\pathinfo($this->getPath(), \PATHINFO_EXTENSION),
['php', 'module', 'install', 'inc', 'theme'],
);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Helper\Renderer\RendererInterface;
/**
* An interface for renderable assets.
*/
interface RenderableInterface {
/**
* Renders the asset.
*/
public function render(RendererInterface $renderer): void;
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\InputOutput\IO;
final class AppendResolver implements ResolverInterface, ResolverFactoryInterface {
/**
* Constructs the object.
*
* @psalm-param int<0, max> $headerSize
*/
public function __construct(private readonly int $headerSize = 0) {
/** @psalm-suppress DocblockTypeContradiction */
if ($headerSize < 0) {
throw new \InvalidArgumentException('Header size must be greater than or equal to 0.');
}
}
/**
* {@inheritdoc}
*/
public static function createResolver(IO $io, mixed $options): self {
return new self($options);
}
/**
* {@inheritdoc}
*/
public function resolve(Asset $asset, string $path): File {
if (!$asset instanceof File) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$new_content = $asset->getContent();
// Remove header from existing content.
if ($this->headerSize > 0) {
$new_content = \implode("\n", \array_slice(\explode("\n", $new_content), $this->headerSize));
}
$existing_content = \file_get_contents($path);
return clone $asset->content($existing_content . "\n" . $new_content);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\File;
final class PrependResolver implements ResolverInterface {
/**
* {@inheritdoc}
*/
public function resolve(Asset $asset, string $path): File {
if (!$asset instanceof File) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$new_content = $asset->getContent();
$existing_content = \file_get_contents($path);
return clone $asset->content($new_content . "\n" . $existing_content);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
final class PreserveResolver implements ResolverInterface {
/**
* {@inheritdoc}
*
* @psalm-return null
*/
public function resolve(Asset $asset, string $path): ?Asset {
return NULL;
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Asset\Symlink;
use DrupalCodeGenerator\InputOutput\IO;
final class ReplaceResolver implements ResolverInterface, ResolverFactoryInterface {
/**
* Constructs the object.
*/
public function __construct(private readonly IO $io) {}
/**
* {@inheritdoc}
*/
public static function createResolver(IO $io, mixed $options): self {
return new self($io);
}
/**
* {@inheritdoc}
*/
public function resolve(Asset $asset, string $path): NULL|File|Symlink {
if (!$asset instanceof File && !$asset instanceof Symlink) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$replace = $this->io->getInput()->getOption('replace') ||
$this->io->getInput()->getOption('dry-run') ||
$this->io->confirm("The file <comment>$path</comment> already exists. Would you like to replace it?");
return $replace ? clone $asset : NULL;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\InputOutput\IO;
final class ResolverDefinition {
/**
* Constructs the object.
*
* @psalm-param class-string<\DrupalCodeGenerator\Asset\Resolver\ResolverInterface> $className
*/
public function __construct(
public readonly string $className,
public readonly mixed $options = NULL,
) {}
/**
* Creates asset resolver.
*/
public function createResolver(IO $io): ResolverInterface {
if (\is_subclass_of($this->className, ResolverFactoryInterface::class)) {
$resolver = $this->className::createResolver($io, $this->options);
}
else {
$resolver = new $this->className();
}
return $resolver;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\InputOutput\IO;
/**
* Interface for classes capable of creating resolvers.
*/
interface ResolverFactoryInterface {
/**
* Creates a resolver.
*/
public static function createResolver(IO $io, mixed $options): ResolverInterface;
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
/**
* Interface resolver.
*
* A resolver is called when the asset with the same path already exists in the
* file system. The purpose of the resolver is to merge the existing asset with
* the one provided by a generator.
*/
interface ResolverInterface {
/**
* Resolves an asset.
*
* Returns the resolved asset or NULL if existing asset is up-to-date.
*
* @throw \InvalidArgumentException
*/
public function resolve(Asset $asset, string $path): ?Asset;
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
/**
* Simple data structure to represent a symlink being generated.
*/
final class Symlink extends Asset {
/**
* Symlink target.
*/
private readonly string $target;
/**
* {@inheritdoc}
*/
public function __construct(string $path, string $target) {
parent::__construct($path);
$this->target = $target;
$this->mode(0644);
}
/**
* Named constructor.
*/
public static function create(string $path, string $target): self {
return new self($path, $target);
}
/**
* Getter for symlink target.
*/
public function getTarget(): string {
return $this->replaceTokens($this->target);
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Attribute;
use DrupalCodeGenerator\GeneratorType;
/**
* Generator definition.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Generator {
public function __construct(
public readonly string $name,
public readonly string $description = '',
public readonly array $aliases = [],
public readonly bool $hidden = FALSE,
public readonly ?string $templatePath = NULL,
public readonly GeneratorType $type = GeneratorType::OTHER,
public readonly ?string $label = NULL,
) {}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator;
use Composer\Autoload\ClassLoader;
use Composer\InstalledVersions;
use Drupal\Core\DrupalKernel;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a handler to bootstrap Drupal.
*/
final class BootstrapHandler {
/**
* Constructs the object.
*/
public function __construct(private readonly ClassLoader $classLoader) {}
/**
* Bootstraps Drupal.
*/
public function bootstrap(): ContainerInterface {
self::assertInstallation();
$root_package = InstalledVersions::getRootPackage();
\chdir($root_package['install_path']);
$request = Request::createFromGlobals();
$kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod');
$kernel->boot();
$kernel->preHandle($request);
// Cancel Drupal error handler to get all errors in STDOUT.
\restore_error_handler();
\error_reporting(\E_ALL);
return $kernel->getContainer();
}
/**
* Asserts Drupal instance.
*
* @throws \RuntimeException
*/
private static function assertInstallation(): void {
$preflight = \defined('Drupal::VERSION') &&
\version_compare(\Drupal::VERSION, '10.0.0-dev', '>=') &&
\class_exists(InstalledVersions::class) &&
\class_exists(Request::class) &&
\class_exists(DrupalKernel::class);
if (!$preflight) {
throw new \RuntimeException('Could not load Drupal.');
}
}
}

View File

@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator as GeneratorDefinition;
use DrupalCodeGenerator\Event\AssetPostProcess;
use DrupalCodeGenerator\Event\AssetPreProcess;
use DrupalCodeGenerator\Exception\ExceptionInterface;
use DrupalCodeGenerator\Exception\SilentException;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Helper\Drupal\NullExtensionInfo;
use DrupalCodeGenerator\InputOutput\DefaultOptions;
use DrupalCodeGenerator\InputOutput\Interviewer;
use DrupalCodeGenerator\InputOutput\IO;
use DrupalCodeGenerator\InputOutput\IOAwareInterface;
use DrupalCodeGenerator\InputOutput\IOAwareTrait;
use DrupalCodeGenerator\Logger\ConsoleLogger;
use DrupalCodeGenerator\Utils;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Base class for code generators.
*
* @method \DrupalCodeGenerator\Application getApplication()
* @method string getName()
* @method \Symfony\Component\Console\Helper\HelperSet getHelperSet()
*/
abstract class BaseGenerator extends Command implements LabelInterface, IOAwareInterface, LoggerAwareInterface {
use IOAwareTrait;
use LoggerAwareTrait;
/**
* {@inheritdoc}
*/
public function __construct() {
parent::__construct();
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*/
protected function configure(): void {
parent::configure();
$definition = $this->getGeneratorDefinition();
$this->setName($definition->name)
->setDescription($definition->description)
->setAliases($definition->aliases)
->setHidden($definition->hidden);
DefaultOptions::apply($this);
}
/**
* {@inheritdoc}
*
* @psalm-suppress PossiblyNullReference
*/
protected function initialize(InputInterface $input, OutputInterface $output): void {
parent::initialize($input, $output);
$logger = new ConsoleLogger($output);
$question_helper = $this->getHelper('question');
$io = new IO($input, $output, $question_helper);
$items = \iterator_to_array($this->getHelperSet());
$items[] = $this;
foreach ($items as $item) {
if ($item instanceof IOAwareInterface) {
$item->io($io);
}
if ($item instanceof LoggerAwareInterface) {
$item->setLogger($logger);
}
}
$template_path = $this->getTemplatePath();
if ($template_path !== NULL) {
$this->getHelper('renderer')->registerTemplatePath($template_path);
}
$this->logger->debug('PHP binary: {binary}', ['binary' => \PHP_BINARY]);
/** @psalm-var array{PHP_SELF: string} $_SERVER */
// @phpcs:ignore SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable.DisallowedSuperGlobalVariable
$this->logger->debug('DCG executable: {dcg}', ['dcg' => \realpath($_SERVER['PHP_SELF'])]);
$this->logger->debug('Working directory: {directory}', ['directory' => $io->getWorkingDirectory()]);
}
/**
* {@inheritdoc}
*
* @noinspection PhpMissingParentCallCommonInspection
* @psalm-suppress PossiblyNullReference
*
* @psalm-return int<0, 1>
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$this->logger->debug('Command: {command}', ['command' => static::class]);
try {
$this->printHeader();
$vars = [];
$assets = new AssetCollection();
$this->generate($vars, $assets);
$this->generateInfoFile($vars, $assets);
$vars = Utils::processVars($vars);
$collected_vars = \preg_replace('/^Array/', '', \print_r($vars, TRUE));
$this->logger->debug('Collected variables: {vars}', ['vars' => $collected_vars]);
foreach ($assets as $asset) {
// Local asset variables take precedence over global ones.
$asset->vars(\array_merge($vars, Utils::processVars($asset->getVars())));
}
$this->render($assets);
// Destination passed through command line option takes precedence over
// destination defined in a generator.
$destination = $input->getOption('destination') ?: $this->getDestination($vars);
$this->logger->debug('Destination directory: {directory}', ['directory' => $destination]);
$dumped_assets = $this->dump($assets, $destination);
$full_path = $input->getOption('full-path');
$this->printSummary($dumped_assets, $full_path ? $destination . '/' : '');
}
catch (ExceptionInterface $exception) {
if (!$exception instanceof SilentException) {
$this->io()->getErrorStyle()->error($exception->getMessage());
}
return self::FAILURE;
}
$this->logger->debug('Memory usage: {memory}', ['memory' => Helper::formatMemory(\memory_get_peak_usage())]);
return self::SUCCESS;
}
/**
* Generates assets.
*/
abstract protected function generate(array &$vars, AssetCollection $assets): void;
/**
* Gets generator definition.
*/
protected function getGeneratorDefinition(): GeneratorDefinition {
$attributes = (new \ReflectionClass(static::class))->getAttributes(GeneratorDefinition::class);
if (\count($attributes) === 0) {
throw new \LogicException(\sprintf('Command %s does not have generator annotation.', static::class));
}
return $attributes[0]->newInstance();
}
/**
* Creates interviewer.
*/
protected function createInterviewer(array &$vars): Interviewer {
$extension_info = match ($this->getGeneratorDefinition()->type) {
GeneratorType::MODULE, GeneratorType::MODULE_COMPONENT => $this->getHelper('module_info'),
GeneratorType::THEME, GeneratorType::THEME_COMPONENT => $this->getHelper('theme_info'),
default => new NullExtensionInfo(),
};
return new Interviewer(
io: $this->io,
vars: $vars,
generatorDefinition: $this->getGeneratorDefinition(),
serviceInfo: $this->getHelper('service_info'),
extensionInfo: $extension_info,
permissionInfo: $this->getHelper('permission_info'),
);
}
/**
* Render assets.
*/
protected function render(AssetCollection $assets): void {
$renderer = $this->getHelper('renderer');
foreach ($assets->getFiles() as $file) {
/** @var \DrupalCodeGenerator\Asset\File $file */
$renderer->renderAsset($file);
}
}
/**
* Dumps assets.
*/
protected function dump(AssetCollection $assets, string $destination): AssetCollection {
$is_dry = $this->io()->getInput()->getOption('dry-run');
$pre_process_event = $this->getApplication()->dispatch(
new AssetPreProcess($assets, $destination, $this->getName(), $is_dry),
);
$assets = $pre_process_event->assets;
$destination = $pre_process_event->destination;
/** @var \DrupalCodeGenerator\Helper\Dumper\DumperInterface $dumper */
$dumper = $this->getHelper($is_dry ? 'dry_dumper' : 'filesystem_dumper');
$dumped_assets = $dumper->dump($assets, $destination);
$post_process_event = $this->getApplication()->dispatch(
new AssetPostProcess($dumped_assets, $destination, $this->getName(), $is_dry),
);
return $post_process_event->assets;
}
/**
* Prints header.
*/
protected function printHeader(): void {
$this->io()->title(\sprintf('Welcome to %s generator!', $this->getAliases()[0] ?? $this->getName()));
}
/**
* Prints summary.
*/
protected function printSummary(AssetCollection $dumped_assets, string $base_path): void {
$printer_name = $this->io()->isVerbose() ? 'assets_table_printer' : 'assets_list_printer';
/** @psalm-suppress UndefinedInterfaceMethod */
$this->getHelper($printer_name)->printAssets($dumped_assets, $base_path);
}
/**
* {@inheritdoc}
*/
public function getLabel(): ?string {
return $this->getGeneratorDefinition()->label;
}
/**
* Returns template path.
*/
final protected function getTemplatePath(): ?string {
return $this->getGeneratorDefinition()->templatePath;
}
/**
* Returns destination for generated files.
*
* @todo Test this.
*/
protected function getDestination(array $vars): string {
if (!isset($vars['machine_name'])) {
return $this->io()->getWorkingDirectory();
}
$definition = $this->getGeneratorDefinition();
$is_new = $definition->type->isNewExtension();
return match ($definition->type) {
GeneratorType::MODULE, GeneratorType::MODULE_COMPONENT =>
$this->getHelper('module_info')->getDestination($vars['machine_name'], $is_new),
GeneratorType::THEME, GeneratorType::THEME_COMPONENT =>
$this->getHelper('theme_info')->getDestination($vars['machine_name'], $is_new),
default => $this->io()->getWorkingDirectory(),
};
}
/**
* Generates info file.
*
* @todo Test this.
* @todo Generate info file for theme components.
*/
protected function generateInfoFile(array &$vars, AssetCollection $assets): void {
if (\count($assets) === 0) {
return;
}
if ($this->getGeneratorDefinition()->type !== GeneratorType::MODULE_COMPONENT) {
return;
}
// @todo Throw an exception if machine name was not provided.
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
$vars['name'] ??= Utils::machine2human($vars['machine_name']);
$info_template = <<< 'TWIG'
name: '{{ name }}'
type: module
description: '@todo Add description.'
package: '@todo Add package'
core_version_requirement: ^10 || ^11
TWIG;
$assets->addFile('{machine_name}.info.yml')
->inlineTemplate($info_template)
->preserveIfExists();
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
/**
* A generator for composer.json file.
*
* @todo Clean-up
* @todo Define destination automatically based on project type.
*/
#[Generator(
name: 'composer',
description: 'Generates a composer.json file',
aliases: ['composer.json'],
templatePath: Application::TEMPLATE_PATH . '/_composer',
type: GeneratorType::OTHER,
label: 'composer.json',
)]
final class Composer extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
// @see https://getcomposer.org/doc/04-schema.md#name
// @todo Test this.
$validator = static function (string $input): string {
if (!\preg_match('#^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$#', $input)) {
throw new \UnexpectedValueException("The package name \"$input\" is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name.");
}
return $input;
};
$vars['project_name'] = $ir->ask('Project name', 'drupal/example', $validator);
[, $vars['machine_name']] = \explode('/', $vars['project_name']);
$vars['description'] = $ir->ask('Description');
$type_choices = [
'drupal-module',
'drupal-custom-module',
'drupal-theme',
'drupal-custom-theme',
'drupal-library',
'drupal-profile',
'drupal-custom-profile',
'drupal-drush',
];
$vars['type'] = $ir->choice('Project type', \array_combine($type_choices, $type_choices));
$vars['drupal_org'] = match($vars['type']) {
'drupal-custom-module', 'drupal-custom-theme', 'drupal-custom-profile' => FALSE,
default => $ir->confirm('Will this project be hosted on drupal.org?'),
};
$assets->addFile('composer.json', 'composer.twig');
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'controller',
description: 'Generates a controller',
templatePath: Application::TEMPLATE_PATH . '/_controller',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Controller extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}Controller');
$vars['services'] = $ir->askServices(FALSE);
if ($ir->confirm('Would you like to create a route for this controller?')) {
$unprefixed_class = Utils::camel2machine(Utils::removeSuffix($vars['class'], 'Controller'));
// Route name like 'foo.foo' would look weird.
if ($unprefixed_class === $vars['machine_name']) {
$unprefixed_class = 'example';
}
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $unprefixed_class);
$vars['unprefixed_route_name'] = \str_replace(
'.', '_', Utils::removePrefix($vars['route_name'], $vars['machine_name'] . '.'),
);
$vars['route_path'] = $ir->ask('Route path', '/{machine_name|u2h}/{unprefixed_route_name|u2h}');
$vars['route_title'] = $ir->ask('Route title', '{unprefixed_route_name|m2t}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'access content');
$assets->addFile('{machine_name}.routing.yml', 'route.twig')->appendIfExists();
}
$assets->addFile('src/Controller/{class}.php', 'controller.twig');
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Drush;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\RegExp;
#[Generator(
name: 'drush:symfony-command',
description: 'Generates Symfony console command',
aliases: ['symfony-command'],
templatePath: Application::TEMPLATE_PATH . '/Drush/_symfony-command',
type: GeneratorType::MODULE_COMPONENT,
)]
final class SymfonyCommand extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$command_name_validator = new RegExp('/^[a-z][a-z0-9-_:]*[a-z0-9]$/', 'The value is not correct command name.');
$vars['command']['name'] = $ir->ask('Command name', '{machine_name}:example', $command_name_validator);
$vars['command']['description'] = $ir->ask('Command description');
$sub_names = \explode(':', $vars['command']['name']);
$short_name = \array_pop($sub_names);
$alias_validator = new RegExp('/^[a-z0-9_-]+$/', 'The value is not correct alias name.');
$vars['command']['alias'] = $ir->ask('Command alias', $short_name, $alias_validator);
$vars['class'] = $ir->askClass('Class', Utils::camelize($short_name) . 'Command');
if ($ir->confirm('Would you like to run the command with Drush')) {
// Make service name using the following guides.
// `foo:example` -> `foo.example` (not `foo:foo_example`)
// `foo` -> `foo.foo` (not `foo`)
$service_name = Utils::removePrefix($vars['command']['name'], $vars['machine_name'] . ':');
if (!$service_name) {
$service_name = $vars['command']['name'];
}
$vars['service_name'] = $vars['machine_name'] . '.' . \str_replace(':', '_', $service_name);
$vars['services'] = $ir->askServices(FALSE);
$assets->addServicesFile('drush.services.yml')->template('services.twig');
}
$assets->addFile('src/Command/{class}.php', 'command.twig');
}
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Entity;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Asset\Resolver\ResolverInterface;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'entity:configuration',
description: 'Generates configuration entity',
aliases: ['config-entity'],
templatePath: Application::TEMPLATE_PATH . '/Entity/_configuration-entity',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ConfigurationEntity extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['entity_type_label'] = $ir->ask('Entity type label', '{name}');
$vars['entity_type_id'] = $ir->ask('Entity type ID', '{entity_type_label|h2m}');
$vars['class_prefix'] = '{entity_type_id|camelize}';
$assets->addFile('src/{class_prefix}ListBuilder.php', 'src/ExampleListBuilder.php.twig');
$assets->addFile('src/Form/{class_prefix}Form.php', 'src/Form/ExampleForm.php.twig');
$assets->addFile('src/{class_prefix}Interface.php', 'src/ExampleInterface.php.twig');
$assets->addFile('src/Entity/{class_prefix}.php', 'src/Entity/Example.php.twig');
$assets->addFile('{machine_name}.routing.yml', 'model.routing.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.action.yml', 'model.links.action.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.menu.yml', 'model.links.menu.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.permissions.yml', 'model.permissions.yml.twig')
->appendIfExists();
$assets->addFile('config/schema/{machine_name}.schema.yml', 'config/schema/model.schema.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.info.yml.twig')
->setVirtual(TRUE)
->resolver($this->getInfoResolver($vars));
}
/**
* Returns resolver for the module info file.
*/
private function getInfoResolver(array $vars): ResolverInterface {
// Add 'configure' link to the info file if it exists.
return new class ($vars) implements ResolverInterface {
public function __construct(private readonly array $vars) {}
public function resolve(Asset $asset, string $path): Asset {
if (!$asset instanceof File) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$resolved = clone $asset;
$existing_content = \file_get_contents($path);
if (!\preg_match('/^configure: /m', $existing_content)) {
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
$content = "{$existing_content}configure: entity.{$this->vars['entity_type_id']}.collection\n";
return $resolved->content($content);
}
return $resolved;
}
};
}
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Entity;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'entity:content',
description: 'Generates content entity',
aliases: ['content-entity'],
templatePath: Application::TEMPLATE_PATH . '/Entity/_content-entity',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ContentEntity extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['entity_type_label'] = $ir->ask('Entity type label', '{name}');
// Make sure the default entity type ID is not like 'example_example'.
// @todo Create a test for this.
$default_entity_type_id = Utils::human2machine($vars['entity_type_label']) === $vars['machine_name'] ?
$vars['machine_name'] : $vars['machine_name'] . '_' . Utils::human2machine($vars['entity_type_label']);
$vars['entity_type_id'] = $ir->ask('Entity type ID', $default_entity_type_id);
$vars['entity_type_id_short'] = $vars['machine_name'] === $vars['entity_type_id'] ?
$vars['entity_type_id'] : Utils::removePrefix($vars['entity_type_id'], $vars['machine_name'] . '_');
$vars['class'] = $ir->ask('Entity class', '{entity_type_label|camelize}');
$vars['entity_base_path'] = $ir->ask('Entity base path', '/{entity_type_id_short|u2h}');
$vars['fieldable'] = $ir->confirm('Make the entity type fieldable?');
$vars['revisionable'] = $ir->confirm('Make the entity type revisionable?', FALSE);
$vars['translatable'] = $ir->confirm('Make the entity type translatable?', FALSE);
$vars['bundle'] = $ir->confirm('The entity type has bundle?', FALSE);
$vars['canonical'] = $ir->confirm('Create canonical page?');
$vars['template'] = $vars['canonical'] && $ir->confirm('Create entity template?');
$vars['access_controller'] = $ir->confirm('Create CRUD permissions?', FALSE);
$vars['label_base_field'] = $ir->confirm('Add "label" base field?');
$vars['status_base_field'] = $ir->confirm('Add "status" base field?');
$vars['created_base_field'] = $ir->confirm('Add "created" base field?');
$vars['changed_base_field'] = $ir->confirm('Add "changed" base field?');
$vars['author_base_field'] = $ir->confirm('Add "author" base field?');
$vars['description_base_field'] = $ir->confirm('Add "description" base field?');
$vars['has_base_fields'] = $vars['label_base_field'] ||
$vars['status_base_field'] ||
$vars['created_base_field'] ||
$vars['changed_base_field'] ||
$vars['author_base_field'] ||
$vars['description_base_field'];
$vars['permissions']['administer'] = $vars['bundle']
? 'administer {entity_type_id} types' : 'administer {entity_type_id}';
if ($vars['access_controller']) {
$vars['permissions']['view'] = 'view {entity_type_id}';
$vars['permissions']['edit'] = 'edit {entity_type_id}';
$vars['permissions']['delete'] = 'delete {entity_type_id}';
$vars['permissions']['create'] = 'create {entity_type_id}';
}
if ($vars['access_controller'] && $vars['revisionable']) {
$vars['permissions']['view_revision'] = 'view {entity_type_id} revision';
$vars['permissions']['revert_revision'] = 'revert {entity_type_id} revision';
$vars['permissions']['delete_revision'] = 'delete {entity_type_id} revision';
}
$vars['rest_configuration'] = $ir->confirm('Create REST configuration for the entity?', FALSE);
if (!\str_starts_with($vars['entity_base_path'], '/')) {
$vars['entity_base_path'] = '/' . $vars['entity_base_path'];
}
if (($vars['fieldable_no_bundle'] = $vars['fieldable'] && !$vars['bundle'])) {
$vars['configure'] = 'entity.{entity_type_id}.settings';
}
elseif ($vars['bundle']) {
$vars['configure'] = 'entity.{entity_type_id}_type.collection';
}
$vars['template_name'] = '{entity_type_id|u2h}.html.twig';
// Contextual links need title suffix to be added to entity template.
if ($vars['template']) {
$assets->addFile('{machine_name}.links.contextual.yml', 'model.links.contextual.yml.twig')
->appendIfExists();
}
$assets->addFile('{machine_name}.links.action.yml', 'model.links.action.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.menu.yml', 'model.links.menu.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.task.yml', 'model.links.task.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.permissions.yml', 'model.permissions.yml.twig')
->appendIfExists();
// Delete action plugins only registered for entity types that have
// 'delete-multiple-confirm' form handler and 'delete-multiple-form' link
// template.
// @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getDeleteMultipleFormRoute
// @see \Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver
$assets->addFile(
'config/install/system.action.{entity_type_id}_delete_action.yml',
'config/install/system.action.example_delete_action.yml.twig',
);
// Save action plugins only registered for entity types that implement
// Drupal\Core\Entity\EntityChangedInterface.
// @see \Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver
if ($vars['changed_base_field']) {
$assets->addFile(
'config/install/system.action.{entity_type_id}_save_action.yml',
'config/install/system.action.example_save_action.yml.twig',
);
}
$assets->addFile('src/Entity/{class}.php', 'src/Entity/Example.php.twig');
$assets->addFile('src/{class}Interface.php', 'src/ExampleInterface.php.twig');
if (!$vars['canonical']) {
$assets->addFile('src/Routing/{class}HtmlRouteProvider.php', 'src/Routing/ExampleHtmlRouteProvider.php.twig');
}
$assets->addFile('src/{class}ListBuilder.php', 'src/ExampleListBuilder.php.twig');
$assets->addFile('src/Form/{class}Form.php', 'src/Form/ExampleForm.php.twig');
if ($vars['fieldable_no_bundle']) {
$assets->addFile('{machine_name}.routing.yml', 'model.routing.yml.twig')
->appendIfExists();
$assets->addFile('src/Form/{class}SettingsForm.php', 'src/Form/ExampleSettingsForm.php.twig');
}
if ($vars['template']) {
$assets->addFile('templates/{entity_type_id|u2h}.html.twig', 'templates/model-example.html.twig.twig');
$assets->addFile('{machine_name}.module', 'model.module.twig')
->appendIfExists(9);
}
if ($vars['access_controller']) {
$assets->addFile('src/{class}AccessControlHandler.php', 'src/ExampleAccessControlHandler.php.twig');
}
if ($vars['rest_configuration']) {
$assets->addFile('config/optional/rest.resource.entity.{entity_type_id}.yml', 'config/optional/rest.resource.entity.example.yml.twig');
}
if ($vars['bundle']) {
$assets->addFile('config/schema/{machine_name}.entity_type.schema.yml', 'config/schema/model.entity_type.schema.yml.twig')
->appendIfExists();
$assets->addFile('src/{class}TypeListBuilder.php', 'src/ExampleTypeListBuilder.php.twig');
$assets->addFile('src/Entity/{class}Type.php', 'src/Entity/ExampleType.php.twig');
$assets->addFile('src/Form/{class}TypeForm.php', 'src/Form/ExampleTypeForm.php.twig');
}
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Entity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\Exception\RuntimeException;
use DrupalCodeGenerator\GeneratorType;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'entity:bundle-class',
description: 'Generate a bundle class for a content entity.',
aliases: ['bundle-class'],
templatePath: Application::TEMPLATE_PATH . '/Entity/_entity-bundle-class',
type: GeneratorType::MODULE_COMPONENT,
)]
final class EntityBundleClass extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly EntityTypeBundleInfoInterface $bundleInfo,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self(
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
);
}
/**
* {@inheritdoc}
*
* @psalm-suppress PossiblyInvalidArgument
* @psalm-suppress PossiblyUndefinedStringArrayOffset
* @psalm-suppress PossiblyInvalidArrayOffset
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
/** @psalm-var array<string, \Drupal\Core\Entity\ContentEntityTypeInterface> $definitions */
$definitions = \array_filter(
$this->entityTypeManager->getDefinitions(),
static fn (EntityTypeInterface $definition): bool => $definition->getGroup() === 'content',
);
$entity_types = \array_map(
static fn (ContentEntityTypeInterface $definition): string => (string) $definition->get('label'),
$definitions,
);
$vars['entity_type_id'] = $ir->choice('Entity type', $entity_types);
// @todo Should this use 'original_class' instead?
$vars['entity_class_fqn'] = $definitions[$vars['entity_type_id']]->get('class');
$vars['entity_class'] = \array_slice(\explode('\\', $vars['entity_class_fqn']), -1)[0];
$vars['namespace'] = 'Drupal\\\{machine_name}\Entity\\\{entity_class}';
$bundles = \array_map(
static fn (array $bundle): string => (string) $bundle['label'],
$this->bundleInfo->getBundleInfo($vars['entity_type_id']),
);
if (\count($bundles) === 0) {
throw new RuntimeException(
\sprintf('The "%s" entity type has no bundles.', $entity_types[$vars['entity_type_id']]),
);
}
// Skip the question if only 1 bundle exists.
$bundle_ids = \count($bundles) === 1 ?
\array_keys($bundles) : $ir->choice('Bundles, comma separated', $bundles, NULL, TRUE);
$vars['classes'] = [];
$vars['classes_fqn'] = [];
/** @psalm-var list<string> $bundle_ids */
foreach ($bundle_ids as $bundle_id) {
$vars['bundle_id'] = $bundle_id;
$vars['class'] = $ir->ask(
\sprintf('Class for "%s" bundle', $bundles[$bundle_id]),
'{bundle_id|camelize}',
);
$assets->addFile('src/Entity/{entity_class}/{class}.php', 'bundle-class.twig')->vars($vars);
// Track all bundle classes to generate hook_entity_bundle_info_alter().
$vars['classes'][$bundle_id] = $vars['class'];
$vars['classes_fqn'][$bundle_id] = '\\' . $vars['namespace'] . '\\' . $vars['class'];
}
$vars['base_class'] = NULL;
if ($ir->confirm('Use a base class?', FALSE)) {
$vars['base_class'] = $ir->ask('Base class', '{entity_type_id|camelize}Base');
$assets->addFile('src/Entity/{entity_class}/{base_class}.php', 'bundle-base-class.twig');
}
// @todo Handle duplicated hooks.
$assets->addFile('{machine_name}.module', 'module.twig')
->appendIfExists(9);
}
}

View File

@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\Required;
use DrupalCodeGenerator\Validator\RequiredMachineName;
#[Generator(
name: 'field',
description: 'Generates a field',
templatePath: Application::TEMPLATE_PATH . '/_field',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Field extends BaseGenerator {
/**
* Field sub-types.
*/
private const array SUB_TYPES = [
'boolean' => [
'label' => 'Boolean',
'list' => FALSE,
'random' => FALSE,
'inline' => FALSE,
'link' => FALSE,
'data_type' => 'boolean',
],
'string' => [
'label' => 'Text',
'list' => TRUE,
'random' => TRUE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'string',
],
'text' => [
'label' => 'Text (long)',
'list' => FALSE,
'random' => TRUE,
'inline' => FALSE,
'link' => FALSE,
'data_type' => 'string',
],
'integer' => [
'label' => 'Integer',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'integer',
],
'float' => [
'label' => 'Float',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'float',
],
'numeric' => [
'label' => 'Numeric',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'float',
],
'email' => [
'label' => 'Email',
'list' => TRUE,
'random' => TRUE,
'inline' => TRUE,
'link' => TRUE,
'data_type' => 'email',
],
'telephone' => [
'label' => 'Telephone',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => TRUE,
'data_type' => 'string',
],
'uri' => [
'label' => 'Url',
'list' => TRUE,
'random' => TRUE,
'inline' => TRUE,
'link' => TRUE,
'data_type' => 'uri',
],
'datetime' => [
'label' => 'Date',
'list' => TRUE,
'random' => FALSE,
'inline' => FALSE,
'link' => FALSE,
'data_type' => 'datetime_iso8601',
],
];
/**
* Date types.
*/
private const array DATE_TYPES = [
'date' => 'Date only',
'datetime' => 'Date and time',
];
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['field_label'] = $ir->ask('Field label', 'Example', new Required());
$vars['field_id'] = $ir->ask('Field ID', '{machine_name}_{field_label|h2m}', new RequiredMachineName());
$subfield_count_validator = static function (mixed $value): int {
if (!(\is_int($value) || \ctype_digit($value)) || (int) $value <= 0) {
throw new \UnexpectedValueException('The value should be greater than zero.');
}
return (int) $value;
};
$vars['subfield_count'] = $ir->ask('How many sub-fields would you like to create?', '3', $subfield_count_validator);
$type_choices = \array_combine(
\array_keys(self::SUB_TYPES),
\array_column(self::SUB_TYPES, 'label'),
);
// Indicates that at least one of sub-fields needs Random component.
$vars['random'] = FALSE;
// Indicates that all sub-fields can be rendered inline.
$vars['inline'] = TRUE;
// Indicates that at least one of sub-fields has limited allowed values.
$vars['list'] = FALSE;
// Indicates that at least one of sub-fields is required.
$vars['required'] = FALSE;
// Indicates that at least one of sub-fields is of email type.
$vars['email'] = FALSE;
// Indicates that at least one of sub-fields can be rendered as a link.
$vars['link'] = FALSE;
// Indicates that at least one of sub-fields is of datetime type.
$vars['datetime'] = FALSE;
$vars['type_class'] = '{field_label|camelize}Item';
$vars['widget_class'] = '{field_label|camelize}Widget';
$vars['formatter_class'] = '{field_label|camelize}DefaultFormatter';
for ($i = 1; $i <= $vars['subfield_count']; $i++) {
$this->io()->writeln(\sprintf('<fg=green>%s</>', \str_repeat('', 50)));
$subfield = new \stdClass();
$subfield->name = $ir->ask("Label for sub-field #$i", "Value $i");
$subfield->machineName = $ir->ask(
"Machine name for sub-field #$i",
Utils::human2machine($subfield->name),
new RequiredMachineName(),
);
/** @var string $type */
$type = $ir->choice("Type of sub-field #$i", $type_choices, 'Text');
$subfield->dateType = $type === 'datetime' ?
$ir->choice("Date type for sub-field #$i", self::DATE_TYPES, 'Date only') : NULL;
$definition = self::SUB_TYPES[$type];
if ($definition['list']) {
$subfield->list = $ir->confirm("Limit allowed values for sub-field #$i?", FALSE);
}
$subfield->required = $ir->confirm("Make sub-field #$i required?", FALSE);
// Build sub-field vars.
$vars['subfields'][$i] = [
'name' => $subfield->name,
'machine_name' => $subfield->machineName,
'type' => $type,
'data_type' => $definition['data_type'],
'list' => $subfield->list ?? FALSE,
'allowed_values_method' => 'allowed' . Utils::camelize($subfield->name, TRUE) . 'Values',
'required' => $subfield->required,
'link' => $definition['link'],
];
if ($subfield->dateType) {
$vars['subfields'][$i]['date_type'] = $subfield->dateType;
// Back to date type ID.
$vars['subfields'][$i]['date_storage_format'] = $subfield->dateType === 'date' ? 'Y-m-d' : 'Y-m-d\TH:i:s';
}
if ($definition['random']) {
$vars['random'] = TRUE;
}
if (!$definition['inline']) {
$vars['inline'] = FALSE;
}
if ($vars['subfields'][$i]['list']) {
$vars['list'] = TRUE;
}
if ($vars['subfields'][$i]['required']) {
$vars['required'] = TRUE;
}
if ($type === 'email') {
$vars['email'] = TRUE;
}
if ($definition['link']) {
$vars['link'] = TRUE;
}
if ($type === 'datetime') {
$vars['datetime'] = TRUE;
}
}
$this->io()->writeln(\sprintf('<fg=green>%s</>', \str_repeat('', 50)));
$vars['storage_settings'] = $ir->confirm('Would you like to create field storage settings form?', FALSE);
$vars['instance_settings'] = $ir->confirm('Would you like to create field instance settings form?', FALSE);
$vars['widget_settings'] = $ir->confirm('Would you like to create field widget settings form?', FALSE);
$vars['formatter_settings'] = $ir->confirm('Would you like to create field formatter settings form?', FALSE);
$vars['table_formatter'] = $ir->confirm('Would you like to create table formatter?', FALSE);
$vars['key_value_formatter'] = $ir->confirm('Would you like to create key-value formatter?', FALSE);
$assets->addFile('src/Plugin/Field/FieldType/{type_class}.php', 'type.twig');
$assets->addFile('src/Plugin/Field/FieldWidget/{widget_class}.php', 'widget.twig');
$assets->addFile('src/Plugin/Field/FieldFormatter/{formatter_class}.php', 'default-formatter.twig');
$assets->addSchemaFile()->template('schema.twig');
$assets->addFile('{machine_name}.libraries.yml', 'libraries.twig')
->appendIfExists();
$assets->addFile('css/{field_id|u2h}-widget.css', 'widget-css.twig');
if ($vars['table_formatter']) {
$vars['table_formatter_class'] = '{field_label|camelize}TableFormatter';
$assets->addFile('src/Plugin/Field/FieldFormatter/{table_formatter_class}.php', '/table-formatter.twig');
}
if ($vars['key_value_formatter']) {
$vars['key_value_formatter_class'] = '{field_label|camelize}KeyValueFormatter';
$assets->addFile('src/Plugin/Field/FieldFormatter/{key_value_formatter_class}.php', 'key-value-formatter.twig');
}
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Form;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
/**
* Config form generator.
*
* @todo Clean-up.
*/
#[Generator(
name: 'form:config',
description: 'Generates a configuration form',
aliases: ['config-form'],
templatePath: Application::TEMPLATE_PATH . '/Form/_config',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Config extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: 'SettingsForm');
$vars['raw_form_id'] = \preg_replace('/_form/', '', Utils::camel2machine($vars['class']));
$vars['form_id'] = '{machine_name}_{raw_form_id}';
$vars['route'] = $ir->confirm('Would you like to create a route for this form?');
if ($vars['route']) {
$default_route_path = \str_replace('_', '-', '/admin/config/system/' . $vars['raw_form_id']);
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $vars['raw_form_id']);
$vars['route_path'] = $ir->ask('Route path', $default_route_path);
$vars['route_title'] = $ir->ask('Route title', '{raw_form_id|m2h}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'administer site configuration');
$assets->addFile('{machine_name}.routing.yml')
->template('routing.twig')
->appendIfExists();
if ($vars['link'] = $ir->confirm('Would you like to create a menu link for this route?')) {
$vars['link_title'] = $ir->ask('Link title', $vars['route_title']);
$vars['link_description'] = $ir->ask('Link description');
// Try to guess parent menu item using route path.
if (\preg_match('#^/admin/config/([^/]+)/[^/]+$#', $vars['route_path'], $matches)) {
$vars['link_parent'] = $ir->ask('Parent menu item', 'system.admin_config_' . $matches[1]);
}
$assets->addFile('{machine_name}.links.menu.yml')
->template('links.menu.twig')
->appendIfExists();
}
}
$assets->addFile('src/Form/{class}.php', 'form.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Form;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
/**
* Confirm form generator.
*
* @todo Clean-up.
*/
#[Generator(
name: 'form:confirm',
description: 'Generates a confirmation form',
aliases: ['confirm-form'],
templatePath: Application::TEMPLATE_PATH . '/Form/_confirm',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Confirm extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: 'ExampleConfirmForm');
$vars['raw_form_id'] = \preg_replace('/_form/', '', Utils::camel2machine($vars['class']));
$vars['form_id'] = '{machine_name}_{raw_form_id}';
$vars['route'] = $ir->confirm('Would you like to create a route for this form?');
if ($vars['route']) {
$default_route_path = \str_replace('_', '-', '/' . $vars['machine_name'] . '/' . $vars['raw_form_id']);
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $vars['raw_form_id']);
$vars['route_path'] = $ir->ask('Route path', $default_route_path);
$vars['route_title'] = $ir->ask('Route title', '{raw_form_id|m2t}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'administer site configuration');
$assets->addFile('{machine_name}.routing.yml')
->template('routing.twig')
->appendIfExists();
}
$assets->addFile('src/Form/{class}.php', 'form.twig');
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Form;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
/**
* A generator for a simple form.
*/
#[Generator(
name: 'form:simple',
description: 'Generates simple form',
aliases: ['form'],
templatePath: Application::TEMPLATE_PATH . '/Form/_simple',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Simple extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: 'ExampleForm');
$vars['raw_form_id'] = Utils::camel2machine(Utils::removeSuffix($vars['class'], 'Form'));
$vars['form_id'] = '{machine_name}_{raw_form_id}';
$vars['route'] = $ir->confirm('Would you like to create a route for this form?');
if ($vars['route']) {
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $vars['raw_form_id']);
$default_route_path = \str_replace('_', '-', '/' . $vars['machine_name'] . '/' . $vars['raw_form_id']);
$vars['route_path'] = $ir->ask('Route path', $default_route_path);
$vars['route_title'] = $ir->ask('Route title', '{raw_form_id|m2t}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'access content');
$assets->addFile('{machine_name}.routing.yml')
->template('routing.twig')
->appendIfExists();
}
$assets->addFile('src/Form/{class}.php', 'form.twig');
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\Choice;
use DrupalCodeGenerator\Validator\Required;
use Symfony\Component\Console\Question\Question;
#[Generator(
name: 'hook',
description: 'Generates a hook',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Hook extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
/** @var \DrupalCodeGenerator\Helper\Drupal\HookInfo $hook_info */
$hook_info = $this->getHelper('hook_info');
$hook_templates = $hook_info->getHookTemplates();
$available_hooks = \array_keys($hook_templates);
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$hook_question = new Question('Hook name');
$validator = new Chained(
new Required(),
new Choice($available_hooks, 'The value is not correct hook name.'),
);
$hook_question->setValidator($validator);
$hook_question->setAutocompleterValues($available_hooks);
$vars['hook_name'] = $this->io()->askQuestion($hook_question);
$vars['file_type'] = $hook_info::getFileType($vars['hook_name']);
$assets->addFile('{machine_name}.{file_type}')
->inlineTemplate($hook_templates[$vars['hook_name']])
->appendIfExists(9);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'install-file',
description: 'Generates an install file',
templatePath: Application::TEMPLATE_PATH . '/_install-file',
type: GeneratorType::MODULE_COMPONENT,
)]
final class InstallFile extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$assets->addFile('{machine_name}.install', 'install.twig');
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'javascript',
description: 'Generates Drupal JavaScript file',
templatePath: Application::TEMPLATE_PATH . '/_javascript',
type: GeneratorType::MODULE_COMPONENT,
)]
final class JavaScript extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['file_name_full'] = $ir->ask('File name', '{machine_name|u2h}.js');
$vars['file_name'] = \pathinfo($vars['file_name_full'], \PATHINFO_FILENAME);
$vars['behavior'] = Utils::camelize($vars['machine_name'], FALSE) . Utils::camelize($vars['file_name']);
if ($ir->confirm('Would you like to create a library for this file?')) {
$vars['library'] = $ir->ask('Library name', '{file_name|h2u}');
$assets->addFile('{machine_name}.libraries.yml', 'libraries.twig')
->appendIfExists();
}
$assets->addFile('js/{file_name_full}', 'javascript.twig');
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
/**
* Interface for generators that provide human-readable label.
*/
interface LabelInterface {
/**
* Returns the human-readable command label.
*/
public function getLabel(): ?string;
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'layout',
description: 'Generates a layout',
templatePath: Application::TEMPLATE_PATH . '/_layout',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Layout extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['layout_name'] = $ir->ask('Layout name', 'Example');
$vars['layout_machine_name'] = $ir->ask('Layout machine name', '{layout_name|h2m}');
$vars['category'] = $ir->ask('Category', '{machine_name|m2h} Layouts');
$vars['js'] = $ir->confirm('Would you like to create JavaScript file for this layout?', FALSE);
$vars['css'] = $ir->confirm('Would you like to create CSS file for this layout?', FALSE);
$assets->addFile('{machine_name}.layouts.yml', 'layouts.twig')
->appendIfExists();
if ($vars['js'] || $vars['css']) {
$assets->addFile('{machine_name}.libraries.yml', 'libraries.twig')
->appendIfExists();
}
$vars['layout_asset_name'] = '{layout_machine_name|u2h}';
$assets->addFile('layouts/{layout_machine_name}/{layout_asset_name}.html.twig', 'template.twig');
if ($vars['js']) {
$assets->addFile('layouts/{layout_machine_name}/{layout_asset_name}.js', 'javascript.twig');
}
if ($vars['css']) {
$assets->addFile('layouts/{layout_machine_name}/{layout_asset_name}.css', 'styles.twig');
}
}
}

View File

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Extension\ModuleExtensionList;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Required;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'module',
description: 'Generates Drupal module',
templatePath: Application::TEMPLATE_PATH . '/_module',
type: GeneratorType::MODULE,
)]
final class Module extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly ModuleExtensionList $moduleList,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container->get('extension.list.module'));
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['name'] = $ir->askName();
$vars['machine_name'] = $ir->askMachineName();
$vars['description'] = $ir->ask('Module description', validator: new Required());
$vars['package'] = $ir->ask('Package', 'Custom');
$dependencies = $ir->ask('Dependencies (comma separated)');
$vars['dependencies'] = $this->buildDependencies($dependencies);
$assets->addFile('{machine_name}/{machine_name}.info.yml', 'model.info.yml.twig');
if ($ir->confirm('Would you like to create module file?', FALSE)) {
$assets->addFile('{machine_name}/{machine_name}.module', 'model.module.twig');
}
if ($ir->confirm('Would you like to create install file?', FALSE)) {
$assets->addFile('{machine_name}/{machine_name}.install', 'model.install.twig');
}
if ($ir->confirm('Would you like to create README.md file?', FALSE)) {
$assets->addFile('{machine_name}/README.md', 'README.md.twig');
}
}
/**
* Builds array of dependencies from comma-separated string.
*/
private function buildDependencies(?string $dependencies_encoded): array {
$dependencies = $dependencies_encoded ? \explode(',', $dependencies_encoded) : [];
foreach ($dependencies as &$dependency) {
$dependency = \str_replace(' ', '_', \trim(\strtolower($dependency)));
// Check if the module name is already prefixed.
if (\str_contains($dependency, ':')) {
continue;
}
// Dependencies should be namespaced in the format {project}:{name}.
$project = $dependency;
try {
// The extension list is internal for extending not for instantiating.
// @see \Drupal\Core\Extension\ExtensionList
/** @psalm-suppress InternalMethod */
$package = $this->moduleList->getExtensionInfo($dependency)['package'] ?? NULL;
if ($package === 'Core') {
$project = 'drupal';
}
}
catch (UnknownExtensionException) {
}
$dependency = $project . ':' . $dependency;
}
$dependency_sorter = static function (string $a, string $b): int {
// Core dependencies go first.
$a_is_drupal = \str_starts_with($a, 'drupal:');
$b_is_drupal = \str_starts_with($b, 'drupal:');
if ($a_is_drupal xor $b_is_drupal) {
return $a_is_drupal ? -1 : 1;
}
return $a <=> $b;
};
\uasort($dependencies, $dependency_sorter);
return $dependencies;
}
}

View File

@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\InputOutput\DefaultOptions;
use DrupalCodeGenerator\InputOutput\IOAwareInterface;
use DrupalCodeGenerator\InputOutput\IOAwareTrait;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
/**
* Implements navigation command.
*/
#[AsCommand(name: 'navigation')]
final class Navigation extends Command implements IOAwareInterface, LoggerAwareInterface {
use IOAwareTrait;
use LoggerAwareTrait;
/**
* Menu tree.
*/
private array $menuTree = [];
/**
* Menu labels.
*/
private array $labels = [
'misc:d7' => 'Drupal 7',
'yml' => 'Yaml',
'misc' => 'Miscellaneous',
];
/**
* {@inheritdoc}
*/
protected function configure(): void {
// As the navigation is default command the help should be relevant to the
// entire DCG application.
$help = <<<'EOT'
<info>dcg</info> Display navigation
<info>dcg plugin:field:widget</info> Run a specific generator
<info>dcg list</info> List all available generators
EOT;
$this
->setName('navigation')
->setDescription('Command line code generator')
->setHelp($help)
->setHidden();
DefaultOptions::apply($this);
}
/**
* {@inheritdoc}
*/
public function getSynopsis($short = FALSE): string {
return 'dcg [options] <generator>';
}
/**
* {@inheritdoc}
*/
protected function initialize(InputInterface $input, OutputInterface $output): void {
parent::initialize($input, $output);
// Build the menu structure.
$this->menuTree = [];
if (!$application = $this->getApplication()) {
throw new \LogicException('Navigation command cannot work without application');
}
foreach ($application->all() as $command) {
if ($command instanceof LabelInterface && !$command->isHidden()) {
/** @var string $command_name */
$command_name = $command->getName();
self::arraySetNestedValue($this->menuTree, \explode(':', $command_name));
// Collect command labels.
if ($label = $command->getLabel()) {
$this->labels[$command_name] = $label;
}
}
}
self::recursiveKsort($this->menuTree);
$style = new OutputFormatterStyle('white', 'blue', ['bold']);
$output->getFormatter()->setStyle('title', $style);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
if ($command_name = $this->selectGenerator($input, $output)) {
if (!$application = $this->getApplication()) {
throw new \LogicException('Navigation command cannot work without application');
}
return $application
->find($command_name)
->run($input, $output);
}
return 0;
}
/**
* Selects a generator.
*
* Returns a generator selected by the user from a multilevel console menu or
* null if user decided to exit the navigation.
*/
private function selectGenerator(InputInterface $input, OutputInterface $output, array $menu_trail = []): ?string {
// Narrow down the menu tree using menu trail.
$active_menu_tree = $this->menuTree;
foreach ($menu_trail as $active_menu_item) {
$active_menu_tree = $active_menu_tree[$active_menu_item];
}
// The $active_menu_tree can be either an array of menu items or TRUE if the
// user has reached the final menu point.
if ($active_menu_tree === TRUE) {
return \implode(':', $menu_trail);
}
$sub_menu_labels = $command_labels = [];
foreach ($active_menu_tree as $menu_item => $subtree) {
$command_name = $menu_trail ? (\implode(':', $menu_trail) . ':' . $menu_item) : $menu_item;
$label = $this->labels[$command_name] ?? \str_replace(['-', '_'], ' ', \ucfirst($menu_item));
\is_array($subtree)
? $sub_menu_labels[$menu_item] = "<comment>$label</comment>"
: $command_labels[$menu_item] = $label;
}
// Generally the choices array consists of the following parts:
// - Reference to the parent menu level.
// - Sorted list of nested menu levels.
// - Sorted list of commands.
\natcasesort($sub_menu_labels);
\natcasesort($command_labels);
$choices = ['..' => '..'] + $sub_menu_labels + $command_labels;
$question = new ChoiceQuestion('<title> Select generator </title>', \array_values($choices));
$answer_label = $this->getHelper('question')->ask($input, $output, $question);
$answer = \array_search($answer_label, $choices);
if ($answer === '..') {
// Exit the application if a user selected zero on the top menu level.
if (\count($menu_trail) === 0) {
return NULL;
}
// Level up.
\array_pop($menu_trail);
}
else {
// Level down.
$menu_trail[] = $answer;
}
return $this->selectGenerator($input, $output, $menu_trail);
}
/**
* Sort multidimensional array by keys.
*
* @param array $array
* An array being sorted.
*/
private static function recursiveKsort(array &$array): void {
foreach ($array as &$value) {
if (\is_array($value)) {
self::recursiveKsort($value);
}
}
\ksort($array);
}
/**
* Sets the property to true in nested array.
*
* @psalm-param list<string> $parents
* An array of parent keys, starting with the outermost key.
*
* @see https://api.drupal.org/api/drupal/includes!common.inc/function/drupal_array_set_nested_value/7
*/
private static function arraySetNestedValue(array &$array, array $parents): void {
$ref = &$array;
foreach ($parents as $parent) {
if (isset($ref) && !\is_array($ref)) {
$ref = [];
}
// @todo Fix this.
/** @psalm-suppress PossiblyNullArrayAccess */
$ref = &$ref[$parent];
}
$ref ??= TRUE;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for configuration entity types.
*/
final class ConfigEntityIds {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$entity_definitions = \array_filter(
$this->entityTypeManager->getDefinitions(),
static fn (EntityTypeInterface $entity_type): bool => $entity_type instanceof ConfigEntityTypeInterface,
);
\ksort($entity_definitions);
$definitions = [];
foreach ($entity_definitions as $type => $entity_definition) {
/** @psalm-var array<string, string> $ids */
$ids = $this->entityTypeManager
->getStorage($type)
->getQuery()
->accessCheck(FALSE)
->execute();
if (\count($ids) > 0) {
$definitions[] = [
'type' => $type,
'label' => $entity_definition->getLabel(),
'class' => Utils::addLeadingSlash($entity_definition->getClass()),
'interface' => ($this->entityInterface)($entity_definition),
'ids' => \array_values($ids),
];
}
}
return File::create('.phpstorm.meta.php/config_entity_ids.php')
->template('config_entity_ids.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\ConfigInfo;
/**
* Generates PhpStorm meta-data for Drupal configuration.
*/
final class Configuration {
/**
* Constructs the object.
*/
public function __construct(
private readonly ConfigInfo $configInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/configuration.php')
->template('configuration.php.twig')
->vars(['configs' => $this->configInfo->getConfigNames()]);
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Database\Connection;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal database.
*/
final class Database {
/**
* Constructs the object.
*/
public function __construct(
private readonly Connection $connection,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$driver = $this->connection->driver();
$tables = [];
// @todo Support PostgreSQL.
if ($driver === 'mysql') {
/** @psalm-suppress PossiblyNullReference */
$tables = $this->connection->query('SHOW TABLES')->fetchCol();
}
elseif ($driver === 'sqlite') {
$query = <<< 'SQL'
SELECT name
FROM sqlite_schema
WHERE type ='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
SQL;
/** @psalm-suppress PossiblyNullReference */
$tables = $this->connection->query($query)->fetchCol();
}
return File::create('.phpstorm.meta.php/database.php')
->template('database.php.twig')
->vars(['tables' => $tables]);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for date formats.
*/
final class DateFormats {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$date_formats = $this->entityTypeManager
->getStorage('date_format')
->loadMultiple();
$date_formats['custom'] = NULL;
return File::create('.phpstorm.meta.php/date_formats.php')
->template('date_formats.php.twig')
->vars(['date_formats' => \array_keys($date_formats)]);
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity bundles.
*/
final class EntityBundles {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly EntityTypeBundleInfoInterface $entityTypeBundleInfo,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$definitions = [];
$entity_definitions = $this->entityTypeManager->getDefinitions();
\ksort($entity_definitions);
$bundle_getters = [
'node' => 'getType',
'comment' => 'getTypeId',
];
foreach ($entity_definitions as $entity_type_id => $entity_definition) {
$definitions[] = [
'type' => $entity_type_id,
'label' => $entity_definition->getLabel(),
'class' => Utils::addLeadingSlash($entity_definition->getClass()),
'interface' => ($this->entityInterface)($entity_definition),
'bundle_getter' => $bundle_getters[$entity_type_id] ?? NULL,
'bundles' => \array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)),
];
}
return File::create('.phpstorm.meta.php/entity_bundles.php')
->template('entity_bundles.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity links.
*/
final class EntityLinks {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
$definitions[] = [
'type' => $entity_type,
'label' => $definition->getLabel(),
'class' => Utils::addLeadingSlash($definition->getClass()),
'interface' => ($this->entityInterface)($definition),
'links' => \array_keys($definition->getLinkTemplates()),
];
}
\asort($definitions);
return File::create('.phpstorm.meta.php/entity_links.php')
->template('entity_links.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity types.
*/
final class EntityTypes {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$normalized_definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $type => $definition) {
$normalized_definitions[$type]['class'] = self::normalizeType($definition->getClass());
$normalized_definitions[$type]['storage'] = self::normalizeType($definition->getStorageClass());
$normalized_definitions[$type]['access_control'] = self::normalizeType($definition->getAccessControlClass());
$normalized_definitions[$type]['list_builder'] = self::normalizeType($definition->getListBuilderClass());
$normalized_definitions[$type]['view_builder'] = self::normalizeType($definition->getViewBuilderClass());
}
\ksort($normalized_definitions);
return File::create('.phpstorm.meta.php/entity_types.php')
->template('entity_types.php.twig')
->vars(['definitions' => $normalized_definitions]);
}
/**
* Normalizes handler type.
*/
private static function normalizeType(?string $class): ?string {
if ($class === NULL) {
return NULL;
}
$class = Utils::addLeadingSlash($class);
/** @psalm-var class-string $interface */
$interface = $class . 'Interface';
return \is_a($class, $interface, TRUE) ? $interface : $class;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for extensions.
*/
final class Extensions {
/**
* Constructs the object.
*/
public function __construct(
private readonly ModuleHandlerInterface $moduleHandler,
private readonly ThemeHandlerInterface $themeHandler,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
// Module handler also manages profiles.
$module_extensions = \array_filter(
$this->moduleHandler->getModuleList(),
static fn (Extension $extension): bool => $extension->getType() === 'module',
);
$modules = \array_keys($module_extensions);
$themes = \array_keys($this->themeHandler->listInfo());
\sort($themes);
return File::create('.phpstorm.meta.php/extensions.php')
->template('extensions.php.twig')
->vars(['modules' => $modules, 'themes' => $themes]);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for field definitions.
*/
final class FieldDefinitions {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly FieldTypePluginManagerInterface $fieldTypePluginManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$entity_types = \array_keys($this->entityTypeManager->getDefinitions());
\sort($entity_types);
$field_types = \array_keys($this->fieldTypePluginManager->getDefinitions());
\sort($field_types);
return File::create('.phpstorm.meta.php/field_definitions.php')
->template('field_definitions.php.twig')
->vars(['entity_types' => $entity_types, 'field_types' => $field_types]);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity fields.
*/
final class Fields {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly EntityFieldManagerInterface $entityFieldManager,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
if (!$definition->entityClassImplements(FieldableEntityInterface::class)) {
continue;
}
$definitions[] = [
'type' => $entity_type,
'label' => $definition->getLabel(),
'class' => Utils::addLeadingSlash($definition->getClass()),
'interface' => ($this->entityInterface)($definition),
'fields' => \array_keys($this->entityFieldManager->getFieldStorageDefinitions($entity_type)),
];
}
return File::create('.phpstorm.meta.php/fields.php')
->template('fields.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal filesystem helpers.
*/
final class FileSystem {
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/file_system.php')
->template('file_system.php.twig');
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\HookInfo;
use DrupalCodeGenerator\Helper\Drupal\ModuleInfo;
/**
* Generates PhpStorm meta-data for Drupal hooks.
*/
final class Hooks {
/**
* Constructs the object.
*/
public function __construct(
private readonly HookInfo $hookInfo,
private readonly ModuleInfo $moduleInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$hooks = \array_keys($this->hookInfo->getHookTemplates());
$modules = \array_keys($this->moduleInfo->getExtensions());
return File::create('.phpstorm.meta.php/hooks.php')
->template('hooks.php.twig')
->vars(['hooks' => $hooks, 'modules' => $modules]);
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for miscellaneous Drupal methods.
*/
final class Miscellaneous {
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/miscellaneous.php')
->template('miscellaneous.php.twig');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\PermissionInfo;
/**
* Generates PhpStorm meta-data for permissions.
*/
final class Permissions {
/**
* Constructs the object.
*/
public function __construct(
private readonly PermissionInfo $permissionInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$permissions = $this->permissionInfo->getPermissionNames();
return File::create('.phpstorm.meta.php/permissions.php')
->template('permissions.php.twig')
->vars(['permissions' => $permissions]);
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'phpstorm-meta',
description: 'Generates PhpStorm metadata',
templatePath: Application::TEMPLATE_PATH . '/_phpstorm-meta',
type: GeneratorType::OTHER,
label: 'PhpStorm metadata',
)]
final class PhpStormMeta extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly ContainerInterface $container,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container);
}
/**
* {@inheritdoc}
*
* @noinspection PhpParamsInspection
* @psalm-suppress ArgumentTypeCoercion
*/
protected function generate(array &$vars, Assets $assets): void {
$service = fn (string $name): object => $this->container->get($name);
$entity_interface = static function (EntityTypeInterface $definition): ?string {
$class = Utils::addLeadingSlash($definition->getClass());
// Most content entity types implement an interface which name follows
// this pattern.
$interface = \str_replace('\Entity\\', '\\', $class) . 'Interface';
return $definition->entityClassImplements($interface) ? $interface : NULL;
};
$assets[] = (new ConfigEntityIds($service('entity_type.manager'), $entity_interface))();
$assets[] = (new Configuration($this->getHelper('config_info')))();
$assets[] = (new Database($service('database')))();
$assets[] = (new DateFormats($service('entity_type.manager')))();
$assets[] = (new EntityBundles($service('entity_type.manager'), $service('entity_type.bundle.info'), $entity_interface))();
$assets[] = (new EntityLinks($service('entity_type.manager'), $entity_interface))();
$assets[] = (new EntityTypes($service('entity_type.manager')))();
$assets[] = (new Extensions($service('module_handler'), $service('theme_handler')))();
$assets[] = (new FieldDefinitions($service('entity_type.manager'), $service('plugin.manager.field.field_type')))();
$assets[] = (new Fields($service('entity_type.manager'), $service('entity_field.manager'), $entity_interface))();
$assets[] = (new FileSystem())();
$assets[] = (new Hooks($this->getHelper('hook_info'), $this->getHelper('module_info')))();
$assets[] = (new Miscellaneous())();
$assets[] = (new Permissions($this->getHelper('permission_info')))();
$assets[] = (new Plugins($this->getHelper('service_info')))();
$assets[] = (new Roles($service('entity_type.manager')))();
$assets[] = (new Routes($this->getHelper('route_info')))();
$assets[] = (new Services($this->getHelper('service_info')))();
$assets[] = (new Settings())();
$assets[] = (new States($service('keyvalue'), $service('cron')))();
}
/**
* {@inheritdoc}
*/
protected function getDestination(array $vars): string {
// Typically the root of the PhpStorm project is one level above of the
// Drupal root.
if (!\file_exists(\DRUPAL_ROOT . '/.idea') && \file_exists(\DRUPAL_ROOT . '/../.idea')) {
return \DRUPAL_ROOT . '/..';
}
return \DRUPAL_ROOT;
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Plugin\DefaultPluginManager;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\ServiceInfo;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for plugins.
*/
final class Plugins {
/**
* Constructs the object.
*/
public function __construct(
private readonly ServiceInfo $serviceInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$plugins = [];
foreach ($this->serviceInfo->getServiceClasses() as $manager_id => $class) {
/** @var class-string $class */
if (!\is_subclass_of($class, DefaultPluginManager::class)) {
continue;
}
/** @var \Drupal\Core\Plugin\DefaultPluginManager $manager */
$manager = $this->serviceInfo->getService($manager_id);
$guessed_interface = $class . 'Interface';
$interface = $manager instanceof $guessed_interface
? $guessed_interface : NULL;
$plugin_ids = \array_keys($manager->getDefinitions());
\sort($plugin_ids);
$plugins[] = [
'manager_id' => $manager_id,
'manager_class' => $class,
'manager_interface' => $interface,
'plugin_interface' => self::getPluginInterface($manager),
'plugin_ids' => $plugin_ids,
];
}
return File::create('.phpstorm.meta.php/plugins.php')
->template('plugins.php.twig')
->vars(['plugins' => $plugins]);
}
/**
* Getter for protected 'pluginInterface' property.
*/
private static function getPluginInterface(DefaultPluginManager $manager): ?string {
$interface = (new \ReflectionClass($manager))
->getProperty('pluginInterface')
->getValue($manager);
return $interface ? Utils::addLeadingSlash($interface) : NULL;
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for roles.
*/
final class Roles {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
// @todo Create a helper for roles.
$roles = $this->entityTypeManager->getStorage('user_role')->loadMultiple();
return File::create('.phpstorm.meta.php/roles.php')
->template('roles.php.twig')
->vars(['roles' => \array_keys($roles)]);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\RouteInfo;
/**
* Generates PhpStorm meta-data for routes.
*/
final class Routes {
/**
* Constructs the object.
*/
public function __construct(
private readonly RouteInfo $routeInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$routes = $this->routeInfo->getRouteNames();
$route_attributes = $this->getRouteAttributes();
return File::create('.phpstorm.meta.php/routes.php')
->template('routes.php.twig')
->vars(['routes' => $routes, 'route_attributes' => $route_attributes]);
}
/**
* Builds attributes suitable for Route autocompletion.
*/
private function getRouteAttributes(): array {
/** @psalm-var array{options: array, requirements: array, defaults: array} $route_attributes */
$route_attributes = [
'options' => [],
'requirements' => [],
'defaults' => [],
];
foreach ($this->routeInfo->getRoutes() as $route) {
$route_attributes['options'] += $route->getOptions();
$route_attributes['requirements'] += $route->getRequirements();
$route_attributes['defaults'] += $route->getDefaults();
}
$is_internal = static fn (string $option_name): bool => \str_starts_with($option_name, '_');
foreach ($route_attributes as $name => $attributes) {
$route_attributes[$name] = \array_filter(\array_keys($route_attributes[$name]), $is_internal);
\sort($route_attributes[$name]);
}
return $route_attributes;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\ServiceInfo;
/**
* Generates PhpStorm meta-data for services.
*/
final class Services {
/**
* Constructs the object.
*/
public function __construct(
private readonly ServiceInfo $serviceInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/services.php')
->template('services.php.twig')
->vars(['services' => $this->serviceInfo->getServiceClasses()]);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Site\Settings as DrupalSettings;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal settings.
*/
final class Settings {
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/settings.php')
->template('settings.php.twig')
->vars(['settings' => \array_keys(DrupalSettings::getAll())]);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\CronInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal states.
*/
final class States {
/**
* Constructs the object.
*/
public function __construct(
private readonly KeyValueFactoryInterface $keyValueStore,
private readonly CronInterface $cron,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$this->cron->run();
$states = \array_keys($this->keyValueStore->get('state')->getAll());
return File::create('.phpstorm.meta.php/states.php')
->template('states.php.twig')
->vars(['states' => $states]);
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredMachineName;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'plugin:action',
description: 'Generates action plugin',
aliases: ['action'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_action',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Action extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel('Action label');
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['category'] = $ir->ask('Action category', 'Custom');
// @todo Create a helper for this.
$definitions = \array_filter(
$this->entityTypeManager->getDefinitions(),
static fn (EntityTypeInterface $definition): bool => $definition instanceof ContentEntityTypeInterface,
);
$entity_type_question = new Question('Entity type to apply the action', 'node');
$entity_type_question->setValidator(new RequiredMachineName());
$entity_type_question->setAutocompleterValues(\array_keys($definitions));
$vars['entity_type'] = $this->io()->askQuestion($entity_type_question);
$vars['configurable'] = $ir->confirm('Make the action configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Action/{class}.php', 'action.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:block',
description: 'Generates block plugin',
aliases: ['block'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_block',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Block extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel('Block admin label');
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Block');
$vars['category'] = $ir->ask('Block category', 'Custom');
$vars['configurable'] = $ir->confirm('Make the block configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$vars['access'] = $ir->confirm('Create access callback?', FALSE);
$assets->addFile('src/Plugin/Block/{class}.php', 'block.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:condition',
description: 'Generates condition plugin',
aliases: ['condition'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_condition',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Condition extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Condition/{class}.php', 'condition.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\RegExp;
use DrupalCodeGenerator\Validator\Required;
/**
* Constraint generator.
*
* @todo Clean-up.
* @todo Create SUT test.
*/
#[Generator(
name: 'plugin:constraint',
description: 'Generates constraint plugin',
aliases: ['constraint'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_constraint',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Constraint extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['plugin_label'] = $ir->askPluginLabel();
// Unlike other plugin types, constraint IDs use camel case.
$validator = new Chained(
new Required(),
new RegExp('/^[a-z][a-z0-9_]*[a-z0-9]$/i', 'The value is not correct constraint ID.'),
);
$vars['plugin_id'] = $ir->ask('Plugin ID', '{name|camelize}{plugin_label|camelize}', $validator);
$unprefixed_plugin_id = Utils::removePrefix($vars['plugin_id'], Utils::camelize($vars['machine_name']));
$vars['class'] = $ir->askPluginClass(default: $unprefixed_plugin_id . 'Constraint');
$input_types = [
'raw_value' => 'Raw value',
'item' => 'Item',
'item_list' => 'Item list',
'entity' => 'Entity',
];
$vars['input_type'] = $ir->choice('Type of data to validate', $input_types);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Validation/Constraint/{class}.php')
->template('constraint.twig');
$assets->addFile('src/Plugin/Validation/Constraint/{class}Validator.php')
->template('validator.twig');
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use Drupal\comment\Plugin\EntityReferenceSelection\CommentSelection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\file\Plugin\EntityReferenceSelection\FileSelection;
use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
use Drupal\taxonomy\Plugin\EntityReferenceSelection\TermSelection;
use Drupal\user\Plugin\EntityReferenceSelection\UserSelection;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredMachineName;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'plugin:entity-reference-selection',
description: 'Generates entity reference selection plugin',
aliases: ['entity-reference-selection'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_entity-reference-selection',
type: GeneratorType::MODULE_COMPONENT,
)]
final class EntityReferenceSelection extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$entity_type_question = new Question('Entity type that can be referenced by this plugin', 'node');
$entity_type_question->setValidator(new RequiredMachineName());
$entity_types = \array_keys($this->entityTypeManager->getDefinitions());
$entity_type_question->setAutocompleterValues($entity_types);
$vars['entity_type'] = $this->io()->askQuestion($entity_type_question);
$vars['plugin_label'] = $ir->askPluginLabel('Plugin label', 'Advanced {entity_type} selection');
$vars['plugin_id'] = $ir->askPluginId(default: '{machine_name}_{entity_type}_selection');
$vars['class'] = $ir->askPluginClass(default: '{entity_type|camelize}Selection');
$vars['configurable'] = $ir->confirm('Provide additional plugin configuration?', FALSE);
$vars['base_class_full'] = match($vars['entity_type']) {
'comment' => CommentSelection::class,
'file' => FileSelection::class,
'node' => NodeSelection::class,
'taxonomy_term' => TermSelection::class,
'user' => UserSelection::class,
default => DefaultSelection::class,
};
$vars['base_class'] = \explode('EntityReferenceSelection\\', $vars['base_class_full'])[1];
$assets->addFile('src/Plugin/EntityReferenceSelection/{class}.php')
->template('entity-reference-selection.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Field;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:field:formatter',
description: 'Generates field formatter plugin',
aliases: ['field-formatter'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Field/_formatter',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Formatter extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Formatter');
$vars['configurable'] = $ir->confirm('Make the formatter configurable?', FALSE);
$assets->addFile('src/Plugin/Field/FieldFormatter/{class}.php', 'formatter.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Field;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:field:type',
description: 'Generates field type plugin',
aliases: ['field-type'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Field/_type',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Type extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Item');
$vars['configurable_storage'] = $ir->confirm('Make the field storage configurable?', FALSE);
$vars['configurable_instance'] = $ir->confirm('Make the field instance configurable?', FALSE);
$assets->addFile('src/Plugin/Field/FieldType/{class}.php', 'type.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Field;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:field:widget',
description: 'Generates field widget plugin',
aliases: ['field-widget'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Field/_widget',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Widget extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Widget');
$vars['configurable'] = $ir->confirm('Make the widget configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Field/FieldWidget/{class}.php', 'widget.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:filter',
description: 'Generates filter plugin',
aliases: ['filter'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_filter',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Filter extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$filter_types = [
'TYPE_HTML_RESTRICTOR' => 'HTML restrictor',
'TYPE_MARKUP_LANGUAGE' => 'Markup language',
'TYPE_TRANSFORM_IRREVERSIBLE' => 'Irreversible transformation',
'TYPE_TRANSFORM_REVERSIBLE' => 'Reversible transformation',
];
$vars['filter_type'] = $ir->choice('Filter type', $filter_types);
$vars['configurable'] = $ir->confirm('Make the filter configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Filter/{class}.php', 'filter.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:menu-link',
description: 'Generates menu-link plugin',
aliases: ['menu-link'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_menu-link',
type: GeneratorType::MODULE_COMPONENT,
)]
final class MenuLink extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askPluginClass('Class', '{machine_name|camelize}MenuLink');
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Menu/{class}.php', 'menu-link.twig');
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Migrate;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:migrate:destination',
description: 'Generates migrate destination plugin',
aliases: ['migrate-destination'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Migrate/_destination',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Destination extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_id'] = $ir->askPluginId(default: NULL);
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/migrate/destination/{class}.php', 'destination.twig');
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Migrate;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:migrate:process',
description: 'Generates migrate process plugin',
aliases: ['migrate-process'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Migrate/_process',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Process extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_id'] = $ir->askPluginId(default: NULL);
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/migrate/process/{class}.php', 'process.twig');
}
}

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