Files
roi-theme/_openspec/specs/estandares-codigo.md
FrankZamora 0f6387ab46 refactor: reorganizar openspec y planificacion con spec recaptcha
- renombrar openspec/ a _openspec/ (carpeta auxiliar)
- mover specs de features a changes/
- crear specs base: arquitectura-limpia, estandares-codigo, nomenclatura
- migrar _planificacion/ con design-system y roi-theme-template
- agregar especificacion recaptcha anti-spam (proposal, tasks, spec)
- corregir rutas y referencias en todas las specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 15:30:45 -06:00

36 KiB

Especificacion de Estandares de Codigo

Purpose

Define los principios SOLID, estandares de POO (Programacion Orientada a Objetos) y estandares generales de codigo que DEBEN seguirse en el desarrollo de ROITheme.

Requirements

Requirement: Principio de Responsabilidad Unica (SRP)

Each class MUST have exactly one reason to change and one responsibility.

Scenario: Responsabilidad de clase Use Case

  • WHEN se crea una clase Use Case
  • THEN DEBE manejar exactamente UNA operacion (Save, Get, Delete, etc.)
  • AND NO DEBE combinar multiples operaciones en una clase

Scenario: Validacion de tamano de clase

  • WHEN se crea un archivo de clase
  • THEN DEBERIA tener menos de 300 lineas
  • AND DEBERIA tener maximo 3-5 metodos privados
  • AND el nombre de la clase DEBE describir su unica responsabilidad

Scenario: Violacion de SRP

  • WHEN una clase contiene save(), get(), delete(), validate(), sendEmail()
  • THEN DEBE dividirse en clases Use Case separadas
  • AND cada clase maneja solo una operacion

Requirement: Principio Abierto/Cerrado (OCP)

Classes MUST be open for extension but closed for modification.

Scenario: Agregar nuevo tipo de componente

  • WHEN se necesita un nuevo tipo de componente
  • THEN se DEBE crear una nueva subclase
  • AND la clase BaseComponent existente NO DEBE modificarse
  • AND NO se DEBERIAN agregar cadenas if/elseif para nuevos tipos

Scenario: Extender funcionalidad base

  • GIVEN que existe una clase abstracta BaseComponent
  • WHEN se necesita comportamiento especializado
  • THEN se DEBE usar herencia para extender
  • AND la clase base DEBE permanecer sin cambios

Requirement: Principio de Sustitucion de Liskov (LSP)

Subclasses MUST be substitutable for their base classes without breaking functionality.

Scenario: Uso polimorfico

  • GIVEN una funcion que acepta parametro BaseComponent
  • WHEN cualquier subclase es pasada
  • THEN la funcion DEBE funcionar correctamente
  • AND NO se DEBERIAN lanzar excepciones inesperadas

Scenario: Cumplimiento de contrato

  • WHEN una subclase sobrescribe un metodo padre
  • THEN DEBE respetar el contrato del metodo original
  • AND las precondiciones NO DEBEN ser mas restrictivas
  • AND las postcondiciones NO DEBEN ser mas permisivas

Requirement: Principio de Segregacion de Interfaces (ISP)

Interfaces MUST be small and specific, not large and general.

Scenario: Validacion de tamano de interface

  • WHEN se define una interface
  • THEN DEBE tener maximo 3-5 metodos
  • AND cada metodo DEBE relacionarse con la misma capacidad

Scenario: Evitar interfaces gordas

  • WHEN existen multiples capacidades no relacionadas
  • THEN se DEBEN crear interfaces separadas
  • AND las clases implementan solo las interfaces que usan
  • AND NO se permiten metodos dummy "No implementado"

Scenario: Diseno correcto de interface

  • WHEN se necesita funcionalidad de cache
  • THEN se DEBE usar CacheInterface con get(), set(), delete()
  • AND ValidatorInterface con validate() es separada

Requirement: Principio de Inversion de Dependencias (DIP)

High-level modules MUST depend on abstractions, not concrete implementations.

Scenario: Inyeccion por constructor con interfaces

  • WHEN una clase necesita dependencias
  • THEN el constructor DEBE recibir interfaces, NO clases concretas
  • AND NO debe haber new ClaseConcreta() dentro del cuerpo de la clase

Scenario: Cableado del Contenedor DI

  • WHEN se necesitan implementaciones concretas
  • THEN el DIContainer DEBE manejar el cableado
  • AND las clases permanecen desacopladas de las implementaciones

Scenario: Dependencia incorrecta

  • WHEN el constructor hace this->repo = new WordPressNavbarRepository()
  • THEN esto DEBE refactorizarse para recibir NavbarRepositoryInterface
  • AND el DIContainer proporciona la implementacion concreta

