Skip to content

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/ or vendor/ 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's core/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: module for themes — use type: theme.
  • Setting core_version_requirement too narrowly — use '^10.3 || ^11' to support both LTS branches if the module has no D11-only dependencies.

See Also