- 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>
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.mdREFERENCIA: 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 |
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
finalpor defecto - Propiedades son
privateoprotected - 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$_POSTo$_GETespecifico)
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