Rename folders to match PHP PSR-4 autoloading conventions: - schemas → Schemas - shared → Shared - Wordpress → WordPress (in all locations) Fixes deployment issues on Linux servers where filesystem is case-sensitive. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
256 lines
9.7 KiB
PHP
256 lines
9.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Shared\Infrastructure\Validators;
|
|
|
|
/**
|
|
* Validador de Fase 01: Schema JSON
|
|
*
|
|
* Valida que el archivo JSON del componente cumple con:
|
|
* - Estructura correcta
|
|
* - Campos obligatorios presentes
|
|
* - Tipos de datos válidos
|
|
* - Grupos y campos requeridos
|
|
*/
|
|
final class Phase01Validator implements PhaseValidatorInterface
|
|
{
|
|
private const ALLOWED_TYPES = ['boolean', 'text', 'textarea', 'url', 'select', 'color'];
|
|
private const ALLOWED_PRIORITIES = [10, 20, 30, 40, 50, 60, 70, 80, 90];
|
|
private const STANDARD_GROUPS = [
|
|
'visibility', 'content', 'typography', 'colors', 'spacing',
|
|
'visual_effects', 'behavior', 'layout', 'links', 'icons', 'media', 'forms'
|
|
];
|
|
|
|
/**
|
|
* Componentes especiales que NO requieren grupo visibility
|
|
*
|
|
* Estos son componentes de inyeccion (no visuales) que:
|
|
* - NO renderizan HTML visual
|
|
* - Inyectan codigo en hooks (wp_head, wp_footer)
|
|
* - Siempre estan activos (controlados por campos vacios/llenos)
|
|
*/
|
|
private const INJECTION_COMPONENTS = ['theme-settings'];
|
|
|
|
public function validate(string $componentName, string $themePath): ValidationResult
|
|
{
|
|
$result = new ValidationResult();
|
|
|
|
// Construir ruta al schema
|
|
$schemaPath = $themePath . '/Schemas/' . $componentName . '.json';
|
|
|
|
$result->addInfo("Validando Schema JSON: schemas/{$componentName}.json");
|
|
|
|
// 1. Verificar que el archivo existe
|
|
if (!file_exists($schemaPath)) {
|
|
$result->addError("Schema JSON no encontrado: {$schemaPath}");
|
|
return $result;
|
|
}
|
|
|
|
// 2. Leer y parsear JSON
|
|
$jsonContent = file_get_contents($schemaPath);
|
|
$schema = json_decode($jsonContent, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$result->addError("JSON inválido: " . json_last_error_msg());
|
|
return $result;
|
|
}
|
|
|
|
// 3. Validar estructura top-level
|
|
$this->validateTopLevelStructure($schema, $componentName, $result);
|
|
|
|
// 4. Validar grupos
|
|
if (isset($schema['groups'])) {
|
|
$this->validateGroups($schema['groups'], $result);
|
|
}
|
|
|
|
// 5. Validar campos obligatorios de visibilidad (excepto componentes de inyeccion)
|
|
$this->validateVisibilityFields($schema, $componentName, $result);
|
|
|
|
// Estadísticas
|
|
$totalFields = $this->countTotalFields($schema);
|
|
$totalGroups = isset($schema['groups']) ? count($schema['groups']) : 0;
|
|
|
|
$result->setStat('Archivo', "schemas/{$componentName}.json");
|
|
$result->setStat('Grupos totales', $totalGroups);
|
|
$result->setStat('Campos totales', $totalFields);
|
|
$result->setStat('Tamaño JSON', strlen($jsonContent) . ' bytes');
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function validateTopLevelStructure(array $schema, string $componentName, ValidationResult $result): void
|
|
{
|
|
// Campos obligatorios
|
|
$requiredFields = ['component_name', 'version', 'description', 'groups'];
|
|
|
|
foreach ($requiredFields as $field) {
|
|
if (!isset($schema[$field])) {
|
|
$result->addError("Campo obligatorio faltante: '{$field}'");
|
|
}
|
|
}
|
|
|
|
// Validar component_name coincide con archivo
|
|
if (isset($schema['component_name']) && $schema['component_name'] !== $componentName) {
|
|
$result->addError(
|
|
"component_name '{$schema['component_name']}' no coincide con nombre de archivo '{$componentName}'"
|
|
);
|
|
}
|
|
|
|
// Validar versión semver
|
|
if (isset($schema['version'])) {
|
|
if (!preg_match('/^\d+\.\d+\.\d+$/', $schema['version'])) {
|
|
$result->addError("Versión '{$schema['version']}' no es semver válido (debe ser X.Y.Z)");
|
|
}
|
|
}
|
|
}
|
|
|
|
private function validateGroups(array $groups, ValidationResult $result): void
|
|
{
|
|
if (empty($groups)) {
|
|
$result->addError("Schema debe tener al menos un grupo");
|
|
return;
|
|
}
|
|
|
|
foreach ($groups as $groupName => $group) {
|
|
// Validar nombre de grupo es snake_case
|
|
if (!preg_match('/^[a-z_]+$/', $groupName)) {
|
|
$result->addError("Nombre de grupo '{$groupName}' debe estar en snake_case (solo minúsculas y _)");
|
|
}
|
|
|
|
// Advertencia si grupo no es estándar
|
|
if (!in_array($groupName, self::STANDARD_GROUPS, true)) {
|
|
$result->addWarning("Grupo '{$groupName}' no es estándar (considerar usar: " . implode(', ', self::STANDARD_GROUPS) . ")");
|
|
}
|
|
|
|
// Validar estructura del grupo
|
|
if (!isset($group['label'])) {
|
|
$result->addError("Grupo '{$groupName}' no tiene 'label'");
|
|
}
|
|
|
|
if (!isset($group['priority'])) {
|
|
$result->addError("Grupo '{$groupName}' no tiene 'priority'");
|
|
} elseif (!in_array($group['priority'], self::ALLOWED_PRIORITIES, true)) {
|
|
$result->addError(
|
|
"Grupo '{$groupName}' tiene priority inválido ({$group['priority']}). " .
|
|
"Debe ser uno de: " . implode(', ', self::ALLOWED_PRIORITIES)
|
|
);
|
|
}
|
|
|
|
if (!isset($group['fields'])) {
|
|
$result->addError("Grupo '{$groupName}' no tiene 'fields'");
|
|
} elseif (!is_array($group['fields']) || empty($group['fields'])) {
|
|
$result->addError("Grupo '{$groupName}' debe tener al menos un campo");
|
|
} else {
|
|
$this->validateFields($groupName, $group['fields'], $result);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function validateFields(string $groupName, array $fields, ValidationResult $result): void
|
|
{
|
|
foreach ($fields as $fieldName => $field) {
|
|
$fullFieldName = "{$groupName}.{$fieldName}";
|
|
|
|
// Validar nombre de campo es snake_case
|
|
if (!preg_match('/^[a-z_]+$/', $fieldName)) {
|
|
$result->addError("Campo '{$fullFieldName}' debe estar en snake_case (solo minúsculas y _)");
|
|
}
|
|
|
|
// Campos obligatorios
|
|
if (!isset($field['type'])) {
|
|
$result->addError("Campo '{$fullFieldName}' no tiene 'type'");
|
|
} elseif (!in_array($field['type'], self::ALLOWED_TYPES, true)) {
|
|
$result->addError(
|
|
"Campo '{$fullFieldName}' tiene type inválido '{$field['type']}'. " .
|
|
"Debe ser uno de: " . implode(', ', self::ALLOWED_TYPES)
|
|
);
|
|
}
|
|
|
|
if (!isset($field['label'])) {
|
|
$result->addError("Campo '{$fullFieldName}' no tiene 'label'");
|
|
}
|
|
|
|
if (!array_key_exists('default', $field)) {
|
|
$result->addError("Campo '{$fullFieldName}' no tiene 'default'");
|
|
}
|
|
|
|
if (!isset($field['editable'])) {
|
|
$result->addError("Campo '{$fullFieldName}' no tiene 'editable'");
|
|
} elseif (!is_bool($field['editable'])) {
|
|
$result->addError("Campo '{$fullFieldName}' tiene 'editable' que no es boolean");
|
|
}
|
|
|
|
// Si type es select, debe tener options
|
|
if (isset($field['type']) && $field['type'] === 'select') {
|
|
if (!isset($field['options']) || !is_array($field['options']) || empty($field['options'])) {
|
|
$result->addError("Campo '{$fullFieldName}' es type 'select' pero no tiene array 'options' válido");
|
|
}
|
|
}
|
|
|
|
// Si tiene required, debe ser boolean
|
|
if (isset($field['required']) && !is_bool($field['required'])) {
|
|
$result->addError("Campo '{$fullFieldName}' tiene 'required' que no es boolean");
|
|
}
|
|
}
|
|
}
|
|
|
|
private function validateVisibilityFields(array $schema, string $componentName, ValidationResult $result): void
|
|
{
|
|
// Componentes de inyeccion no requieren grupo visibility
|
|
if (in_array($componentName, self::INJECTION_COMPONENTS, true)) {
|
|
$result->addInfo("✓ Componente de inyección '{$componentName}' - grupo visibility no requerido");
|
|
return;
|
|
}
|
|
|
|
if (!isset($schema['groups']['visibility'])) {
|
|
$result->addError("Grupo 'visibility' es obligatorio y no está presente");
|
|
return;
|
|
}
|
|
|
|
$visibilityFields = $schema['groups']['visibility']['fields'] ?? [];
|
|
|
|
// Campos obligatorios de visibilidad
|
|
$requiredVisibilityFields = [
|
|
'is_enabled' => 'boolean',
|
|
'show_on_desktop' => 'boolean',
|
|
'show_on_mobile' => 'boolean',
|
|
];
|
|
|
|
foreach ($requiredVisibilityFields as $fieldName => $expectedType) {
|
|
if (!isset($visibilityFields[$fieldName])) {
|
|
$result->addError("Campo obligatorio de visibilidad faltante: 'visibility.{$fieldName}'");
|
|
} elseif (isset($visibilityFields[$fieldName]['type']) && $visibilityFields[$fieldName]['type'] !== $expectedType) {
|
|
$result->addError(
|
|
"Campo 'visibility.{$fieldName}' debe ser type '{$expectedType}' " .
|
|
"(encontrado: '{$visibilityFields[$fieldName]['type']}')"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function countTotalFields(array $schema): int
|
|
{
|
|
$count = 0;
|
|
|
|
if (isset($schema['groups'])) {
|
|
foreach ($schema['groups'] as $group) {
|
|
if (isset($group['fields'])) {
|
|
$count += count($group['fields']);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
public function getPhaseNumber(): int|string
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
public function getPhaseDescription(): string
|
|
{
|
|
return 'Schema JSON';
|
|
}
|
|
}
|