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,145 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user