Controller and WooCommerce

I stumbled upon the issue that I can’t access any data from the controller in the woocommerce templates.
I tried with a custom controller as well as with soberwp/controller and it doesn’t work with both.

I implemented the woocommerce templates according to the solution in this post Latest Sage 9, Blade, and WooCommerce

Anyone else stumbled upon that problem?

Are you using regular PHP templates for the Woocommerce ones, or Blade templates? Any data set up in controllers in that way is only going to be available to Blade.

Yes, using Blade templates and I get the issue now, thanks.
Any idea how to overcome the problem though? I’d love to keep using sage 9 with Blade templates for future WooCommerce projects.

Anyone found a solution about this ? The @debug(‘hierarchy’) is outputing correct hierarchy and i created one of the listed controlers. Thanks.

I found a fix.

From what i saw, variable was correctly passing to the woocommerce.blade.php template.
But not in included template (lower in hierarchy, content-product for example, or in my case other custom template (i added) included in woocommerce_content()) because of woocommerce_content() method called in Woocommerce.blade.php.

Here’s what i’ve done to make it work:

  1. Update Woocommerce.blade.php (wich should be located in /resources/views, not within /resources/views/woocommerce/) with @php(App\woocommerce_content(get_defined_vars()))
    to get all binded variables sent to woocommerce_content() - full content:

    @extends(‘layouts.app’)
    @section(‘content’)
    @php(App\woocommerce_content(get_defined_vars()))
    @endsection

  2. We need override wc_get_template_part() method to pass variables, and then pass them to wc_get_template_part filter, here is the method (in my theme so) :

     function wc_get_template_part( $slug, $name = '', $third = null, $args = []) {
         $template = '';
    
         // Look in yourtheme/slug-name.php and yourtheme/woocommerce/slug-name.php
         if ( $name && ! WC_TEMPLATE_DEBUG_MODE ) {
             $template = locate_template( array( "{$slug}-{$name}.php", WC()->template_path() . "{$slug}-{$name}.php" ) );
         }
    
         // Get default slug-name.php
         if ( ! $template && $name && file_exists( WC()->plugin_path() . "/templates/{$slug}-{$name}.php" ) ) {
             $template = WC()->plugin_path() . "/templates/{$slug}-{$name}.php";
         }
    
         // If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php
         if ( ! $template && ! WC_TEMPLATE_DEBUG_MODE ) {
             $template = locate_template( array( "{$slug}.php", WC()->template_path() . "{$slug}.php" ) );
         }
    
         // Allow 3rd party plugins to filter template file from their plugin.
         $template = apply_filters( 'wc_get_template_part', $template, $slug, $name, $args );
    
         if ( $template ) {
             load_template( $template, false );
         }
     } 
    
  3. Update the sage wc_get_template_part filter (that i edited a bit to get blade or PHP if not blade in my resources/views/woocommerce) to pass variables to echo template($bladeTemplate, $args); see :

add_filter(‘wc_get_template_part’, function ($template, $slug, $name, $args) {

$bladeTemplate = false;

// Look in yourtheme/slug-name.blade.php and yourtheme/woocommerce/slug-name.blade.php
if ($name && !WC_TEMPLATE_DEBUG_MODE) {
    $bladeTemplate = locate_template(["{$slug}-{$name}.blade.php", 'resources/views/' . WC()->template_path() . "{$slug}-{$name}.blade.php"]);
}

// If template file doesn't exist, look in yourtheme/slug.blade.php and yourtheme/woocommerce/slug.blade.php
if (!$template && !WC_TEMPLATE_DEBUG_MODE) {
    $bladeTemplate = locate_template(["{$slug}.blade.php", 'resources/views/' . WC()->template_path() . "{$slug}.blade.php"]);
}

if ($bladeTemplate) {
    echo template($bladeTemplate, $args);

    // Return a blank file to make WooCommerce happy
    //return get_theme_file_path('index.php');
    return null;
}

//try to look for PHP files within resources/views/woocommerce
$normalTemplate = false;
if ($name && !WC_TEMPLATE_DEBUG_MODE) {
    $normalTemplate = locate_template(["{$slug}-{$name}.php", 'resources/views/' . WC()->template_path() . "{$slug}-{$name}.php"]);
}
if (!$normalTemplate && !WC_TEMPLATE_DEBUG_MODE) {
    $normalTemplate = locate_template(["{$slug}.php", 'resources/views/' . WC()->template_path() . "{$slug}.php"]);
}

if ($normalTemplate) {
    return get_theme_file_path($normalTemplate); //work even without
}

return $template;

}, PHP_INT_MAX, 4);

