Skip to content

1.9.0 New rules, increased test coverage and bug fixes

Choose a tag to compare

@floriankraemer floriankraemer released this 09 Feb 23:16
· 2 commits to master since this release
35d68c7

πŸŽ‰ New Features

✨ New Rule: Property Must Match Rule

Enforce Property Conventions on Classes

  • Class: Phauthentic\PHPStanRules\Architecture\PropertyMustMatchRule
  • Purpose: Validates that classes matching specified patterns have properties with expected names, types, and visibility scopes
  • Key Features:
    • Required Properties: Enforce that matching classes must have certain properties
    • Type Validation: Check property types including support for union types (int|string) and intersection types (Foo&Bar)
    • Visibility Enforcement: Ensure properties use the correct visibility (public, protected, private)
    • Nullable Support: Optionally accept both a type and its nullable variant (e.g., int and ?int)
    • Flexible Matching: Uses regex patterns to target specific classes
  • Configuration: Supports propertyPatterns parameter with classPattern and properties sub-configuration
  • Error Messages:
    • Class {class} must have property ${name}. (when required property is missing)
    • Property {class}::${name} should be of type {expected}, {actual} given. (type mismatch)
    • Property {class}::${name} must be {visibility}. (visibility mismatch)

Configuration Example:

services:
    -
        class: Phauthentic\PHPStanRules\Architecture\PropertyMustMatchRule
        arguments:
            propertyPatterns:
                -
                    classPattern: '/^App\\Entity\\.*$/'
                    properties:
                        -
                            name: 'id'
                            type: 'int'
                            visibilityScope: 'private'
                            required: true
                        -
                            name: 'createdAt'
                            type: 'DateTimeImmutable'
                            visibilityScope: 'private'
                            required: true
                            nullable: true
        tags:
            - phpstan.rules.rule

✨ New Rule: Forbidden Accessors Rule

Enforce Encapsulation and the "Tell, Don't Ask" Principle

  • Class: Phauthentic\PHPStanRules\Architecture\ForbiddenAccessorsRule
  • Purpose: Forbids public and/or protected getters (getXxx()) and setters (setXxx()) on classes matching specified patterns
  • Key Features:
    • Configurable Accessor Types: Independently control whether getters, setters, or both are forbidden
    • Visibility Control: Choose which visibility levels to check (public, protected)
    • Pattern Matching: Uses regex patterns against fully qualified class names
    • Anonymous Class Safe: Properly skips anonymous classes
  • Configuration: Supports classPatterns, forbidGetters, forbidSetters, and visibility parameters
  • Error Messages:
    • Class {class} must not have a {visibility} getter method {method}().
    • Class {class} must not have a {visibility} setter method {method}().

Configuration Example:

services:
    -
        class: Phauthentic\PHPStanRules\Architecture\ForbiddenAccessorsRule
        arguments:
            classPatterns:
                - '/^App\\Domain\\.*Entity$/'
                - '/^App\\Domain\\.*ValueObject$/'
            forbidGetters: true
            forbidSetters: true
            visibility:
                - public
        tags:
            - phpstan.rules.rule

Use Cases:

  • Enforce immutability on domain entities by forbidding setters
  • Apply the "Tell, Don't Ask" principle by forbidding both getters and setters
  • Restrict only public accessors while allowing protected ones for inheritance

✨ New Rule: Forbidden Static Methods Rule

Prevent Static Method Calls

  • Class: Phauthentic\PHPStanRules\Architecture\ForbiddenStaticMethodsRule
  • Purpose: Forbids specific static method calls matching regex patterns to encourage dependency injection and testability
  • Key Features:
    • Multi-Level Granularity: Supports namespace-level, class-level, and method-level patterns
    • Keyword Resolution: Resolves self, static, and parent keywords to actual class names before matching
    • Dynamic Call Safe: Skips dynamic class names ($class::method()) and dynamic method names (Class::$method())
  • Configuration: Supports forbiddenStaticMethods parameter with an array of regex patterns matching FQCN::methodName
  • Error Messages:
    • Static method call "{call}" is forbidden.

Configuration Example:

services:
    -
        class: Phauthentic\PHPStanRules\Architecture\ForbiddenStaticMethodsRule
        arguments:
            forbiddenStaticMethods:
                - '/^App\\Legacy\\.*::.*/'        # Namespace-level: all legacy static calls
                - '/^App\\Utils\\StaticHelper::.*/' # Class-level: all methods on StaticHelper
                - '/^DateTime::createFromFormat$/'  # Method-level: specific method only
        tags:
            - phpstan.rules.rule

✨ New: ClassNameResolver Trait

Reusable Utilities for FQCN Resolution

  • Trait: Phauthentic\PHPStanRules\Architecture\ClassNameResolver
  • Purpose: Extracts common fully qualified class name resolution and type conversion utilities into a reusable trait
  • Key Methods:
    • resolveFullClassName() β€” Builds the FQCN from a Class_ or Interface_ node and scope; returns null for anonymous classes
    • getTypeAsString() β€” Converts type nodes (Identifier, Name, NullableType, UnionType, IntersectionType) to their string representation
    • matchesAnyPattern() β€” Checks if a string matches any of the given regex patterns
    • normalizeClassName() β€” Removes the leading backslash from a class name

✨ New: Interactive Rule Config Builder

  • File: rule-builder.html
  • Purpose: An interactive HTML tool for building PHPStan rule configurations visually, making it easier to create correct phpstan.neon configuration without memorizing all parameters

πŸ”§ Code Quality Improvements

