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

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Framework\Router\Result;
use App\Framework\Http\Status;
use App\Framework\Pagination\ValueObjects\PaginationResponse;
use App\Framework\Router\ActionResult;
/**
* API Data Result
*
* Standardized API response with data and optional metadata.
* Supports both paginated and simple data responses.
*
* Response Structure (Paginated):
* {
* "data": [...],
* "meta": {
* "result_count": 10,
* "limit": 10,
* "has_more": true,
* "current_page": 1,
* "total_pages": 5,
* ...
* },
* "timestamp": "2024-01-20T10:30:00Z"
* }
*
* Response Structure (Simple):
* {
* "data": {...},
* "meta": {...},
* "timestamp": "2024-01-20T10:30:00Z"
* }
*/
final readonly class ApiDataResult implements ActionResult
{
/**
* @param PaginationResponse|array<mixed>|mixed $data Response data
* @param array<string, mixed>|null $additionalMeta Additional metadata
*/
private function __construct(
public PaginationResponse|array $data,
public ?array $additionalMeta = null,
public Status $status = Status::OK
) {}
/**
* Create with pagination support (uses framework's PaginationResponse)
*/
public static function paginated(
PaginationResponse $paginationResponse,
?array $additionalMeta = null,
Status $status = Status::OK
): self {
return new self(
data: $paginationResponse,
additionalMeta: $additionalMeta,
status: $status
);
}
/**
* Create simple data response without pagination
*
* @param mixed $data
* @param array<string, mixed>|null $meta
*/
public static function simple(
mixed $data,
?array $meta = null,
Status $status = Status::OK
): self {
return new self(
data: ['data' => $data],
additionalMeta: $meta,
status: $status
);
}
/**
* Create from array data
*
* @param array<mixed> $data
* @param array<string, mixed>|null $meta
*/
public static function fromArray(
array $data,
?array $meta = null,
Status $status = Status::OK
): self {
return new self(
data: ['data' => $data],
additionalMeta: $meta,
status: $status
);
}
/**
* Check if response is paginated
*/
public function isPaginated(): bool
{
return $this->data instanceof PaginationResponse;
}
/**
* Convert to array for JSON serialization
*/
public function toArray(): array
{
if ($this->isPaginated()) {
/** @var PaginationResponse $paginationResponse */
$paginationResponse = $this->data;
$response = $paginationResponse->toArray();
// Merge additional meta if provided
if ($this->additionalMeta !== null) {
$response['meta'] = array_merge(
$response['meta'],
$this->additionalMeta
);
}
} else {
$response = $this->data;
// Add meta section for simple responses
if ($this->additionalMeta !== null || !isset($response['meta'])) {
$response['meta'] = $this->additionalMeta ?? [];
}
}
// Add timestamp to response
$response['timestamp'] = (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339);
return $response;
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Framework\Router\Result;
use App\Framework\Exception\ErrorCode;
use App\Framework\Http\Status;
use App\Framework\Router\ActionResult;
/**
* API Error Result
*
* Standardized API error response using framework's ErrorCode system.
*
* Response Structure:
* {
* "error": {
* "code": "HTTP002",
* "category": "HTTP",
* "message": "Requested resource not found",
* "severity": "warning",
* "recovery_hint": "Verify resource URL and existence",
* "details": {...}
* },
* "timestamp": "2024-01-20T10:30:00Z",
* "path": "/api/users/123"
* }
*/
final readonly class ApiErrorResult implements ActionResult
{
/**
* @param ErrorCode $errorCode Framework ErrorCode enum
* @param string|null $customMessage Optional custom message (overrides ErrorCode description)
* @param array<string, mixed>|null $details Additional error details
* @param string|null $path Request path
*/
public function __construct(
public ErrorCode $errorCode,
public ?string $customMessage = null,
public ?array $details = null,
public ?string $path = null,
public Status $status = Status::BAD_REQUEST
) {}
/**
* Create from ErrorCode with automatic status mapping
*/
public static function fromErrorCode(
ErrorCode $errorCode,
?string $customMessage = null,
?array $details = null,
?string $path = null
): self {
// Map ErrorCode to HTTP Status
$status = self::mapErrorCodeToStatus($errorCode);
return new self(
errorCode: $errorCode,
customMessage: $customMessage,
details: $details,
path: $path,
status: $status
);
}
/**
* Map ErrorCode to HTTP Status
*/
private static function mapErrorCodeToStatus(ErrorCode $errorCode): Status
{
return match ($errorCode->getCategory()) {
'HTTP' => match ($errorCode->getValue()) {
'HTTP001' => Status::BAD_REQUEST,
'HTTP002' => Status::NOT_FOUND,
'HTTP003' => Status::METHOD_NOT_ALLOWED,
'HTTP004' => Status::TOO_MANY_REQUESTS,
'HTTP005' => Status::INTERNAL_SERVER_ERROR,
default => Status::BAD_REQUEST,
},
'AUTH' => match ($errorCode->getValue()) {
'AUTH001', 'AUTH002', 'AUTH004' => Status::UNAUTHORIZED,
'AUTH003' => Status::FORBIDDEN,
default => Status::UNAUTHORIZED,
},
'VAL' => Status::UNPROCESSABLE_ENTITY,
'DB' => Status::INTERNAL_SERVER_ERROR,
'API' => Status::BAD_GATEWAY,
default => Status::INTERNAL_SERVER_ERROR,
};
}
/**
* Convert to array for JSON serialization
*/
public function toArray(): array
{
$error = [
'code' => $this->errorCode->getValue(),
'category' => $this->errorCode->getCategory(),
'message' => $this->customMessage ?? $this->errorCode->getDescription(),
'severity' => $this->errorCode->getSeverity()->value,
'recovery_hint' => $this->errorCode->getRecoveryHint(),
];
// Add retry information if recoverable
if ($this->errorCode->isRecoverable()) {
$error['recoverable'] = true;
if ($retryAfter = $this->errorCode->getRetryAfterSeconds()) {
$error['retry_after_seconds'] = $retryAfter;
}
}
if ($this->details !== null) {
$error['details'] = $this->details;
}
$response = [
'error' => $error,
'timestamp' => (new \DateTimeImmutable())->format(\DateTimeInterface::RFC3339),
];
if ($this->path !== null) {
$response['path'] = $this->path;
}
return $response;
}
/**
* Create with path from request
*/
public function withPath(string $path): self
{
return new self(
errorCode: $this->errorCode,
customMessage: $this->customMessage,
details: $this->details,
path: $path,
status: $this->status
);
}
/**
* Create with additional details
*/
public function withDetails(array $details): self
{
return new self(
errorCode: $this->errorCode,
customMessage: $this->customMessage,
details: array_merge($this->details ?? [], $details),
path: $this->path,
status: $this->status
);
}
/**
* Create with custom message
*/
public function withMessage(string $message): self
{
return new self(
errorCode: $this->errorCode,
customMessage: $message,
details: $this->details,
path: $this->path,
status: $this->status
);
}
}