Here the other Sage filter in case of :

add_filter('wc_get_template', function ($located, $template_name, $args, $template_path, $default_path) {

    $bladeTemplateName = str_replace('.php', '.blade.php', $template_name);
    $bladeTemplate = locate_template([$bladeTemplateName, 'resources/views/' . WC()->template_path() . $bladeTemplateName]);

    if ($bladeTemplate) {
        return template_path($bladeTemplate, $args);
    }

    return $located;
}, PHP_INT_MAX, 5); 
  1. update woocommerce_content() method

Now we need to edit the woocommerce_content() method called from Woocommerce.blade.php, so i added it in my theme to override it:

function woocommerce_content($args = [])
{
    if (is_singular('product')) {

        while (have_posts()) : the_post();

            wc_get_template_part('content', 'single-product', null, $args);

        endwhile;
    } else {  ?>
        //here i call my custom template with wc_get_template_part and passing args
        <?php if (apply_filters('woocommerce_show_page_title', true)) : ?>
            <h1 class="page-title"><?php /*woocommerce_page_title(); */?></h1>

        <?php endif; ?>

        <?php do_action('woocommerce_archive_description'); ?>

        <?php if (have_posts()) : ?>

            <?php do_action('woocommerce_before_shop_loop'); ?>

            <?php woocommerce_product_loop_start(); ?>

            <?php woocommerce_product_subcategories(); ?>

            <?php while (have_posts()) : the_post(); ?>

                <?php wc_get_template_part('content', 'product', null, $args); ?>

            <?php endwhile; // end of the loop. ?>

            <?php woocommerce_product_loop_end(); ?>

            <?php do_action('woocommerce_after_shop_loop'); ?>

        <?php elseif (!woocommerce_product_subcategories(['before' => woocommerce_product_loop_start(false), 'after' => woocommerce_product_loop_end(false)])) : ?>

            <?php do_action('woocommerce_no_products_found'); ?>

        <?php endif;
    }
} 

