Viewed   4.4k times

I've been pulling my hair out with this all day, please forgive the short description, I just need to validate my sanity!!

As the title says, I'm trying to create two or three different single-product layouts within woocommerce. The minimum is trying to achieve would be to have multiple single-product folders each with their own name and configurations.

No matter which way I try to override the single-product.php and make this file use logic to check for the product_cat and give out templates accordingly, I either the page not loading or what I write is skipped over and the default is loaded.

So far I've been through the following methods multiple times, trying to piece together what may be outdated code or otherwise causing all the fuss:

WooCommerce - How to create multiple single product template based on category?

Woocommerce single product - template by categories

Creating a different template file for certain Product Categories - Wordpress/Woocommerce?

I was more hoping someone may know something about this that I'm obviously missing as there are many articles out there on what to try and most claim success but I'm unable to do so.

[Update] using template_include code from @helgatheviking

No success just yet but here's where I'm up to;

File structure team-shops is the category I'm trying to get

  • /mytheme/woocommerce/single-product.php - no changes
  • /mytheme/woocommerce/content-single-product.php
  • /mytheme/woocommerce/single-product-team-shops.php - changed line 37 to<?php wc_get_template_part( 'content', 'single-product-team-shops' ); ?>
  • /mytheme/woocommerce/content-single-product-team-shops.php - added additional id to #product-id (line 39)
  • /mytheme/woocommerce/single-product-team-shops/ folder with all single product files to change.

As I said above this isn't working but hopefully with what I've provided the problem may be more obvious.

Thanks again for any help :)

