PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / shortcode / single-product.php

single-product.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/shortcode/single-product.php

152 lines 4.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace StoreEngine\Shortcode;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use StoreEngine\Utils\Helper;
9 use StoreEngine\Utils\Template;
10
11 /**
12 * [storeengine_single_product] — the full single-product view.
13 *
14 * Two modes:
15 *
16 * - With `id` (or `slug`): renders that product's complete single-product view
17 * (gallery, summary, add to cart, description, FAQ, reviews) anywhere — a
18 * page, a widget, a builder block. The queried post is swapped out for the
19 * duration and restored afterwards, so it is safe inside another loop.
20 * - With no attributes: the original behaviour, rendering the queried product
21 * through the full `single-product.php` page template. Unchanged so existing
22 * content using the bare shortcode keeps working.
23 */
24 class SingleProduct {
25
26 /**
27 * Products currently mid-render, keyed by id.
28 *
29 * A product whose description embeds itself — or embeds a second product
30 * that embeds the first — would otherwise recurse until the process dies.
31 *
32 * @var array<int,bool>
33 */
34 protected static array $rendering = [];
35
36 public function __construct() {
37 add_shortcode( 'storeengine_single_product', array( $this, 'render' ) );
38 }
39
40 /**
41 * @param array|string $atts Shortcode attributes.
42 */
43 public function render( $atts = [] ): string {
44 $atts = shortcode_atts(
45 [
46 'id' => 0,
47 'slug' => '',
48 ],
49 $atts,
50 'storeengine_single_product'
51 );
52
53 $product_id = $this->resolve_product_id( $atts );
54
55 if ( $product_id ) {
56 return $this->render_product( $product_id );
57 }
58
59 // Legacy, attribute-less form: render the queried product.
60 ob_start();
61 Template::get_template( 'single-product.php' );
62 $output = Helper::remove_line_break( ob_get_clean() );
63
64 return Helper::remove_tag_space( $output );
65 }
66
67 /**
68 * Turn the `id` / `slug` attributes into a product id.
69 *
70 * @param array $atts Parsed attributes.
71 */
72 protected function resolve_product_id( array $atts ): int {
73 $id = absint( $atts['id'] );
74
75 if ( $id ) {
76 return $id;
77 }
78
79 $slug = sanitize_title( (string) $atts['slug'] );
80
81 if ( ! $slug ) {
82 return 0;
83 }
84
85 $post = get_page_by_path( $slug, OBJECT, Helper::PRODUCT_POST_TYPE );
86
87 return $post ? (int) $post->ID : 0;
88 }
89
90 /**
91 * Render one product's single-product content, standalone.
92 *
93 * @param int $product_id Product id.
94 */
95 protected function render_product( int $product_id ): string {
96 $post = get_post( $product_id );
97
98 if ( ! $post || Helper::PRODUCT_POST_TYPE !== $post->post_type ) {
99 return '';
100 }
101
102 // Embedding must not become a way to read a draft/private/trashed
103 // product that the visitor could not open directly.
104 if ( 'publish' !== $post->post_status && ! current_user_can( 'read_post', $product_id ) ) {
105 return '';
106 }
107
108 if ( isset( self::$rendering[ $product_id ] ) ) {
109 return '';
110 }
111
112 $product = Helper::get_product( $product_id );
113
114 if ( ! $product ) {
115 return '';
116 }
117
118 self::$rendering[ $product_id ] = true;
119
120 // Swap the globals the single-product templates read from, remembering
121 // whatever loop we may have interrupted.
122 $prev_post = $GLOBALS['post'] ?? null;
123 $prev_product = $GLOBALS['product'] ?? null;
124
125 $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
126 $GLOBALS['product'] = $product; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
127 setup_postdata( $post );
128
129 ob_start();
130 echo '<div class="storeengine-single-product storeengine-single-product--embed">';
131 Template::get_template_part( 'content', 'single-product' );
132 echo '</div>';
133 $html = ob_get_clean();
134
135 $GLOBALS['post'] = $prev_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
136 $GLOBALS['product'] = $prev_product; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
137
138 if ( $prev_post instanceof \WP_Post ) {
139 setup_postdata( $prev_post );
140 } else {
141 wp_reset_postdata();
142 }
143
144 unset( self::$rendering[ $product_id ] );
145
146 // Deliberately not run through Helper::remove_line_break() like the
147 // legacy path above: it collapses every whitespace run, which would
148 // flatten <pre> blocks inside a merchant's product description.
149 return $html;
150 }
151 }
152