Files
wp-bootstrap/inc/Template/ContextBuilder.php
magdev 3165e60639
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m7s
Create Release Package / Build Release (push) Successful in 1m41s
feat: Bootstrap 5 block renderer, widget cards, and sidebar post layout (v1.1.0)
Add BlockRenderer class injecting Bootstrap classes into 8 core block types
(table, button, buttons, image, search, quote, pullquote, list) via per-block
render_block filters using WP_HTML_Tag_Processor.

Add WidgetRenderer class wrapping sidebar widgets in Bootstrap card components
with h4 heading hierarchy via dynamic_sidebar_params and widget_block_content
filters.

Add widget SCSS stylesheet for list styling, search input-group, tag cloud
pills, and card-flush list positioning.

Add single-sidebar.html.twig as the default post template with two-column
Bootstrap layout (col-lg-8 content, col-lg-4 sidebar). Full-width available
via template selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:43:43 +01:00

548 lines
16 KiB
PHP

<?php
/**
* Twig Template Context Builder.
*
* Gathers WordPress data into structured arrays for Twig templates.
*
* @package WPBootstrap
* @since 0.1.1
*/
namespace WPBootstrap\Template;
class ContextBuilder
{
private NavWalker $navWalker;
public function __construct()
{
$this->navWalker = new NavWalker();
}
/**
* Build the complete context for the current request.
*/
public function build(): array
{
$context = [
'site' => $this->getSiteData(),
'menu' => $this->getMenuData('primary'),
'footer_menu' => $this->getMenuData('footer'),
'dark_mode' => true,
'layout' => 'default',
'header_variant' => $this->getHeaderVariant(),
'footer_variant' => $this->getFooterVariant(),
'user' => $this->getUserData(),
];
if (is_singular()) {
$context['post'] = $this->getPostData();
if (is_singular('post')) {
$context['comments'] = $this->getCommentsData();
$context['post_navigation'] = $this->getPostNavigation();
$context['more_posts'] = $this->getMorePosts();
}
}
if (is_home() || is_archive() || is_search()) {
$context['posts'] = $this->getPostsLoop();
$context['pagination'] = $this->getPagination();
}
if (is_archive()) {
$context['archive'] = $this->getArchiveData();
}
if (is_search()) {
$context['search_query'] = get_search_query();
}
// Sidebar layout detection.
if (is_home()) {
$pageId = (int) get_option('page_for_posts');
if ($pageId) {
$templateSlug = get_page_template_slug($pageId);
if ($templateSlug === 'home-sidebar') {
$context['layout'] = 'sidebar';
}
}
$context['sidebar'] = $this->getSidebarData();
}
// Sidebar data for pages using the "Page with Sidebar" template.
if (is_page()) {
$slug = get_page_template_slug();
if ($slug === 'page-sidebar') {
$context['sidebar'] = $this->getSidebarData();
}
}
// Posts always get sidebar data (sidebar is the default layout).
if (is_singular('post')) {
$context['sidebar'] = $this->getSidebarData();
}
return $context;
}
/**
* Get global site information.
*/
private function getSiteData(): array
{
return [
'name' => get_bloginfo('name'),
'description' => get_bloginfo('description'),
'url' => home_url('/'),
'charset' => get_bloginfo('charset'),
];
}
/**
* Get current user data for header/navigation.
*/
private function getUserData(): array
{
if (! is_user_logged_in()) {
return ['logged_in' => false];
}
$user = wp_get_current_user();
$account_url = function_exists('wc_get_page_permalink')
? wc_get_page_permalink('myaccount')
: admin_url('profile.php');
return [
'logged_in' => true,
'display_name' => $user->display_name,
'avatar' => get_avatar($user->ID, 32, '', '', ['class' => 'rounded-circle']),
'account_url' => $account_url,
];
}
/**
* Get navigation menu items for a location.
*/
private function getMenuData(string $location): array
{
$locations = get_nav_menu_locations();
if (isset($locations[$location])) {
$menu = wp_get_nav_menu_object($locations[$location]);
if ($menu) {
$items = wp_get_nav_menu_items($menu->term_id);
if ($items) {
return $this->navWalker->buildTree($items);
}
}
}
// Fallback for primary: list top-level pages.
if ($location === 'primary') {
return $this->getPagesFallback();
}
return [];
}
/**
* Fallback menu from published pages.
*/
private function getPagesFallback(): array
{
$pages = get_pages([
'sort_column' => 'menu_order,post_title',
'parent' => 0,
]);
$items = [];
foreach ($pages as $page) {
$items[] = [
'id' => $page->ID,
'title' => $page->post_title,
'url' => get_permalink($page->ID),
'target' => '',
'classes' => '',
'active' => is_page($page->ID),
'children' => [],
];
}
return $items;
}
/**
* Get single post/page data.
*/
private function getPostData(): array
{
global $post;
return [
'id' => $post->ID,
'title' => wp_specialchars_decode( get_the_title() ),
'url' => get_permalink(),
'content' => apply_filters('the_content', get_the_content()),
'excerpt' => get_the_excerpt(),
'date' => get_the_date(),
'date_iso' => get_the_date('c'),
'modified' => get_the_modified_date(),
'author' => [
'name' => get_the_author(),
'url' => get_author_posts_url(get_the_author_meta('ID')),
],
'thumbnail' => get_the_post_thumbnail_url($post->ID, 'large') ?: '',
'categories' => $this->getTermsList('category'),
'tags' => $this->getTermsList('post_tag'),
'type' => get_post_type(),
];
}
/**
* Get posts for the main query loop.
*/
private function getPostsLoop(): array
{
global $wp_query;
$posts = [];
if ($wp_query->have_posts()) {
while ($wp_query->have_posts()) {
$wp_query->the_post();
$posts[] = [
'id' => get_the_ID(),
'title' => wp_specialchars_decode( get_the_title() ),
'url' => get_permalink(),
'excerpt' => get_the_excerpt(),
'date' => get_the_date(),
'date_iso' => get_the_date('c'),
'author' => [
'name' => get_the_author(),
'url' => get_author_posts_url(get_the_author_meta('ID')),
],
'thumbnail' => get_the_post_thumbnail_url(null, 'medium_large') ?: '',
'categories' => $this->getTermsList('category'),
'tags' => $this->getTermsList('post_tag'),
'read_more' => __('Read more', 'wp-bootstrap'),
];
}
wp_reset_postdata();
}
return $posts;
}
/**
* Build pagination data.
*/
private function getPagination(): array
{
global $wp_query;
$totalPages = (int) $wp_query->max_num_pages;
$currentPage = max(1, get_query_var('paged'));
if ($totalPages <= 1) {
return [];
}
$pages = [];
for ($i = 1; $i <= $totalPages; $i++) {
$pages[] = [
'number' => $i,
'url' => get_pagenum_link($i),
'is_current' => ($i === $currentPage),
];
}
return [
'pages' => $pages,
'current' => $currentPage,
'total' => $totalPages,
'prev_url' => ($currentPage > 1) ? get_pagenum_link($currentPage - 1) : null,
'next_url' => ($currentPage < $totalPages) ? get_pagenum_link($currentPage + 1) : null,
'prev_text' => __('Previous', 'wp-bootstrap'),
'next_text' => __('Next', 'wp-bootstrap'),
];
}
/**
* Get archive page data.
*/
private function getArchiveData(): array
{
return [
// wp_kses_post() allows safe HTML (headings, links, spans) while stripping
// script/event-handler attributes that could be injected via term descriptions.
'title' => wp_kses_post(get_the_archive_title()),
'description' => wp_kses_post(get_the_archive_description()),
];
}
/**
* Get comments for the current post.
*/
private function getCommentsData(): array
{
$postId = get_the_ID();
$count = (int) get_comments_number($postId);
$comments = get_comments([
'post_id' => $postId,
'status' => 'approve',
'orderby' => 'comment_date_gmt',
'order' => 'ASC',
]);
return [
'list' => $this->buildCommentTree($comments),
'count' => $count,
'title' => sprintf(
_n('%s Comment', '%s Comments', $count, 'wp-bootstrap'),
number_format_i18n($count)
),
'form' => $this->getCommentFormHtml(),
'is_open' => comments_open($postId),
];
}
/**
* Build a nested comment tree from flat comments.
*/
private function buildCommentTree(array $comments, int $parentId = 0): array
{
$tree = [];
foreach ($comments as $comment) {
if ((int) $comment->comment_parent !== $parentId) {
continue;
}
$tree[] = [
'id' => (int) $comment->comment_ID,
// Escape at source — comment_author is user-supplied, store as safe text.
'author' => esc_html($comment->comment_author),
// esc_url() strips dangerous schemes (javascript:, data:) and encodes for HTML.
'author_url' => esc_url($comment->comment_author_url),
'avatar_url' => get_avatar_url($comment, ['size' => 40]),
'date' => get_comment_date('', $comment),
'date_iso' => get_comment_date('c', $comment),
'content' => apply_filters('comment_text', $comment->comment_content, $comment),
'edit_url' => current_user_can('edit_comment', $comment->comment_ID)
? get_edit_comment_link($comment)
: '',
'reply_url' => get_comment_reply_link([
'depth' => 1,
'max_depth' => get_option('thread_comments_depth', 5),
], $comment),
'children' => $this->buildCommentTree($comments, (int) $comment->comment_ID),
];
}
return $tree;
}
/**
* Capture the WordPress comment form HTML.
*/
private function getCommentFormHtml(): string
{
if (! comments_open()) {
return '';
}
ob_start();
comment_form([
'title_reply' => __('Leave a Comment', 'wp-bootstrap'),
'title_reply_before' => '<h3 id="reply-title" class="comment-reply-title h5 mb-3">',
'title_reply_after' => '</h3>',
'class_form' => 'needs-validation',
'class_submit' => 'btn btn-primary',
'submit_button' => '<input name="%1$s" type="submit" id="%2$s" class="%3$s" value="%4$s" />',
'comment_field' => '<div class="mb-3"><label for="comment" class="form-label">' . __('Comment', 'wp-bootstrap') . '</label><textarea id="comment" name="comment" class="form-control" rows="5" required></textarea></div>',
]);
return ob_get_clean();
}
/**
* Get previous/next post navigation.
*/
private function getPostNavigation(): array
{
$prev = get_previous_post();
$next = get_next_post();
$navigation = [];
if ($prev) {
$navigation['previous'] = [
'title' => wp_specialchars_decode( get_the_title($prev) ),
'url' => get_permalink($prev),
];
}
if ($next) {
$navigation['next'] = [
'title' => wp_specialchars_decode( get_the_title($next) ),
'url' => get_permalink($next),
];
}
return $navigation;
}
/**
* Get recent posts for the "More posts" section.
*/
private function getMorePosts(int $count = 3): array
{
$currentId = get_the_ID();
$query = new \WP_Query([
'posts_per_page' => $count,
'post__not_in' => [$currentId],
'orderby' => 'date',
'order' => 'DESC',
]);
$posts = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$posts[] = [
'id' => get_the_ID(),
'title' => wp_specialchars_decode( get_the_title() ),
'url' => get_permalink(),
'date' => get_the_date(),
'date_iso' => get_the_date('c'),
'thumbnail' => get_the_post_thumbnail_url(null, 'medium_large') ?: '',
];
}
wp_reset_postdata();
}
return $posts;
}
/**
* Get sidebar widget data.
*
* If the 'primary-sidebar' widget area has widgets assigned,
* their rendered HTML is returned. Otherwise, fallback data
* (recent posts, tags) is provided for the default Twig sidebar.
*/
private function getSidebarData(): array
{
$widgets_active = is_active_sidebar( 'primary-sidebar' );
$widgets_html = '';
if ( $widgets_active ) {
ob_start();
dynamic_sidebar( 'primary-sidebar' );
$widgets_html = ob_get_clean();
}
return [
'widgets_active' => $widgets_active,
'widgets_html' => $widgets_html,
'recent_posts' => $this->getSidebarRecentPosts(),
'tags' => $this->getSidebarTags(),
];
}
/**
* Get recent posts for sidebar.
*/
private function getSidebarRecentPosts(int $count = 4): array
{
$query = new \WP_Query([
'posts_per_page' => $count,
'orderby' => 'date',
'order' => 'DESC',
]);
$posts = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$posts[] = [
'title' => wp_specialchars_decode( get_the_title() ),
'url' => get_permalink(),
'date' => get_the_date(),
];
}
wp_reset_postdata();
}
return $posts;
}
/**
* Get tags for sidebar tag cloud.
*/
private function getSidebarTags(int $count = 15): array
{
$tags = get_tags([
'number' => $count,
'orderby' => 'count',
'order' => 'DESC',
]);
if (! $tags || is_wp_error($tags)) {
return [];
}
$items = [];
foreach ($tags as $tag) {
$items[] = [
'name' => $tag->name,
'url' => get_tag_link($tag->term_id),
'count' => $tag->count,
];
}
return $items;
}
/**
* Get the active header variant.
*
* @since 0.2.0
*/
private function getHeaderVariant(): string
{
return get_theme_mod('wp_bootstrap_header_variant', 'default');
}
/**
* Get the active footer variant.
*
* @since 0.2.0
*/
private function getFooterVariant(): string
{
return get_theme_mod('wp_bootstrap_footer_variant', 'default');
}
/**
* Get terms list for a taxonomy.
*/
private function getTermsList(string $taxonomy): array
{
$terms = get_the_terms(get_the_ID(), $taxonomy);
if (! $terms || is_wp_error($terms)) {
return [];
}
$list = [];
foreach ($terms as $term) {
$list[] = [
'name' => $term->name,
'url' => get_term_link($term),
];
}
return $list;
}
}