[Think I've got it]

Ok so I think I've something that works, at least for now it seems to, still have some further testing to do but any thoughts more than welcome, this is what I've got so far along with a single-product-team-shops folder in my theme

add_filter( 'woocommerce_locate_template', 'so_25789472_locate_template', 10, 3 );

function so_25789472_locate_template( $template, $template_name, $template_path ){

    $term_id = 2854;
    $taxonomy_name = 'product_cat';
    $term_children = get_term_children( $term_id, $taxonomy_name );

    foreach ( $term_children as $child ) {

    // on single posts with mock category and only for single-product/something.php templates
    if( is_product() && has_term( $child, 'product_cat' ) && strpos( $template_name, 'single-product/') !== false ){

        // replace single-product with single-product-mock in template name
        $mock_template_name = str_replace("single-product/", "single-product-team-shops/", $template_name );

        // look for templates in the single-product-mock/ folder
        $mock_template = locate_template(
            array(
                trailingslashit( $template_path ) . $mock_template_name,
                $mock_template_name
            )
        );

        // if found, replace template with that in the single-product-mock/ folder
        if ( $mock_template ) {
            $template = $mock_template;
        }
    }}

    return $template;
}

 Answers

2

Use a single-product-custom.php template for any product in the "custom" category:

add_filter( 'template_include', 'so_43621049_template_include' );

function so_43621049_template_include( $template ) {
  if ( is_singular('product') && (has_term( 'custom', 'product_cat')) ) {
    $template = get_stylesheet_directory() . '/woocommerce/single-product-custom.php';
  } 
  return $template;
}

NB: If you use the same action hooks in your single-product-custom.php template you will get the same look as the default single-product.php. You could 'rename' all the hooks and then could add existing functions (such as those for add to cart buttons, etc) to the new hooks in order to achieve a totally custom look.

Friday, September 9, 2022
3

There is probably more than one way to do this, but they all kind of turn around the idea of filtering the template before it is included.

You could totally skip WooCommerce's simple-product.php template (without needing to override that template) and go directly to simple-product-mock.php and create everything there. You'd do that by filtering template_include.

add_filter( 'template_include', 'so_25789472_template_include' );

function so_25789472_template_include( $template ) {
  if ( is_singular('product') && (has_term( 'mock', 'product_cat')) ) {
    $template = get_stylesheet_directory() . '/woocommerce/single-product-mock.php';
  } 
  return $template;
}

You could edit single-product-mock.php to call content-single-product-mock.php and hard-code that file. Nothing requires you to keep using Woo's hooks and functions. The point of them is just to make it easy for you to customize.

Or to be really tricky, you could duplicate templates such as single-product/title.php into a single-product-mock folder... ex: single-product-mock/title.php and then any time we're on a single product template in the mock category, we'll intercept calls to the single-product/something.php template and redirect them to single-product-mock/something.php if it exists and keep pointing to the single-product/something.php if it does not. We'll do this via the woocommerce_locate_template filter.

add_filter( 'woocommerce_locate_template', 'so_25789472_locate_template', 10, 3 );

function so_25789472_locate_template( $template, $template_name, $template_path ){

    // on single posts with mock category and only for single-product/something.php templates
    if( is_product() && has_term( 'mock', 'product_cat' ) && strpos( $template_name, 'single-product/') !== false ){

        // replace single-product with single-product-mock in template name
        $mock_template_name = str_replace("single-product/", "single-product-mock/", $template_name );

        // look for templates in the single-product-mock/ folder
        $mock_template = locate_template(
            array(
                trailingslashit( $template_path ) . $mock_template_name,
                $mock_template_name
            )
        );

        // if found, replace template with that in the single-product-mock/ folder
        if ( $mock_template ) {
            $template = $mock_template;
        }
    }

    return $template;
}

Update to filter wc_get_template instead.

/**
 * Change wc template part for product with a specific category
 *
 * @param  string $templates
 * @param  string $slug
 * @param  string $name
 * @return string
 */
function so_25789472_get_template_part( $template, $slug, $name ) {
  if ( $slug == 'content' && $name = 'single-product' && has_term( 'test', 'product_cat' ) ) {
    $template = locate_template( array( WC()->template_path() . 'content-single-product-test.php' ) );
  } 
  return $template;
}
add_filter( 'wc_get_template_part', 'so_25789472_get_template_part', 10, 3 );
Tuesday, December 27, 2022
5

Well I found the solve here
https://developer.wordpress.org/reference/functions/get_the_terms/

and here's the code :

$test = $_product->get_attributes();
if(!empty($test)) {
    $terms = get_the_terms($product_id, "pa_size");
    foreach ( $terms as $term ) {
    echo "<option>" . $term->name . "</option>";
    }
} else {
    echo "Not Specified";
}
Wednesday, August 31, 2022
 
2

If you have already integrate WooCommerce in your theme, you might to override woocommerce_content() function.

It works perfectly on WooCommerce 2.5.5

  • Place you custom template files to your_theme/woocommerce/

  • Add code-snippet listed below to your theme functions.php:

/** 
 * Override 'woocommerce_content' function
 */

if ( ! function_exists( 'woocommerce_content' ) ) {

/**
 * Output WooCommerce content.
 *
 * This function is only used in the optional 'woocommerce.php' template.
 * which people can add to their themes to add basic woocommerce support.
 * without hooks or modifying core templates.
 *
 */
function woocommerce_content() {

    if ( is_singular( 'product' ) ) {

        while ( have_posts() ) : the_post();

            // Template depends from category slug
        
            if ( has_term( 'my-cat-slug', 'product_cat' ) ) {

              woocommerce_get_template_part( 'content', 'single-product-dogs' );  

            } else {

              woocommerce_get_template_part( 'content', 'single-product' ); 

            }

        endwhile;

    } else { ?>

        <?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' ); ?>

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

            <?php woocommerce_product_loop_end(); ?>

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

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

            <?php wc_get_template( 'loop/no-products-found.php' ); ?>

        <?php endif;

    }
  }
}
Friday, October 28, 2022
 
3

The action(s) you want are these:

add_action('woocommerce_update_product', 'productPublished');

add_action('woocommerce_new_product', 'productPublished');

function productPublished($product_id){
    //...
}

You can find them both (where they are triggered from) in the Woo source code here:

https://docs.woocommerce.com/wc-apidocs/source-class-WC_Product_Data_Store_CPT.html#237

I actually looked them up backwards by first finding where the source code for saving products was, and then looked for hooks in those methods (create/update).

 //found on Line 134 method create
 do_action( 'woocommerce_new_product', $id );


 //found on Line 237 method update
 do_action( 'woocommerce_update_product', $product->get_id() );

You'll also have to change this line:

function productPublished ($ID , $post , $update){
    $product = wc_get_product( $post->ID);
}

To

function productPublished($product_id){
    $product = wc_get_product( $product_id);
   //....
}

I don't think the other arguments (that are missing) matter to your code. Such as if it's an update or a new product, I also don't see $post used except to get the product ID, which we already have.

UPDATE (determine args for callback)

If you are not sure about the callback's arguments, you can look in the source code (as I did above) Or if you can find it in documentation Or as a last resort you can simply output them. The best way to output them is one of these 3

  • func_get_args() - "Returns an array comprising a function's argument list" http://php.net/manual/en/function.func-get-args.php
  • debug_print_backtrace() - "Prints a backtrace (similar to stacktrace)" https://secure.php.net/manual/en/function.debug-print-backtrace.php
  • Exception::getTraceAsString() try/catch and throw an exception to output the stacktrace http://php.net/manual/en/exception.gettraceasstring.php

Here is an example I built with a minimally/simplified working hook system modeled after WordPress's. For testing reasons and because it's really not that hard to build when you know how it works:

//global storage (functions, not classes)
global $actions;
$actions = [];

//substitute wordpress add_action function (for testing only) 
function add_action($action, $callback, $priorty=10, $num_args=1){
    global $actions;
    $actions[$action][] = [
         'exec' => $callback,
         'priorty'=>$priorty,
         'num_args' => $num_args
    ];

}
//substitute wordpress do_action function (for testing only) 
function do_action($action, ...$args){
    // PHP5.6+ variadic (...$args) wraps all following arguments in an array inside $args (sort of the opposite of func_get_args)
    global $actions;

    if(empty($actions[$action])) return;
    //sort by priory
    usort($actions[$action], function($a,$b){
       //PHP7+ "spaceship" comparison operator (<=>)
       return $b['priorty']<=>$a['priorty'];
    });

    foreach($actions[$action] as $settings){
        //reduce the argument list
        call_user_func_array($settings['exec'], array_slice($args, 0, $settings['num_args']));
    }
}

//test callback
function callback1(){
     echo "nn".__FUNCTION__."n";
    print_r(func_get_args());
}

//test callback
function callback2(){
    echo "nn".__FUNCTION__."n";
    try{
        //throw an empty exception
        throw new Exception;
    }catch(Exception $e){
         //pre tags preserve whitespace (white-space : pre)
        echo "<pre>";
        //output the stacktrace of the callback
        echo $e->getTraceAsString();
        echo "nn</pre>";
   }
}

//Register Hook callbacks, added in opposite order, sorted by priority
add_action('someaction', 'callback2', 5, 4);
add_action('someaction', 'callback1', 1, 5);

//execute hook
do_action('someaction', 1234, 'foo', ['one'=>1], new stdClass, false);

Output:

callback2
<pre>#0 [...][...](25): callback2(1234, 'foo', Array, Object(stdClass))
#1 [...][...](52): do_action('someaction', 1234, 'foo', Array, Object(stdClass), false)
#2 {main}

</pre>

callback1
Array
(
    [0] => 1234
    [1] => foo
    [2] => Array
        (
            [one] => 1
        )

    [3] => stdClass Object
        (
        )
    [4] =>
)

Sandbox

Stacktrace As you can see in the first output, we have a complete stacktrace of the application (minus the redacted info), including the function calls and the arguments used for those calls. Also note in this example, I registered it 2nd but the priority (set in add_action) made it execute first. Lastly, only 4 of the 5 arguments were used (also do to how add_action was called).

So do_action was called like this (with 'action' and 5 other args):

 do_action('someaction', 1234, 'foo', Array, Object(stdClass), false)

And the actual callback was called like this (without 'action' and only 4 other args):

 callback2(1234, 'foo', Array, Object(stdClass))

Function Arguments This is a bit more strait forward, but it doesn't give you the original call so you won't know the maximum number of args (which you can get from the call to do_action in the stacktrace). But if you just want a quick peek at the incoming data, it's perfect. Also I should mention this one uses all 5 args, which you can clearly see in the array for the second output. The [4] => is false, this is typically how print_r displays false (as just empty).

Debug Backtrace Unfortunately, debug_print_backtrace is disabled (for security reasons) in the sandbox, and Exception stacktrace is heavily redacted (typically it has file names and lines where functions are called from and located at) also for security reasons. Both of these can return arguments from things like connecting to the Database, which would contain the DB password in plain text, just for example. Anyway, debug_print_backtrace is pretty close to what an exception stack trace looks like anyway.

Summery

But in any case, this should give you an idea of what the data looks like. We can use functions like this (and Reflection) to interrogate the application at run time. I am sure there are other/more ways to do similar things too.

PS. It should go without saying, but I will say it anyway, these methods above work with any PHP function, and can be quite useful. Also as noted above, you should never show stacktraces on a live production machine.

Anyway, good luck.

Tuesday, August 30, 2022
 
Only authorized users can answer the search term. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share