- 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>
1236 lines
36 KiB
Markdown
1236 lines
36 KiB
Markdown
# 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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|