fix(structure): Correct case-sensitivity for Linux compatibility

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>
This commit is contained in:
FrankZamora
2025-11-26 22:53:34 -06:00
parent a2548ab5c2
commit 90863cd8f5
92 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
namespace ROITheme\Shared\Infrastructure\Validators;
/**
* Validador de Fase 04: FormBuilders (UI Admin)
*
* Valida que:
* - FormBuilder existe en ubicación correcta (admin/)
* - Namespace y clase correctos
* - Inyecta AdminDashboardRenderer
* - Tiene método buildForm()
* - Es modular (métodos privados build*)
* - Usa AdminDashboardRenderer correctamente
* - Usa Bootstrap 5
* - Escaping correcto
* - NO accede directamente a BD
*/
final class Phase04Validator implements PhaseValidatorInterface
{
public function validate(string $componentName, string $themePath): ValidationResult
{
$result = new ValidationResult();
$result->addInfo("Validando FormBuilder para: {$componentName}");
// Buscar FormBuilder en Admin/
$formBuilderPath = $this->findFormBuilderPath($componentName, $themePath);
if ($formBuilderPath === null) {
$result->addError("FormBuilder no encontrado en Admin/");
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
$result->addInfo("Ubicación esperada: Admin/{$pascalCaseName}/Infrastructure/Ui/*FormBuilder.php");
return $result;
}
$result->addInfo("Archivo encontrado: {$formBuilderPath}");
// Leer contenido
$content = file_get_contents($formBuilderPath);
// Validaciones del FormBuilder
$this->validateNamespaceAndClass($content, $componentName, $result);
$this->validateRendererInjection($content, $result);
$this->validateBuildFormMethod($content, $result);
$this->validateModularity($content, $result);
$this->validateRendererUsage($content, $result);
$this->validateBootstrapCompliance($content, $result);
$this->validateEscaping($content, $result);
$this->validateNoDirectDatabaseAccess($content, $result);
// Validaciones de integración con Admin Panel (CRÍTICAS)
$this->validateComponentRegistration($componentName, $themePath, $result);
$this->validateAjaxFieldMapping($componentName, $themePath, $content, $result);
$this->validateResetButton($componentName, $content, $result);
// Estadísticas
$fileSize = filesize($formBuilderPath);
$lineCount = substr_count($content, "\n") + 1;
$buildMethodsCount = $this->countBuildMethods($content);
$result->setStat('Archivo', basename($formBuilderPath));
$result->setStat('Líneas', $lineCount);
$result->setStat('Métodos build*', $buildMethodsCount);
$result->setStat('Tamaño', $fileSize . ' bytes');
if ($lineCount > 500) {
$result->addWarning("Archivo excede 500 líneas ({$lineCount}) - considerar dividir en múltiples FormBuilders.");
} elseif ($lineCount > 300) {
$result->addInfo(" Archivo tiene {$lineCount} líneas (recomendado: <300, aceptable: <500)");
}
if ($buildMethodsCount < 2) {
$result->addWarning("Solo {$buildMethodsCount} método(s) build*. FormBuilders modulares deben tener al menos 2-3 métodos privados.");
}
return $result;
}
private function findFormBuilderPath(string $componentName, string $themePath): ?string
{
// Convertir kebab-case a PascalCase (para carpetas y archivos)
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
// Buscar en Admin/ con PascalCase (PSR-4 standard)
$adminPath = $themePath . '/Admin/' . $pascalCaseName . '/Infrastructure/Ui/' . $pascalCaseName . 'FormBuilder.php';
if (file_exists($adminPath)) {
return $adminPath;
}
return null;
}
private function validateNamespaceAndClass(string $content, string $componentName, ValidationResult $result): void
{
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
// Validar namespace (Ui en PascalCase, primera letra mayúscula)
if (!preg_match('/namespace\s+ROITheme\\\\Admin\\\\' . preg_quote($pascalCaseName, '/') . '\\\\Infrastructure\\\\Ui;/', $content)) {
$result->addError("Namespace incorrecto. Debe ser: ROITheme\\Admin\\{$pascalCaseName}\\Infrastructure\\Ui");
}
// Validar clase final
if (!preg_match('/final\s+class\s+' . preg_quote($pascalCaseName, '/') . 'FormBuilder/', $content)) {
$result->addError("Clase debe ser: final class {$pascalCaseName}FormBuilder");
}
}
private function validateRendererInjection(string $content, ValidationResult $result): void
{
// Verificar que constructor recibe AdminDashboardRenderer
if (!preg_match('/public\s+function\s+__construct\([^)]*AdminDashboardRenderer\s+\$renderer/s', $content)) {
$result->addError("Constructor NO inyecta AdminDashboardRenderer");
}
// Verificar propiedad privada
if (!preg_match('/private\s+AdminDashboardRenderer\s+\$renderer/', $content)) {
$result->addError("Falta propiedad: private AdminDashboardRenderer \$renderer");
}
}
private function validateBuildFormMethod(string $content, ValidationResult $result): void
{
// Verificar método buildForm existe
if (!preg_match('/public\s+function\s+buildForm\s*\(\s*string\s+\$componentId\s*\)\s*:\s*string/', $content)) {
$result->addError("Falta método: public function buildForm(string \$componentId): string");
}
}
private function validateModularity(string $content, ValidationResult $result): void
{
// Contar métodos privados build*
$buildMethodsCount = $this->countBuildMethods($content);
if ($buildMethodsCount === 0) {
$result->addError("FormBuilder NO tiene métodos privados build* - Debe ser modular");
} elseif ($buildMethodsCount < 2) {
$result->addWarning("Solo {$buildMethodsCount} método build* - Considerar más modularidad");
} else {
$result->addInfo("✓ Modularidad: {$buildMethodsCount} métodos build* detectados");
}
}
private function countBuildMethods(string $content): int
{
preg_match_all('/private\s+function\s+build[A-Z][a-zA-Z]*\s*\(/', $content, $matches);
return count($matches[0]);
}
private function validateRendererUsage(string $content, ValidationResult $result): void
{
// Verificar que usa $this->renderer->getFieldValue()
if (!preg_match('/\$this->renderer->getFieldValue\s*\(/', $content)) {
$result->addWarning("No se detectó uso de \$this->renderer->getFieldValue() - Verificar que obtiene datos del renderer");
}
}
private function validateBootstrapCompliance(string $content, ValidationResult $result): void
{
$bootstrapClasses = [
'form-control' => false,
'form-select' => false,
'form-check' => false,
'card' => false,
'row' => false,
];
foreach ($bootstrapClasses as $class => $found) {
// Buscar clase dentro de atributos class="..." o class='...'
// Acepta: class="row", class="row g-3", class="form-check form-switch", etc.
if (preg_match('/class=["\'][^"\']*\b' . preg_quote($class, '/') . '\b[^"\']*["\']/', $content)) {
$bootstrapClasses[$class] = true;
}
}
$usedClasses = array_keys(array_filter($bootstrapClasses));
if (count($usedClasses) === 0) {
$result->addWarning("No se detectaron clases Bootstrap 5 - Verificar que usa Bootstrap");
} else {
$result->addInfo("✓ Bootstrap 5: Usa " . implode(', ', $usedClasses));
}
}
private function validateEscaping(string $content, ValidationResult $result): void
{
$escapingUsed = false;
if (preg_match('/esc_attr\s*\(/', $content)) {
$escapingUsed = true;
}
if (preg_match('/esc_html\s*\(/', $content)) {
$escapingUsed = true;
}
if (!$escapingUsed) {
$result->addWarning("No se detectó uso de esc_attr() o esc_html() - Verificar escaping de valores");
}
}
private function validateNoDirectDatabaseAccess(string $content, ValidationResult $result): void
{
// Verificar que NO usa global $wpdb
if (preg_match('/global\s+\$wpdb/', $content)) {
$result->addError("FormBuilder usa 'global \$wpdb' - Acceso a BD debe estar en Repository o AdminDashboardRenderer");
}
// Verificar que NO usa funciones directas de BD
$dbFunctions = ['get_option', 'update_option', 'get_post_meta', 'update_post_meta'];
foreach ($dbFunctions as $func) {
if (preg_match('/\b' . $func . '\s*\(/', $content)) {
$result->addError("FormBuilder usa '{$func}()' - Debe usar AdminDashboardRenderer, NO acceder BD directamente");
}
}
}
/**
* Valida que el componente está registrado en AdminDashboardRenderer::getComponents()
*/
private function validateComponentRegistration(string $componentName, string $themePath, ValidationResult $result): void
{
$dashboardRendererPath = $themePath . '/Admin/Infrastructure/Ui/AdminDashboardRenderer.php';
if (!file_exists($dashboardRendererPath)) {
$result->addWarning("No se encontró AdminDashboardRenderer.php - No se puede validar registro del componente");
return;
}
$dashboardContent = file_get_contents($dashboardRendererPath);
// Buscar si el componente está registrado en getComponents()
// Busca patrones como: 'cta-lets-talk' => [ o "cta-lets-talk" => [
$pattern = '/[\'"]' . preg_quote($componentName, '/') . '[\'"]\s*=>\s*\[/';
if (!preg_match($pattern, $dashboardContent)) {
$result->addError("Componente NO registrado en AdminDashboardRenderer::getComponents()");
$result->addInfo("Agregar en Admin/Infrastructure/Ui/AdminDashboardRenderer.php método getComponents():");
$result->addInfo("'{$componentName}' => ['id' => '{$componentName}', 'label' => '...', 'icon' => 'bi-...'],");
} else {
$result->addInfo("✓ Componente registrado en AdminDashboardRenderer::getComponents()");
}
}
/**
* Valida que existe mapeo AJAX para los campos del FormBuilder
*/
private function validateAjaxFieldMapping(string $componentName, string $themePath, string $formBuilderContent, ValidationResult $result): void
{
$ajaxHandlerPath = $themePath . '/Admin/Infrastructure/Api/Wordpress/AdminAjaxHandler.php';
if (!file_exists($ajaxHandlerPath)) {
$result->addWarning("No se encontró AdminAjaxHandler.php - No se puede validar mapeo AJAX");
return;
}
$ajaxContent = file_get_contents($ajaxHandlerPath);
// Extraer todos los IDs de campos del FormBuilder (id="...")
preg_match_all('/id=["\']([a-zA-Z0-9_]+)["\']/', $formBuilderContent, $fieldMatches);
$formFieldIds = array_unique($fieldMatches[1]);
// Filtrar solo los IDs que corresponden a inputs (no contenedores)
// Los IDs de inputs generalmente tienen un patrón como componentNameFieldName
$fieldPrefix = $this->getFieldPrefix($componentName);
$inputFieldIds = array_filter($formFieldIds, function($id) use ($fieldPrefix) {
// Solo IDs que empiezan con el prefijo del componente
return stripos($id, $fieldPrefix) === 0;
});
if (empty($inputFieldIds)) {
$result->addWarning("No se detectaron campos con prefijo '{$fieldPrefix}' en FormBuilder");
return;
}
// Verificar que cada campo tiene mapeo en getFieldMapping()
$unmappedFields = [];
foreach ($inputFieldIds as $fieldId) {
// Buscar el ID en el contenido del AjaxHandler
if (!preg_match('/[\'"]' . preg_quote($fieldId, '/') . '[\'"]\s*=>/', $ajaxContent)) {
$unmappedFields[] = $fieldId;
}
}
$totalFields = count($inputFieldIds);
$mappedFields = $totalFields - count($unmappedFields);
if (count($unmappedFields) > 0) {
$result->addError("Mapeo AJAX incompleto: {$mappedFields}/{$totalFields} campos mapeados");
$result->addInfo("Campos sin mapeo en AdminAjaxHandler::getFieldMapping():");
foreach (array_slice($unmappedFields, 0, 5) as $field) {
$result->addInfo(" - {$field}");
}
if (count($unmappedFields) > 5) {
$result->addInfo(" ... y " . (count($unmappedFields) - 5) . " más");
}
} else {
$result->addInfo("✓ Mapeo AJAX completo: {$totalFields}/{$totalFields} campos mapeados");
}
}
/**
* Valida que el botón "Restaurar valores por defecto" sigue el patrón correcto
*
* El patrón correcto es:
* - class="btn-reset-defaults"
* - data-component="nombre-componente"
*
* NO usar id="reset*Defaults" (patrón antiguo hardcodeado)
*/
private function validateResetButton(string $componentName, string $content, ValidationResult $result): void
{
// Verificar patrón correcto: class="btn-reset-defaults" data-component="..."
$correctPattern = '/class=["\'][^"\']*btn-reset-defaults[^"\']*["\'][^>]*data-component=["\']' . preg_quote($componentName, '/') . '["\']/';
$correctPatternAlt = '/data-component=["\']' . preg_quote($componentName, '/') . '["\'][^>]*class=["\'][^"\']*btn-reset-defaults[^"\']*["\']/';
$hasCorrectPattern = preg_match($correctPattern, $content) || preg_match($correctPatternAlt, $content);
// Verificar patrón incorrecto: id="reset*Defaults" (hardcodeado)
$incorrectPattern = '/id=["\']reset[A-Za-z]+Defaults["\']/';
$hasIncorrectPattern = preg_match($incorrectPattern, $content);
if ($hasIncorrectPattern && !$hasCorrectPattern) {
$result->addError("Botón reset usa patrón hardcodeado (id=\"reset*Defaults\")");
$result->addInfo("Cambiar a: class=\"btn-reset-defaults\" data-component=\"{$componentName}\"");
} elseif (!$hasCorrectPattern) {
$result->addWarning("No se detectó botón 'Restaurar valores por defecto' con patrón dinámico");
$result->addInfo("Agregar: <button class=\"btn-reset-defaults\" data-component=\"{$componentName}\">...</button>");
} else {
$result->addInfo("✓ Botón reset con patrón dinámico correcto (data-component=\"{$componentName}\")");
}
}
/**
* Obtiene el prefijo de campos para un componente
* Permite prefijos personalizados más cortos para componentes con nombres largos
*/
private function getFieldPrefix(string $componentName): string
{
// Mapeo de prefijos personalizados (más cortos/legibles)
$customPrefixes = [
'top-notification-bar' => 'topBar',
];
if (isset($customPrefixes[$componentName])) {
return $customPrefixes[$componentName];
}
// Por defecto: convertir kebab-case a camelCase
$pascalCaseName = str_replace('-', '', ucwords($componentName, '-'));
return lcfirst($pascalCaseName);
}
public function getPhaseNumber(): int|string
{
return 4;
}
public function getPhaseDescription(): string
{
return 'FormBuilders (UI Admin)';
}
}