πŸ—οΈ Refactored Rules to Use ClassNameResolver Trait

Multiple existing rules were refactored to use the new ClassNameResolver trait, reducing code duplication and improving consistency:

  • AttributeRule β€” Replaced inline FQCN resolution with resolveFullClassName()
  • CatchExceptionOfTypeNotAllowedRule β€” Added class name normalization for robust comparison (leading backslash handling)
  • ClassMustBeFinalRule β€” Simplified to use resolveFullClassName() and matchesAnyPattern()
  • ClassMustBeReadonlyRule β€” Simplified to use resolveFullClassName() and matchesAnyPattern()
  • ClassMustHaveSpecificationDocblockRule β€” Replaced private matchesPatterns() with trait's matchesAnyPattern()
  • MethodMustReturnTypeRule β€” Replaced private getTypeAsString() with trait version; now uses FQCN for method matching
  • MethodSignatureMustMatchRule β€” Replaced private getTypeAsString() with trait version; now uses FQCN for method matching

πŸ”’ Improved Anonymous Class Handling

All class-level rules now properly skip anonymous classes (classes without names) via the resolveFullClassName() method returning null, preventing false positives and potential errors.

βœ… Constructor Parameter Validation

Both PropertyMustMatchRule and ForbiddenAccessorsRule validate constructor parameters at instantiation time, throwing InvalidArgumentException for invalid configurations (empty patterns, invalid visibility values).

πŸ”§ Regex Error Handling

PropertyMustMatchRule and ForbiddenAccessorsRule now properly handle preg_match errors, throwing InvalidArgumentException with descriptive messages when an invalid regex pattern is provided.

πŸ“¦ Dependency Updates

  • Updated phpunit/phpunit from ^12.0 to ^12.5.8

πŸ“š Documentation Improvements

πŸ“š New Documentation Files

  • Forbidden Accessors Rule: Complete documentation with examples for forbidding getters, setters, or both at configurable visibility levels
  • Forbidden Dependencies Rule: Dedicated documentation for the renamed rule with FQCN checking, allowed dependencies, and a Mermaid flowchart explaining the allow/forbid logic
  • Forbidden Static Methods Rule: Full documentation covering namespace-level, class-level, and method-level pattern granularity, including self/static/parent handling
  • Property Must Match Rule: Comprehensive documentation with examples for required properties, optional validation, and nullable types

πŸ“š Updated Documentation

  • README.md: Added Forbidden Accessors Rule, Forbidden Dependencies Rule, Forbidden Static Methods Rule, and Property Must Match Rule to the Architecture Rules list; marked Dependency Constraints Rule as deprecated
  • docs/Rules.md: Added overview sections and full configuration examples for all new rules

βœ… New Test Coverage

New Test Files

  • PropertyMustMatchRuleTest: Core functionality tests for property name, type, and required validation
  • PropertyMustMatchRuleNullableTest: Tests for nullable type matching
  • PropertyMustMatchRuleIntersectionTypeTest: Tests for intersection type support
  • PropertyMustMatchRuleInvalidVisibilityTest: Tests for invalid visibility validation
  • PropertyMustMatchRuleInvalidRegexTest: Tests for invalid regex pattern handling
  • PropertyMustMatchRuleExceptionsTest: Tests for constructor validation
  • ForbiddenAccessorsRuleTest: Core functionality tests
  • ForbiddenAccessorsRuleGettersOnlyTest: Tests for forbidding only getters
  • ForbiddenAccessorsRuleSettersOnlyTest: Tests for forbidding only setters
  • ForbiddenAccessorsRulePrivateVisibilityTest: Tests for private visibility checking
  • ForbiddenAccessorsRuleProtectedVisibilityTest: Tests for protected visibility checking
  • ForbiddenAccessorsRuleExceptionsTest: Tests for constructor validation
  • ForbiddenStaticMethodsRuleTest: Core functionality tests
  • ForbiddenStaticMethodsRuleSelfStaticParentTest: Tests for self/static/parent keyword resolution
  • MethodMustReturnTypeRuleEdgeCasesTest: Edge case tests for existing rule
  • MethodsReturningBoolMustFollowNamingConventionEdgeCasesTest: Edge case tests for existing rule

Enhanced Existing Tests

  • CircularModuleDependencyRuleTest: Added tests for non-modular and anonymous classes
  • ClassMustBeFinalRuleTest: Added test for anonymous class handling

New Test Data Files

  • 18 new test data files covering all new rules and edge cases

πŸ“Š Statistics

  • 64 files changed with 4,752 lines added and 220 lines removed
  • 3 new rules created
  • 1 new trait extracted for shared utilities
  • 16 new test files for comprehensive coverage
  • 18 new test data files

πŸš€ Migration Guide

For Existing Users

No Action Required! All existing configurations continue to work without any changes. The refactoring to use the ClassNameResolver trait is purely internal and does not affect configuration or behavior.

Note: Rules that previously matched against short class names (e.g., MethodMustReturnTypeRule, MethodSignatureMustMatchRule) now match against fully qualified class names. If your patterns relied on short class names, you may need to update them to include the namespace. For example:

Before:

pattern: '/^MyService::doSomething$/'

After:

pattern: '/^App\\Service\\MyService::doSomething$/'

Or use a wildcard to match any namespace:

pattern: '/.*MyService::doSomething$/'

For New Rules

All new rules are opt-in and require explicit configuration in your phpstan.neon. See the individual rule documentation for detailed configuration examples.


Full Changelog: 1.8.0...1.9.0