Requirement: Encapsulacion de Propiedades

Class properties MUST be encapsulated with controlled access.

Scenario: Visibilidad de propiedades

  • WHEN se define una propiedad de clase
  • THEN DEBE ser private o protected
  • AND el acceso DEBE ser via metodos getter
  • AND la mutacion DEBE ser via metodos setter o metodos de negocio

Scenario: Encapsulacion de Value Object

  • GIVEN un ValueObject como ComponentName
  • WHEN es construido
  • THEN la validacion DEBE ocurrir en el constructor
  • AND el valor DEBE ser inmutable despues de la construccion
  • AND los detalles internos NO DEBEN exponerse

Requirement: Guias de Herencia

Inheritance MUST be used appropriately with limited depth.

Scenario: Limite de profundidad de herencia

  • WHEN se usa herencia
  • THEN la profundidad maxima DEBE ser 2-3 niveles
  • AND las cadenas de herencia profundas DEBEN evitarse

Scenario: Comportamiento comun en clase base

  • WHEN multiples clases comparten comportamiento comun
  • THEN se DEBERIA crear una clase base abstracta
  • AND las subclases especializan con comportamiento adicional

Requirement: Polimorfismo Correcto

Methods MUST accept base types or interfaces to enable polymorphism.

Scenario: Tipos de parametros de metodo

  • WHEN un metodo acepta parametro de componente
  • THEN el type hint DEBERIA ser BaseComponent o ComponentInterface
  • AND cualquier subclase/implementacion DEBE funcionar correctamente

Scenario: Polimorfismo de repository

  • WHEN un Use Case usa un repository
  • THEN DEBE aceptar RepositoryInterface
  • AND WordPressRepository y MockRepository funcionan transparentemente

Requirement: Estandares PHP Estrictos

All PHP code MUST follow strict type safety and naming conventions.

Scenario: Declaracion de tipos estrictos

  • WHEN se crea un archivo PHP
  • THEN DEBE comenzar con declare(strict_types=1)
  • AND los tipos de retorno DEBEN declararse
  • AND los tipos de parametros DEBEN declararse

Scenario: Convencion de namespace

  • WHEN se crea una clase
  • THEN el namespace DEBE seguir ROITheme[Contexto][Componente][Capa]
  • AND DEBE soportar autoloading PSR-4

Scenario: Declaracion de clase

  • WHEN se crea una clase
  • THEN DEBERIA ser final por defecto
  • AND solo hacerla no-final cuando se pretende herencia
  • AND el nombre de clase DEBE ser PascalCase

Requirement: Modularidad del Codigo

Code MUST be organized into independent and cohesive modules.

Scenario: Independencia de modulos

  • WHEN se crea un modulo
  • THEN DEBE ser autocontenido
  • AND NO DEBE depender de otros modulos (solo de Shared/)
  • AND eliminarlo NO DEBE romper otros modulos

Scenario: Alta cohesion

  • WHEN el codigo se coloca en un modulo
  • THEN todo el codigo DEBE relacionarse con el proposito de ese modulo
  • AND el codigo no relacionado DEBE estar en Shared/ u otro modulo

Scenario: Bajo acoplamiento

  • WHEN los modulos interactuan
  • THEN DEBEN comunicarse a traves de interfaces de Shared/
  • AND las dependencias directas entre modulos estan prohibidas

Requirement: DRY - No Te Repitas

Code duplication MUST be eliminated through appropriate abstraction.

Scenario: Ubicacion de codigo compartido

  • WHEN el codigo es usado por multiples modulos
  • THEN DEBE moverse al nivel apropiado de Shared/
  • AND los modulos DEBEN importar de Shared/

Scenario: Deteccion de duplicacion

  • WHEN existe codigo similar en 2+ lugares
  • THEN DEBE refactorizarse a Shared/
  • AND las ubicaciones originales importan de Shared/

Requirement: KISS - Mantenlo Simple

Solutions MUST be simple and avoid over-engineering.

Scenario: Uso de patrones

  • WHEN se considera un patron de diseno
  • THEN DEBE resolver un problema real
  • AND se DEBEN preferir soluciones mas simples
  • AND la abstraccion excesiva DEBE evitarse

Scenario: Claridad del codigo

  • WHEN se escribe codigo
  • THEN DEBERIA ser auto-documentado
  • AND los comentarios DEBERIAN ser innecesarios para entender
  • AND la logica compleja DEBERIA extraerse a metodos bien nombrados

Requirement: Separacion de Responsabilidades por Capa

Each layer MUST have distinct responsibilities.

