Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 270 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,284 @@
# Extended php-stan-rules
[![License](https://poser.pugx.org/phpstan/phpstan-strict-rules/license)](https://packagist.org/packages/phpstan/phpstan-strict-rules)
# PHPStan Extended Rules

### Add new rules:
- If class name include `Command`, php doc attribute `@see` should be included class with `CommandHandler` in name (if class include invoke method the rule does not apply)
- If class name ends EventListener, class should be included attribute AsEventListener
- Return type of method do not equal @return attribute in phpDoc
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![PHP Version](https://img.shields.io/badge/php-%3E8.3-blue.svg)](https://php.net/)
[![PHPStan](https://img.shields.io/badge/PHPStan-^2.0-blue.svg)](https://phpstan.org/)

## Installation
A collection of custom PHPStan rules that enforce coding standards and improve code quality in PHP projects. This extension provides additional static analysis rules focused on naming conventions, annotations, and PHPDoc consistency.

To use this extension, require it in [Composer](https://getcomposer.org/):
## 🎯 Features

This package includes three powerful rules that help maintain high code quality:

### 1. Command-Handler Relationship Rule
**Rule**: `CommandClassShouldBeHelpCommandHandlerClass`

Enforces that classes ending with "Command" must have a `@see` PHPDoc tag pointing to their corresponding CommandHandler class.

**Example**:
```php
/**
* @see CreateUserCommandHandler
*/
class CreateUserCommand
{
// Command implementation
}
```
composer require --dev simtel/phpstan-rules

**Exception**: Classes with an `__invoke` method are exempt from this rule.

### 2. Event Listener Attribute Rule
**Rule**: `EventListenerClassShouldBeIncludeAsListenerAttribute`

Ensures that classes ending with "EventListener" are properly annotated with the `#[AsEventListener]` attribute.

**Example**:
```php
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
class UserRegisteredEventListener
{
// Event listener implementation
}
```

Add rule to configuration:
### 3. Redundant PHPDoc Return Type Rule
**Rule**: `NotShouldPhpdocReturnIfExistTypeHint`

Prevents redundant or conflicting `@return` PHPDoc annotations when native return type hints are already declared.

**Good**:
```php
public function getUser(): User
{
return $this->user;
}
```
rules:
- Simtel\PHPStanRules\Rule\CommandClassShouldBeHelpCommandHandlerClass
- Simtel\PHPStanRules\Rule\EventListenerClassShouldBeIncludeAsListenerAttribute
- Simtel\PHPStanRules\Rule\NotShouldPhpdocReturnIfExistTypeHint

**Bad**:
```php
/**
* @return User
*/
public function getUser(): User // Redundant @return annotation
{
return $this->user;
}
```

Or add extension to root config:
## 📦 Installation

### System Requirements

- **PHP**: >8.3
- **PHPStan**: ^2.0
- **PHPStan PHPDoc Parser**: ^2.2

### Install via Composer

Install the package as a development dependency:

```bash
composer require --dev simtel/phpstan-rules
```

## ⚙️ Configuration

### Option 1: Include All Rules (Recommended)

Add the bundled configuration to your `phpstan.neon` or `phpstan.dist.neon`:

```neon
includes:
- vendor/simtel/phpstan-rules/rules.neon
- vendor/simtel/phpstan-rules/rules.neon
```

### Option 2: Manual Rule Registration

For granular control, register specific rules:

```neon
parameters:
rules:
- Simtel\PHPStanRules\Rule\CommandClassShouldBeHelpCommandHandlerClass
- Simtel\PHPStanRules\Rule\EventListenerClassShouldBeIncludeAsListenerAttribute
- Simtel\PHPStanRules\Rule\NotShouldPhpdocReturnIfExistTypeHint
```

### Complete Configuration Example

```neon
includes:
- vendor/simtel/phpstan-rules/rules.neon

parameters:
level: 8
paths:
- src/
excludePaths:
- tests/
ignoreErrors:
# Exclude specific patterns if needed
- '#Command class should be include phpDoc with @see attribute#'
path: src/Deprecated/
```

## 🔧 Development

### Setting Up Development Environment

1. Clone the repository:
```bash
git clone <repository-url> phpstan-rules
cd phpstan-rules
```

2. Install dependencies:
```bash
composer install
```

### Development Commands

#### Running Tests
```bash
# Run all tests
vendor/bin/phpunit

# Run specific test class
vendor/bin/phpunit tests/Rules/CommandClassShouldBeHelpCommandHandlerClassTest.php
```

#### Code Style
```bash
# Check coding standards
vendor/bin/ecs check

# Fix coding standards automatically
vendor/bin/ecs fix
```

#### Static Analysis
```bash
# Run PHPStan on the project itself
vendor/bin/phpstan analyse
```

### Project Structure

```
.
├── src/Rule/ # Rule implementations
│ ├── CommandClassShouldBeHelpCommandHandlerClass.php
│ ├── EventListenerClassShouldBeIncludeAsListenerAttribute.php
│ └── NotShouldPhpdocReturnIfExistTypeHint.php
├── tests/
│ ├── Fixture/ # Test code samples
│ │ ├── EventListener/
│ │ └── Return/
│ ├── Rules/ # Unit tests for rules
│ └── data/ # Additional test data
├── composer.json # Package configuration
├── ecs.php # ECS configuration
└── README.md # This file
```

### Creating New Rules

1. **Implement the Rule Interface**:
```php
<?php

namespace Simtel\PHPStanRules\Rule;

use PHPStan\Rules\Rule;

/**
* @implements Rule<NodeType>
*/
final class YourNewRule implements Rule
{
public function getNodeType(): string
{
return NodeType::class;
}

public function processNode(Node $node, Scope $scope): array
{
// Rule implementation
}
}
```

2. **Create Test Cases**:
```php
<?php

namespace Simtel\PHPStanRules\Tests\Rules;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

class YourNewRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new YourNewRule();
}

public function testValidCase(): void
{
$this->analyse([__DIR__ . '/../Fixture/Valid.php'], []);
}
}
```

3. **Add Fixture Files**: Create test PHP files in `tests/Fixture/` to validate rule behavior.

### Testing Strategy

The project uses PHPUnit with PHPStan's `RuleTestCase` base class:

- **Fixture Files**: Real PHP code examples in `tests/Fixture/`
- **Data Files**: Additional test scenarios in `tests/data/`
- **Unit Tests**: Each rule has corresponding test class in `tests/Rules/`

### Contributing Guidelines

1. **Code Standards**: Follow PSR-4 autoloading and use ECS for code style
2. **Testing**: All rules must have comprehensive tests with both positive and negative cases
3. **Documentation**: Update README.md and add inline documentation for new rules
4. **Performance**: Ensure rules execute efficiently within PHPStan's analysis pipeline

## 🐛 Troubleshooting

### Common Issues

#### "Rule Not Found" Errors
**Cause**: Incorrect namespace or class name in `phpstan.neon`.

**Solution**: Verify the fully qualified class names and check for typos.

#### Rules Not Being Applied
**Cause**: Configuration file not included or rules not registered.

**Solution**: Ensure `vendor/simtel/phpstan-rules/rules.neon` is included in your PHPStan configuration.

#### Performance Issues
**Cause**: Rules may be analyzing too many files or performing expensive operations.

**Solution**: Use `excludePaths` to limit analysis scope or optimize rule logic.

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## 📞 Support

If you encounter any issues or have questions, please [open an issue](../../issues) on GitHub.
22 changes: 22 additions & 0 deletions ecs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;

return ECSConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])

->withRules([
NoUnusedImportsFixer::class,
])
->withPreparedSets(
psr12: true,
symplify: true,
spaces: true,
)
;