(i can edit this method to add template part depending stuff, eg: a specific part for taxonomy templates).

  1. My archive-product and single-product .blade.php contain (from Controller not passing data to WooCommerce template · Issue #16 · soberwp/controller · GitHub) it’s needed to pass variables to the woocommerce.blade.php template):

    //echo App\Template(‘woocommerce’); //defaultly adviced

    //Controller not passing data to WooCommerce template · Issue #16 · soberwp/controller · GitHub
    $template = ‘woocommerce’;
    $data = collect(get_body_class())->reduce(function ($data, $class) use ($template) {
    return apply_filters(“sage/template/{$class}/data”, $data, $template);
    }, );

    echo App\Template($template, $data);

This is it, now i have access to all my variables from Taxonomy Controller into my Taxonomy template !

In the end :

  1. We overrided woocommerce_content() called in woocommerce.blade.php to pass get_defined_vars() (found it used in Sober WP Controller here https://github.com/soberwp/controller/blob/master/controller.php) as all controller variables are defaultly successfuly binded to Woocommerce.blade.php(archive-product and single-product pass the variables to woocommerce.blade.php)
  2. woocommerce_content() will call wc_get_template_part() and i think that is where we loose variables (or directly cause of woocommerce_content() call). so we edited this method to accept a “$args” parameter.
  3. This $args parameter is passed to the filter wc_get_template_part used by Sage to override for blade.
  4. In the sage wc_get_template_part filter, we pass the $args variable to echo template to have all the binded variables available (wich render the blade)
  5. archive-product and single-product pass the variables to woocommerce.blade.php
  6. variables are available in templates
  7. I think the default woocommerce_content() make system loose variables, as all is defaultly working in woocommerce.blade.php but not under.

I don’t know if it’s the good way, but it works as for now.

EDIT:
Idk if related, but title() method from App controller dont work as it’s declared as static method. Removing the static fixes the issue. EDIT: as seen in doc, static methods are not called on Controller instanciation to bind returned data to the view, but need to be called from \App. This is really nice in the end to have both :slight_smile:

EDIT:
The override for single-product.php don’t seems to work… Even if the archive-product.php work perfectly, i’m unable to override the single-product.php. If directly update the woocommerce (/wp-plugins/) files, it works. So this is the template override that is not working here, i don’t get why. If anyone have idea. I finally had to tweak the template_include filter to change path for single-product and correctly get my Blade.

//fix for single-product.php
if(str_contains($template, ‘single-product.php’)) {
$template = get_stylesheet_directory() . ‘/views/woocommerce/single-product.blade.php’;
}

3 Likes

Thanks for all the code, but I am stuck with a couple of questions.

I am currently developing a theme with the Sage 9.0.0-beta.4 library and Woocommerce integration.

What version of Sage did you use for the code you posted? I more or less get a result when the original files in the Woocommerce plugin directory (archive-product.php & single-product.php) are completely quoted out and I add echo App\Template('woocommerce'); at the end of these files.

Then everything loads fine, but when you would update Woocommerce it will override these changes. This is, offcourse, a unwanted situation. It needs to keep working no matter what I would update.

Did you do any of these things, or just left them as is when you install Woocommerce for the first time?

Also, which Controller are you using? Is it under resources/controllers or app/Controllers ?
I tried both but didn’t get any result.

To get this straight, these are the steps you took:

  1. Created a file named woocommerce.blade.php in resources/views containing the @php(App\woocommerce_content(get_defined_vars()))

  2. You are showing the original wc_get_template_part function from wc-core-functions.php in the Woocommerce plugin directory. But didn’t do anything with it right?

  3. The code you applied to override the original wc_get_template_part function and did you put it in filters.php under themes/your_theme_name/app or setup.php, or just changed the original?

In case of php files you use the first piece of code and incase of blade files you use the second? Or you just put both in them and let the site figure it out?

  1. Here you update the woocommerce_content() function with your own, did you let the original(Woocommerce plugin directory) be as is and put this code in the setup.php under themes/your_theme_name/app or just edited the original?

  2. You apply some code to the archive.blade.php & single-product.blade.php pretty straight forward.

Thanks in advance!

Edit December 22nd

Together with a colleague found out that we had to change the following to get the correct templates:

1. Edit helpers.php

Edit the function filter_templates in helpers.php under themes/your_theme_name/app to look in the resources/views/woocommerce directory like so:

function filter_templates($templates)
{
return collect($templates)
        ->map(function ($template) {
            return preg_replace('#\.(blade\.)?php$#', '', ltrim($template));
        })
        ->flatMap(function ($template) {
            $paths = apply_filters('sage/filter_templates/paths', ['views', 'resources/views', 'resources/views/woocommerce']);
            return collect($paths)
                ->flatMap(function ($path) use ($template) {
                    return [
                        "{$path}/{$template}.blade.php",
                        "{$path}/{$template}.php",
                        "{$template}.blade.php",
                        "{$template}.php",
                    ];
                });
        })
        ->filter()
        ->unique()
        ->all();
}

This lets the function locate_template also look in the resources/views/woocommerce folder, where we declare our blade templates

2. Change the wc_get_template_part because of #1.

Wherever locate_template is used look for resources/views . WC()->template_path() . and remove the resources/views part of it.

This change needs to be done because the filter_templates function, which we changed in #1 already looks in the resources/views directory but not the resources/views/woocommerce directory.

  1. In the resources root we created a directory named woocommerce with the archive-product.php (for example) which contained this:
<?php
    $template = 'woocommerce.archive-product';
    $data = collect(get_body_class())->reduce(function ($data, $class) use ($template) {
        return apply_filters("sage/template/{$class}/data", $data, $template);
    }, []);

    echo App\Template($template, $data);

Notice that $template is filled with the woocommerce.archive-product, this is done so the template that we look for in resources/views/woocommerce is archive-product.blade.php

4. create archive-product.blade.php and more

As you could have guessed reading #3 we need to create a archive-product.blade.php in the resources/views/woocommerce folder.

This file contains the normal blade template stuff like @extends and all that yada yada. But also this:

@while(have_posts()) @php(the_post())
    {{ App\wc_get_template_part('content', 'product', null, get_defined_vars()) }}
@endwhile

With this we load a new blade template for the products named content-product.blade.php (more on #5) and we really get control over our Woocommerce content, you can style it the way you would like, fetch the needed data etc.

5. Create the last blade view and see the awesomeness rise

As mentioned in #4 we also created a new content-product.blade.php because Woocommerce loads this file by default, and we wanted control over this. So we looked it up in the Woocommerce plugin templates folder and found it there, so we created our own file named content-product.blade.php in the resources/views/woocommerce folder which contains:

@php
  global $product;
@endphp

<li @php(post_class())>
  {!! do_action( 'woocommerce_before_shop_loop_item' ) !!}
  {!! do_action( 'woocommerce_before_shop_loop_item_title' ) !!}
  {!! do_action( 'woocommerce_shop_loop_item_title' ) !!}
  {!! do_action( 'woocommerce_after_shop_loop_item_title' ) !!}
  {!! do_action( 'woocommerce_after_shop_loop_item' ) !!}
</li>

The do_action's are just tests. With a simple var_dump you can check the contents of $product and use as needed.


The same goes for all the other files that Woocommerce loads when viewing a product, ordering etc.

Hope this helps someone who crashed at the same point I did.

Edit 8th of January

I further changed the setup.php so I can call wc_get_template function within blade files.
Here is what I added to the setup.php:

function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) {
    if ( ! $template_path ) {
        $template_path = WC()->template_path();
    }

    // Look within passed path within the theme - this is priority.
    $template = locate_template(
        array(
            trailingslashit( $template_path ) . $template_name,
            $template_name,
        )
    );

    // Get default template/
    if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) {
        $template = $default_path . $template_name;
    }

    // Return what we found.
    return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path );
}

