Files
michaelschiemer/src/Framework/LiveComponents/Controllers/LiveComponentController.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

366 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\LiveComponents\Controllers;
use App\Framework\Attributes\Route;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Method;
use App\Framework\Http\Status;
use App\Framework\Http\UploadedFile;
use App\Framework\LiveComponents\Batch\BatchProcessor;
use App\Framework\LiveComponents\Batch\BatchRequest;
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\Contracts\LifecycleAware;
use App\Framework\LiveComponents\Contracts\SupportsFileUpload;
use App\Framework\LiveComponents\LiveComponentHandler;
use App\Framework\LiveComponents\Rendering\FragmentRenderer;
use App\Framework\LiveComponents\ValueObjects\ActionParameters;
use App\Framework\LiveComponents\ValueObjects\ComponentAction;
use App\Framework\LiveComponents\ValueObjects\ComponentData;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Router\Result\JsonResult;
final readonly class LiveComponentController
{
public function __construct(
private ComponentRegistry $componentRegistry,
private LiveComponentHandler $handler,
private FragmentRenderer $fragmentRenderer,
private BatchProcessor $batchProcessor
) {
}
#[Route('/live-component/{id}', method: Method::POST)]
public function handleAction(string $id, HttpRequest $request): JsonResult
{
$action = ComponentAction::fromRequest($request, $id);
// Get state from request (as array, will be passed to component constructor)
$stateArray = $request->parsedBody?->get('state', []);
$initialData = is_array($stateArray) ? $stateArray : null;
// Get fragments parameter if present (for partial rendering)
$fragmentNames = $request->parsedBody?->get('fragments', []);
$fragmentNames = is_array($fragmentNames) ? $fragmentNames : [];
// Resolve component with current state (registry will convert array to State VO)
$component = $this->componentRegistry->resolve(
$action->componentId,
$initialData
);
// Handle action using LiveComponentHandler (composition)
// Returns ComponentUpdate with empty HTML
$update = $this->handler->handle(
$component,
$action->method,
$action->params
);
// Create new component instance with updated data for rendering
$updatedComponent = $this->componentRegistry->resolve(
$action->componentId,
$update->state->data // Array will be converted to State VO by registry
);
// Render fragments or full HTML based on request
if (! empty($fragmentNames)) {
// Partial rendering: extract requested fragments
$fragmentCollection = $this->fragmentRenderer->renderFragments(
$updatedComponent,
$fragmentNames
);
// If fragments found, return them
if (! $fragmentCollection->isEmpty()) {
return new JsonResult([
'fragments' => $fragmentCollection->toAssociativeArray(),
'state' => $update->state->toArray(),
'events' => $update->events,
]);
}
// Fallback to full render if fragments not found
}
// Full rendering (default or fallback)
$html = $this->componentRegistry->render($updatedComponent);
// Return ComponentUpdate with rendered HTML
return new JsonResult([
'html' => $html,
'state' => $update->state->toArray(), // ✅ Prevent double-encoding
'events' => $update->events,
]);
}
#[Route('/live-component/{id}/upload', method: Method::POST)]
public function handleUpload(string $id, HttpRequest $request): JsonResult
{
// Convert string ID to ComponentId
$componentId = ComponentId::fromString($id);
// Get state from request (as array)
$stateArray = $request->parsedBody?->get('state', []);
$initialData = is_array($stateArray) ? $stateArray : null;
$params = $request->parsedBody?->get('params', []);
// Resolve component (registry will convert array to State VO)
$component = $this->componentRegistry->resolve(
$componentId,
$initialData
);
// Check if component supports file uploads
if (! $component instanceof SupportsFileUpload) {
return new JsonResult([
'success' => false,
'error' => 'Component does not support file uploads',
], Status::BAD_REQUEST);
}
// Get uploaded file from request
$uploadedFiles = $request->files?->all() ?? [];
if (empty($uploadedFiles)) {
return new JsonResult([
'success' => false,
'error' => 'No file uploaded',
], Status::BAD_REQUEST);
}
// Get first uploaded file (support for multiple files can be added later)
$file = reset($uploadedFiles);
if (! $file instanceof UploadedFile) {
return new JsonResult([
'success' => false,
'error' => 'Invalid file upload',
], Status::BAD_REQUEST);
}
// Validate file
$validationErrors = $component->validateUpload($file);
if (! empty($validationErrors)) {
return new JsonResult([
'success' => false,
'errors' => $validationErrors,
], Status::UNPROCESSABLE_ENTITY);
}
try {
// Handle upload through handler (like regular actions)
$update = $this->handler->handleUpload(
$component,
$file,
ActionParameters::fromArray(is_array($params) ? $params : [])
);
// Create new component instance with updated data for rendering
$updatedComponent = $this->componentRegistry->resolve(
$componentId,
$update->state->data // Array will be converted to State VO
);
// Render HTML using ComponentRegistry
$html = $this->componentRegistry->render($updatedComponent);
// Return success response
return new JsonResult([
'success' => true,
'html' => $html,
'state' => $update->state->toArray(), // ✅ Prevent double-encoding
'events' => $update->events,
'file' => [
'name' => $file->name,
'size' => $file->size,
'type' => $file->type,
],
]);
} catch (\Exception $e) {
return new JsonResult([
'success' => false,
'error' => $e->getMessage(),
], Status::INTERNAL_SERVER_ERROR);
}
}
/**
* Handle lazy loading of component
*
* Renders component HTML on-demand when it enters viewport.
* Used by LazyComponentLoader for performance optimization.
*
* @param string $id Component ID
* @param HttpRequest $request HTTP request
* @return JsonResult JSON response with component HTML and state
*/
#[Route('/live-component/{id}/lazy-load', method: Method::GET)]
public function handleLazyLoad(string $id, HttpRequest $request): JsonResult
{
try {
$componentId = ComponentId::fromString($id);
// Resolve component with initial state (null = component will use its defaults)
$component = $this->componentRegistry->resolve(
$componentId,
initialData: null
);
// Render component HTML with wrapper
$html = $this->componentRegistry->renderWithWrapper($component);
// Get component state
$componentState = $component->state;
// Get CSRF token for component (each component has its own token)
$csrfToken = $this->componentRegistry->generateCsrfToken($componentId);
return new JsonResult([
'success' => true,
'html' => $html,
'state' => $componentState->toArray(),
'csrf_token' => $csrfToken,
'component_id' => $componentId->toString(),
]);
} catch (\Exception $e) {
return new JsonResult([
'success' => false,
'error' => $e->getMessage(),
'error_code' => 'LAZY_LOAD_FAILED',
], Status::INTERNAL_SERVER_ERROR);
}
}
/**
* Handle component destruction lifecycle hook
*
* Called by client-side JavaScript when component element is removed from DOM.
* Only calls onDestroy() if component implements LifecycleAware interface.
*/
#[Route('/live-component/{id}/destroy', method: Method::POST)]
public function handleDestroy(string $id, HttpRequest $request): JsonResult
{
try {
$componentId = ComponentId::fromString($id);
// Get state from request
$stateArray = $request->parsedBody?->get('state', []);
$initialData = is_array($stateArray) ? $stateArray : null;
// Resolve component with current state
$component = $this->componentRegistry->resolve(
$componentId,
$initialData
);
// Only call onDestroy() if component implements LifecycleAware
if ($component instanceof LifecycleAware) {
try {
$component->onDestroy();
} catch (\Throwable $e) {
// Log but don't fail - component is being destroyed anyway
error_log("Lifecycle hook onDestroy() failed for " . get_class($component) . ": " . $e->getMessage());
}
}
return new JsonResult([
'success' => true,
'message' => 'Component destroyed',
]);
} catch (\Exception $e) {
// Even if destroy fails, return success - component is gone from client
return new JsonResult([
'success' => true,
'message' => 'Component destroyed (with errors)',
'error' => $e->getMessage(),
]);
}
}
/**
* Handle batch request - multiple component operations in one request
*
* Request Format:
* {
* "operations": [
* {
* "componentId": "counter:demo",
* "method": "increment",
* "params": {"amount": 5},
* "fragments": ["counter-display"],
* "operationId": "op-1"
* },
* {
* "componentId": "user-stats:123",
* "method": "refresh",
* "operationId": "op-2"
* }
* ]
* }
*
* Response Format:
* {
* "results": [
* {
* "success": true,
* "operationId": "op-1",
* "fragments": {"counter-display": "<span>5</span>"},
* "state": {"count": 5},
* "events": []
* },
* {
* "success": false,
* "operationId": "op-2",
* "error": "Component not found",
* "errorCode": "COMPONENT_NOT_FOUND"
* }
* ],
* "total_operations": 2,
* "success_count": 1,
* "failure_count": 1
* }
*
* Features:
* - Executes operations sequentially with error isolation
* - Per-operation error handling (one failure doesn't stop batch)
* - Fragment support for partial updates
* - 60-80% fewer HTTP requests for multi-component updates
*/
#[Route('/live-component/batch', method: Method::POST)]
public function handleBatch(HttpRequest $request): JsonResult
{
try {
// Parse batch request
$requestData = $request->parsedBody?->toArray() ?? [];
$batchRequest = BatchRequest::fromArray($requestData);
// Process batch
$batchResponse = $this->batchProcessor->process($batchRequest);
// Return batch response
return new JsonResult($batchResponse->toArray());
} catch (\InvalidArgumentException $e) {
// Invalid batch request format
return new JsonResult([
'error' => $e->getMessage(),
'error_code' => 'INVALID_BATCH_REQUEST',
], Status::BAD_REQUEST);
} catch (\Throwable $e) {
// Unexpected error
return new JsonResult([
'error' => 'Batch processing failed: ' . $e->getMessage(),
'error_code' => 'BATCH_PROCESSING_FAILED',
], Status::INTERNAL_SERVER_ERROR);
}
}
}