---
title: "WooCommerce PSR-4 & DI Architecture Guide | Maksut"
description: "Build safer WooCommerce extensions with PSR-4 and dependency injection: clean layers, testable pricing rules, migration steps and architecture review."
url: "https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/"
language: "en-US"
datePublished: "2026-09-21T11:05:34+00:00"
dateModified: "2026-09-23T20:22:11+00:00"
author: "Maksut"
---

# PSR-4 & Dependency Injection in WooCommerce: Clean Architecture for Scale

Building Testable, Maintainable E-commerce Systems Beyond Hooks and Globals

**PSR-4 autoloading** and **dependency injection (DI)** let you grow WooCommerce beyond ad hoc `functions.php` hooks: **domain-centric** PHP (entities, value objects and application services) lives in Composer-mapped namespaces, while WordPress and WooCommerce remain at the **infrastructure edge**. The payoff is **testable** pricing and catalog logic, explicit dependencies instead of opaque globals, and a safer path for multi-SKU stores, B2B rules and connected systems. This is the architecture to consider before a custom WooCommerce feature becomes another fragile plugin patch.

---

## The WooCommerce technical debt trap

Typical WooCommerce customization stacks **hooks** and **globals**—fine for small catalogs, painful when rules compound. Debugging turns into archaeology: stack traces disappear into `do_action()` chains, state hides behind `global $product`, and automated tests drag in the full WordPress bootstrap.

Symptoms creep in: a pricing tweak works alone but fights dynamic coupons; checkout edge cases fail under concurrency; SQL multiplies because caches cannot see dependencies across procedural code. The underlying issue is **coupling**—UI, domain rules, and persistence smeared together.

### The globals anti-pattern

`global $woocommerce, $product, $post` creates **implicit dependencies** static analysis cannot see. Refactors fail quietly because types appear only at runtime. DI replaces that with **constructor-injected** contracts—still wired through WordPress at the edges, but explicit in your code.

## Clean Architecture layers for WooCommerce

Four rings, one rule: **dependencies point inward**. Inner layers stay framework-agnostic; outer layers talk to WooCommerce and WordPress.

Dependencies point inward: domain stays plain PHP; WooCommerce types live in infrastructure.

### Layer 1: Domain (pure PHP)

Rules that could run in a CLI or another framework. No `WC_*`, no `wpdb`.

```
// src/Domain/ValueObjects/Money.php
namespace Acme\WooEngine\Domain\ValueObjects;

final class Money
{
    private function __construct(
        private int $cents,
        private string $currency
    ) {}

    public static function fromDecimal(float $amount, string $currency): self
    {
        return new self((int) round($amount * 100), $currency);
    }

    public function currency(): string
    {
        return $this->currency;
    }

    public function add(Money $other): self
    {
        $this->assertSameCurrency($other);
        return new self($this->cents + $other->cents, $this->currency);
    }

    public function subtract(Money $other): self
    {
        $this->assertSameCurrency($other);
        return new self(max(0, $this->cents - $other->cents), $this->currency);
    }

    public function toDecimal(): float
    {
        return $this->cents / 100;
    }

    private function assertSameCurrency(Money $other): void
    {
        if ($this->currency !== $other->currency) {
            throw new \InvalidArgumentException('Currency mismatch');
        }
    }
}
```

### Layer 2: Application (use cases)

Orchestrates domain objects; declares ports (interfaces) infrastructure will implement.

```
// src/Application/Contracts/ProductRepositoryInterface.php
namespace Acme\WooEngine\Application\Contracts;

use Acme\WooEngine\Domain\Entities\CustomProduct;

interface ProductRepositoryInterface
{
    public function findById(int $id): ?CustomProduct;
    public function save(CustomProduct $product): void;
}

// src/Application/Contracts/DiscountRepositoryInterface.php
namespace Acme\WooEngine\Application\Contracts;

interface DiscountRepositoryInterface
{
    /** @return list<object{amount: float}> */
    public function findActiveForUser(int $userId): array;
}

// src/Application/Services/PricingEngine.php
namespace Acme\WooEngine\Application\Services;

use Acme\WooEngine\Application\Contracts\DiscountRepositoryInterface;
use Acme\WooEngine\Domain\ValueObjects\Money;

final class PricingEngine
{
    public function __construct(
        private DiscountRepositoryInterface $discounts
    ) {}

    public function calculateFinalPrice(Money $base, int $productId, int $userId): Money
    {
        $total = 0.0;
        foreach ($this->discounts->findActiveForUser($userId) as $d) {
            $total += $d->amount;
        }
        return $base->subtract(Money::fromDecimal($total, $base->currency()));
    }
}
```

