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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -15,6 +15,8 @@ use PHPStan\Rules\RuleErrorBuilder;
* - CQRS Commands (Business Logic): NO "Command" suffix (SendEmail, CreateUser)
* - Console Commands (Infrastructure): WITH "Command" suffix (HealthCheckCommand)
*
* Console commands are identified by the #[ConsoleCommand] attribute.
*
* @implements Rule<InClassNode>
*/
final class CommandNamingRule implements Rule
@@ -33,13 +35,13 @@ final class CommandNamingRule implements Rule
$className = $classReflection->getName();
$shortName = $classReflection->getNativeReflection()->getShortName();
// Check if it's a Console Command (implements ConsoleCommand interface)
$isConsoleCommand = $classReflection->implementsInterface('App\\Framework\\Console\\ConsoleCommand');
// Check if it's a Console Command (has #[ConsoleCommand] attribute)
$isConsoleCommand = $this->hasConsoleCommandAttribute($classReflection);
// Check if it's a CQRS Command (in /Commands/ namespace or has CommandHandler)
$isCqrsCommand = $this->isCqrsCommand($className, $classReflection);
if (!$isConsoleCommand && !$isCqrsCommand) {
if (! $isConsoleCommand && ! $isCqrsCommand) {
return []; // Not a command
}
@@ -47,7 +49,7 @@ final class CommandNamingRule implements Rule
if ($isConsoleCommand) {
// Console Commands MUST have "Command" suffix
if (!str_ends_with($shortName, 'Command')) {
if (! str_ends_with($shortName, 'Command')) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Console command "%s" must have "Command" suffix. Rename to "%sCommand".',
$shortName,
@@ -71,7 +73,7 @@ final class CommandNamingRule implements Rule
}
// CQRS Commands should follow Verb-Noun pattern
if (!$this->followsVerbNounPattern($shortName)) {
if (! $this->followsVerbNounPattern($shortName)) {
$errors[] = RuleErrorBuilder::message(sprintf(
'CQRS command "%s" should follow Verb-Noun pattern (e.g., SendEmail, CreateUser, UpdateProfile).',
$shortName
@@ -84,12 +86,21 @@ final class CommandNamingRule implements Rule
return $errors;
}
private function hasConsoleCommandAttribute(\PHPStan\Reflection\ClassReflection $classReflection): bool
{
// Check if class has #[ConsoleCommand] attribute
$nativeReflection = $classReflection->getNativeReflection();
$attributes = $nativeReflection->getAttributes('App\\Framework\\Attributes\\ConsoleCommand');
return count($attributes) > 0;
}
private function isCqrsCommand(string $className, \PHPStan\Reflection\ClassReflection $classReflection): bool
{
// Check if in /Commands/ namespace (CQRS convention)
if (str_contains($className, '\\Commands\\')) {
// Exclude Console commands namespace
if (!str_contains($className, '\\Console\\Commands\\')) {
if (! str_contains($className, '\\Console\\Commands\\')) {
return true;
}
}
@@ -116,14 +127,15 @@ final class CommandNamingRule implements Rule
$verbs = [
'Send', 'Create', 'Update', 'Delete', 'Store', 'Process',
'Execute', 'Handle', 'Generate', 'Validate', 'Register',
'Activate', 'Deactivate', 'Cancel', 'Approve', 'Reject'
'Activate', 'Deactivate', 'Cancel', 'Approve', 'Reject',
];
foreach ($verbs as $verb) {
if (str_starts_with($name, $verb)) {
// Check if there's a noun after the verb
$noun = substr($name, strlen($verb));
return !empty($noun) && ctype_upper($noun[0]);
return ! empty($noun) && ctype_upper($noun[0]);
}
}

View File

@@ -35,7 +35,7 @@ final class ControllerNamingRule implements Rule
$shortName = $classReflection->getNativeReflection()->getShortName();
// Skip if not in Application namespace (controllers are in Application)
if (!str_starts_with($className, 'App\\Application\\')) {
if (! str_starts_with($className, 'App\\Application\\')) {
return [];
}
@@ -45,12 +45,13 @@ final class ControllerNamingRule implements Rule
foreach ($method->getAttributes() as $attribute) {
if (str_contains($attribute->getName(), 'Route')) {
$hasRouteAttribute = true;
break 2;
}
}
}
if (!$hasRouteAttribute) {
if (! $hasRouteAttribute) {
return []; // Not a controller
}
@@ -82,11 +83,12 @@ final class ControllerNamingRule implements Rule
foreach ($actionVerbs as $verb) {
if (str_starts_with($shortName, $verb)) {
$startsWithActionVerb = true;
break;
}
}
if (!str_starts_with($shortName, 'Show') && !$startsWithActionVerb) {
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
@@ -107,14 +109,14 @@ final class ControllerNamingRule implements Rule
$shortName = $classReflection->getNativeReflection()->getShortName();
foreach ($classReflection->getNativeReflection()->getMethods() as $method) {
if (!$method->isPublic() || $method->isStatic()) {
if (! $method->isPublic() || $method->isStatic()) {
continue;
}
// Check for Route attribute
$routeAttributes = array_filter(
$method->getAttributes(),
fn($attr) => str_contains($attr->getName(), 'Route')
fn ($attr) => str_contains($attr->getName(), 'Route')
);
foreach ($routeAttributes as $routeAttr) {
@@ -126,7 +128,7 @@ final class ControllerNamingRule implements Rule
? $args['method']->name
: $args['method'];
if ($methodName !== 'GET' && !str_contains((string)$methodName, 'GET')) {
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,
@@ -138,7 +140,7 @@ final class ControllerNamingRule implements Rule
// Check return type (Show* should return ViewResult)
$returnType = $method->getReturnType();
if ($returnType && !str_contains($returnType->__toString(), 'ViewResult')) {
if ($returnType && ! str_contains($returnType->__toString(), 'ViewResult')) {
$violations[] = sprintf(
'Show controller "%s" method "%s()" should return ViewResult, not "%s".',
$shortName,

View File

@@ -44,8 +44,8 @@ final class ExceptionNamingRule implements Rule
$shortName = $classReflection->getNativeReflection()->getShortName();
// Check if class extends Exception or FrameworkException
if (!$classReflection->isSubclassOf(\Exception::class) &&
!$classReflection->isSubclassOf('App\\Framework\\Exception\\FrameworkException')) {
if (! $classReflection->isSubclassOf(\Exception::class) &&
! $classReflection->isSubclassOf('App\\Framework\\Exception\\FrameworkException')) {
return [];
}
@@ -60,7 +60,7 @@ final class ExceptionNamingRule implements Rule
if ($isTechnicalException) {
// Technical exceptions MUST have "Exception" suffix
if (!str_ends_with($shortName, 'Exception')) {
if (! str_ends_with($shortName, 'Exception')) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Technical exception "%s" must have "Exception" suffix. Rename to "%sException".',
$shortName,

View File

@@ -0,0 +1,58 @@
<?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 Interface Naming Convention:
* - NO "Interface" suffix
* - Descriptive names (Cache, Logger, Repository, not CacheInterface)
*
* @implements Rule<InClassNode>
*/
final class InterfaceNamingRule implements Rule
{
public function getNodeType(): string
{
return InClassNode::class;
}
/**
* @param InClassNode $node
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $node->getClassReflection();
// Only check interfaces
if (! $classReflection->isInterface()) {
return [];
}
$shortName = $classReflection->getNativeReflection()->getShortName();
$errors = [];
// Rule: NO "Interface" suffix
if (str_ends_with($shortName, 'Interface')) {
$suggestedName = preg_replace('/Interface$/', '', $shortName);
$errors[] = RuleErrorBuilder::message(sprintf(
'Interface "%s" should not have "Interface" suffix. Rename to "%s".',
$shortName,
$suggestedName
))
->identifier('naming.interface.noSuffix')
->tip('Interfaces should be named by their behavior or contract: Cache, Logger, Repository')
->build();
}
return $errors;
}
}

View File

@@ -128,6 +128,31 @@ final readonly class PriceValueObject { }
---
### 5. InterfaceNamingRule
**Enforces:**
- ✅ NO "Interface" suffix
- ✅ Descriptive, behavior-focused names
**Examples:**
```php
// ✅ CORRECT
interface Cache { }
interface Logger { }
interface Repository { }
interface EventDispatcher { }
// ❌ WRONG
interface CacheInterface { }
interface LoggerInterface { }
interface RepositoryInterface { }
```
**Error Identifiers:**
- `naming.interface.noSuffix`
---
## Running PHPStan
### Full Analysis
@@ -174,6 +199,7 @@ rules:
- App\Framework\Quality\PHPStan\Rules\Naming\ExceptionNamingRule
- App\Framework\Quality\PHPStan\Rules\Naming\CommandNamingRule
- App\Framework\Quality\PHPStan\Rules\Naming\ValueObjectNamingRule
- App\Framework\Quality\PHPStan\Rules\Naming\InterfaceNamingRule
```
## Gradual Migration

View File

@@ -34,7 +34,7 @@ final class ValueObjectNamingRule implements Rule
$shortName = $classReflection->getNativeReflection()->getShortName();
// Check if it's a Value Object
if (!$this->isValueObject($classReflection)) {
if (! $this->isValueObject($classReflection)) {
return [];
}
@@ -73,12 +73,12 @@ final class ValueObjectNamingRule implements Rule
// Value Objects are typically:
// 1. Readonly classes
if (!$nativeReflection->isReadOnly()) {
if (! $nativeReflection->isReadOnly()) {
return false;
}
// 2. Final classes
if (!$nativeReflection->isFinal()) {
if (! $nativeReflection->isFinal()) {
return false;
}
@@ -99,7 +99,7 @@ final class ValueObjectNamingRule implements Rule
// 4. Has only readonly public properties and no methods with business logic
$publicMethods = array_filter(
$nativeReflection->getMethods(),
fn($m) => $m->isPublic() && !$m->isConstructor() && !$m->isStatic()
fn ($m) => $m->isPublic() && ! $m->isConstructor() && ! $m->isStatic()
);
// Value Objects typically have few public methods (mostly transformations/getters)