function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {

    if ( ! empty( $args ) && is_array( $args ) ) {
        extract( $args );
    }

    $located = wc_locate_template( $template_name, $template_path, $default_path );

    if ( ! file_exists( $located ) ) {
        wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '<code>' . $located . '</code>' ), '2.1' );
        return;
    }

    // Allow 3rd party plugin filter template file from their plugin.
    $located = apply_filters( 'wc_get_template', $located, $template_name, $args, $template_path, $default_path );

    do_action( 'woocommerce_before_template_part', $template_name, $template_path, $located, $args );

    include( $located );

    do_action( 'woocommerce_after_template_part', $template_name, $template_path, $located, $args );
}

The filters.php file already had this, like mentioned in @mtxz 's example:

add_filter('wc_get_template', function ($located, $template_name, $args, $template_path, $default_path) {

    $bladeTemplateName = str_replace('.php', '.blade.php', $template_name);
    $bladeTemplate = locate_template([$bladeTemplateName, 'resources/views/' . WC()->template_path() . $bladeTemplateName]);

    if ($bladeTemplate) {
        return template_path($bladeTemplate, $args);
    }

    return $located;
}, PHP_INT_MAX, 5);

In the wc_locate_template function I made a minor change, basically just deleted one if statement:

if ( ! $default_path ) {
        $default_path = WC()->plugin_path() . '/templates/';
}

I copied it’s contents from woocommerce/includes/wp-core-functions.php, therefore this delete. It looks for the templates directory in the plugin folder.

With this code added to the setup.php I can make use of custom templates instead of parts.

Example:

Woocommerce makes use of the wc_get_template function to call the title of a single product.

I wanted some extra flexibility with this, so I can customize the title.php file without it being overwritten whenever Woocommerce updated.

Now I have this folder structure under views (sage 9):

views
| - woocommerce (here live all the basic Woocommerce related blade files)
|    | - single-product (here live my custom templates related to single product)
|    |    | title.php etc.
|    | archive.product.blade.php etc.
| 404.blade.php etc.

Like the folder single-product I am now able to add directories for each individual directory(that I want to override) that exists in Woocommerce’s templates directory and write my own code based on the original files.

So I created the directory single-product under the woocommerce directory in the views directory.

The templates are being used in the following order:

  1. single-product.blade.php
  2. single-product.blade.php looks for content-single-product.blade.php
  3. content-single-product.blade.php looks for single-product/title.php

If found confusing, look in the templates directory in the Woocommerce plugin, it will make more sense.

single-product.blade.php

@extends('layouts.app')

@include('partials.headers.woocommerce-header')

@section('content')
  <div class="container">
    <div class="row">
      @while(have_posts()) @php(the_post())

      {{ App\wc_get_template_part('content', 'single-product', null, get_defined_vars()) }}

      @endwhile
    </div>
  </div>
@endsection

Here we call for the wc_get_template_part function located under the App namespace.
It will look for a template named content-single-product within the same directory.

content-single-product.blade.php

@php
  global $product;
@endphp

<div class="col-6">
  <div class="img-fluid">
    {{ App\wc_get_template('single-product/image.php') }}
  </div>
</div>
<div class="col-6">
  {{ App\wc_get_template('single-product/title.php') }}
</div>

In this file, we go hunting for the image.php and title.php in the single-product directory, also under the App namespace.