### Layer 3: Infrastructure (WordPress / WooCommerce)

Implements ports with `wpdb`, `WC_Product`, REST, etc.

```
// src/Infrastructure/Persistence/WpdbProductRepository.php
namespace Acme\WooEngine\Infrastructure\Persistence;

use Acme\WooEngine\Application\Contracts\ProductRepositoryInterface;
use Acme\WooEngine\Domain\Entities\CustomProduct;

final class WpdbProductRepository implements ProductRepositoryInterface
{
    public function __construct(private \wpdb $db) {}

    public function findById(int $id): ?CustomProduct
    {
        $row = $this->db->get_row($this->db->prepare(
            "SELECT * FROM {$this->db->posts} WHERE ID = %d AND post_type = %s",
            $id,
            'custom_product'
        ), ARRAY_A);

        return $row ? $this->hydrate($row) : null;
    }

    private function hydrate(array $row): CustomProduct
    {
        return new CustomProduct(
            id: (int) $row['ID'],
            name: $row['post_title'],
            meta: (string) get_post_meta((int) $row['ID'], '_custom_data', true)
        );
    }

    public function save(CustomProduct $product): void
    {
        // Persist via wp_insert_post / meta — omitted
    }
}
```

## PSR-4 autoloading

Composer maps namespaces to `src/`; no manual `require_once` trees.

```
{
  "name": "acme/woocommerce-engine",
  "type": "wordpress-plugin",
  "require": {
    "php": "^8.1",
    "php-di/php-di": "^7.0"
  },
  "autoload": {
    "psr-4": {
      "Acme\\WooEngine\\": "src/"
    }
  },
  "autoload-dev": {
    "psr-4": {
      "Acme\\WooEngine\\Tests\\": "tests/"
    }
  }
}
```

```
acme-woo-engine/
├── composer.json
├── phpunit.xml
├── src/                          # Acme\WooEngine\
│   ├── Domain/
│   ├── Application/
│   │   ├── Contracts/
│   │   └── Services/
│   └── Infrastructure/
│       ├── Persistence/
│       ├── WooCommerce/
│       └── WordPress/
├── tests/
│   ├── Unit/Domain/
│   └── Integration/
└── woocommerce-engine.php
```

## Dependency injection with PHP-DI

Bind interfaces to concrete classes; wrap WordPress globals as factories where needed.

```
// src/Infrastructure/Container/ContainerFactory.php
namespace Acme\WooEngine\Infrastructure\Container;

use Acme\WooEngine\Application\Contracts\ProductRepositoryInterface;
use Acme\WooEngine\Infrastructure\Persistence\WpdbProductRepository;
use DI\ContainerBuilder;

final class ContainerFactory
{
    public static function build(): \Psr\Container\ContainerInterface
    {
        $builder = new ContainerBuilder();
        $builder->addDefinitions([
            \wpdb::class => \DI\factory(static function (): \wpdb {
                global $wpdb;
                return $wpdb;
            }),
            ProductRepositoryInterface::class => \DI\autowire(WpdbProductRepository::class),
        ]);
        return $builder->build();
    }
}
```

### Plugin bootstrap

```
<?php
/**
 * Plugin Name: WooCommerce Engine (PSR-4 / DI)
 * Version: 2.0.0
 */

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use Acme\WooEngine\Infrastructure\Container\ContainerFactory;
use Acme\WooEngine\Infrastructure\WooCommerce\ProductTypeRegistrar;

$container = ContainerFactory::build();

add_action('woocommerce_loaded', static function () use ($container): void {
    $container->get(ProductTypeRegistrar::class)->register();
});

add_action('rest_api_init', static function () use ($container): void {
    $container->get(\Acme\WooEngine\Infrastructure\Rest\PricingController::class)->registerRoutes();
});
```

