Contrib Project Scaffolding: info.yml, composer.json, CI Config
When to Use
Use this when creating a new contrib module or theme, or when setting up CI for an existing one. The correct scaffolding ensures the project installs properly, declares its dependencies clearly, and runs CI correctly.
Directory Layout
The module repo is the project root. web/ and vendor/ are generated locally and .gitignore'd:
my_module/
├── my_module.info.yml # REQUIRED — declares the module to Drupal
├── composer.json # Required if non-Drupal deps; best practice always
├── src/ # PSR-4 classes (Form/, Plugin/, Service/, …)
│ └── ...
├── my_module.module # Optional procedural hooks
├── config/
│ ├── install/ # Default config shipped with the module
│ └── schema/ # Config schema definitions
├── templates/ # Twig templates
├── tests/
│ └── src/
│ ├── Unit/
│ ├── Kernel/
│ ├── Functional/
│ └── FunctionalJavascript/
├── .gitlab-ci.yml # Recommended — CI test matrix
├── phpcs.xml.dist # phpcs configuration (scope to src/ + tests/)
├── phpstan.neon # phpstan configuration
├── phpunit.xml.dist # phpunit configuration
├── .cspell-project-words.txt # Project-specific spell-check words
├── .gitignore # Excludes /web/ /vendor/ /.ddev/ …
└── web/ # Generated by ddev-drupal-contrib — NOT committed
*.info.yml — Required Keys
name: My Module
type: module
description: 'What this module does.'
core_version_requirement: '^10.3 || ^11'
package: Custom
# Optional but common:
dependencies:
- drupal:field
- drupal:views
test_dependencies:
- drupal:testing
php: '8.1'
lifecycle: stable # stable | experimental | deprecated | obsolete
Required keys: name, type, core_version_requirement. package groups modules in the admin UI. lifecycle marks module maturity; omit for stable modules (default).
composer.json — When Required and What to Include
composer.json is only required when the module has non-Drupal (vendor) dependencies. Drupal.org derives Drupal-to-Drupal dependencies from .info.yml automatically. Including composer.json is best practice regardless — it signals professionalism and supports CI.
{
"name": "drupal/my_module",
"type": "drupal-module",
"description": "What this module does.",
"license": "GPL-2.0-or-later",
"require": {
"drupal/core": "^10.3 || ^11"
},
"require-dev": {
"drupal/core-dev": "^10.3 || ^11",
"drupal/coder": "^8.3",
"phpstan/phpstan": "^1.12 || ^2.1",
"mglaman/phpstan-drupal": "^1.3 || ^2.0"
},
"autoload": {
"psr-4": {
"Drupal\\my_module\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Drupal\\Tests\\my_module\\": "tests/src/"
}
}
}
.gitignore — Contribution-Correct
# Generated by ddev-drupal-contrib — never commit
/web/
/vendor/
# DDEV environment
/.ddev/
# Composer lock (project decision — many contrib modules do not commit it)
# /composer.lock
# IDE
/.idea/
/.vscode/
# OS
.DS_Store
Thumbs.db
CI Config Files — What a Maintainer Adds
# .gitlab-ci.yml
include:
- project: 'project/gitlab_templates'
file: '/includes/include.drupalci.main.yml'
ref: '$_GITLAB_TEMPLATES_REF'
- project: 'project/gitlab_templates'
file: '/includes/include.drupalci.variables.yml'
ref: '$_GITLAB_TEMPLATES_REF'
- project: 'project/gitlab_templates'
file: '/includes/include.drupalci.workflows.yml'
ref: '$_GITLAB_TEMPLATES_REF'
variables:
_TARGET_CORE: "^10 || ^11"
_TARGET_PHP: "8.3"
OPT_IN_TEST_PREVIOUS_MAJOR: "1"
For the phpunit.xml.dist, the schema depends on the target Drupal version — never use a fixed version because the schema differs between Drupal 10 (PHPUnit 9.x, <coverage> block, DrupalListener) and Drupal 11 (PHPUnit 11, <source> block, failOnWarning). Generate from the core phpunit.xml.dist of the target version.
PHPUnit Test Taxonomy
| Type | Base class | Needs | Dir |
|---|---|---|---|
| Unit | Drupal\Tests\UnitTestCase |
Nothing — pure PHP | tests/src/Unit/ |
| Kernel | Drupal\KernelTests\KernelTestBase |
Drupal services; no full DB install | tests/src/Kernel/ |
| Functional | Drupal\Tests\BrowserTestBase |
Full site install, DB, SIMPLETEST_BASE_URL |
tests/src/Functional/ |
| FunctionalJavascript | Drupal\FunctionalJavascriptTests\WebDriverTestBase |
Above + ChromeDriver | tests/src/FunctionalJavascript/ |
Namespace convention: Drupal\Tests\<module>\<TestType>\…
Common Mistakes
- Including
web/orvendor/in the repo — they are generated by the add-on and must be gitignored. - Hardcoding PHPUnit version in
phpunit.xml.dist— copy from the target core'score/phpunit.xml.dist; the schema differs between D10 (PHPUnit 9.x) and D11 (PHPUnit 11). - Omitting
composer.json— include it even without non-Drupal dependencies; it enables CI and signals a maintained project. - Using
type: modulefor themes — usetype: theme. - Setting
core_version_requirementtoo narrowly — use'^10.3 || ^11'to support both LTS branches if the module has no D11-only dependencies.
See Also
- Drupal Contribution Environment: DDEV + Add-ons
- The drupalci Pipeline & gitlab_templates
- Serving as a Drupal Module/Theme Maintainer
- Reference: drupal.org/docs/develop/creating-modules/let-drupal-know-about-your-module-with-an-infoyml-file
- Reference: drupal.org/docs/develop/using-composer/managing-dependencies-for-a-contributed-project