Scenario: Responsabilidades por capa

  • WHEN se escribe codigo
  • THEN Domain contiene logica de negocio
  • AND Application contiene orquestacion
  • AND Infrastructure contiene implementacion tecnica
  • AND UI contiene solo presentacion

Scenario: Validacion de responsabilidades cruzadas

  • WHEN se valida ubicacion de codigo
  • THEN SQL NO DEBE estar en Domain/Application
  • AND HTML NO DEBE estar en Domain/Application
  • AND logica de negocio NO DEBE estar en Infrastructure

Requirement: Limites de Tamano de Archivo

Files MUST be kept small and focused.

Scenario: Tamano de archivo de clase

  • WHEN se crea un archivo de clase
  • THEN DEBERIA tener menos de 300 lineas
  • AND si es mas grande, DEBERIA dividirse en clases mas pequenas

Scenario: Tamano de metodo

  • WHEN se escribe un metodo
  • THEN DEBERIA tener menos de 30 lineas
  • AND metodos complejos DEBERIAN extraerse a metodos auxiliares

Requirement: Convenciones de Nomenclatura

Names MUST be clear, descriptive, and follow conventions.

Scenario: Nomenclatura de clases

  • WHEN se nombra una clase
  • THEN el nombre DEBE describir su unica responsabilidad
  • AND las clases Use Case DEBEN nombrarse [Accion][Entidad]UseCase
  • AND las clases Repository DEBEN nombrarse [Implementacion][Entidad]Repository

Scenario: Nomenclatura de metodos

  • WHEN se nombra un metodo
  • THEN DEBE describir lo que hace el metodo
  • AND DEBERIA comenzar con un verbo
  • AND metodos booleanos DEBERIAN comenzar con is/has/can

Scenario: Nomenclatura de variables

  • WHEN se nombra una variable
  • THEN DEBE ser descriptiva
  • AND las abreviaturas DEBEN evitarse
  • AND nombres de una letra solo para contadores de bucle

Requirement: Validacion Pre-Commit

Code MUST pass validation before commit.

Scenario: Verificacion de cumplimiento SOLID

  • WHEN el codigo esta listo para commit
  • THEN SRP cada clase tiene una responsabilidad
  • AND OCP nuevas caracteristicas via extension, no modificacion
  • AND LSP las subclases son sustituibles
  • AND ISP las interfaces son pequenas 3-5 metodos
  • AND DIP el constructor recibe interfaces

Scenario: Verificacion de cumplimiento POO

  • WHEN el codigo esta listo para commit
  • THEN las propiedades son private/protected
  • AND la profundidad de herencia es max 2-3 niveles
  • AND el polimorfismo esta implementado correctamente
  • AND la abstraccion oculta complejidad

Scenario: Verificacion de calidad

  • WHEN el codigo esta listo para commit
  • THEN los archivos tienen menos de 300 lineas
  • AND los nombres son claros y descriptivos
  • AND no existe duplicacion de codigo
  • AND no hay sobre-ingenieria presente

Requirement: Escaping Obligatorio en Output HTML

All HTML output MUST use WordPress escaping functions for security.

Scenario: Escaping de textos

  • WHEN se genera output de texto en HTML
  • THEN DEBE usar esc_html() para contenido de texto

Scenario: Escaping de atributos

  • WHEN se genera un atributo HTML
  • THEN DEBE usar esc_attr() para valores de atributos

Scenario: Escaping de URLs

  • WHEN se genera una URL en href o src
  • THEN DEBE usar esc_url() para URLs

Scenario: Escaping de textareas

  • WHEN se genera contenido para textarea
  • THEN DEBE usar esc_textarea() para el valor

Scenario: Prohibicion de output sin escaping

  • WHEN se revisa codigo de Renderer o FormBuilder
  • THEN NO DEBE existir echo o print de variables sin escaping
  • AND NO DEBE existir interpolacion directa de variables en HTML

Ejemplos de Codigo PHP para SOLID

REFERENCIA: Para nomenclatura de clases, ver _openspec/specs/nomenclatura.md REFERENCIA: Para ubicacion de archivos, ver _openspec/specs/arquitectura-limpia.md

Ejemplo SRP: Single Responsibility Principle

<?php
declare(strict_types=1);

// INCORRECTO - Multiples responsabilidades en una clase
final class ContactFormManager
{
    public function render(): string { /* renderiza HTML */ }
    public function validate(array $data): bool { /* valida datos */ }
    public function save(array $data): void { /* guarda en BD */ }
    public function sendEmail(array $data): void { /* envia email */ }
    public function generateCSS(): string { /* genera CSS */ }
}

