- 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.
169 lines
5.8 KiB
PHP
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}"
|
|
};
|
|
}
|
|
}
|