diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9ef2300ef0c50e93f3314c50d6c063aae35667fa..4f106d72d637ee099249c783093c79461b74ffd8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,3 +7,37 @@ include:
       - "/includes/include.drupalci.main.yml"
       - "/includes/include.drupalci.variables.yml"
       - "/includes/include.drupalci.workflows.yml"
+
+.skip-phpmd-rule: &skip-phpmd-rule
+  if: '$SKIP_PHPMD == "1"'
+  when: never
+
+.phpmd-base:
+  stage: validate
+  rules:
+    - !reference [.opt-in-current-rule]
+    - *skip-phpmd-rule
+    - !reference [.php-files-exist-rule]
+  needs:
+    - composer
+  before_script:
+    - curl -fsSL https://phpmd.org/static/latest/phpmd.phar -o vendor/bin/phpmd
+    - chmod +x vendor/bin/phpmd
+  script:
+    - php --version
+    - vendor/bin/phpmd --version
+    - vendor/bin/phpmd $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME gitlab $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/.phpmd.xml --exclude 'tests/*,**/tests/*' > phpmd-quality-report.json || true
+    - vendor/bin/phpmd $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME text $_WEB_ROOT/modules/custom/$CI_PROJECT_NAME/.phpmd.xml --exclude 'tests/*,**/tests/*'
+  allow_failure: true
+  artifacts:
+    expose_as: phpmd
+    when: always
+    expire_in: 6 mos
+    reports:
+      codequality: phpmd-quality-report.json
+    name: artifacts-$CI_PIPELINE_ID-$CI_JOB_NAME_SLUG
+    paths:
+      - phpmd-quality-report.json
+
+phpmd:
+  extends: .phpmd-base