// CORRECTO - Una responsabilidad por clase
// Infrastructure/Ui/ContactFormRenderer.php
final class ContactFormRenderer implements RendererInterface
{
    public function render(Component $component): string { /* solo renderiza */ }
    public function supports(string $componentType): bool { /* solo identifica */ }
}

// Domain/Validators/ContactFormValidator.php
final class ContactFormValidator
{
    public function validate(array $data): ValidationResult { /* solo valida */ }
}

// Infrastructure/Persistence/ContactFormRepository.php
final class ContactFormRepository implements ContactFormRepositoryInterface
{
    public function save(ContactFormData $data): void { /* solo persiste */ }
}

// Infrastructure/Services/EmailService.php
final class EmailService implements EmailServiceInterface
{
    public function send(Email $email): void { /* solo envia */ }
}

Ejemplo OCP: Open/Closed Principle

<?php
declare(strict_types=1);

// INCORRECTO - Modificar clase existente para agregar tipos
final class ComponentRenderer
{
    public function render(string $type, array $data): string
    {
        if ($type === 'contact-form') {
            return $this->renderContactForm($data);
        } elseif ($type === 'newsletter') {
            return $this->renderNewsletter($data);
        } elseif ($type === 'featured-image') {  // Cada nuevo tipo = modificacion
            return $this->renderFeaturedImage($data);
        }
        return '';
    }
}

// CORRECTO - Extender sin modificar (usando interfaces)
// Domain/Contracts/RendererInterface.php
use ROITheme\Shared\Domain\Entities\Component;

interface RendererInterface
{
    public function render(Component $component): string;
    public function supports(string $componentType): bool;
}

// ContactFormRenderer.php - implementa interface
final class ContactFormRenderer implements RendererInterface
{
    public function supports(string $componentType): bool
    {
        return $componentType === 'contact-form';
    }

    public function render(Component $component): string { /* ... */ }
}

// NewsletterRenderer.php - nueva clase, sin modificar existentes
final class NewsletterRenderer implements RendererInterface
{
    public function supports(string $componentType): bool
    {
        return $componentType === 'newsletter';
    }

    public function render(Component $component): string { /* ... */ }
}

Ejemplo LSP: Liskov Substitution Principle

<?php
declare(strict_types=1);

// INCORRECTO - Subclase rompe contrato de la base
abstract class BaseValidator
{
    abstract public function validate(array $data): bool;
}

final class EmailValidator extends BaseValidator
{
    public function validate(array $data): bool
    {
        // VIOLA LSP: lanza excepcion no esperada
        if (!isset($data['email'])) {
            throw new \InvalidArgumentException('Email required');
        }
        return filter_var($data['email'], FILTER_VALIDATE_EMAIL) !== false;
    }
}

// CORRECTO - Subclase respeta contrato
abstract class BaseValidator
{
    abstract public function validate(array $data): ValidationResult;
}

final class EmailValidator extends BaseValidator
{
    public function validate(array $data): ValidationResult
    {
        // Respeta contrato: siempre retorna ValidationResult
        if (!isset($data['email'])) {
            return ValidationResult::failure('Email es requerido');
        }

        if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            return ValidationResult::failure('Email invalido');
        }

        return ValidationResult::success();
    }
}

// Uso polimorfico - funciona con cualquier validator
function processValidation(BaseValidator $validator, array $data): void
{
    $result = $validator->validate($data);  // Siempre funciona
    if (!$result->isValid()) {
        // manejar error
    }
}

Ejemplo ISP: Interface Segregation Principle

<?php
declare(strict_types=1);

// INCORRECTO - Interface gorda con metodos no relacionados
interface ComponentInterface
{
    public function render(Component $component): string;
    public function validate(array $data): bool;
    public function save(array $data): void;
    public function delete(int $id): void;
    public function export(): string;
    public function import(string $data): void;
    public function generateCSS(array $data): string;
    public function getSchema(): array;
}

// CORRECTO - Interfaces pequenas y especificas
use ROITheme\Shared\Domain\Entities\Component;

interface RendererInterface
{
    public function render(Component $component): string;
    public function supports(string $componentType): bool;
}

interface ValidatorInterface
{
    public function validate(array $data): ValidationResult;
}

interface CSSGeneratorInterface
{
    public function generate(array $styles): string;
    public function generateInlineStyles(array $styles): string;
}

interface RepositoryInterface
{
    public function save(array $data): void;
    public function findById(int $id): ?array;
    public function delete(int $id): void;
}

// Clase implementa SOLO las interfaces que necesita
final class ContactFormRenderer implements RendererInterface
{
    public function __construct(
        private CSSGeneratorInterface $cssGenerator  // Solo lo que necesita
    ) {}

