Commit 3d459701722 for woocommerce
commit 3d459701722b0e0b14d76d98a59dba484904b3e1
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Wed Jun 10 12:38:33 2026 +0300
Add a Composer PSR-4 fallback autoloader to prevent in-place-upgrade class-not-found fatals (#65338)
* fix(asset-data-registry): hook enqueue on admin_enqueue_scripts with idempotency
This revives PR #65005 verbatim — same author, same diff, same tests.
Hook enqueue_asset_data() on admin_enqueue_scripts (at PHP_INT_MAX, after
the wc-settings dependency injection at priority 14) so that the data
registry runs at the enqueue phase rather than during footer printing.
This addresses #54657 where shipping method instantiation triggered
via the registry happened too late for hooks like admin_enqueue_scripts
that extensions register in WC_Shipping_Method constructors.
A fallback hook on admin_print_footer_scripts is kept so that any late
add() calls still get emitted, and enqueue_asset_data() is now idempotent
(guarded by $asset_data_enqueued) so the payload is never printed twice.
The same mechanism also defuses an in-place upgrade race surfaced by
the 10.7→10.8 fatal report tracked in 65337 — when admin_enqueue_scripts
fires BEFORE update.php's body runs the file swap, settings page classes
get loaded from the pre-upgrade files and pinned in PHP's class table.
The post-swap admin_print_footer_scripts hook becomes a no-op due to the
idempotency flag, preventing any "Class not found" fatal on new src/Enums
classes added by the upgrade. Validated end-to-end on WP 6.9 + WC 10.7 +
Jetpack 15.8 + WooPayments 10.8 + Yoast SEO 27.7 via a real wp-admin
upgrade through /wp-admin/update.php — upgrade completes cleanly, debug
log empty.
Closes #54657
Refs #65337
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert: drop the AssetDataRegistry enqueue-timing approach
The admin_enqueue_scripts/PHP_INT_MAX + idempotency-flag change did not fix
the target issue and introduced a silent late-add() data-loss regression
(empirically validated). Replaced by a Composer PSR-4 fallback autoloader.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(autoloader): add WooCommerce-scoped Composer PSR-4 fallback builder
Refs #65338
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(autoloader): register PSR-4 fallback to survive in-place-upgrade class misses
When WordPress swaps WooCommerce's files mid-request during an in-place
upgrade, the Jetpack autoloader's in-memory classmap snapshot cannot resolve
classes that are new in the upgraded version, fataling the request. Register a
WooCommerce-scoped Composer PSR-4 ClassLoader as an appended fallback so those
classes resolve from disk instead.
Refs #65338
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: add changelog entry for the PSR-4 upgrade fallback
Refs #65338
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: address final-review nits on the PSR-4 fallback
Add @since 11.0.0 to build_woocommerce_psr4_fallback() and rename the
test's system-under-test variable to $sut per the unit-test convention.
Refs #65338
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(autoloader): rebuild the PSR-4 fallback loader on each miss
WooCommerce registered a single WC-scoped Composer ClassLoader as the appended PSR-4 fallback. Composer's ClassLoader keeps a per-instance negative cache (missingClasses) and short-circuits repeat lookups for a class it has already missed. During a WordPress in-place upgrade the files are swapped mid-request, so if anything probes a future WooCommerce class (e.g. a defensive class_exists) before the swap, that one instance caches the miss and then keeps refusing the same class after the new file is on disk — for the rest of the request. That is the exact "class not found" fatal the fallback exists to prevent.
Register a closure that builds a fresh ClassLoader per WooCommerce-namespace miss instead. Each resolution starts with an empty negative cache, so a pre-swap miss no longer defeats a post-swap hit, while Composer's PSR-4 resolution and the Automattic\WooCommerce* scoping are preserved. The appended fallback only fires once every other autoloader (Jetpack included) has missed a WooCommerce class, so the per-miss rebuild cost is negligible.
The bootstrap drops to a single register_woocommerce_psr4_fallback() call (no instance register()/unset dance), and the tests are reworked to cover the per-miss loader and the appended SPL handler.
* fix(autoloader): scope the PSR-4 fallback to first-party src classes
The upgrade fallback was scoped to every Automattic\WooCommerce\* prefix in the Composer map, including the bundled third-party packages under Automattic\WooCommerce\Vendor\ (lib/packages). That contradicted its own docblock and let the fallback load WooCommerce's bundled copy of a vendor library over the version the Jetpack autoloader coordinates across plugins — turning a clean fatal into a subtle mixed-version state. The in-place-upgrade fatal it targets is always a first-party src/ class, so the extra breadth bought nothing.
Scope the loader to the single Automattic\WooCommerce\ -> src/ prefix; bundled Vendor\ and the non-runtime prefixes (Blueprint, tests, build tooling) are excluded, and the docblock + PR claim are corrected to match.
Move the Composer ClassLoader require_once inside the try/catch so a torn or partially-written ClassLoader.php mid-upgrade degrades to a null fallback instead of fataling the bootstrap. Make registration idempotent (a static guard returns the existing handler) so a re-entrant bootstrap, WP-CLI, or a test without teardown never stacks duplicate autoloaders.
Extract the per-miss resolution into find_scoped_file() and test the actual guarantee: a class missed before its file lands on disk resolves after, in the same request — both at the resolver level (temp PSR-4 dir) and end to end through the registered handler. Housekeeping: a use import for ClassLoader, require_once in the handler, $prototype renamed to $availability_probe, and @internal on the methods that are public only for tests.
* docs(changelog): scope the upgrade-fallback entry to first-party src classes
---------
Co-authored-by: Ayush Pahwa <ayush.pahwa@automattic.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/65338-composer-psr4-upgrade-fallback b/plugins/woocommerce/changelog/65338-composer-psr4-upgrade-fallback
new file mode 100644
index 00000000000..685df6aae94
--- /dev/null
+++ b/plugins/woocommerce/changelog/65338-composer-psr4-upgrade-fallback
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Register a WooCommerce-scoped Composer PSR-4 fallback autoloader so that future in-place upgrades can resolve newly-added first-party Automattic\WooCommerce classes under src/ that the in-memory Jetpack classmap snapshot would otherwise miss, preventing "class not found" fatals during the upgrade request.
diff --git a/plugins/woocommerce/src/Autoloader.php b/plugins/woocommerce/src/Autoloader.php
index d1314896dd6..4cd906a8791 100644
--- a/plugins/woocommerce/src/Autoloader.php
+++ b/plugins/woocommerce/src/Autoloader.php
@@ -5,6 +5,8 @@
namespace Automattic\WooCommerce;
+use Composer\Autoload\ClassLoader;
+
defined( 'ABSPATH' ) || exit;
/**
@@ -42,6 +44,167 @@ class Autoloader {
return $autoloader_result;
}
+ /**
+ * Build a WooCommerce-scoped Composer PSR-4 ClassLoader to use as a fallback
+ * to the Jetpack autoloader.
+ *
+ * The Jetpack autoloader reads its classmap into an in-memory snapshot once
+ * per request and never refreshes it. During a WordPress in-place upgrade the
+ * plugin files are swapped mid-request, so a class that is new in the upgraded
+ * version cannot be found in the snapshot and the request fatals. This loader,
+ * registered as an appended (lowest-priority) fallback, resolves such classes
+ * from disk via PSR-4.
+ *
+ * Scoped to the first-party `Automattic\WooCommerce\` (src/) namespace only —
+ * the family that actually fatals during an in-place upgrade (e.g.
+ * `Enums\DefaultCustomerAddress`). Every other prefix in the Composer map is
+ * deliberately excluded: bundled third-party packages
+ * (`Automattic\WooCommerce\Vendor\` → lib/packages) so the fallback can never
+ * load WooCommerce's bundled copy over the version the Jetpack autoloader
+ * coordinates across plugins, and the non-runtime prefixes (Blueprint, tests,
+ * build tooling) which never fatal during a front-end upgrade request.
+ *
+ * Returns the configured (but NOT registered) loader so the caller controls
+ * registration and tests can exercise it without touching the global SPL stack.
+ *
+ * @internal Public only so {@see self::register_woocommerce_psr4_fallback()} and
+ * the unit tests can build the loader in isolation.
+ *
+ * @since 11.0.0
+ *
+ * @return ClassLoader|null The loader, or null if the Composer files are
+ * unavailable or a foreign ClassLoader shape is present.
+ */
+ public static function build_woocommerce_psr4_fallback(): ?ClassLoader {
+ $base = dirname( __DIR__ );
+ $psr4_map = $base . '/vendor/composer/autoload_psr4.php';
+
+ if ( ! is_readable( $psr4_map ) ) {
+ return null;
+ }
+
+ try {
+ // Reuse an already-loaded ClassLoader (another plugin or wp-cli may have
+ // loaded it from a different path); requiring our copy then would fatal
+ // with "Cannot declare class ... already in use". Kept inside the try so a
+ // torn/partially-written ClassLoader.php during a vendor-bundle upgrade
+ // degrades to a null fallback instead of fataling the bootstrap.
+ if ( ! class_exists( ClassLoader::class, false ) ) {
+ $classloader_file = $base . '/vendor/composer/ClassLoader.php';
+ if ( ! is_readable( $classloader_file ) ) {
+ return null;
+ }
+ require_once $classloader_file;
+ }
+
+ $psr4_entries = require $psr4_map;
+ if ( ! is_array( $psr4_entries ) ) {
+ return null;
+ }
+
+ $loader = new ClassLoader();
+ foreach ( $psr4_entries as $namespace => $paths ) {
+ // First-party src/ only — exclude bundled Vendor\ and non-runtime prefixes.
+ if ( 'Automattic\\WooCommerce\\' === $namespace ) {
+ $loader->setPsr4( $namespace, $paths );
+ }
+ }
+ return $loader;
+ } catch ( \Throwable $e ) {
+ // Foreign/ancient ClassLoader shape, or a torn Composer file — skip the
+ // fallback rather than fatal the bootstrap.
+ return null;
+ }
+ }
+
+ /**
+ * Register the WooCommerce-scoped PSR-4 fallback as an appended (lowest-priority)
+ * SPL autoloader, so it is consulted only after every other autoloader — including
+ * the primary Jetpack autoloader — has missed.
+ *
+ * The handler resolves each miss with a throwaway loader (see {@see self::find_scoped_file()})
+ * rather than a single long-lived `ClassLoader`. Composer's `ClassLoader` records a
+ * per-instance negative cache (`missingClasses`) on a PSR-4 miss and short-circuits
+ * subsequent lookups for that class; a shared instance would therefore cache a miss for a
+ * class probed *before* an in-place upgrade swaps the files, then keep refusing that same
+ * class *after* the new file is on disk — for the remainder of the request. A fresh loader
+ * per miss keeps every resolution honest while still reusing Composer's PSR-4 resolution.
+ *
+ * Registration is idempotent: at most one handler is ever added per request.
+ *
+ * @since 11.0.0
+ *
+ * @return \Closure|null The registered autoloader, or null if the Composer files are
+ * unavailable (nothing was registered).
+ */
+ public static function register_woocommerce_psr4_fallback(): ?\Closure {
+ static $registered_handler = null;
+
+ // Idempotent: a re-entrant bootstrap, WP-CLI, or a test without teardown must not
+ // stack duplicate handlers (each one re-builds a loader + stats the FS on every miss).
+ if ( null !== $registered_handler ) {
+ return $registered_handler;
+ }
+
+ // Build once ONLY to validate availability and snapshot the scoped PSR-4 map. The handler
+ // rebuilds a throwaway loader per miss from this captured map (for performance — the map
+ // is read once, not on every miss). Do NOT collapse this into a shared loader or a per-miss
+ // build() call: either reintroduces the negative-cache bug the fresh-per-miss design avoids.
+ $availability_probe = self::build_woocommerce_psr4_fallback();
+ if ( null === $availability_probe ) {
+ return null;
+ }
+ $psr4_entries = $availability_probe->getPrefixesPsr4();
+
+ $handler = static function ( string $class_name ) use ( $psr4_entries ) {
+ $file = self::find_scoped_file( $class_name, $psr4_entries );
+ if ( null !== $file ) {
+ require_once $file;
+ }
+ };
+
+ spl_autoload_register( $handler, true, false );
+ $registered_handler = $handler;
+
+ return $handler;
+ }
+
+ /**
+ * Resolve a WooCommerce `src/` class to a file via a throwaway PSR-4 `ClassLoader`.
+ *
+ * A new loader per call is deliberate (and is the property the fallback exists for): Composer's
+ * `ClassLoader` keeps a per-instance negative cache, so a single shared instance that missed a
+ * class *before* an in-place upgrade swapped the files would keep refusing it *after* the new
+ * file is on disk. Building fresh here guarantees a class missed pre-swap resolves post-swap,
+ * within the same request.
+ *
+ * @internal Public only so the registered autoload handler and the unit tests can drive it.
+ *
+ * @param string $class_name Fully-qualified class name.
+ * @param array<string, list<string>> $psr4_entries Pre-scoped PSR-4 prefix => dirs map.
+ *
+ * @return string|null Absolute file path to require, or null on a miss or a
+ * non-`Automattic\WooCommerce\` class.
+ */
+ public static function find_scoped_file( string $class_name, array $psr4_entries ): ?string {
+ if ( 0 !== strpos( $class_name, 'Automattic\\WooCommerce\\' ) ) {
+ return null;
+ }
+
+ try {
+ $loader = new ClassLoader();
+ foreach ( $psr4_entries as $namespace => $paths ) {
+ $loader->setPsr4( $namespace, $paths );
+ }
+ $file = $loader->findFile( $class_name );
+ } catch ( \Throwable $e ) {
+ // Foreign/malformed ClassLoader — miss rather than fatal the autoload path.
+ return null;
+ }
+
+ return false !== $file ? $file : null;
+ }
+
/**
* If the autoloader is missing, add an admin notice.
*/
diff --git a/plugins/woocommerce/tests/php/src/AutoloaderTest.php b/plugins/woocommerce/tests/php/src/AutoloaderTest.php
new file mode 100644
index 00000000000..3597a33aed4
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/AutoloaderTest.php
@@ -0,0 +1,205 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests;
+
+use Automattic\WooCommerce\Autoloader;
+use Composer\Autoload\ClassLoader;
+
+/**
+ * Tests for the WooCommerce-scoped Composer PSR-4 fallback autoloader.
+ *
+ * @package Automattic\WooCommerce\Tests
+ */
+class AutoloaderTest extends \WC_Unit_Test_Case {
+
+ /**
+ * The builder returns a ClassLoader scoped to the first-party `src/` namespace
+ * only: it resolves a real src class, and refuses the bundled `Vendor\` packages,
+ * non-WooCommerce vendor namespaces, and non-existent classes.
+ *
+ * @testdox build_woocommerce_psr4_fallback() resolves src classes only.
+ */
+ public function test_build_woocommerce_psr4_fallback_scopes_to_src(): void {
+ $sut = Autoloader::build_woocommerce_psr4_fallback();
+
+ $this->assertInstanceOf(
+ ClassLoader::class,
+ $sut,
+ 'Builder must return a ClassLoader when the Composer files are present (they ship in the build).'
+ );
+
+ // Positive: resolves a real WooCommerce src class from disk via PSR-4.
+ $this->assertNotFalse(
+ $sut->findFile( 'Automattic\\WooCommerce\\Enums\\DefaultCustomerAddress' ),
+ 'Fallback must resolve a WooCommerce src class.'
+ );
+
+ // Excluded: bundled third-party under Vendor\ (lib/packages) must NOT resolve, so the
+ // fallback can never load WooCommerce's bundled copy over the Jetpack-coordinated version.
+ $this->assertFalse(
+ $sut->findFile( 'Automattic\\WooCommerce\\Vendor\\Psr\\Container\\ContainerInterface' ),
+ 'Fallback must exclude bundled Vendor\\ packages.'
+ );
+
+ // Excluded: a non-WooCommerce vendor namespace that exists in the full map.
+ $this->assertFalse(
+ $sut->findFile( 'Opis\\JsonSchema\\Validator' ),
+ 'Fallback must be scoped to WooCommerce src and refuse non-WooCommerce namespaces.'
+ );
+
+ // Bogus: must not invent files for non-existent classes.
+ $this->assertFalse(
+ $sut->findFile( 'Automattic\\WooCommerce\\Nope\\Does_Not_Exist_XYZ' ),
+ 'Fallback must not resolve non-existent classes.'
+ );
+ }
+
+ /**
+ * Each builder call returns a distinct ClassLoader, so Composer's per-instance
+ * negative cache (missingClasses) is never shared across resolutions.
+ *
+ * @testdox build_woocommerce_psr4_fallback() returns a fresh loader each call.
+ */
+ public function test_build_woocommerce_psr4_fallback_is_not_shared(): void {
+ $first = Autoloader::build_woocommerce_psr4_fallback();
+ $second = Autoloader::build_woocommerce_psr4_fallback();
+
+ $this->assertInstanceOf( ClassLoader::class, $first );
+ $this->assertInstanceOf( ClassLoader::class, $second );
+ $this->assertNotSame(
+ $first,
+ $second,
+ 'Each call must return a distinct loader so the negative cache is never shared across resolutions.'
+ );
+ }
+
+ /**
+ * The core guarantee: a class missed *before* its file lands on disk resolves
+ * *after*, within the same request — because each call resolves with a throwaway
+ * loader that carries no negative cache from the earlier miss.
+ *
+ * @testdox find_scoped_file() resolves a class once its file appears mid-request.
+ */
+ public function test_find_scoped_file_resolves_after_the_file_appears(): void {
+ $base = sys_get_temp_dir() . '/wc_autoloader_' . str_replace( '.', '', uniqid( '', true ) );
+ $file = $base . '/Widget.php';
+ $class = 'Automattic\\WooCommerce\\ReproNs\\Widget';
+ $map = array( 'Automattic\\WooCommerce\\ReproNs\\' => array( $base ) );
+
+ try {
+ wp_mkdir_p( $base );
+
+ // Miss: the class file does not exist yet.
+ $this->assertNull(
+ Autoloader::find_scoped_file( $class, $map ),
+ 'Must miss while the class file is absent.'
+ );
+
+ // The file appears mid-request (as a WordPress in-place upgrade would swap it in).
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture; WP_Filesystem adds no value here.
+ file_put_contents( $file, "<?php\nnamespace Automattic\\WooCommerce\\ReproNs;\nclass Widget {}\n" );
+ clearstatcache( true, $file );
+
+ // Resolve: a fresh loader (no carried-over negative cache) finds the new file.
+ $resolved = Autoloader::find_scoped_file( $class, $map );
+ $this->assertNotNull( $resolved, 'Must resolve once the file is on disk.' );
+ $this->assertSame(
+ realpath( $file ),
+ realpath( (string) $resolved ),
+ 'Must resolve to the file that appeared on disk.'
+ );
+ } finally {
+ if ( file_exists( $file ) ) {
+ wp_delete_file( $file );
+ }
+ if ( is_dir( $base ) ) {
+ rmdir( $base ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Test fixture cleanup.
+ }
+ }
+ }
+
+ /**
+ * The resolver ignores classes outside the `Automattic\WooCommerce\` namespace.
+ *
+ * @testdox find_scoped_file() ignores non-WooCommerce classes.
+ */
+ public function test_find_scoped_file_ignores_non_woocommerce_classes(): void {
+ $map = array( 'Automattic\\WooCommerce\\' => array( dirname( WC_PLUGIN_FILE ) . '/src' ) );
+
+ $this->assertNull(
+ Autoloader::find_scoped_file( 'Opis\\JsonSchema\\Validator', $map ),
+ 'Must ignore classes outside the Automattic\\WooCommerce\\ namespace.'
+ );
+ }
+
+ /**
+ * End-to-end: the autoloader registered by the bootstrap actually `require`s a real
+ * src class that appears on disk after an earlier miss, in the same request.
+ *
+ * @testdox the registered handler requires a src class that appears after a miss.
+ */
+ public function test_registered_handler_requires_an_appearing_src_class(): void {
+ $handler = Autoloader::register_woocommerce_psr4_fallback();
+ $this->assertInstanceOf( \Closure::class, $handler, 'Bootstrap must register a handler.' );
+
+ $suffix = 'ReproFixture' . str_replace( '.', '', uniqid( '', true ) );
+ $dir = dirname( WC_PLUGIN_FILE ) . '/src/' . $suffix;
+ $file = $dir . '/Widget.php';
+ $class = 'Automattic\\WooCommerce\\' . $suffix . '\\Widget';
+
+ $this->assertFalse( class_exists( $class, false ), 'Precondition: fixture class must not be loaded.' );
+
+ try {
+ // File absent: the handler is a no-op (miss), never a fatal.
+ $handler( $class );
+ $this->assertFalse(
+ class_exists( $class, false ),
+ 'Handler must not load a class whose file is absent.'
+ );
+
+ // File appears mid-request: the handler resolves it from disk and requires it.
+ wp_mkdir_p( $dir );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Test fixture; WP_Filesystem adds no value here.
+ file_put_contents( $file, "<?php\nnamespace Automattic\\WooCommerce\\{$suffix};\nclass Widget {}\n" );
+ clearstatcache( true, $file );
+
+ $handler( $class );
+ $this->assertTrue(
+ class_exists( $class, false ),
+ 'Handler must require a src class that appeared on disk after an earlier miss.'
+ );
+ } finally {
+ if ( file_exists( $file ) ) {
+ wp_delete_file( $file );
+ }
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Test fixture cleanup.
+ }
+ }
+ }
+
+ /**
+ * Registration is idempotent: repeated calls return the same handler and never
+ * stack duplicate autoloaders on the SPL stack.
+ *
+ * @testdox register_woocommerce_psr4_fallback() is idempotent.
+ */
+ public function test_register_woocommerce_psr4_fallback_is_idempotent(): void {
+ $first = Autoloader::register_woocommerce_psr4_fallback();
+ $stack_after = spl_autoload_functions();
+ $second = Autoloader::register_woocommerce_psr4_fallback();
+
+ $this->assertInstanceOf( \Closure::class, $first );
+ $this->assertSame( $first, $second, 'Repeat registration must return the same handler.' );
+ $this->assertSame(
+ $stack_after,
+ spl_autoload_functions(),
+ 'Repeat registration must not add a duplicate handler to the SPL stack.'
+ );
+ $this->assertTrue(
+ in_array( $first, spl_autoload_functions(), true ),
+ 'The registered handler must be present on the SPL stack.'
+ );
+ }
+}
diff --git a/plugins/woocommerce/woocommerce.php b/plugins/woocommerce/woocommerce.php
index ee35dde1684..83f82adc907 100644
--- a/plugins/woocommerce/woocommerce.php
+++ b/plugins/woocommerce/woocommerce.php
@@ -29,6 +29,14 @@ if ( ! \Automattic\WooCommerce\Autoloader::init() ) {
}
\Automattic\WooCommerce\Packages::init();
+// Register a WooCommerce-scoped Composer PSR-4 autoloader on the SPL stack as a
+// low-priority (appended) fallback, consulted only after every other autoloader
+// — including the primary Jetpack autoloader — has missed. When a WordPress
+// in-place upgrade swaps WooCommerce's files mid-request, the Jetpack classmap
+// snapshot (captured at request start, never refreshed) cannot resolve a class
+// that is new in the upgraded version; this fallback resolves it from disk.
+\Automattic\WooCommerce\Autoloader::register_woocommerce_psr4_fallback();
+
// Include the main WooCommerce class.
if ( ! class_exists( 'WooCommerce', false ) ) {
include_once dirname( WC_PLUGIN_FILE ) . '/includes/class-woocommerce.php';