feat(theme): add [roi_post_grid] shortcode for static pages
- Create PostGridShortcodeRegistrar for WordPress shortcode registration - Implement RenderPostGridUseCase following Clean Architecture - Add PostGridQueryBuilder for custom WP_Query construction - Add PostGridShortcodeRenderer for HTML/CSS generation - Register shortcode in DIContainer with proper DI - Add shortcode usage guide in post-grid admin panel - Fix sidebar layout: add hide_for_logged_in check to wrapper visibility Shortcode attributes: category, tag, author, posts_per_page, columns, show_pagination, show_thumbnail, show_excerpt, show_meta, etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,12 @@ use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
|
||||
use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentVisibilityRepository;
|
||||
use ROITheme\Shared\Application\UseCases\CheckWrapperVisibilityUseCase;
|
||||
use ROITheme\Shared\Infrastructure\Wordpress\BodyClassHooksRegistrar;
|
||||
// Post Grid Shortcode (Plan post-grid-shortcode)
|
||||
use ROITheme\Shared\Domain\Contracts\PostGridQueryBuilderInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\PostGridShortcodeRendererInterface;
|
||||
use ROITheme\Shared\Infrastructure\Query\PostGridQueryBuilder;
|
||||
use ROITheme\Shared\Infrastructure\Ui\PostGridShortcodeRenderer;
|
||||
use ROITheme\Shared\Application\UseCases\RenderPostGrid\RenderPostGridUseCase;
|
||||
|
||||
/**
|
||||
* DIContainer - Contenedor de Inyección de Dependencias
|
||||
@@ -493,4 +499,47 @@ final class DIContainer
|
||||
}
|
||||
return $this->instances['bodyClassHooksRegistrar'];
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Post Grid Shortcode System
|
||||
// ===============================
|
||||
|
||||
/**
|
||||
* Obtiene el query builder para post grid shortcode
|
||||
*/
|
||||
public function getPostGridQueryBuilder(): PostGridQueryBuilderInterface
|
||||
{
|
||||
if (!isset($this->instances['postGridQueryBuilder'])) {
|
||||
$this->instances['postGridQueryBuilder'] = new PostGridQueryBuilder();
|
||||
}
|
||||
return $this->instances['postGridQueryBuilder'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el renderer para post grid shortcode
|
||||
*/
|
||||
public function getPostGridShortcodeRenderer(): PostGridShortcodeRendererInterface
|
||||
{
|
||||
if (!isset($this->instances['postGridShortcodeRenderer'])) {
|
||||
$this->instances['postGridShortcodeRenderer'] = new PostGridShortcodeRenderer(
|
||||
$this->getCSSGeneratorService()
|
||||
);
|
||||
}
|
||||
return $this->instances['postGridShortcodeRenderer'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el caso de uso para renderizar post grid shortcode
|
||||
*/
|
||||
public function getRenderPostGridUseCase(): RenderPostGridUseCase
|
||||
{
|
||||
if (!isset($this->instances['renderPostGridUseCase'])) {
|
||||
$this->instances['renderPostGridUseCase'] = new RenderPostGridUseCase(
|
||||
$this->getPostGridQueryBuilder(),
|
||||
$this->getPostGridShortcodeRenderer(),
|
||||
$this->getComponentSettingsRepository()
|
||||
);
|
||||
}
|
||||
return $this->instances['renderPostGridUseCase'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,15 +61,35 @@ final class WordPressComponentVisibilityRepository implements WrapperVisibilityC
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Delega a PageVisibilityHelper que ya implementa:
|
||||
* Evalúa múltiples criterios de exclusión:
|
||||
* - hide_for_logged_in: Ocultar para usuarios logueados
|
||||
* - Visibilidad por tipo de página (home, posts, pages, archives, search)
|
||||
* - Exclusiones por categoría, post ID, URL pattern
|
||||
*/
|
||||
public function isNotExcluded(string $componentName): bool
|
||||
{
|
||||
// Verificar hide_for_logged_in
|
||||
if ($this->shouldHideForLoggedIn($componentName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return PageVisibilityHelper::shouldShow($componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si debe ocultarse para usuarios logueados
|
||||
*/
|
||||
private function shouldHideForLoggedIn(string $componentName): bool
|
||||
{
|
||||
$value = $this->getVisibilityAttribute($componentName, 'hide_for_logged_in');
|
||||
|
||||
if ($value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->toBool($value) && is_user_logged_in();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un atributo del grupo visibility desde la BD
|
||||
*
|
||||
|
||||
109
Shared/Infrastructure/Query/PostGridQueryBuilder.php
Normal file
109
Shared/Infrastructure/Query/PostGridQueryBuilder.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Query;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\PostGridQueryBuilderInterface;
|
||||
|
||||
/**
|
||||
* Implementacion de PostGridQueryBuilderInterface
|
||||
*
|
||||
* RESPONSABILIDAD: Construir WP_Query a partir de parametros de filtro.
|
||||
* No genera HTML ni obtiene settings.
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Query
|
||||
*/
|
||||
final class PostGridQueryBuilder implements PostGridQueryBuilderInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build(array $params): \WP_Query
|
||||
{
|
||||
$args = [
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => (int) ($params['posts_per_page'] ?? 9),
|
||||
'orderby' => $params['orderby'] ?? 'date',
|
||||
'order' => $params['order'] ?? 'DESC',
|
||||
'paged' => (int) ($params['paged'] ?? 1),
|
||||
];
|
||||
|
||||
// Offset
|
||||
if (!empty($params['offset'])) {
|
||||
$args['offset'] = (int) $params['offset'];
|
||||
}
|
||||
|
||||
// Filtro por categoria(s)
|
||||
if (!empty($params['category'])) {
|
||||
$args['category_name'] = $this->sanitizeSlugs($params['category']);
|
||||
}
|
||||
|
||||
// Excluir categoria(s)
|
||||
if (!empty($params['exclude_category'])) {
|
||||
$excludeIds = $this->getCategoryIds($params['exclude_category']);
|
||||
if (!empty($excludeIds)) {
|
||||
$args['category__not_in'] = $excludeIds;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtro por tag(s)
|
||||
if (!empty($params['tag'])) {
|
||||
$args['tag'] = $this->sanitizeSlugs($params['tag']);
|
||||
}
|
||||
|
||||
// Filtro por autor
|
||||
if (!empty($params['author'])) {
|
||||
$author = $params['author'];
|
||||
if (is_numeric($author)) {
|
||||
$args['author'] = (int) $author;
|
||||
} else {
|
||||
$user = get_user_by('login', $author);
|
||||
if ($user) {
|
||||
$args['author'] = $user->ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Excluir posts por ID
|
||||
if (!empty($params['exclude_posts'])) {
|
||||
$excludeIds = array_map('intval', explode(',', $params['exclude_posts']));
|
||||
$excludeIds = array_filter($excludeIds, fn($id) => $id > 0);
|
||||
if (!empty($excludeIds)) {
|
||||
$args['post__not_in'] = $excludeIds;
|
||||
}
|
||||
}
|
||||
|
||||
return new \WP_Query($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza slugs separados por coma
|
||||
*/
|
||||
private function sanitizeSlugs(string $slugs): string
|
||||
{
|
||||
$parts = explode(',', $slugs);
|
||||
$sanitized = array_map('sanitize_title', $parts);
|
||||
return implode(',', $sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene IDs de categorias desde slugs
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function getCategoryIds(string $slugs): array
|
||||
{
|
||||
$parts = explode(',', $slugs);
|
||||
$ids = [];
|
||||
|
||||
foreach ($parts as $slug) {
|
||||
$term = get_term_by('slug', trim($slug), 'category');
|
||||
if ($term instanceof \WP_Term) {
|
||||
$ids[] = $term->term_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
377
Shared/Infrastructure/Ui/PostGridShortcodeRenderer.php
Normal file
377
Shared/Infrastructure/Ui/PostGridShortcodeRenderer.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\PostGridShortcodeRendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
|
||||
/**
|
||||
* Implementacion de PostGridShortcodeRendererInterface
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML y CSS del shortcode [roi_post_grid].
|
||||
* No construye queries ni obtiene settings de BD.
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Ui
|
||||
*/
|
||||
final class PostGridShortcodeRenderer implements PostGridShortcodeRendererInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSGeneratorInterface $cssGenerator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render(\WP_Query $query, array $settings, array $options): string
|
||||
{
|
||||
if (!$query->have_posts()) {
|
||||
return $this->renderNoPostsMessage($settings, $options);
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($settings, $options);
|
||||
$html = $this->buildHTML($query, $settings, $options);
|
||||
|
||||
return sprintf("<style>%s</style>\n%s", $css, $html);
|
||||
}
|
||||
|
||||
private function renderNoPostsMessage(array $settings, array $options): string
|
||||
{
|
||||
$colors = $settings['colors'] ?? [];
|
||||
$message = 'No se encontraron publicaciones';
|
||||
|
||||
$bgColor = $colors['card_bg_color'] ?? '#ffffff';
|
||||
$textColor = $colors['excerpt_color'] ?? '#6b7280';
|
||||
$borderColor = $colors['card_border_color'] ?? '#e5e7eb';
|
||||
|
||||
$selector = $this->getSelector($options);
|
||||
|
||||
$css = $this->cssGenerator->generate("{$selector} .no-posts", [
|
||||
'background-color' => $bgColor,
|
||||
'color' => $textColor,
|
||||
'border' => "1px solid {$borderColor}",
|
||||
'border-radius' => '0.5rem',
|
||||
'padding' => '2rem',
|
||||
'text-align' => 'center',
|
||||
]);
|
||||
|
||||
$containerClass = $this->getContainerClass($options);
|
||||
|
||||
return sprintf(
|
||||
"<style>%s</style>\n<div class=\"%s\"><div class=\"no-posts\"><p class=\"mb-0\">%s</p></div></div>",
|
||||
$css,
|
||||
esc_attr($containerClass),
|
||||
esc_html($message)
|
||||
);
|
||||
}
|
||||
|
||||
private function getSelector(array $options): string
|
||||
{
|
||||
$id = $options['id'] ?? '';
|
||||
return !empty($id)
|
||||
? ".roi-post-grid-shortcode-{$id}"
|
||||
: '.roi-post-grid-shortcode';
|
||||
}
|
||||
|
||||
private function getContainerClass(array $options): string
|
||||
{
|
||||
$id = $options['id'] ?? '';
|
||||
$customClass = $options['class'] ?? '';
|
||||
|
||||
$class = !empty($id)
|
||||
? "roi-post-grid-shortcode roi-post-grid-shortcode-{$id}"
|
||||
: 'roi-post-grid-shortcode';
|
||||
|
||||
if (!empty($customClass)) {
|
||||
$class .= ' ' . sanitize_html_class($customClass);
|
||||
}
|
||||
|
||||
return $class;
|
||||
}
|
||||
|
||||
private function generateCSS(array $settings, array $options): string
|
||||
{
|
||||
$colors = $settings['colors'] ?? [];
|
||||
$spacing = $settings['spacing'] ?? [];
|
||||
$effects = $settings['visual_effects'] ?? [];
|
||||
$typography = $settings['typography'] ?? [];
|
||||
|
||||
$selector = $this->getSelector($options);
|
||||
$cssRules = [];
|
||||
|
||||
// Colores
|
||||
$cardBgColor = $colors['card_bg_color'] ?? '#ffffff';
|
||||
$cardTitleColor = $colors['card_title_color'] ?? '#0E2337';
|
||||
$cardHoverBgColor = $colors['card_hover_bg_color'] ?? '#f9fafb';
|
||||
$cardBorderColor = $colors['card_border_color'] ?? '#e5e7eb';
|
||||
$cardHoverBorderColor = $colors['card_hover_border_color'] ?? '#FF8600';
|
||||
$excerptColor = $colors['excerpt_color'] ?? '#6b7280';
|
||||
$metaColor = $colors['meta_color'] ?? '#9ca3af';
|
||||
$categoryBgColor = $colors['category_bg_color'] ?? '#FFF5EB';
|
||||
$categoryTextColor = $colors['category_text_color'] ?? '#FF8600';
|
||||
|
||||
// Spacing
|
||||
$gridGap = $spacing['grid_gap'] ?? '1.5rem';
|
||||
$cardPadding = $spacing['card_padding'] ?? '1.25rem';
|
||||
|
||||
// Visual effects
|
||||
$cardBorderRadius = $effects['card_border_radius'] ?? '0.5rem';
|
||||
$cardShadow = $effects['card_shadow'] ?? '0 1px 3px rgba(0,0,0,0.1)';
|
||||
$cardHoverShadow = $effects['card_hover_shadow'] ?? '0 4px 12px rgba(0,0,0,0.15)';
|
||||
$cardTransition = $effects['card_transition'] ?? 'all 0.3s ease';
|
||||
|
||||
// Typography
|
||||
$cardTitleSize = $typography['card_title_size'] ?? '1.1rem';
|
||||
$cardTitleWeight = $typography['card_title_weight'] ?? '600';
|
||||
$excerptSize = $typography['excerpt_size'] ?? '0.9rem';
|
||||
$metaSize = $typography['meta_size'] ?? '0.8rem';
|
||||
|
||||
// Container
|
||||
$cssRules[] = $this->cssGenerator->generate($selector, [
|
||||
'margin-bottom' => '2rem',
|
||||
]);
|
||||
|
||||
// Row
|
||||
$cssRules[] = $this->cssGenerator->generate("{$selector} .row", [
|
||||
'row-gap' => $gridGap,
|
||||
]);
|
||||
|
||||
// Card
|
||||
$cssRules[] = "{$selector} .card {
|
||||
background: {$cardBgColor};
|
||||
border: 1px solid {$cardBorderColor};
|
||||
border-radius: {$cardBorderRadius};
|
||||
box-shadow: {$cardShadow};
|
||||
transition: {$cardTransition};
|
||||
height: 100%;
|
||||
}";
|
||||
|
||||
$cssRules[] = "{$selector} .card:hover {
|
||||
background: {$cardHoverBgColor};
|
||||
border-color: {$cardHoverBorderColor};
|
||||
box-shadow: {$cardHoverShadow};
|
||||
transform: translateY(-2px);
|
||||
}";
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate("{$selector} .card-body", [
|
||||
'padding' => $cardPadding,
|
||||
]);
|
||||
|
||||
$cssRules[] = "{$selector} .card-img-top {
|
||||
border-radius: {$cardBorderRadius} {$cardBorderRadius} 0 0;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}";
|
||||
|
||||
$cssRules[] = "{$selector} .card-title {
|
||||
color: {$cardTitleColor};
|
||||
font-size: {$cardTitleSize};
|
||||
font-weight: {$cardTitleWeight};
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.75rem;
|
||||
}";
|
||||
|
||||
$cssRules[] = "{$selector} a:hover .card-title {
|
||||
color: {$cardHoverBorderColor};
|
||||
}";
|
||||
|
||||
$cssRules[] = "{$selector} .card-text {
|
||||
color: {$excerptColor};
|
||||
font-size: {$excerptSize};
|
||||
line-height: 1.6;
|
||||
}";
|
||||
|
||||
$cssRules[] = "{$selector} .post-meta {
|
||||
color: {$metaColor};
|
||||
font-size: {$metaSize};
|
||||
}";
|
||||
|
||||
$cssRules[] = "{$selector} .post-category {
|
||||
background: {$categoryBgColor};
|
||||
color: {$categoryTextColor};
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}";
|
||||
|
||||
return implode("\n", $cssRules);
|
||||
}
|
||||
|
||||
private function buildHTML(\WP_Query $query, array $settings, array $options): string
|
||||
{
|
||||
$columns = (int) ($options['columns'] ?? 3);
|
||||
$showThumbnail = $this->toBool($options['show_thumbnail'] ?? true);
|
||||
$showExcerpt = $this->toBool($options['show_excerpt'] ?? true);
|
||||
$showMeta = $this->toBool($options['show_meta'] ?? true);
|
||||
$showCategories = $this->toBool($options['show_categories'] ?? true);
|
||||
$excerptLength = (int) ($options['excerpt_length'] ?? 20);
|
||||
$showPagination = $this->toBool($options['show_pagination'] ?? false);
|
||||
|
||||
$containerClass = $this->getContainerClass($options);
|
||||
$colClass = $this->getColumnClass($columns);
|
||||
|
||||
$html = sprintf('<div class="%s">', esc_attr($containerClass));
|
||||
$html .= '<div class="row">';
|
||||
|
||||
while ($query->have_posts()) {
|
||||
$query->the_post();
|
||||
$html .= $this->buildCardHTML(
|
||||
$colClass,
|
||||
$showThumbnail,
|
||||
$showExcerpt,
|
||||
$showMeta,
|
||||
$showCategories,
|
||||
$excerptLength
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
if ($showPagination && $query->max_num_pages > 1) {
|
||||
$html .= $this->buildPaginationHTML($query, $options);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getColumnClass(int $columns): string
|
||||
{
|
||||
return match ($columns) {
|
||||
1 => 'col-12',
|
||||
2 => 'col-12 col-md-6',
|
||||
4 => 'col-12 col-md-6 col-lg-3',
|
||||
default => 'col-12 col-md-6 col-lg-4',
|
||||
};
|
||||
}
|
||||
|
||||
private function buildCardHTML(
|
||||
string $colClass,
|
||||
bool $showThumbnail,
|
||||
bool $showExcerpt,
|
||||
bool $showMeta,
|
||||
bool $showCategories,
|
||||
int $excerptLength
|
||||
): string {
|
||||
$permalink = get_permalink();
|
||||
$title = get_the_title();
|
||||
|
||||
$html = sprintf('<div class="%s mb-4">', esc_attr($colClass));
|
||||
$html .= sprintf('<a href="%s" class="text-decoration-none">', esc_url($permalink));
|
||||
$html .= '<div class="card h-100">';
|
||||
|
||||
if ($showThumbnail) {
|
||||
$html .= $this->buildImageHTML();
|
||||
}
|
||||
|
||||
$html .= '<div class="card-body">';
|
||||
|
||||
if ($showCategories) {
|
||||
$html .= $this->buildCategoriesHTML();
|
||||
}
|
||||
|
||||
$html .= sprintf('<h3 class="card-title">%s</h3>', esc_html($title));
|
||||
|
||||
if ($showMeta) {
|
||||
$html .= $this->buildMetaHTML();
|
||||
}
|
||||
|
||||
if ($showExcerpt) {
|
||||
$html .= $this->buildExcerptHTML($excerptLength);
|
||||
}
|
||||
|
||||
$html .= '</div></div></a></div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildImageHTML(): string
|
||||
{
|
||||
if (has_post_thumbnail()) {
|
||||
return get_the_post_thumbnail(null, 'medium_large', [
|
||||
'class' => 'card-img-top',
|
||||
'loading' => 'lazy'
|
||||
]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function buildCategoriesHTML(): string
|
||||
{
|
||||
$categories = get_the_category();
|
||||
if (empty($categories)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '<div class="post-categories mb-2">';
|
||||
foreach (array_slice($categories, 0, 2) as $category) {
|
||||
$html .= sprintf(
|
||||
'<span class="post-category">%s</span>',
|
||||
esc_html($category->name)
|
||||
);
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMetaHTML(): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="post-meta mb-2"><small>%s | %s</small></div>',
|
||||
esc_html(get_the_date()),
|
||||
esc_html(get_the_author())
|
||||
);
|
||||
}
|
||||
|
||||
private function buildExcerptHTML(int $length): string
|
||||
{
|
||||
$excerpt = get_the_excerpt();
|
||||
if (empty($excerpt)) {
|
||||
$excerpt = wp_trim_words(get_the_content(), $length, '...');
|
||||
} else {
|
||||
$excerpt = wp_trim_words($excerpt, $length, '...');
|
||||
}
|
||||
|
||||
return sprintf('<p class="card-text">%s</p>', esc_html($excerpt));
|
||||
}
|
||||
|
||||
private function buildPaginationHTML(\WP_Query $query, array $options): string
|
||||
{
|
||||
$id = $options['id'] ?? '';
|
||||
$queryVar = !empty($id) ? "paged_{$id}" : 'paged';
|
||||
$currentPage = max(1, (int) get_query_var($queryVar, 1));
|
||||
$totalPages = $query->max_num_pages;
|
||||
|
||||
$html = '<nav class="pagination-wrapper mt-4"><ul class="pagination justify-content-center">';
|
||||
|
||||
for ($i = 1; $i <= $totalPages; $i++) {
|
||||
$activeClass = ($i === $currentPage) ? ' active' : '';
|
||||
$url = add_query_arg($queryVar, $i);
|
||||
$html .= sprintf(
|
||||
'<li class="page-item%s"><a class="page-link" href="%s">%d</a></li>',
|
||||
$activeClass,
|
||||
esc_url($url),
|
||||
$i
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</ul></nav>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
return $value === 'true' || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Wordpress;
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||
use ROITheme\Shared\Application\UseCases\RenderPostGrid\RenderPostGridRequest;
|
||||
|
||||
/**
|
||||
* Registra el shortcode [roi_post_grid] en WordPress
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Registrar shortcode via add_shortcode
|
||||
* - Sanitizar atributos del shortcode
|
||||
* - Delegar ejecucion a RenderPostGridUseCase
|
||||
*
|
||||
* NO RESPONSABLE DE:
|
||||
* - Logica de negocio
|
||||
* - Construccion de queries
|
||||
* - Generacion de HTML/CSS
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Wordpress
|
||||
*/
|
||||
final class PostGridShortcodeRegistrar
|
||||
{
|
||||
private const SHORTCODE_TAG = 'roi_post_grid';
|
||||
|
||||
/**
|
||||
* Registra el shortcode en WordPress
|
||||
*/
|
||||
public static function register(): void
|
||||
{
|
||||
add_shortcode(self::SHORTCODE_TAG, [new self(), 'handleShortcode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback del shortcode
|
||||
*
|
||||
* @param array|string $atts Atributos del shortcode
|
||||
* @return string HTML del grid
|
||||
*/
|
||||
public function handleShortcode($atts): string
|
||||
{
|
||||
$atts = $this->sanitizeAttributes($atts);
|
||||
|
||||
// Obtener paged desde query var si existe
|
||||
$id = $atts['id'] ?? '';
|
||||
$queryVar = !empty($id) ? "paged_{$id}" : 'paged';
|
||||
$atts['paged'] = max(1, (int) get_query_var($queryVar, 1));
|
||||
|
||||
// Crear request DTO
|
||||
$request = RenderPostGridRequest::fromArray($atts);
|
||||
|
||||
// Obtener UseCase desde DIContainer
|
||||
$container = DIContainer::getInstance();
|
||||
$useCase = $container->getRenderPostGridUseCase();
|
||||
|
||||
return $useCase->execute($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitiza atributos del shortcode
|
||||
*
|
||||
* @param array|string $atts
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function sanitizeAttributes($atts): array
|
||||
{
|
||||
$atts = shortcode_atts([
|
||||
'category' => '',
|
||||
'exclude_category' => '',
|
||||
'tag' => '',
|
||||
'author' => '',
|
||||
'posts_per_page' => '9',
|
||||
'columns' => '3',
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'show_pagination' => 'false',
|
||||
'offset' => '0',
|
||||
'exclude_posts' => '',
|
||||
'show_thumbnail' => 'true',
|
||||
'show_excerpt' => 'true',
|
||||
'show_meta' => 'true',
|
||||
'show_categories' => 'true',
|
||||
'excerpt_length' => '20',
|
||||
'class' => '',
|
||||
'id' => '',
|
||||
], $atts, self::SHORTCODE_TAG);
|
||||
|
||||
// Sanitizar cada valor
|
||||
return array_map(function ($value) {
|
||||
return is_string($value) ? sanitize_text_field($value) : $value;
|
||||
}, $atts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user