    public function render(Component $component): string { /* ... */ }
    public function supports(string $componentType): bool { /* ... */ }
}

Ejemplo DIP: Dependency Inversion Principle

<?php
declare(strict_types=1);

// INCORRECTO - Dependencia directa de clase concreta
final class ContactFormRenderer
{
    private WordPressCSSGenerator $cssGenerator;

    public function __construct()
    {
        // Acoplamiento directo - imposible de testear
        $this->cssGenerator = new WordPressCSSGenerator();
    }
}

// CORRECTO - Dependencia de abstraccion (interface)
// Domain/Contracts/CSSGeneratorInterface.php
interface CSSGeneratorInterface
{
    public function generate(array $styles): string;
}

// Infrastructure/Ui/ContactFormRenderer.php
final class ContactFormRenderer implements RendererInterface
{
    public function __construct(
        private CSSGeneratorInterface $cssGenerator  // Interface, no clase concreta
    ) {}

    public function render(Component $component): string
    {
        $data = $component->getData();
        $css = $this->cssGenerator->generate($data['styles'] ?? []);
        // ...
    }
}

// Infrastructure/Services/WordPressCSSGenerator.php (implementacion)
final class WordPressCSSGenerator implements CSSGeneratorInterface
{
    public function generate(array $styles): string { /* ... */ }
}

// El DIContainer conecta la interface con la implementacion
// functions.php o bootstrap
$container->bind(CSSGeneratorInterface::class, WordPressCSSGenerator::class);

Manejo de Errores WordPress

Cuando usar wp_die()

<?php
declare(strict_types=1);

// wp_die() - Para errores fatales que deben terminar la ejecucion
// Casos de uso: errores de permisos, nonces invalidos, recursos no encontrados

// AJAX handler con error fatal
public function handleAjaxRequest(): void
{
    // Verificar nonce - error fatal si falla
    if (!wp_verify_nonce($_POST['nonce'] ?? '', 'roi_theme_action')) {
        wp_die(
            esc_html__('Security check failed', 'roi-theme'),
            esc_html__('Error', 'roi-theme'),
            ['response' => 403]
        );
    }

    // Verificar permisos - error fatal si falla
    if (!current_user_can('manage_options')) {
        wp_die(
            esc_html__('Permission denied', 'roi-theme'),
            esc_html__('Error', 'roi-theme'),
            ['response' => 403]
        );
    }
}

Cuando usar WP_Error

<?php
declare(strict_types=1);

// WP_Error - Para errores recuperables que deben comunicarse al llamador
// Casos de uso: validacion fallida, operaciones que pueden fallar

public function saveComponentSettings(array $data): \WP_Error|array
{
    // Validacion - retorna WP_Error si falla
    if (empty($data['component_name'])) {
        return new \WP_Error(
            'missing_component_name',
            __('Component name is required', 'roi-theme'),
            ['status' => 400]
        );
    }

    // Operacion BD - puede fallar
    $result = $this->repository->save($data);

    if ($result === false) {
        return new \WP_Error(
            'save_failed',
            __('Failed to save component settings', 'roi-theme'),
            ['status' => 500]
        );
    }

    return $data;  // Exito
}

// Uso del WP_Error
$result = $service->saveComponentSettings($data);

if (is_wp_error($result)) {
    // Manejar error
    $errorCode = $result->get_error_code();
    $errorMessage = $result->get_error_message();
    // ...
}

Cuando usar Excepciones

<?php
declare(strict_types=1);

// Excepciones - Para errores de programacion o situaciones excepcionales
// Casos de uso: Domain layer, argumentos invalidos, estados imposibles

// Domain/Exceptions/InvalidComponentNameException.php
final class InvalidComponentNameException extends \DomainException
{
    public static function empty(): self
    {
        return new self('Component name cannot be empty');
    }

    public static function invalidFormat(string $name): self
    {
        return new self(
            sprintf('Component name "%s" must be in kebab-case format', $name)
        );
    }
}

// Uso en Domain layer
final class ComponentName
{
    private string $value;

    public function __construct(string $value)
    {
        if (empty($value)) {
            throw InvalidComponentNameException::empty();
        }

        if (!preg_match('/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/', $value)) {
            throw InvalidComponentNameException::invalidFormat($value);
        }

        $this->value = $value;
    }
}

// Infrastructure captura y convierte a WP_Error si es necesario
try {
    $componentName = new ComponentName($input);
} catch (InvalidComponentNameException $e) {
    return new \WP_Error('invalid_component', $e->getMessage());
}

Sanitizacion y Validacion

