|string $atts */ public function form( $atts = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- shortcode signature Assets::need(); $privacyId = (int) Plugin::opt( 'privacy_page_id' ); $vars = [ 'title' => Multilingual::translate( 'form_title', (string) Plugin::opt( 'form_title' ) ), 'action' => admin_url( 'admin-post.php' ), 'nonce' => wp_create_nonce( FormHandler::NONCE ), 'redirect' => self::currentUrl(), 'fields' => Plugin::fields(), 'privacy_url' => $privacyId > 0 ? (string) get_permalink( $privacyId ) : '', 'thanks' => Multilingual::translate( 'thanks_text', (string) Plugin::opt( 'thanks_text' ) ), 'state' => FormHandler::flash(), // keys: ok, errors, old 'lang' => Multilingual::current(), 'text_min' => Validator::TEXT_MIN, 'text_max' => Validator::TEXT_MAX, ]; /** @param array $vars Template variables of the review form. */ $vars = (array) apply_filters( 'admirly_form_vars', $vars ); return self::render( 'form', $vars ); } /** @param array|string $atts */ public function wall( $atts = [] ): string { // columns: 1–4; limit: 0 = all approved reviews. Extensions add their own attributes through `admirly_wall_atts`. $defaults = [ 'columns' => 3, 'limit' => 0, ]; /** @param array $defaults */ $defaults = (array) apply_filters( 'admirly_wall_atts', $defaults ); $a = shortcode_atts( $defaults, (array) $atts, 'admirly_wall' ); Assets::need(); $all = self::visibleItems( -1 ); $items = (int) $a['limit'] > 0 ? array_slice( $all, 0, (int) $a['limit'] ) : $all; $vars = [ 'items' => $items, 'cols' => max( 1, min( 4, (int) $a['columns'] ) ), 'avg' => self::average( $all ), 'count' => count( $all ), 'form_url' => self::formUrl(), ]; /** @param array $vars @param array $a Shortcode attributes. @param array> $all Every approved review. */ $vars = (array) apply_filters( 'admirly_wall_vars', $vars, $a, $all ); return self::render( 'wall', $vars ); } /** @param array|string $atts */ public function strip( $atts = [] ): string { // layout: grid (default) | scroll (horizontal strip); link: the "See all reviews" link below $a = shortcode_atts( [ 'limit' => 6, 'layout' => 'grid', 'link' => 1, ], (array) $atts, 'admirly_strip' ); Assets::need(); $n = max( 1, min( 12, (int) $a['limit'] ) ); // Bounded query: language filtering may drop some rows, so fetch a small multiple when it is active. $fetch = Plugin::opt( 'language_mode' ) === 'current' ? $n * 4 : $n; $items = array_slice( self::visibleItems( $fetch ), 0, $n ); return self::render( 'strip', [ 'items' => $items, 'layout' => $a['layout'] === 'scroll' ? 'scroll' : 'grid', 'show_link' => (bool) (int) $a['link'], 'wall_url' => self::wallUrl(), ] ); } /** Prints one review card (shared by wall and strip). @param array $item */ public static function printCard( array $item ): void { $item['role_label'] = Cpt::cardLabel( $item ); $item['initials'] = self::initials( $item['name'] ); /** @param array $item Card data; extensions may add keys used by their templates. */ $item = (array) apply_filters( 'admirly_card_item', $item ); self::display( 'card', [ 'item' => $item ] ); } /** Approved reviews for the current language (per the language_mode setting). @return array> */ public static function visibleItems( int $limit ): array { return Lang::filter( self::items( $limit ), (string) Plugin::opt( 'language_mode' ), Multilingual::current() ); } /** Approved reviews, newest first. @return array> */ public static function items( int $limit ): array { if ( $limit === 0 ) { return []; } $posts = get_posts( [ 'post_type' => Plugin::CPT, 'post_status' => 'publish', 'posts_per_page' => $limit, 'orderby' => 'date', 'order' => 'DESC', 'no_found_rows' => true, 'suppress_filters' => false, ] ); return array_map( [ Cpt::class, 'item' ], $posts ); } /** @param array> $items */ public static function average( array $items ): float { $rated = array_filter( array_column( $items, 'rating' ), static function ( $r ) { return $r > 0; } ); if ( $rated === [] ) { return 0.0; } return round( array_sum( $rated ) / count( $rated ), 1 ); } public static function initials( string $name ): string { $parts = preg_split( '/\s+/', trim( $name ) ) ?: []; $ini = ''; foreach ( array_slice( $parts, 0, 2 ) as $p ) { $ini .= mb_strtoupper( mb_substr( $p, 0, 1 ) ); } return $ini !== '' ? $ini : '•'; } public static function formUrl(): string { $id = Multilingual::pageId( (int) Plugin::opt( 'form_page_id' ) ); return $id > 0 ? (string) get_permalink( $id ) : ''; } public static function wallUrl(): string { $id = Multilingual::pageId( (int) Plugin::opt( 'wall_page_id' ) ); return $id > 0 ? (string) get_permalink( $id ) : ''; } private static function currentUrl(): string { $id = get_queried_object_id(); if ( $id ) { return (string) get_permalink( $id ); } // Fallback: the current request URL without the one-time flash token (safe on subdirectory installs). $host = sanitize_text_field( wp_unslash( (string) ( $_SERVER['HTTP_HOST'] ?? '' ) ) ); $uri = sanitize_text_field( wp_unslash( (string) ( $_SERVER['REQUEST_URI'] ?? '/' ) ) ); return esc_url_raw( remove_query_arg( 'admirly', set_url_scheme( 'http://' . $host . $uri ) ) ); } /** Renders a template to a string (shortcodes must return their markup). @param array $vars */ public static function render( string $template, array $vars ): string { ob_start(); self::display( $template, $vars ); return (string) ob_get_clean(); } /** Includes a template in place; templates escape their own output. @param array $vars */ public static function display( string $template, array $vars ): void { $file = ADMIRLY_DIR . 'templates/' . $template . '.php'; if ( ! is_file( $file ) ) { return; } ( static function () use ( $file, $vars ) { extract( $vars, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract require $file; } )(); } }