single-product/title.php

the_title( '<h1 class="product_title entry-title">', '</h1>' );

Here we will load the title with it’s size & classes.


Hope this makes sense & helps

3 Likes

Wow thanks for all the effort!

If I understand correctly, the difference with @arthurkirkosa approach in this topic is that the woocommerce folder is located directly under resources and you won’t be able to use blade templates right?

Still pretty confusing though with all the different snippets from you and @mtxz
Any chance I could have a look at your setup code in a gist or something like that?

That would be awesome!
Thanks

No problem!

After reading it is a little confusing indeed, I will try to setup a Gist/Github directory with all the needed code within the next week.

I didn’t use any of the code from the topic you mention, when the Gist/Github directory is up and running I will post a link!

2 hours later

Here is the Github Repository with a readme, feel free to post issues, fork it etc.

Hope this helps!

1 Like

That would be brilliant!
I’m setting up a WooCommerce site too at the moment, and would really like to use the whole Sage setup with Blade templating and the Sober controllers like i do with ‘normal’ projects :slight_smile:

2 Likes

.
. Please update the result when you get done. I also really confuse about the code…

I’m trying to implement your code, but I’m missing your woocommerce-header.
Is this essential for your setup?

@include('partials.headers.woocommerce-header')

Thanks!

Update:
Never mind, I don’t think the @include('partials.headers.woocommerce-header') rule is neccessary.

Everything is working pretty well actually!

I removed the woocommerce_content & wc_get_template functions from your app/setup.php and the custom archive-product & single-product templates are still working!

I also think you’re working with an older version of WooCommerce, because I’m getting errors from this file: resources/views/woocommerce/content-single-product.blade.php

Looking at the original WooCommerce templates from the plugin folder, these 2 templates don’t exist in WC version 3.2.6:

{{ App\wc_get_template('add-to-cart/simple.php') }}
{{ App\wc_get_template('single-product/reviews.php') }}
{{ App\wc_get_template_part('single', 'product-review') }}

If I replace them with:

{{ App\wc_get_template('single-product/add-to-cart/simple.php') }}
{{ App\wc_get_template('single-product-reviews.php') }}

The errors are gone!

Thanks!

I noticed someone else on Github had the same issue, I will try to update the repo tonight.

@Twansparant Great to hear that the issue has been resolved.

I indeed have a ‘older’ version, 3.2.5 :sweat_smile:

Tonight I will have a look if the woocommerce_content & wc_get_template functions can be removed from the app/setup.php, this could be because they also exist in app/filters.php.

The last part of your post is preference related. In the Woocommerce version I have installed, I found it very neat that the add-to-cart etc. are in their own folder within the Woocommerce folder, therefore I implemented it the same way.

Also because then you can use just “one” file for all the products to be “added to the cart” (I hope) if you get what I mean.

Ah ok, but that file you referenced was not in the repo, that’s why it was causing errors.

1 Like

Right, I thought it would be unnecessary to include all the template files I use in my code, since we are all developers :wink:

If wanted/needed I could still provide them :+1:

No that’s allright, I was just checking if the path was correct :slight_smile:

1 Like

Did anyone test the variable product and get it to work correctly with these instructions and code snippets? For example, the product image or price will be updated to the product view when the variation changes. In my testing, the price of a variation product will only change in the shopping cart and not in a single product view as it should work.

Did anyone test the variable product and get it to work correctly with these instructions and code snippets?

I just did, and can confirm the price variations are working fine for me in the archive-product and single-product view?

Please refer to :

Hello i found issue

you need in filters.php

replace add_filter(‘template_include’, function ($template) {

}

to

add_filter(‘template_include’, function ($template) {
$isTax = str_contains($template,‘taxonomy-product-cat’);

if (!$isTax) {
    collect( [ 'get_header', 'wp_head' ] )->each( function ( $tag ) {
        ob_start();
        do_action( $tag );
        $output = ob_get_clean();
        remove_all_actions( $tag );
        add_action( $tag, function () use ( $output ) {
            echo $output;
        } );
    } );
    $data = collect( get_body_class() )->reduce( function ( $data, $class ) use ( $template ) {
        return apply_filters( "sage/template/{$class}/data", $data, $template );
    }, [] );
    
    if ( $template ) {
        echo template( $template, $data );
        return get_stylesheet_directory() . '/index.php';
    }
    
    return $template;
} else {
    return $template;
}

}, PHP_INT_MAX);