Tabla de Funciones de Sanitizacion

Tipo de Dato Funcion Ejemplo
Texto simple sanitize_text_field() Nombres, titulos
Email sanitize_email() Direcciones de correo
URL esc_url_raw() URLs para BD
Entero positivo absint() IDs, cantidades
Entero (puede ser negativo) intval() Posiciones, offsets
HTML seguro wp_kses_post() Contenido con formato
Nombre de archivo sanitize_file_name() Uploads
Key/slug sanitize_key() component_name
Clase CSS sanitize_html_class() Clases dinamicas
Textarea sanitize_textarea_field() Textos multilinea
Hexadecimal (color) sanitize_hex_color() Colores

Ejemplos de Sanitizacion

<?php
declare(strict_types=1);

// AJAX Handler con sanitizacion completa
public function handleFormSubmission(): void
{
    // Verificar nonce primero
    check_ajax_referer('roi_theme_form', 'nonce');

    // Sanitizar cada campo segun su tipo
    $data = [
        'name'    => sanitize_text_field($_POST['name'] ?? ''),
        'email'   => sanitize_email($_POST['email'] ?? ''),
        'phone'   => sanitize_text_field($_POST['phone'] ?? ''),
        'message' => sanitize_textarea_field($_POST['message'] ?? ''),
        'url'     => esc_url_raw($_POST['url'] ?? ''),
        'post_id' => absint($_POST['post_id'] ?? 0),
    ];

    // Validar despues de sanitizar
    if (empty($data['name']) || empty($data['email'])) {
        wp_send_json_error(['message' => 'Name and email are required']);
        return;
    }

    if (!is_email($data['email'])) {
        wp_send_json_error(['message' => 'Invalid email address']);
        return;
    }

    // Procesar datos sanitizados y validados
    $this->processForm($data);

    wp_send_json_success(['message' => 'Form submitted successfully']);
}

Sanitizacion de Arrays y JSON

<?php
declare(strict_types=1);

// Sanitizar array de configuracion de componente
public function sanitizeComponentData(array $data): array
{
    return [
        'component_name' => sanitize_key($data['component_name'] ?? ''),
        'visibility' => [
            'is_enabled'      => (bool)($data['visibility']['is_enabled'] ?? false),
            'show_on_desktop' => (bool)($data['visibility']['show_on_desktop'] ?? true),
            'show_on_mobile'  => (bool)($data['visibility']['show_on_mobile'] ?? true),
        ],
        'content' => [
            'title'       => sanitize_text_field($data['content']['title'] ?? ''),
            'description' => wp_kses_post($data['content']['description'] ?? ''),
        ],
        'styles' => [
            'background_color' => sanitize_hex_color($data['styles']['background_color'] ?? ''),
            'text_color'       => sanitize_hex_color($data['styles']['text_color'] ?? ''),
            'padding'          => absint($data['styles']['padding'] ?? 0),
        ],
    ];
}

// Sanitizar JSON recibido
public function sanitizeJsonInput(string $json): array
{
    $data = json_decode($json, true);

    if (!is_array($data)) {
        return [];
    }

    return $this->sanitizeComponentData($data);
}

Escaping para Output

Tabla de Funciones de Escaping

Contexto Funcion Cuando usar
Texto en HTML esc_html() Contenido entre tags
Atributo HTML esc_attr() Valores de atributos
URL en href/src esc_url() Links, imagenes
Textarea value esc_textarea() Contenido de textarea
HTML permitido wp_kses_post() Contenido con formato
JavaScript esc_js() Strings en JS inline
Traduccion + escape esc_html__() Textos traducibles
Traduccion + attr esc_attr__() Atributos traducibles

Ejemplos de Escaping en Renderers

<?php
declare(strict_types=1);

use ROITheme\Shared\Domain\Entities\Component;

final class ContactFormRenderer implements RendererInterface
{
    public function render(Component $component): string
    {
        $data = $component->getData();
        $title = $data['content']['title'] ?? '';
        $description = $data['content']['description'] ?? '';
        $buttonText = $data['content']['button_text'] ?? 'Submit';
        $buttonUrl = $data['content']['button_url'] ?? '#';
        $customClass = $data['styles']['custom_class'] ?? '';

        $html = '<div class="contact-form ' . esc_attr($customClass) . '">';

        // esc_html() para texto visible
        $html .= '<h2>' . esc_html($title) . '</h2>';

        // wp_kses_post() para HTML permitido
        $html .= '<div class="description">' . wp_kses_post($description) . '</div>';

        // esc_url() para URLs
        $html .= '<a href="' . esc_url($buttonUrl) . '" ';

        // esc_attr() para atributos
        $html .= 'class="' . esc_attr('btn btn-primary') . '">';

        // esc_html() para texto del boton
        $html .= esc_html($buttonText);
        $html .= '</a>';

        $html .= '</div>';

        return $html;
    }
}