## Custom product type (illustrative)

Keep `WC_Product` subclasses thin; inject `PricingEngine` from the container—**avoid** calling `ContainerFactory::build()` inside the product constructor in production (service locator anti-pattern). Prefer factory registration that passes dependencies explicitly.

```
// Sketch: resolve PricingEngine once, inject via factory / setter
final class SubscriptionBoxProduct extends \WC_Product
{
    public function __construct($product = 0, private ?PricingEngine $pricing = null)
    {
        parent::__construct($product);
        $this->product_type = 'subscription_box';
    }

    public function get_price($context = 'view')
    {
        if ($this->pricing === null) {
            return parent::get_price($context);
        }
        $base = Money::fromDecimal((float) parent::get_price('edit'), get_woocommerce_currency());
        $final = $this->pricing->calculateFinalPrice($base, $this->get_id(), get_current_user_id());
        return (string) $final->toDecimal();
    }
}
```

### Testing advantage

`PricingEngine` depends on `DiscountRepositoryInterface`—PHPUnit can inject a mock and run rules without loading WordPress:

```
public function test_applies_discounts(): void
{
    $mock = $this->createMock(DiscountRepositoryInterface::class);
    $mock->method('findActiveForUser')->willReturn([
        (object) ['amount' => 10.0],
        (object) ['amount' => 5.0],
    ]);

    $engine = new PricingEngine($mock);
    $result = $engine->calculateFinalPrice(Money::fromDecimal(100, 'USD'), 1, 1);

    self::assertSame(85.0, $result->toDecimal());
}
```

## Migration path: hooks to architecture

1. **Phase 1: Extract domain**

   Move pure calculations out of `functions.php` into `src/Domain/`; behavior unchanged.
2. **Phase 2: Repositories**

   Wrap `$wpdb` / `WP_Query` behind interfaces in `Infrastructure\Persistence`.
3. **Phase 3: Container**

   Wire new features through PHP-DI; legacy hooks coexist.
4. **Phase 4: Tests**

   Fast unit tests for domain/application; `WP_UnitTestCase` for integration paths.

