templatePath = WC_BOOTSTRAP_PATH . 'templates/'; } /** * Register the template override hooks. * * Only registers if the parent theme's TwigService is available. * * @return void */ public function register(): void { if ( ! class_exists( TwigService::class ) ) { return; } add_action( 'woocommerce_before_template_part', [ $this, 'beforeTemplatePart' ], 10, 4 ); add_action( 'woocommerce_after_template_part', [ $this, 'afterTemplatePart' ], 10, 4 ); } /** * Before WooCommerce includes a PHP template. * * If a matching Twig template exists, renders it and starts output * buffering to capture (and later discard) the PHP template output. * * @param string $templateName Template name (e.g., 'cart/cart.php'). * @param string $templatePath Template path override. * @param string $located Full path to the located PHP template. * @param array $args Template context variables. * @return void */ public function beforeTemplatePart( string $templateName, string $templatePath, string $located, array $args ): void { $twigTemplate = $this->resolveTwigTemplate( $templateName ); if ( null === $twigTemplate ) { return; // No Twig override — let PHP render normally. } try { $twig = TwigService::getInstance(); $context = $args; // Inject the global $product into the Twig context. // WooCommerce PHP templates access it via `global $product;` but Twig // templates have isolated variable scopes and need it passed explicitly. if ( ! isset( $context['product'] ) && ! empty( $GLOBALS['product'] ) ) { $context['product'] = $GLOBALS['product']; } echo $twig->render( $twigTemplate, $context ); // Buffer the upcoming PHP include so we can discard it. ob_start(); $this->bufferStack[] = $templateName; } catch ( \Throwable $e ) { // Twig render failed — let PHP render as fallback. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( 'WC Bootstrap: Twig render failed for %s — %s', $templateName, $e->getMessage() ) ); } } } /** * After WooCommerce includes a PHP template. * * If we started buffering for this template, discards the PHP output. * * @param string $templateName Template name. * @param string $templatePath Template path override. * @param string $located Full path to the located PHP template. * @param array $args Template context variables. * @return void */ public function afterTemplatePart( string $templateName, string $templatePath, string $located, array $args ): void { if ( ! empty( $this->bufferStack ) && end( $this->bufferStack ) === $templateName ) { ob_end_clean(); // Discard PHP template output. array_pop( $this->bufferStack ); } } /** * Resolve a WooCommerce template name to a Twig template path. * * Maps 'cart/cart.php' to 'cart/cart.html.twig' and checks that * the file exists in the child theme's templates/ directory. * * @param string $templateName WooCommerce template name. * @return string|null Twig template path relative to templates/, or null if not found. */ private function resolveTwigTemplate( string $templateName ): ?string { $twigName = preg_replace( '/\.php$/', '.html.twig', $templateName ); $fullPath = $this->templatePath . $twigName; if ( file_exists( $fullPath ) ) { return $twigName; } return null; } }