Escaping con Traducciones

<?php
declare(strict_types=1);

// CORRECTO - Usar funciones combinadas
$html .= '<label>' . esc_html__('Email Address', 'roi-theme') . '</label>';
$html .= '<input type="email" placeholder="' . esc_attr__('Enter your email', 'roi-theme') . '">';

// CORRECTO - Con sprintf para variables
$html .= sprintf(
    /* translators: %s: user name */
    esc_html__('Hello, %s!', 'roi-theme'),
    esc_html($userName)
);

// INCORRECTO - NO escapar resultado de traduccion con variable
$html .= esc_html(sprintf(__('Hello, %s!', 'roi-theme'), $userName));  // XSS vulnerable

Hooks WordPress

add_action() - Cuando y Como

<?php
declare(strict_types=1);

// Estructura de add_action
add_action(
    'hook_name',           // Nombre del hook
    [$this, 'methodName'], // Callback (callable)
    10,                    // Prioridad (default: 10, menor = antes)
    1                      // Numero de argumentos (default: 1)
);

// Ejemplos con prioridades
class PluginLoader
{
    public function register(): void
    {
        // Prioridad baja (5) - ejecuta antes que otros
        add_action('init', [$this, 'loadTextDomain'], 5);

        // Prioridad normal (10) - orden por defecto
        add_action('init', [$this, 'registerPostTypes'], 10);

        // Prioridad alta (20) - ejecuta despues que otros
        add_action('init', [$this, 'initializeComponents'], 20);

        // Admin only
        add_action('admin_menu', [$this, 'addAdminMenu']);
        add_action('admin_enqueue_scripts', [$this, 'enqueueAdminAssets']);

        // Frontend only
        add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendAssets']);

        // AJAX handlers
        add_action('wp_ajax_roi_save_settings', [$this, 'handleSaveSettings']);
        add_action('wp_ajax_nopriv_roi_contact_form', [$this, 'handleContactForm']);
    }
}

add_filter() - Cuando y Como

<?php
declare(strict_types=1);

// Estructura de add_filter
add_filter(
    'filter_name',         // Nombre del filtro
    [$this, 'methodName'], // Callback
    10,                    // Prioridad
    2                      // Numero de argumentos
);

// Ejemplos
class ContentFilter
{
    public function register(): void
    {
        // Filtro de contenido
        add_filter('the_content', [$this, 'addComponentsToContent'], 10, 1);

        // Filtro con multiples argumentos
        add_filter('post_thumbnail_html', [$this, 'modifyThumbnail'], 10, 5);

        // Filtro propio del tema
        add_filter('roi_theme_filter_component_data', [$this, 'filterData'], 10, 2);
    }

    public function addComponentsToContent(string $content): string
    {
        // SIEMPRE retornar el valor (modificado o no)
        if (!is_single()) {
            return $content;  // Retornar sin modificar
        }

        $components = $this->renderComponents();
        return $content . $components;  // Retornar modificado
    }

    // Filtro con multiples argumentos
    public function modifyThumbnail(
        string $html,
        int $postId,
        int $thumbId,
        string $size,
        array $attr
    ): string {
        // Todos los parametros disponibles
        return str_replace('class="', 'class="lazy-load ', $html);
    }
}

Nomenclatura de Hooks Propios

<?php
declare(strict_types=1);

// Actions propios del tema
// Formato: roi_theme_[accion]
do_action('roi_theme_before_render', $componentName, $data);
do_action('roi_theme_after_render', $componentName, $html);
do_action('roi_theme_component_saved', $componentName, $settings);

// Filters propios del tema
// Formato: roi_theme_filter_[que_se_filtra]
$data = apply_filters('roi_theme_filter_component_data', $data, $componentName);
$html = apply_filters('roi_theme_filter_rendered_output', $html, $componentName);
$styles = apply_filters('roi_theme_filter_css_styles', $styles, $componentName);

Recursos y Cleanup

Transients

<?php
declare(strict_types=1);

// Guardar transient (cache temporal)
set_transient(
    'roi_theme_component_cache_' . $componentName,  // Key unico
    $data,                                           // Datos a guardar
    HOUR_IN_SECONDS                                  // Expiracion (1 hora)
);

// Constantes de tiempo disponibles
// MINUTE_IN_SECONDS = 60
// HOUR_IN_SECONDS = 3600
// DAY_IN_SECONDS = 86400
// WEEK_IN_SECONDS = 604800

