feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support

BREAKING CHANGE: Requires PHP 8.5.0RC3

Changes:
- Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm
- Enable ext-uri for native WHATWG URL parsing support
- Update composer.json PHP requirement from ^8.4 to ^8.5
- Add ext-uri as required extension in composer.json
- Move URL classes from Url.php85/ to Url/ directory (now compatible)
- Remove temporary PHP 8.4 compatibility workarounds

Benefits:
- Native URL parsing with Uri\WhatWg\Url class
- Better performance for URL operations
- Future-proof with latest PHP features
- Eliminates PHP version compatibility issues
This commit is contained in:
2025-10-27 09:31:28 +01:00
parent 799f74f00a
commit c8b47e647d
81 changed files with 6988 additions and 601 deletions

View File

@@ -28,7 +28,7 @@ final readonly class MLDashboardAdminController
#[Route(path: '/admin/ml/dashboard', method: Method::GET, name: AdminRoutes::ML_DASHBOARD)]
public function dashboard(HttpRequest $request): ViewResult
{
$timeWindowHours = (int) ($request->queryParameters['timeWindow'] ?? 24);
$timeWindowHours = $request->query->getInt('timeWindow', 24);
$timeWindow = Duration::fromHours($timeWindowHours);
// Get all models
@@ -115,6 +115,12 @@ final readonly class MLDashboardAdminController
$byType[$typeName] = ($byType[$typeName] ?? 0) + 1;
}
// Fetch confusion matrices
$confusionMatrices = $this->getConfusionMatrices($allModels, $timeWindow);
// Fetch registry summary
$registrySummary = $this->getRegistrySummary($allModels);
$data = [
'title' => 'ML Model Dashboard',
'page_title' => 'Machine Learning Model Dashboard',
@@ -143,6 +149,17 @@ final readonly class MLDashboardAdminController
'has_alerts' => count($degradationAlerts) > 0,
'alert_count' => count($degradationAlerts),
// Confusion matrices
'confusion_matrices' => $confusionMatrices,
'has_confusion_matrices' => count($confusionMatrices) > 0,
// Registry summary
'registry_total_versions' => $registrySummary['total_versions'],
'registry_production_count' => $registrySummary['production_count'],
'registry_development_count' => $registrySummary['development_count'],
'registry_models' => $registrySummary['models'],
'has_registry_summary' => count($registrySummary['models']) > 0,
// Links
'api_dashboard_url' => '/api/ml/dashboard',
'api_health_url' => '/api/ml/dashboard/health',
@@ -172,4 +189,109 @@ final readonly class MLDashboardAdminController
return $allModels;
}
/**
* Get confusion matrices for classification models
*/
private function getConfusionMatrices(array $allModels, Duration $timeWindow): array
{
$matrices = [];
foreach ($allModels as $metadata) {
$metrics = $this->performanceMonitor->getCurrentMetrics(
$metadata->modelName,
$metadata->version,
$timeWindow
);
if (isset($metrics['confusion_matrix'])) {
$cm = $metrics['confusion_matrix'];
$total = $metrics['total_predictions'];
$fpRate = $total > 0 ? $cm['false_positive'] / $total : 0.0;
$fnRate = $total > 0 ? $cm['false_negative'] / $total : 0.0;
$matrices[] = [
'model_name' => $metadata->modelName,
'version' => $metadata->version->toString(),
'type' => $metadata->modelType->value,
'true_positives' => number_format($cm['true_positive']),
'true_negatives' => number_format($cm['true_negative']),
'false_positives' => number_format($cm['false_positive']),
'false_negatives' => number_format($cm['false_negative']),
'fp_rate' => round($fpRate, 4),
'fn_rate' => round($fnRate, 4),
'fp_rate_percent' => round($fpRate * 100, 2),
'fn_rate_percent' => round($fnRate * 100, 2),
'fp_rate_badge' => $fpRate > 0.1 ? 'warning' : 'success',
'fn_rate_badge' => $fnRate > 0.1 ? 'warning' : 'success',
'total_predictions' => $total,
];
}
}
return $matrices;
}
/**
* Get model registry summary statistics
*/
private function getRegistrySummary(array $allModels): array
{
// Group by model name
$modelGroups = [];
$productionCount = 0;
$developmentCount = 0;
foreach ($allModels as $metadata) {
$modelName = $metadata->modelName;
if (!isset($modelGroups[$modelName])) {
$modelGroups[$modelName] = [
'model_name' => $modelName,
'type' => $metadata->modelType->value,
'versions' => [],
];
}
$modelGroups[$modelName]['versions'][] = [
'version' => $metadata->version->toString(),
'environment' => $metadata->environment,
];
// Count environments
if ($metadata->environment === 'production') {
$productionCount++;
} elseif ($metadata->environment === 'development') {
$developmentCount++;
}
}
// Calculate summary per model
$modelsSummary = [];
foreach ($modelGroups as $modelName => $group) {
// Sort versions
$versions = array_column($group['versions'], 'version');
usort($versions, 'version_compare');
// Get latest environment
$latestVersion = end($versions);
$latestVersionData = array_filter($group['versions'], fn($v) => $v['version'] === $latestVersion);
$latestEnv = !empty($latestVersionData) ? reset($latestVersionData)['environment'] : 'unknown';
$modelsSummary[] = [
'model_name' => $modelName,
'type' => $group['type'],
'version_count' => count($versions),
'latest_version' => $latestVersion,
'environment' => $latestEnv,
];
}
return [
'total_versions' => count($allModels),
'production_count' => $productionCount,
'development_count' => $developmentCount,
'models' => $modelsSummary,
];
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Application\Admin\Notifications;
use App\Framework\Attributes\Route;
use App\Framework\Http\Method;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Status;
use App\Framework\Router\Result\ViewResult;
use App\Framework\Http\Responses\JsonResponse;
use App\Framework\Notification\Storage\NotificationRepository;
use App\Framework\Notification\ValueObjects\NotificationId;
use App\Framework\Meta\MetaData;
/**
* Admin Notifications Controller
*
* Displays and manages system notifications for administrators
*/
final readonly class NotificationsAdminController
{
public function __construct(
private NotificationRepository $notificationRepository
) {}
/**
* Display notifications list
*/
#[Route(path: '/admin/notifications', method: Method::GET)]
public function index(HttpRequest $request): ViewResult
{
// For now, use 'admin' as recipient ID
// TODO: Replace with actual authenticated admin user ID
$adminUserId = 'admin';
// Get pagination parameters
$page = $request->query->getInt('page', 1);
$perPage = 20;
$offset = ($page - 1) * $perPage;
// Fetch notifications
$notifications = $this->notificationRepository->findByUser(
$adminUserId,
limit: $perPage,
offset: $offset
);
// Get unread count
$unreadCount = $this->notificationRepository->countUnreadByUser($adminUserId);
// Transform notifications for template
$notificationsList = array_map(function ($notification) {
return [
'id' => $notification->id->toString(),
'type' => $notification->type->toString(),
'title' => $notification->title,
'body' => $notification->body,
'priority' => $notification->priority->value,
'status' => $notification->status->value,
'is_read' => $notification->status->value === 'read',
'created_at' => $notification->createdAt->format('Y-m-d H:i:s'),
'created_at_human' => $this->getHumanReadableTime($notification->createdAt->toDateTime()),
'action_url' => $notification->actionUrl,
'action_label' => $notification->actionLabel ?? 'View Details',
'icon' => $this->getNotificationIcon($notification->type->toString()),
'badge_class' => $this->getPriorityBadgeClass($notification->priority->value),
];
}, $notifications);
$data = [
'notifications' => $notificationsList,
'unread_count' => $unreadCount,
'current_page' => $page,
'per_page' => $perPage,
'has_more' => count($notifications) === $perPage,
'has_notifications' => count($notifications) > 0,
'has_unread' => $unreadCount > 0, // Boolean flag for if attribute
'show_pagination' => count($notifications) === $perPage || $page > 1, // Boolean flag
];
return new ViewResult(
template: 'notification-index',
metaData: new MetaData(
title: 'Notifications - Admin',
description: 'System notifications for administrators'
),
data : $data
);
}
/**
* Mark notification as read
*/
#[Route(path: '/admin/notifications/{id}/read', method: Method::POST)]
public function markAsRead(HttpRequest $request, string $id): JsonResponse
{
$notificationId = NotificationId::fromString($id);
$success = $this->notificationRepository->markAsRead($notificationId);
if (!$success) {
return new JsonResponse(
body: ['error' => 'Notification not found'],
status: Status::NOT_FOUND
);
}
// Get updated unread count
$unreadCount = $this->notificationRepository->countUnreadByUser('admin');
return new JsonResponse([
'success' => true,
'unread_count' => $unreadCount,
]);
}
/**
* Mark all notifications as read
*/
#[Route(path: '/admin/notifications/read-all', method: Method::POST)]
public function markAllAsRead(HttpRequest $request): JsonResponse
{
$count = $this->notificationRepository->markAllAsReadForUser('admin');
return new JsonResponse([
'success' => true,
'marked_count' => $count,
'unread_count' => 0,
]);
}
/**
* Get unread count for badge
*/
#[Route(path: '/admin/notifications/unread-count', method: Method::GET)]
public function getUnreadCount(HttpRequest $request): JsonResponse
{
$unreadCount = $this->notificationRepository->countUnreadByUser('admin');
return new JsonResponse([
'unread_count' => $unreadCount,
]);
}
/**
* Get human-readable time difference
*/
private function getHumanReadableTime(\DateTimeInterface $timestamp): string
{
$now = new \DateTime();
$diff = $now->diff($timestamp);
if ($diff->y > 0) {
return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago';
}
if ($diff->m > 0) {
return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago';
}
if ($diff->d > 0) {
return $diff->d . ' day' . ($diff->d > 1 ? 's' : '') . ' ago';
}
if ($diff->h > 0) {
return $diff->h . ' hour' . ($diff->h > 1 ? 's' : '') . ' ago';
}
if ($diff->i > 0) {
return $diff->i . ' minute' . ($diff->i > 1 ? 's' : '') . ' ago';
}
return 'Just now';
}
/**
* Get icon for notification type
*/
private function getNotificationIcon(string $type): string
{
return match ($type) {
'ml_performance_degradation' => '⚠️',
'ml_model_deployed' => '🚀',
'ml_training_complete' => '✅',
'system_alert' => '🚨',
'security_alert' => '🔒',
'info' => '',
default => '📢',
};
}
/**
* Get badge class for priority
*/
private function getPriorityBadgeClass(string $priority): string
{
return match ($priority) {
'urgent' => 'admin-badge--danger',
'high' => 'admin-badge--warning',
'normal' => 'admin-badge--info',
'low' => 'admin-badge--secondary',
default => 'admin-badge--info',
};
}
}

View File

@@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{meta.title}</title>
<meta name="description" content="{meta.description}">
<link rel="stylesheet" href="/css/admin.css">
<style>
.notification-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.notification-header h1 {
margin: 0;
font-size: 2rem;
color: var(--color-text);
}
.unread-badge {
background: var(--color-danger);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 600;
}
.notification-actions {
margin-bottom: 1.5rem;
}
.btn-mark-all {
background: var(--color-primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: background 0.2s;
}
.btn-mark-all:hover {
background: var(--color-primary-dark);
}
.notification-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.notification-item {
background: white;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
transition: all 0.2s;
position: relative;
}
.notification-item.unread {
border-left: 4px solid var(--color-primary);
background: #f8f9fa;
}
.notification-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.notification-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.notification-title-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
}
.notification-icon {
font-size: 1.5rem;
}
.notification-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text);
margin: 0;
}
.notification-meta {
display: flex;
align-items: center;
gap: 0.75rem;
}
.notification-time {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.admin-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.admin-badge--danger {
background: #fee;
color: #c33;
}
.admin-badge--warning {
background: #ffc;
color: #c93;
}
.admin-badge--info {
background: #e3f2fd;
color: #1976d2;
}
.admin-badge--secondary {
background: #f5f5f5;
color: #666;
}
.notification-body {
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: 1rem;
}
.notification-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-action {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--color-primary);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: color 0.2s;
}
.notification-action:hover {
color: var(--color-primary-dark);
}
.btn-mark-read {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
}
.btn-mark-read:hover {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 2rem;
padding: 1rem;
}
.pagination-link {
padding: 0.5rem 1rem;
background: white;
border: 1px solid var(--color-border);
border-radius: 0.25rem;
color: var(--color-text);
text-decoration: none;
transition: all 0.2s;
}
.pagination-link:hover {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--color-text-muted);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: var(--color-text);
}
</style>
</head>
<body>
<div class="admin-layout">
<aside class="admin-sidebar">
<div class="admin-sidebar-header">
<h2>Admin Panel</h2>
</div>
<nav class="admin-nav">
<a href="/admin/dashboard" class="admin-nav-link">
📊 Dashboard
</a>
<a href="/admin/ml/dashboard" class="admin-nav-link">
🤖 ML Dashboard
</a>
<a href="/admin/notifications" class="admin-nav-link admin-nav-link--active">
🔔 Notifications
<span class="unread-badge" if="$unread_count > 0">{{ $unread_count }}</span>
</a>
<a href="/admin/users" class="admin-nav-link">
👥 Users
</a>
<a href="/admin/settings" class="admin-nav-link">
⚙️ Settings
</a>
</nav>
</aside>
<main class="admin-main">
<div class="notification-container">
<div class="notification-header">
<h1>Notifications</h1>
<span class="unread-badge" if="$unread_count > 0">{{ $unread_count }} unread</span>
</div>
<div class="notification-actions" if="$unread_count > 0">
<button class="btn-mark-all" onclick="markAllAsRead()">
Mark all as read
</button>
</div>
<div class="notification-list" if="$has_notifications">
<div foreach="$notifications as $notification" class="notification-item" data-id="{{ $notification['id'] }}">
<div class="notification-item-header">
<div class="notification-title-wrapper">
<span class="notification-icon">{{ $notification['icon'] }}</span>
<h3 class="notification-title">{{ $notification['title'] }}</h3>
</div>
<div class="notification-meta">
<span class="admin-badge {{ $notification['badge_class'] }}">{{ $notification['priority'] }}</span>
<span class="notification-time">{{ $notification['created_at_human'] }}</span>
</div>
</div>
<div class="notification-body">
{{ $notification['body'] }}
</div>
<div class="notification-footer">
<div>
<a href="{{ $notification['action_url'] }}" class="notification-action" if="$notification['action_url']">
{{ $notification['action_label'] }}
</a>
</div>
<button
class="btn-mark-read"
onclick="markAsRead('{{ $notification['id'] }}')"
>
Mark as read
</button>
</div>
</div>
</div>
<div class="pagination" if="$has_more || $current_page > 1">
<a href="/admin/notifications?page={{ $current_page - 1 }}" class="pagination-link" if="$current_page > 1">
Previous
</a>
<span class="pagination-info">Page {{ $current_page }}</span>
<a href="/admin/notifications?page={{ $current_page + 1 }}" class="pagination-link" if="$has_more">
Next
</a>
</div>
<div class="empty-state" if="!$has_notifications">
<div class="empty-state-icon">📭</div>
<h2 class="empty-state-title">No notifications</h2>
<p>You're all caught up! No notifications to display.</p>
</div>
</div>
</main>
</div>
<script>
async function markAsRead(notificationId) {
try {
const response = await fetch(`/admin/notifications/${notificationId}/read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
if (data.success) {
// Update UI
const item = document.querySelector(`[data-id="${notificationId}"]`);
if (item) {
item.classList.remove('unread');
const button = item.querySelector('.btn-mark-read');
if (button) {
button.textContent = ' Read';
button.disabled = true;
}
}
// Update unread count badge
updateUnreadCount(data.unread_count);
}
} catch (error) {
console.error('Error marking notification as read:', error);
alert('Failed to mark notification as read. Please try again.');
}
}
async function markAllAsRead() {
if (!confirm('Mark all notifications as read?')) {
return;
}
try {
const response = await fetch('/admin/notifications/read-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
if (data.success) {
// Reload page to show updated state
window.location.reload();
}
} catch (error) {
console.error('Error marking all notifications as read:', error);
alert('Failed to mark all notifications as read. Please try again.');
}
}
function updateUnreadCount(count) {
const badges = document.querySelectorAll('.unread-badge');
badges.forEach(badge => {
if (count === 0) {
badge.style.display = 'none';
} else {
badge.textContent = count;
badge.style.display = '';
}
});
}
// Poll for new notifications every 30 seconds
setInterval(async () => {
try {
const response = await fetch('/admin/notifications/unread-count');
const data = await response.json();
const currentBadge = document.querySelector('.unread-badge');
const currentCount = currentBadge ? parseInt(currentBadge.textContent) || 0 : 0;
if (data.unread_count !== currentCount) {
updateUnreadCount(data.unread_count);
if (data.unread_count > currentCount) {
console.log('New notifications received');
}
}
} catch (error) {
console.error('Error polling for notifications:', error);
}
}, 30000);
</script>
</body>
</html>

View File

@@ -228,6 +228,123 @@
</div>
</div>
<!-- Confusion Matrices Section -->
<div class="admin-card" if="{{ $has_confusion_matrices }}">
<div class="admin-card__header">
<h3 class="admin-card__title">Classification Performance (Confusion Matrices)</h3>
</div>
<div class="admin-card__content">
<div class="admin-grid admin-grid--2-col">
<div foreach="$confusion_matrices as $matrix" class="confusion-matrix-card">
<h4 class="confusion-matrix-card__title">{{ $matrix['model_name'] }} v{{ $matrix['version'] }}</h4>
<div class="confusion-matrix">
<div class="confusion-matrix__grid">
<!-- True Positive -->
<div class="confusion-matrix__cell confusion-matrix__cell--tp">
<div class="confusion-matrix__cell-label">True Positive</div>
<div class="confusion-matrix__cell-value">{{ $matrix['true_positives'] }}</div>
</div>
<!-- False Positive -->
<div class="confusion-matrix__cell confusion-matrix__cell--fp">
<div class="confusion-matrix__cell-label">False Positive</div>
<div class="confusion-matrix__cell-value">{{ $matrix['false_positives'] }}</div>
</div>
<!-- False Negative -->
<div class="confusion-matrix__cell confusion-matrix__cell--fn">
<div class="confusion-matrix__cell-label">False Negative</div>
<div class="confusion-matrix__cell-value">{{ $matrix['false_negatives'] }}</div>
</div>
<!-- True Negative -->
<div class="confusion-matrix__cell confusion-matrix__cell--tn">
<div class="confusion-matrix__cell-label">True Negative</div>
<div class="confusion-matrix__cell-value">{{ $matrix['true_negatives'] }}</div>
</div>
</div>
<div class="confusion-matrix__rates">
<div class="admin-stat-item">
<span class="admin-stat-item__label">False Positive Rate</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--{{ $matrix['fp_rate_badge'] }}">
{{ $matrix['fp_rate_percent'] }}%
</span>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">False Negative Rate</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--{{ $matrix['fn_rate_badge'] }}">
{{ $matrix['fn_rate_percent'] }}%
</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Model Registry Summary -->
<div class="admin-card" if="{{ $has_registry_summary }}">
<div class="admin-card__header">
<h3 class="admin-card__title">Model Registry Summary</h3>
</div>
<div class="admin-card__content">
<div class="admin-grid admin-grid--3-col">
<div class="admin-stat-item">
<span class="admin-stat-item__label">Total Versions</span>
<span class="admin-stat-item__value">{{ $registry_total_versions }}</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Production Models</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--success">{{ $registry_production_count }}</span>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Development Models</span>
<span class="admin-stat-item__value">
<span class="admin-badge admin-badge--info">{{ $registry_development_count }}</span>
</span>
</div>
</div>
<div class="admin-table-container">
<table class="admin-table admin-table--compact">
<thead>
<tr>
<th>Model Name</th>
<th>Total Versions</th>
<th>Type</th>
<th>Latest Version</th>
<th>Environment</th>
</tr>
</thead>
<tbody>
<tr foreach="$registry_models as $regModel">
<td><strong>{{ $regModel['model_name'] }}</strong></td>
<td>{{ $regModel['version_count'] }}</td>
<td>
<span class="admin-badge admin-badge--info">{{ $regModel['type'] }}</span>
</td>
<td><code>{{ $regModel['latest_version'] }}</code></td>
<td>
<span class="admin-badge admin-badge--{{ $regModel['environment'] === 'production' ? 'success' : 'info' }}">
{{ $regModel['environment'] }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- API Information Card -->
<div class="admin-card">
<div class="admin-card__header">
@@ -247,6 +364,18 @@
<code>GET {{ $api_health_url }}</code>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Confusion Matrices</span>
<span class="admin-stat-item__value">
<code>GET /api/ml/dashboard/confusion-matrices</code>
</span>
</div>
<div class="admin-stat-item">
<span class="admin-stat-item__label">Registry Summary</span>
<span class="admin-stat-item__value">
<code>GET /api/ml/dashboard/registry-summary</code>
</span>
</div>
</div>
</div>
</div>

View File

@@ -350,8 +350,8 @@ final readonly class MLABTestingController
)]
public function calculateSampleSize(HttpRequest $request): JsonResult
{
$confidenceLevel = (float) ($request->queryParameters['confidence_level'] ?? 0.95);
$marginOfError = (float) ($request->queryParameters['margin_of_error'] ?? 0.05);
$confidenceLevel = $request->query->getFloat('confidence_level', 0.95);
$marginOfError = $request->query->getFloat('margin_of_error', 0.05);
// Validate parameters
if ($confidenceLevel < 0.5 || $confidenceLevel > 0.99) {

View File

@@ -91,7 +91,7 @@ final readonly class MLDashboardController
)]
public function getDashboardData(HttpRequest $request): JsonResult
{
$timeWindowHours = (int) ($request->queryParameters['timeWindow'] ?? 24);
$timeWindowHours = $request->query->getInt('timeWindow', 24);
$timeWindow = Duration::fromHours($timeWindowHours);
// Get all models
@@ -280,7 +280,7 @@ final readonly class MLDashboardController
)]
public function getAlerts(HttpRequest $request): JsonResult
{
$severityFilter = $request->queryParameters['severity'] ?? null;
$severityFilter = $request->query->get('severity');
$allModels = $this->getAllModels();
$timeWindow = Duration::fromHours(1);

View File

@@ -74,7 +74,7 @@ final readonly class MLModelsController
)]
public function listModels(HttpRequest $request): JsonResult
{
$typeFilter = $request->queryParameters['type'] ?? null;
$typeFilter = $request->query->get('type');
// Get all model names
$modelNames = $this->registry->getAllModelNames();
@@ -161,7 +161,7 @@ final readonly class MLModelsController
)]
public function getModel(string $modelName, HttpRequest $request): JsonResult
{
$versionString = $request->queryParameters['version'] ?? null;
$versionString = $request->query->get('version');
try {
if ($versionString !== null) {
@@ -253,8 +253,8 @@ final readonly class MLModelsController
)]
public function getMetrics(string $modelName, HttpRequest $request): JsonResult
{
$versionString = $request->queryParameters['version'] ?? null;
$timeWindowHours = (int) ($request->queryParameters['timeWindow'] ?? 1);
$versionString = $request->query->get('version');
$timeWindowHours = $request->query->getInt('timeWindow', 1);
try {
if ($versionString !== null) {
@@ -439,7 +439,7 @@ final readonly class MLModelsController
)]
public function unregisterModel(string $modelName, HttpRequest $request): JsonResult
{
$versionString = $request->queryParameters['version'] ?? null;
$versionString = $request->query->get('version');
if ($versionString === null) {
return new JsonResult([

View File

@@ -37,9 +37,9 @@ interface LiveComponentState extends SerializableState
* Create State VO from array data (from client or storage)
*
* @param array $data Raw state data
* @return static Hydrated state object
* @return self Hydrated state object
*/
public static function fromArray(array $data): static;
public static function fromArray(array $data): self;
/**
* Convert State VO to array for serialization