Files
roi-theme/Public/AdsensePlacement/Infrastructure/Api/WordPress/AdsenseVisibilityController.php
FrankZamora 26546e1d69 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>
2025-12-11 13:03:14 -06:00

146 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
namespace ROITheme\Public\AdsensePlacement\Infrastructure\Api\WordPress;
use ROITheme\Public\AdsensePlacement\Application\UseCases\CheckAdsenseVisibilityUseCase;
use ROITheme\Public\AdsensePlacement\Domain\ValueObjects\UserContext;
use WP_REST_Request;
use WP_REST_Response;
/**
* REST Controller para el endpoint de visibilidad de AdSense.
*
* Infrastructure Layer - Maneja HTTP y traduce a/desde Domain.
*
* Endpoint: GET /wp-json/roi-theme/v1/adsense-placement/visibility
*
* @package ROITheme\Public\AdsensePlacement\Infrastructure\Api\WordPress
*/
final class AdsenseVisibilityController
{
private const NAMESPACE = 'roi-theme/v1';
private const ROUTE = '/adsense-placement/visibility';
private const NONCE_ACTION = 'roi_adsense_visibility';
public function __construct(
private CheckAdsenseVisibilityUseCase $useCase
) {
}
/**
* Registra la ruta REST del endpoint.
*/
public function registerRoutes(): void
{
register_rest_route(self::NAMESPACE, self::ROUTE, [
'methods' => 'GET',
'callback' => [$this, 'handleRequest'],
'permission_callback' => '__return_true',
'args' => [
'post_id' => [
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
// IMPORTANTE: postId=0 es valido (paginas de archivo, home, etc.)
'validate_callback' => static fn($value): bool => $value >= 0,
],
'nonce' => [
'required' => false,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
}
/**
* Maneja la peticion REST.
*/
public function handleRequest(WP_REST_Request $request): WP_REST_Response
{
$this->sendNoCacheHeaders();
// Validar nonce si se proporciona
$nonce = $request->get_param('nonce');
if ($nonce !== null && !wp_verify_nonce($nonce, self::NONCE_ACTION)) {
return new WP_REST_Response([
'show_ads' => false,
'reasons' => ['invalid_nonce'],
'cache_seconds' => 0,
'timestamp' => time(),
], 403);
}
$postId = (int) $request->get_param('post_id');
$userContext = $this->buildUserContext();
$decision = $this->useCase->execute($postId, $userContext);
// El timestamp se inyecta aqui (Infrastructure) para mantener Domain puro
return new WP_REST_Response($decision->toArray(time()), 200);
}
/**
* Construye UserContext desde el estado actual de WordPress.
*/
private function buildUserContext(): UserContext
{
$isLoggedIn = is_user_logged_in();
$userRoles = [];
if ($isLoggedIn) {
$user = wp_get_current_user();
$userRoles = array_map(
static fn(string $role): int => self::roleToId($role),
$user->roles
);
}
// isMobile se determina por el cliente, no el servidor
// El cliente enviara esta info, pero por defecto asumimos false
$isMobile = false;
return new UserContext(
isLoggedIn: $isLoggedIn,
isMobile: $isMobile,
userRoles: $userRoles
);
}
/**
* Convierte nombre de rol a ID numerico para consistencia.
*/
private static function roleToId(string $role): int
{
$roleMap = [
'administrator' => 1,
'editor' => 2,
'author' => 3,
'contributor' => 4,
'subscriber' => 5,
];
return $roleMap[$role] ?? 0;
}
/**
* Envia headers para prevenir cache en proxies/CDNs.
*/
private function sendNoCacheHeaders(): void
{
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
header('Vary: Cookie');
}
/**
* Obtiene la accion del nonce para generacion en frontend.
*/
public static function getNonceAction(): string
{
return self::NONCE_ACTION;
}
}