// Obtener transient
$cached = get_transient('roi_theme_component_cache_' . $componentName);

if ($cached === false) {
    // No existe o expiro - regenerar
    $cached = $this->generateComponentData($componentName);
    set_transient('roi_theme_component_cache_' . $componentName, $cached, HOUR_IN_SECONDS);
}

return $cached;

// Eliminar transient (cuando datos cambian)
delete_transient('roi_theme_component_cache_' . $componentName);

// Eliminar todos los transients del tema (cleanup)
public function clearAllComponentCache(): void
{
    global $wpdb;

    $wpdb->query(
        "DELETE FROM {$wpdb->options}
         WHERE option_name LIKE '_transient_roi_theme_component_cache_%'
         OR option_name LIKE '_transient_timeout_roi_theme_component_cache_%'"
    );
}

Object Cache

<?php
declare(strict_types=1);

// Object cache (session-level, mas rapido que transients)
wp_cache_set(
    'component_' . $componentName,  // Key
    $data,                           // Datos
    'roi_theme',                     // Grupo
    3600                             // Expiracion (segundos)
);

// Obtener de cache
$cached = wp_cache_get('component_' . $componentName, 'roi_theme');

if ($cached === false) {
    $cached = $this->loadComponentFromDB($componentName);
    wp_cache_set('component_' . $componentName, $cached, 'roi_theme', 3600);
}

// Eliminar de cache
wp_cache_delete('component_' . $componentName, 'roi_theme');

// Eliminar grupo completo
wp_cache_flush_group('roi_theme');  // Solo si object cache soporta

Cleanup en Desactivacion/Desinstalacion

<?php
declare(strict_types=1);

// En plugin principal o functions.php
register_deactivation_hook(__FILE__, 'roi_theme_deactivate');
register_uninstall_hook(__FILE__, 'roi_theme_uninstall');

function roi_theme_deactivate(): void
{
    // Limpiar transients
    global $wpdb;
    $wpdb->query(
        "DELETE FROM {$wpdb->options}
         WHERE option_name LIKE '_transient_roi_theme_%'"
    );

    // Limpiar scheduled events
    wp_clear_scheduled_hook('roi_theme_daily_cleanup');
}

function roi_theme_uninstall(): void
{
    // Solo en desinstalacion completa
    // Eliminar opciones
    delete_option('roi_theme_settings');
    delete_option('roi_theme_version');

    // Eliminar tabla personalizada (si existe)
    global $wpdb;
    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}roi_theme_component_settings");

    // Eliminar user meta
    delete_metadata('user', 0, 'roi_theme_preferences', '', true);
}

Checklist Pre-Commit Detallado

Verificaciones de Sintaxis

  • declare(strict_types=1) al inicio de cada archivo PHP
  • Namespace correcto: ROITheme\[Context]\[Component]\[Layer]
  • Tipos de retorno declarados en todos los metodos
  • Tipos de parametros declarados
  • Sin errores de PHP (ejecutar archivo)

Verificaciones de Estilo

  • Clases son final por defecto
  • Propiedades son private o protected
  • Nombres de clase en PascalCase
  • Nombres de metodos en camelCase
  • Nombres de constantes en UPPER_SNAKE_CASE
  • Archivos <= 300 lineas
  • Metodos <= 30 lineas

Verificaciones de Seguridad

  • Variables de usuario sanitizadas (sanitize_*())
  • Output escapado (esc_*())
  • Nonce verificado en forms/AJAX
  • Permisos verificados (current_user_can())
  • Sin SQL directo (usar $wpdb->prepare())
  • Sin eval(), exec(), shell_exec()
  • Sin $_REQUEST (usar $_POST o $_GET especifico)

Verificaciones de Arquitectura

  • Domain NO tiene dependencias de WordPress
  • Domain NO tiene echo/print/HTML
  • Application NO tiene dependencias de Infrastructure
  • Infrastructure implementa interfaces de Domain
  • DI via constructor (interfaces, no clases concretas)
  • Sin new ClaseConcreta() fuera del DIContainer

Verificaciones de Nomenclatura

  • component_name en kebab-case
  • Carpetas de modulo en PascalCase
  • Archivos JSON schema en kebab-case
  • supports() retorna kebab-case
  • Hooks propios con prefijo roi_theme_

Verificaciones de WordPress

  • Hooks registrados con prioridad explicita
  • Texto traducible usa __() o _e()
  • Assets encolados correctamente (no hardcoded)
  • AJAX handlers usan wp_send_json_*
  • Transients usan constantes de tiempo

Última actualización: 2026-01-08