diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a8d57dd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000..e834bc8 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,27 @@ +name: PHP-CS-Fixer + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHP-CS-Fixer + run: composer cs-check diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..3854536 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,27 @@ +name: PHPStan + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPStan + run: composer phpstan diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..a85fab0 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,27 @@ +name: PHPUnit + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + phpunit: + name: PHPUnit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPUnit + run: composer test diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml new file mode 100644 index 0000000..074798a --- /dev/null +++ b/.github/workflows/rector.yml @@ -0,0 +1,27 @@ +name: Rector + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + rector: + name: Rector + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run Rector + run: composer rector-dry-run diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index cf1a1d8..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: test -on: - push: - -jobs: - test: - strategy: - matrix: - php: ['8.0', '8.2'] - composer: ['--prefer-lowest', ''] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: composer:v2 - - run: composer update ${{ matrix.composer }} - - run: composer validate - - run: vendor/bin/php-cs-fixer fix --dry-run --diff diff --git a/.gitignore b/.gitignore index 3f1433d..54f5fb2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ vendor/ .php-cs-fixer.cache composer.lock +.idea +.phpstan-cache + +/vendor/ +/.phpunit.cache/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 78d4513..a7656b6 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,11 +1,13 @@ in('src') -; +require_once __DIR__ . '/vendor/autoload.php'; -$config = new Valantic\PhpCsFixerConfig\Config(); +use Valantic\PhpCsFixerConfig\ConfigFactory; -return $config - ->setFinder($finder) -; +return ConfigFactory::createValanticConfig() + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ) + ->setRiskyAllowed(true); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3be2557 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 valantic CEC Schweiz AG + +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. diff --git a/README.md b/README.md index e298fcc..e915bc1 100644 --- a/README.md +++ b/README.md @@ -1 +1,65 @@ -# php-cs-fixer-config +# PHP-CS-Fixer Config for Valantic Projects + +This package provides standard PHP-CS-Fixer configurations used in projects built by Valantic. + +## Installation + +```bash +composer require --dev valantic/php-cs-fixer-config +``` + +> **Note:** This package requires PHP 8.1 or higher. + +## Usage + +Create a `.php-cs-fixer.php` or `.php-cs-fixer.dist.php` file in your project root with one of the following configurations: + +### Basic Configuration + +```php + false, + // Add your custom rules here + ]) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ) + // Enable risky rules (recommended as the ruleset includes risky rules) + ->setRiskyAllowed(true) +; +``` + +## Development + +This package provides several Composer scripts to help with development: + +```bash +# Run PHP-CS-Fixer in dry-run mode with diff output +composer cs-check + +# Run PHP-CS-Fixer to fix code style issues +composer cs-fix + +# Run PHPStan for static analysis +composer phpstan + +# Run Rector in dry-run mode +composer rector-dry-run + +# Run Rector and apply changes +composer rector + +# Run PHPUnit tests +composer test + +# Run all checks (cs-check, phpstan, rector-dry-run, test) +composer check +``` diff --git a/composer.json b/composer.json index 8d569f4..6695adb 100644 --- a/composer.json +++ b/composer.json @@ -3,12 +3,46 @@ "description": "Provides a standard php-cs-fixer configuration used in projects built by Valantic.", "license": "MIT", "require": { - "php": "^8.0", - "friendsofphp/php-cs-fixer": "^3.16" + "php": "^8.1", + "friendsofphp/php-cs-fixer": "^3.76" + }, + "require-dev": { + "phpunit/phpunit": "^12.2.6", + "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-strict-rules": "^2.0.4", + "rector/rector": "^2.1", + "roave/security-advisories": "dev-latest", + "phpstan/extension-installer": "^1.4.3" }, "autoload": { "psr-4": { "Valantic\\PhpCsFixerConfig\\": "src" } + }, + "autoload-dev": { + "psr-4": { + "Valantic\\PhpCsFixerConfig\\Tests\\": "tests" + } + }, + "scripts": { + "cs-check": "php-cs-fixer fix --dry-run --diff", + "cs-fix": "php-cs-fixer fix", + "phpstan": "phpstan analyse", + "test": "phpunit", + "rector": "rector process", + "rector-dry-run": "rector process --dry-run", + "check": [ + "@cs-check", + "@phpstan", + "@rector-dry-run", + "@test" + ], + "post-update-cmd": "composer bump -D" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..65dcb04 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + level: 8 + paths: + - src + - tests + excludePaths: + - vendor + tmpDir: .phpstan-cache + strictRules: + dynamicCallOnStaticMethod: false diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..49f3705 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + tests/Unit + + + + + src + + + diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..2b2efea --- /dev/null +++ b/rector.php @@ -0,0 +1,24 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + codingStyle: true, + typeDeclarations: true, + privatization: true, + naming: false, + instanceOf: true, + earlyReturn: true, + strictBooleans: true + ) + ->withPhpSets() + ->withAttributesSets(symfony: true, phpunit: true); diff --git a/src/Config.php b/src/Config.php deleted file mode 100644 index cc1de1f..0000000 --- a/src/Config.php +++ /dev/null @@ -1,211 +0,0 @@ - true, - 'align_multiline_comment' => true, - 'array_indentation' => true, - 'array_syntax' => true, - 'backtick_to_shell_exec' => true, - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => [ - 'statements' => [ - 'return', - ], - ], - 'cast_spaces' => true, - 'class_attributes_separation' => [ - 'elements' => [ - 'method' => 'one', - ], - ], - 'class_definition' => [ - 'single_line' => true, - ], - 'class_reference_name_casing' => true, - 'clean_namespace' => true, - 'concat_space' => ['spacing' => 'one'], - 'curly_braces_position' => [ - 'allow_single_line_anonymous_functions' => true, - 'allow_single_line_empty_anonymous_classes' => true, - ], - 'declare_parentheses' => true, - 'echo_tag_syntax' => true, - 'empty_loop_body' => ['style' => 'braces'], - 'empty_loop_condition' => true, - 'fully_qualified_strict_types' => true, - 'function_typehint_space' => true, - 'general_phpdoc_tag_rename' => [ - 'replacements' => [ - 'inheritDocs' => 'inheritDoc', - ], - ], - 'global_namespace_import' => [ - 'import_classes' => false, - 'import_constants' => false, - 'import_functions' => false, - ], - 'include' => true, - 'increment_style' => true, - 'integer_literal_case' => true, - 'lambda_not_used_import' => true, - 'linebreak_after_opening_tag' => true, - 'magic_constant_casing' => true, - 'magic_method_casing' => true, - 'method_argument_space' => [ - 'on_multiline' => 'ignore', - ], - 'method_chaining_indentation' => true, - 'multiline_whitespace_before_semicolons' => [ - 'strategy' => 'new_line_for_chained_calls', - ], - 'native_function_casing' => true, - 'native_function_type_declaration_casing' => true, - 'no_alias_language_construct_call' => true, - 'no_alternative_syntax' => true, - 'no_binary_string' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => [ - 'tokens' => [ - 'attribute', - 'case', - 'continue', - 'curly_brace_block', - 'default', - 'extra', - 'parenthesis_brace_block', - 'square_brace_block', - 'switch', - 'throw', - 'use', - ], - ], - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => true, - 'no_multiline_whitespace_around_double_arrow' => true, - 'no_null_property_initialization' => true, - 'no_short_bool_cast' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_around_offset' => true, - 'no_superfluous_phpdoc_tags' => [ - 'allow_mixed' => true, - 'allow_unused_params' => true, - ], - 'no_trailing_comma_in_singleline' => true, - 'no_unneeded_control_parentheses' => [ - 'statements' => [ - 'break', - 'clone', - 'continue', - 'echo_print', - 'others', - 'return', - 'switch_case', - 'yield', - 'yield_from', - ], - ], - 'no_unneeded_curly_braces' => [ - 'namespaces' => true, - ], - 'no_unneeded_import_alias' => true, - 'no_unset_cast' => true, - 'no_unused_imports' => true, - 'no_useless_concat_operator' => true, - 'no_useless_nullsafe_operator' => true, - 'no_whitespace_before_comma_in_array' => true, - 'normalize_index_brace' => true, - 'object_operator_without_whitespace' => true, - 'operator_linebreak' => [ - 'only_booleans' => true, - ], - 'ordered_imports' => [ - 'imports_order' => [ - 'class', - 'function', - 'const', - ], - 'sort_algorithm' => 'alpha', - ], - 'php_unit_fqcn_annotation' => true, - 'php_unit_method_casing' => true, - 'phpdoc_align' => [ - 'align' => 'left', - ], - 'phpdoc_annotation_without_dot' => true, - 'phpdoc_indent' => true, - 'phpdoc_inline_tag_normalizer' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_alias_tag' => true, - 'phpdoc_no_package' => true, - 'phpdoc_no_useless_inheritdoc' => true, - 'phpdoc_order' => [ - 'order' => [ - 'param', - 'return', - 'throws', - ], - ], - 'phpdoc_return_self_reference' => true, - 'phpdoc_scalar' => true, - 'phpdoc_separation' => true, - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_summary' => true, - 'phpdoc_tag_type' => [ - 'tags' => [ - 'inheritDoc' => 'inline', - ], - ], - 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_trim_consecutive_blank_line_separation' => true, - 'phpdoc_types' => true, - 'phpdoc_types_order' => [ - 'null_adjustment' => 'always_last', - 'sort_algorithm' => 'none', - ], - 'phpdoc_var_without_name' => true, - 'protected_to_private' => true, - 'return_assignment' => true, - 'semicolon_after_instruction' => true, - 'simple_to_complex_string_variable' => true, - 'single_class_element_per_statement' => true, - 'single_import_per_statement' => true, - 'single_line_comment_spacing' => true, - 'single_line_comment_style' => [ - 'comment_types' => [ - 'hash', - ], - ], - 'single_line_throw' => true, - 'single_quote' => true, - 'single_space_around_construct' => true, - 'space_after_semicolon' => [ - 'remove_in_empty_for_expressions' => true, - ], - 'standardize_increment' => true, - 'standardize_not_equals' => true, - 'switch_continue_to_break' => true, - 'trailing_comma_in_multiline' => [ - 'elements' => ['arguments', 'arrays', 'match', 'parameters'], - ], - 'trim_array_spaces' => true, - 'types_spaces' => true, - 'unary_operator_spaces' => true, - 'whitespace_after_comma_in_array' => true, - 'yoda_style' => [ - 'equal' => false, - 'identical' => false, - 'less_and_greater' => false, - ], - ]; - } -} diff --git a/src/ConfigFactory.php b/src/ConfigFactory.php new file mode 100644 index 0000000..e9b4434 --- /dev/null +++ b/src/ConfigFactory.php @@ -0,0 +1,21 @@ +> $additionalRules + */ + public static function createValanticConfig(array $additionalRules = []): Config + { + $config = new Config(); + $config->setRules(array_merge(RuleSet::getValanticRules(), $additionalRules)); + + return $config; + } +} diff --git a/src/RuleSet.php b/src/RuleSet.php new file mode 100644 index 0000000..a8f70b0 --- /dev/null +++ b/src/RuleSet.php @@ -0,0 +1,169 @@ +> + */ + public static function getValanticRules(): array + { + $rules = [ + '@PER-CS2.0' => true, + '@PER-CS2.0:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'array_push' => false, + 'native_constant_invocation' => false, + 'native_function_invocation' => false, + 'no_useless_return' => true, + 'self_accessor' => false, // do not enable self_accessor as it breaks pimcore models relying on get_called_class() + 'strict_comparison' => true, + 'strict_param' => true, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + 'blank_line_before_statement' => [ + 'statements' => [ + 'break', + 'case', + 'continue', + 'declare', + 'default', + 'do', + 'exit', + 'for', + 'foreach', + 'goto', + 'if', + 'include', + 'include_once', + // 'phpdoc', + 'require', + 'require_once', + 'return', + 'switch', + 'throw', + 'try', + 'while', + 'yield', + 'yield_from', + ], + ], + 'concat_space' => ['spacing' => 'one'], + 'declare_strict_types' => true, + 'increment_style' => [ + 'style' => 'post', + ], + 'method_argument_space' => [ + 'attribute_placement' => 'standalone', + 'on_multiline' => 'ensure_fully_multiline', + ], + 'method_chaining_indentation' => true, + 'multiline_comment_opening_closing' => true, + 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'], + 'no_superfluous_phpdoc_tags' => ['allow_hidden_params' => false, 'remove_inheritdoc' => true], + 'no_unset_on_property' => true, + 'no_useless_else' => true, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'case', + 'constant', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property', + 'property_public_abstract', + 'property_protected_abstract', + 'property_static', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'destruct', + 'magic', + 'phpunit', + 'method', + 'method_abstract', + 'method_public_abstract', + 'method_public_abstract_static', + 'method_protected_abstract', + 'method_protected_abstract_static', + 'method_private_abstract', + 'method_private_abstract_static', + 'method_static', + 'method_public_static', + 'method_protected_static', + 'method_private_static', + 'method_public', + 'method_protected', + 'method_private', + ], + ], + 'phpdoc_align' => ['align' => 'left', 'spacing' => 1], + 'phpdoc_to_comment' => true, + 'phpdoc_annotation_without_dot' => false, + 'ordered_imports' => [ + 'imports_order' => ['class', 'function', 'const'], + 'sort_algorithm' => 'alpha', + ], + 'regular_callable_call' => true, + 'return_assignment' => true, + 'single_line_throw' => false, + 'trailing_comma_in_multiline' => [ + 'after_heredoc' => true, + 'elements' => ['arguments', 'array_destructuring', 'arrays', 'match', 'parameters'], + ], + ]; + + return self::addPhpVersionSpecificRules($rules); + } + + private static function getCurrentPhpVersion(): string + { + return PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + } + + /** + * @param array> $rules + * + * @return array> + */ + private static function addPhpVersionSpecificRules(array $rules): array + { + $phpVersion = self::getCurrentPhpVersion(); + + $version = (int) str_replace('.', '', $phpVersion); + + $availableRuleSets = \PhpCsFixer\RuleSet\RuleSets::getSetDefinitionNames(); + + for ($majorMinor = 80; $majorMinor <= 99; $majorMinor++) { + if ($majorMinor > $version) { + break; + } + + $migrationSet = sprintf('@PHP%dMigration', $majorMinor); + + if (in_array($migrationSet, $availableRuleSets, true)) { + $rules[$migrationSet] = true; + } + + $riskyMigrationSet = sprintf('%s:risky', $migrationSet); + + if (in_array($riskyMigrationSet, $availableRuleSets, true)) { + $rules[$riskyMigrationSet] = true; + } + } + + return $rules; + } +} diff --git a/tests/Unit/PhpCsFixerConfigTest.php b/tests/Unit/PhpCsFixerConfigTest.php new file mode 100644 index 0000000..0567a1f --- /dev/null +++ b/tests/Unit/PhpCsFixerConfigTest.php @@ -0,0 +1,91 @@ +assertNotEmpty($rules); + + $this->assertArrayHasKey('@PER-CS2.0', $rules); + $this->assertArrayHasKey('@PER-CS2.0:risky', $rules); + $this->assertArrayHasKey('@Symfony', $rules); + $this->assertArrayHasKey('@Symfony:risky', $rules); + $this->assertArrayHasKey('array_push', $rules); + $this->assertArrayHasKey('yoda_style', $rules); + + // Check that PHP version specific rules are included + $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + $version = (int) str_replace('.', '', $phpVersion); + + if ($version >= 80) { + $this->assertArrayHasKey('@PHP80Migration', $rules); + + // Check if the risky rule set exists + $availableRuleSets = RuleSets::getSetDefinitionNames(); + + if (in_array('@PHP80Migration:risky', $availableRuleSets, true)) { + $this->assertArrayHasKey('@PHP80Migration:risky', $rules); + } + } + + if ($version >= 81) { + $this->assertArrayHasKey('@PHP81Migration', $rules); + } + } + + /** + * Test that createValanticConfig() returns a Config object with the basic ruleset. + */ + public function testCreateValanticConfig(): void + { + $config = ConfigFactory::createValanticConfig(); + + $this->assertInstanceOf(Config::class, $config); + + $rules = $config->getRules(); + $this->assertNotEmpty($rules); + + // Check that basic rules are included + $this->assertArrayHasKey('@PER-CS2.0', $rules); + $this->assertArrayHasKey('@PER-CS2.0:risky', $rules); + $this->assertArrayHasKey('@Symfony', $rules); + $this->assertArrayHasKey('@Symfony:risky', $rules); + + // Check that risky is not allowed by default + $this->assertFalse($config->getRiskyAllowed()); + } + + /** + * Test that createValanticConfig() with additional rules merges them correctly. + */ + public function testCreateValanticConfigWithAdditionalRules(): void + { + $additionalRules = [ + 'array_syntax' => ['syntax' => 'short'], + 'custom_rule' => true, + ]; + + $config = ConfigFactory::createValanticConfig($additionalRules); + + $rules = $config->getRules(); + $this->assertArrayHasKey('array_syntax', $rules); + $this->assertArrayHasKey('custom_rule', $rules); + $this->assertEquals(['syntax' => 'short'], $rules['array_syntax']); + $this->assertTrue($rules['custom_rule']); + } +}