For a real store, architecture should follow the business rule it protects: pricing, catalogue eligibility, customer roles, fulfilment, subscriptions or an external integration. See [WooCommerce engineering](https://maksut.net/services/woocommerce/) for delivery work and [custom plugin development](https://maksut.net/services/wordpress/plugin-development/) when the correct product is a maintained extension rather than another theme-level snippet.

## Performance notes

- **Compiled container:** PHP-DI can compile definitions for production to cut reflection cost.
- **Lazy services:** Defer heavy repos until first use where the container supports it.
- **Centralized queries:** Repositories are the place to batch-load and cache hydrate.

## Frequently asked questions

**Does PSR-4 conflict with WordPress coding standards?**

No. PSR-4 is **autoloading layout**; you can still follow WPCS for spacing and naming inside files. Composer’s autoloader is loaded from `vendor/autoload.php` in your plugin bootstrap.

**Will a DI container slow WooCommerce?**

Well-configured containers add negligible overhead versus manual `new` chains; **compilation** and avoiding per-request rebuilds matter. Profile your stack—bottlenecks are usually SQL and plugins, not DI resolution alone.

**How do I test code that calls WordPress functions?**

Keep **unit** tests on domain/application (no WP). Use **integration** tests with `WP_UnitTestCase` for infrastructure that calls `get_post_meta`, cart APIs, etc.

**Can this coexist with other WooCommerce plugins?**

Yes. Adapters register hooks (`woocommerce_product_class`, checkout actions) at the boundary; core WooCommerce and third-party plugins keep working.

**Is this overkill for small shops?**

Often yes when you have a small catalogue, flat pricing and few integrations—core WooCommerce plus a small, well-contained extension can be enough. When rules, bundles, B2B tiers or connected systems grow in complexity, PSR-4 and DI become easier to justify. Pair the decision with a [custom plugin development](https://maksut.net/services/wordpress/plugin-development/) plan rather than adding business logic to a theme.

## WooCommerce architecture review

PSR-4, DI, and clean layers for stores that outgrew hook soup.

[WooCommerce engineering](https://maksut.net/services/woocommerce/)

PSR-4 migration planning, DI wiring, and testable domain modeling for complex stores.

[Request architecture audit](https://maksut.net/contact/?svc=WooCommerce&intent=architecture-review#contact-form)

From hook soup to typed, testable layers.

**Technical note:** These are illustrative PHP and WooCommerce architecture patterns, not a drop-in plugin. Validate your PHP, WooCommerce and Composer versions in staging before production. Reviewed: September 2026.

```json
{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://maksut.net/#website","url":"https://maksut.net/","name":"Maksut — Digital Systems Builder","description":"Custom WordPress and WooCommerce development with technical SEO and AI search visibility for businesses in the UK, Europe and worldwide. Work directly with Maksut.","publisher":{"@id":"https://maksut.net/#person"},"inLanguage":["en","tr"]},{"@type":"WebPage","@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#webpage","url":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/","name":"WooCommerce PSR-4 & DI Architecture Guide | Maksut","isPartOf":{"@id":"https://maksut.net/#website"},"inLanguage":"en-US","datePublished":"2026-09-21T11:05:34+00:00","dateModified":"2026-09-23T20:22:11+00:00","mainEntity":{"@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#article"},"breadcrumb":{"@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#breadcrumblist"}},{"@type":"Article","@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#article","isPartOf":{"@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#webpage"},"headline":"WooCommerce PSR-4 & DI Architecture Guide | Maksut","url":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/","inLanguage":"en-US","publisher":{"@id":"https://maksut.net/#person"},"wordCount":1325,"mainEntityOfPage":{"@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#webpage"},"author":{"@id":"https://maksut.net/#person"},"datePublished":"2026-09-21T11:05:34+00:00","dateModified":"2026-09-23T20:22:11+00:00","description":"Build safer WooCommerce extensions with PSR-4 and dependency injection: clean layers, testable pricing rules, migration steps and architecture review.","articleSection":"WooCommerce Engineering","keywords":["Clean Architecture","Composer WordPress","Dependency Injection","domain-driven design","enterprise e-commerce","object-oriented WooCommerce","PHP-DI","PSR-4 autoloading","repository pattern","scalable WooCommerce","technical debt refactoring","test-driven development","unit testing WordPress","WC_Product customization","WooCommerce plugin development"],"speakable":{"@type":"SpeakableSpecification","cssSelector":[".entry-content .mks-aeo__direct > p.voice-answer:first-child"]}},{"@type":"BreadcrumbList","@id":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/#breadcrumblist","itemListElement":[{"@type":"ListItem","position":1,"name":"Maksut.net","item":"https://maksut.net/"},{"@type":"ListItem","position":2,"name":"WooCommerce Engineering","item":"https://maksut.net/category/woocommerce-engineering/"},{"@type":"ListItem","position":3,"name":"WooCommerce PSR-4 & DI Architecture Guide | Maksut","item":"https://maksut.net/woocommerce-psr4-dependency-injection-clean-architecture/"}]},{"@type":"Person","@id":"https://maksut.net/#person","name":"Maksut","alternateName":["Maksut Maksutoğlu","Nebudil"],"url":"https://maksut.net/","jobTitle":"WordPress & WooCommerce Systems Builder","description":"WordPress and WooCommerce developer specialising in technical SEO and AI search visibility for businesses in the UK, Europe and worldwide.","knowsAbout":["Answer Engine Optimization","Generative Engine Optimization","AI Search Optimization","Content Strategy","Local SEO","Technical SEO","Schema.org","Entity SEO","Google Business Profile","Structured Data","WooCommerce","WooCommerce Maintenance","WordPress Development","WordPress Maintenance"],"knowsLanguage":["en","tr","fa"],"address":{"@type":"PostalAddress","addressLocality":"Istanbul","addressCountry":"TR"}}]}
```
