diff --git a/.gitignore b/.gitignore
index d3af2954728264266a3b446fab3124d1bf420f23..65c86731a56c039d8f15e84190610680a821fa29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
 /recipes
 /.ddev/config.local.yml
 composer.lock
+.php-cs-fixer.cache
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4514871125db4b51c1ec8ff905e1998ffe4b249e
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,54 @@
+################
+# GitLabCI template for Drupal projects.
+#
+# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification.
+# It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
+# As long as you include the project, ref and three files below, any future updates added by the Drupal Association will be used in your
+# pipelines automatically. However, you can modify this template if you have additional needs for your project.
+# The full documentation is on https://project.pages.drupalcode.org/gitlab_templates/
+################
+
+# For information on alternative values for 'ref' see https://project.pages.drupalcode.org/gitlab_templates/info/templates-version/
+# To test a Drupal 7 project, change the first include filename from .main.yml to .main-d7.yml
+include:
+  - project: $_GITLAB_TEMPLATES_REPO
+    ref: $_GITLAB_TEMPLATES_REF
+    file:
+      - "/includes/include.drupalci.main.yml"
+      - "/includes/include.drupalci.variables.yml"
+      - "/includes/include.drupalci.workflows.yml"
+
+################
+# Pipeline configuration variables are defined with default values and descriptions in the file
+# https://git.drupalcode.org/project/gitlab_templates/-/blob/main/includes/include.drupalci.variables.yml
+# Uncomment the lines below if you want to override any of the variables. The following is just an example.
+################
+# variables:
+#   SKIP_ESLINT: '1'
+#   OPT_IN_TEST_NEXT_MAJOR: '1'
+#   _CURL_TEMPLATES_REF: 'main'
+
+variables:
+  SKIP_CSPELL: '1'
+  SKIP_PHPCS: '1'
+
+php-cs-fixer:
+  stage: validate
+  rules:
+    - allow_failure: true
+    - when: on_success
+  needs:
+    - composer
+  script:
+    - cd $CI_PROJECT_DIR && pwd
+    - vendor/bin/php-cs-fixer fix --dry-run $_WEB_ROOT/modules/custom --format=junit > junit.xml
+  allow_failure: true
+  artifacts:
+    expose_as: junit
+    expire_in: 6 mos
+    when: always
+    name: artifacts-$CI_PIPELINE_ID-$CI_JOB_NAME_SLUG
+    paths:
+      - junit.xml
+    reports:
+      junit: junit.xml
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
new file mode 100644
index 0000000000000000000000000000000000000000..350bdef1c72bf1e6fc351123f3c086d60519ec01
--- /dev/null
+++ b/.php-cs-fixer.php
@@ -0,0 +1,60 @@
+<?php
+
+use Ergebnis\License;
+use drupol\PhpCsFixerConfigsDrupal\Config\Drupal8;
+
+$finder = PhpCsFixer\Finder::create()
+  ->in(__DIR__)
+  ->notPath(['web', 'vendor', '.ddev', 'recipes'])
+  ->name('*.module')
+  ->name('*.inc')
+  ->name('*.install')
+  ->name('*.test')
+  ->name('*.profile')
+  ->name('*.theme');
+
+$license = License\Type\None::text(
+  License\Range::since(
+    License\Year::fromString('2025'),
+    new DateTimeZone('UTC')
+  ),
+  License\Holder::fromString('Dezső Biczó'),
+  License\Url::fromString('http://www.gnu.org/licenses/old-licenses/gpl-2.0.html')
+);
+
+$config = new Drupal8();
+$config->setFinder($finder);
+
+$rules = $config->getRules();
+$rules = array_merge($rules, [
+  'header_comment' => [
+    'comment_type' => 'PHPDoc',
+    'header' => $license->header(),
+    'location' => 'after_declare_strict',
+    'separate' => 'both',
+  ],
+  'ordered_class_elements' => [
+    'order' => [
+      'use_trait',
+      'constant_public',
+      'constant_protected',
+      'constant_private',
+      'property_public',
+      'property_protected',
+      'property_private',
+      'construct',
+      'destruct',
+      'magic',
+      'method_static',
+      'phpunit',
+      'method_public',
+      'method_protected',
+      'method_private',
+    ],
+    'sort_algorithm' => 'alpha',
+    'case_sensitive' => false,
+  ],
+]);
+
+$config->setRules($rules);
+return $config;
diff --git a/composer.json b/composer.json
index 37cc83688f7751ff5df684dd1247b3c6b7b9544e..b45729114da9daed94c9358157ab2d1f226a7cf8 100644
--- a/composer.json
+++ b/composer.json
@@ -7,9 +7,18 @@
     "php": "8.1.* || 8.2.* || 8.3.*",
     "drupal/core": "^10.3.2 || ^11.0.0"
   },
+  "config": {
+    "sort-packages": true,
+    "allow-plugins": {
+      "phpstan/extension-installer": true
+    }
+  },
   "require-dev": {
-    "mglaman/phpstan-drupal": "^2.0.0",
-    "phpstan/phpstan": "^2.1.0",
-    "phpstan/phpstan-deprecation-rules": "^2.0.1"
+    "drupol/phpcsfixer-configs-drupal": "^2.2",
+    "ergebnis/license": "^2.6",
+    "mglaman/phpstan-drupal": "^1.3.3",
+    "phpstan/extension-installer": "^1.4",
+    "phpstan/phpstan": "^1.12.19",
+    "phpstan/phpstan-deprecation-rules": "^1.2.1"
   }
 }
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000000000000000000000000000000000000..0f3de974ccb0d68f58a5958db9027925a895f81b
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,23 @@
+includes:
+  - phpstan-baseline.neon
+  - phar://phpstan.phar/conf/bleedingEdge.neon
+
+parameters:
+  level: max
+  paths:
+    - web/modules/custom
+
+  ignoreErrors:
+    # Drupal codes have arrays all over the therefore it is more productive and
+    # sensible if we disable this check.
+    # See https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type
+    # - identifier: missingType.iterableValue
+
+  drupal:
+    bleedingEdge:
+      checkCoreDeprecatedHooksInApiFiles: true
+      checkContribDeprecatedHooksInApiFiles: true
+    rules:
+      testClassSuffixNameRule: true
+      dependencySerializationTraitPropertyRule: true
+      accessResultConditionRule: true