feat(api): implement javascript-first architecture for cache compatibility
- Add REST endpoint GET /roi-theme/v1/adsense-placement/visibility - Add Domain layer: UserContext, VisibilityDecision, AdsenseSettings VOs - Add Application layer: CheckAdsenseVisibilityUseCase - Add Infrastructure: AdsenseVisibilityChecker, Controller, Enqueuer - Add JavaScript controller with localStorage caching - Add test plan for production validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\Contracts;
|
||||
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\UserContext;
|
||||
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\VisibilityDecision;
|
||||
|
||||
/**
|
||||
* Interface para el servicio que evalua visibilidad de anuncios.
|
||||
*
|
||||
* Domain Layer - Define el contrato sin dependencias de infraestructura.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\Contracts
|
||||
*/
|
||||
interface AdsenseVisibilityCheckerInterface
|
||||
{
|
||||
/**
|
||||
* Evalua si los anuncios deben mostrarse para el contexto dado.
|
||||
*
|
||||
* @param int $postId ID del post (0 para paginas de archivo/home)
|
||||
* @param UserContext $userContext Contexto del usuario
|
||||
* @return VisibilityDecision Decision con razones y cache
|
||||
*/
|
||||
public function check(int $postId, UserContext $userContext): VisibilityDecision;
|
||||
}
|
||||
163
Public/AdsensePlacement/Domain/ValueObjects/AdsenseSettings.php
Normal file
163
Public/AdsensePlacement/Domain/ValueObjects/AdsenseSettings.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object que encapsula la configuracion de AdSense relevante para visibilidad.
|
||||
*
|
||||
* Inmutable despues de construccion. Sin dependencias de WordPress.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\ValueObjects
|
||||
*/
|
||||
final class AdsenseSettings
|
||||
{
|
||||
/**
|
||||
* @param bool $isEnabled Si AdSense esta activo globalmente
|
||||
* @param bool $showOnDesktop Si se muestra en desktop
|
||||
* @param bool $showOnMobile Si se muestra en mobile
|
||||
* @param bool $hideForLoggedIn Si se oculta para usuarios logueados
|
||||
* @param bool $javascriptFirstMode Si el modo JS-first esta activo
|
||||
* @param array<int> $excludedCategoryIds IDs de categorias excluidas
|
||||
* @param array<int> $excludedPostIds IDs de posts excluidos
|
||||
* @param array<string> $excludedPostTypes Post types excluidos
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $isEnabled,
|
||||
private bool $showOnDesktop,
|
||||
private bool $showOnMobile,
|
||||
private bool $hideForLoggedIn,
|
||||
private bool $javascriptFirstMode,
|
||||
private array $excludedCategoryIds = [],
|
||||
private array $excludedPostIds = [],
|
||||
private array $excludedPostTypes = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->isEnabled;
|
||||
}
|
||||
|
||||
public function showOnDesktop(): bool
|
||||
{
|
||||
return $this->showOnDesktop;
|
||||
}
|
||||
|
||||
public function showOnMobile(): bool
|
||||
{
|
||||
return $this->showOnMobile;
|
||||
}
|
||||
|
||||
public function hideForLoggedIn(): bool
|
||||
{
|
||||
return $this->hideForLoggedIn;
|
||||
}
|
||||
|
||||
public function isJavascriptFirstMode(): bool
|
||||
{
|
||||
return $this->javascriptFirstMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getExcludedCategoryIds(): array
|
||||
{
|
||||
return $this->excludedCategoryIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getExcludedPostIds(): array
|
||||
{
|
||||
return $this->excludedPostIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getExcludedPostTypes(): array
|
||||
{
|
||||
return $this->excludedPostTypes;
|
||||
}
|
||||
|
||||
public function isPostExcluded(int $postId): bool
|
||||
{
|
||||
return in_array($postId, $this->excludedPostIds, true);
|
||||
}
|
||||
|
||||
public function isPostTypeExcluded(string $postType): bool
|
||||
{
|
||||
return in_array($postType, $this->excludedPostTypes, true);
|
||||
}
|
||||
|
||||
public function isCategoryExcluded(int $categoryId): bool
|
||||
{
|
||||
return in_array($categoryId, $this->excludedCategoryIds, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea instancia desde array de configuracion de BD.
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $settings Configuracion agrupada
|
||||
*/
|
||||
public static function fromArray(array $settings): self
|
||||
{
|
||||
$visibility = $settings['visibility'] ?? [];
|
||||
$behavior = $settings['behavior'] ?? [];
|
||||
$forms = $settings['forms'] ?? [];
|
||||
|
||||
return new self(
|
||||
isEnabled: (bool) ($visibility['is_enabled'] ?? false),
|
||||
showOnDesktop: (bool) ($visibility['show_on_desktop'] ?? true),
|
||||
showOnMobile: (bool) ($visibility['show_on_mobile'] ?? true),
|
||||
hideForLoggedIn: (bool) ($visibility['hide_for_logged_in'] ?? false),
|
||||
javascriptFirstMode: (bool) ($behavior['javascript_first_mode'] ?? false),
|
||||
excludedCategoryIds: self::parseIds($forms['exclude_categories'] ?? ''),
|
||||
excludedPostIds: self::parseIds($forms['exclude_post_ids'] ?? ''),
|
||||
excludedPostTypes: self::parsePostTypes($forms['exclude_post_types'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea string de IDs separados por coma a array de enteros.
|
||||
*
|
||||
* @return array<int>
|
||||
*/
|
||||
private static function parseIds(string $value): array
|
||||
{
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn(string $id): int => (int) trim($id),
|
||||
explode(',', $value)
|
||||
),
|
||||
static fn(int $id): bool => $id > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsea string de post types separados por coma.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
private static function parsePostTypes(string $value): array
|
||||
{
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn(string $type): string => trim($type),
|
||||
explode(',', $value)
|
||||
),
|
||||
static fn(string $type): bool => $type !== ''
|
||||
);
|
||||
}
|
||||
}
|
||||
82
Public/AdsensePlacement/Domain/ValueObjects/UserContext.php
Normal file
82
Public/AdsensePlacement/Domain/ValueObjects/UserContext.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object que encapsula el contexto del usuario para decisiones de visibilidad.
|
||||
*
|
||||
* Inmutable despues de construccion. Sin dependencias de WordPress.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\ValueObjects
|
||||
*/
|
||||
final class UserContext
|
||||
{
|
||||
/**
|
||||
* @param bool $isLoggedIn Si el usuario tiene sesion activa
|
||||
* @param bool $isMobile Si el dispositivo es movil (viewport < 992px)
|
||||
* @param array<int> $userRoles IDs de roles del usuario
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $isLoggedIn,
|
||||
private bool $isMobile,
|
||||
private array $userRoles = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function isLoggedIn(): bool
|
||||
{
|
||||
return $this->isLoggedIn;
|
||||
}
|
||||
|
||||
public function isMobile(): bool
|
||||
{
|
||||
return $this->isMobile;
|
||||
}
|
||||
|
||||
public function isDesktop(): bool
|
||||
{
|
||||
return !$this->isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function getUserRoles(): array
|
||||
{
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
public function hasRole(int $roleId): bool
|
||||
{
|
||||
return in_array($roleId, $this->userRoles, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea instancia desde array (para deserializacion).
|
||||
*
|
||||
* @param array{is_logged_in: bool, is_mobile: bool, user_roles?: array<int>} $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
isLoggedIn: $data['is_logged_in'] ?? false,
|
||||
isMobile: $data['is_mobile'] ?? false,
|
||||
userRoles: $data['user_roles'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializa a array para respuesta JSON.
|
||||
*
|
||||
* @return array{is_logged_in: bool, is_mobile: bool, user_roles: array<int>}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'is_logged_in' => $this->isLoggedIn,
|
||||
'is_mobile' => $this->isMobile,
|
||||
'user_roles' => $this->userRoles,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\AdsensePlacement\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object que representa la decision de mostrar o no anuncios.
|
||||
*
|
||||
* Inmutable despues de construccion. Sin dependencias de WordPress.
|
||||
* El timestamp se inyecta en toArray() para mantener Domain puro.
|
||||
*
|
||||
* @package ROITheme\Public\AdsensePlacement\Domain\ValueObjects
|
||||
*/
|
||||
final class VisibilityDecision
|
||||
{
|
||||
private const DEFAULT_CACHE_SECONDS = 300; // 5 minutos
|
||||
|
||||
/**
|
||||
* @param bool $showAds Si se deben mostrar los anuncios
|
||||
* @param array<string> $reasons Razones de la decision (para debugging)
|
||||
* @param int $cacheSeconds Tiempo de cache en segundos
|
||||
*/
|
||||
public function __construct(
|
||||
private bool $showAds,
|
||||
private array $reasons = [],
|
||||
private int $cacheSeconds = self::DEFAULT_CACHE_SECONDS
|
||||
) {
|
||||
}
|
||||
|
||||
public function shouldShowAds(): bool
|
||||
{
|
||||
return $this->showAds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getReasons(): array
|
||||
{
|
||||
return $this->reasons;
|
||||
}
|
||||
|
||||
public function getCacheSeconds(): int
|
||||
{
|
||||
return $this->cacheSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Crea decision positiva (mostrar ads).
|
||||
*/
|
||||
public static function show(int $cacheSeconds = self::DEFAULT_CACHE_SECONDS): self
|
||||
{
|
||||
return new self(
|
||||
showAds: true,
|
||||
reasons: ['all_conditions_passed'],
|
||||
cacheSeconds: $cacheSeconds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Crea decision negativa (ocultar ads).
|
||||
*
|
||||
* @param array<string> $reasons
|
||||
*/
|
||||
public static function hide(array $reasons, int $cacheSeconds = self::DEFAULT_CACHE_SECONDS): self
|
||||
{
|
||||
return new self(
|
||||
showAds: false,
|
||||
reasons: $reasons,
|
||||
cacheSeconds: $cacheSeconds
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializa a array para respuesta JSON.
|
||||
*
|
||||
* El timestamp se inyecta aqui (no en constructor) para mantener Domain puro.
|
||||
*
|
||||
* @param int $timestamp Unix timestamp actual
|
||||
* @return array{show_ads: bool, reasons: array<string>, cache_seconds: int, timestamp: int}
|
||||
*/
|
||||
public function toArray(int $timestamp): array
|
||||
{
|
||||
return [
|
||||
'show_ads' => $this->showAds,
|
||||
'reasons' => $this->reasons,
|
||||
'cache_seconds' => $this->cacheSeconds,
|
||||
'timestamp' => $timestamp,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user