Files
michaelschiemer/src/Framework/Quality/PHPStan/Rules/Naming/ControllerNamingRule.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

169 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Quality\PHPStan\Rules\Naming;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
/**
* Enforces Controller Naming Convention:
* - View Controllers: Show{Feature} (only GET requests, ViewResult)
* - Action Controllers: {Verb}{Feature} (POST/PUT/DELETE, Business Logic)
* - NO "Controller" suffix
*
* @implements Rule<InClassNode>
*/
final class ControllerNamingRule implements Rule
{
public function getNodeType(): string
{
return InClassNode::class;
}
/**
* @param InClassNode $node
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
$className = $classReflection->getName();
$shortName = $classReflection->getNativeReflection()->getShortName();
// Skip if not in Application namespace (controllers are in Application)
if (! str_starts_with($className, 'App\\Application\\')) {
return [];
}
// Check if class has Route attributes (controller indicator)
$hasRouteAttribute = false;
foreach ($classReflection->getNativeReflection()->getMethods() as $method) {
foreach ($method->getAttributes() as $attribute) {
if (str_contains($attribute->getName(), 'Route')) {
$hasRouteAttribute = true;
break 2;
}
}
}
if (! $hasRouteAttribute) {
return []; // Not a controller
}
$errors = [];
// Rule 1: NO "Controller" suffix
if (str_ends_with($shortName, 'Controller')) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Controller "%s" should not have "Controller" suffix. Use Show{Feature} for views or {Verb}{Feature} for actions.',
$shortName
))
->identifier('naming.controller.noSuffix')
->build();
}
// Rule 2: Show* pattern validation
if (str_starts_with($shortName, 'Show')) {
$violations = $this->validateShowController($classReflection);
foreach ($violations as $violation) {
$errors[] = RuleErrorBuilder::message($violation)
->identifier('naming.controller.showPattern')
->build();
}
}
// Rule 3: Action controller pattern (Submit, Create, Update, Delete, etc.)
$actionVerbs = ['Submit', 'Create', 'Update', 'Delete', 'Handle', 'Process', 'Execute'];
$startsWithActionVerb = false;
foreach ($actionVerbs as $verb) {
if (str_starts_with($shortName, $verb)) {
$startsWithActionVerb = true;
break;
}
}
if (! str_starts_with($shortName, 'Show') && ! $startsWithActionVerb) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Controller "%s" must start with "Show" (for views) or an action verb (Submit, Create, Update, Delete, Handle, Process, Execute).',
$shortName
))
->identifier('naming.controller.pattern')
->build();
}
return $errors;
}
/**
* @return string[]
*/
private function validateShowController(\PHPStan\Reflection\ClassReflection $classReflection): array
{
$violations = [];
$shortName = $classReflection->getNativeReflection()->getShortName();
foreach ($classReflection->getNativeReflection()->getMethods() as $method) {
if (! $method->isPublic() || $method->isStatic()) {
continue;
}
// Check for Route attribute
$routeAttributes = array_filter(
$method->getAttributes(),
fn ($attr) => str_contains($attr->getName(), 'Route')
);
foreach ($routeAttributes as $routeAttr) {
$args = $routeAttr->getArguments();
// Check if method is not GET (Show* should only have GET routes)
if (isset($args['method'])) {
$methodName = is_object($args['method'])
? $args['method']->name
: $args['method'];
if ($methodName !== 'GET' && ! str_contains((string)$methodName, 'GET')) {
$violations[] = sprintf(
'Show controller "%s" has non-GET route in method "%s()". Show controllers should only handle GET requests. Use %s for POST/PUT/DELETE.',
$shortName,
$method->getName(),
$this->suggestActionName($shortName, $methodName)
);
}
}
// Check return type (Show* should return ViewResult)
$returnType = $method->getReturnType();
if ($returnType && ! str_contains($returnType->__toString(), 'ViewResult')) {
$violations[] = sprintf(
'Show controller "%s" method "%s()" should return ViewResult, not "%s".',
$shortName,
$method->getName(),
$returnType->__toString()
);
}
}
}
return $violations;
}
private function suggestActionName(string $showClassName, string $httpMethod): string
{
$feature = str_replace('Show', '', $showClassName);
return match(strtoupper($httpMethod)) {
'POST' => "Submit{$feature}",
'PUT', 'PATCH' => "Update{$feature}",
'DELETE' => "Delete{$feature}",
default => "{Verb}{$feature}"
};
}
}