FIFA WORLDCUP OFFER : 50% Off On ALL ITEMS Get It Now >

How to Build Dependency Injection in WordPress Plugins: Complete Guide

How to Build Dependency Injection in WordPress Plugins: Complete Guide

How to Build Dependency Injection in WordPress Plugins

Introduction

As a WordPress plugin grows, classes rarely work independently.

A product service may need a repository.

A report service may need a database abstraction and a logger.

An API integration may need an HTTP client.

An order service may require a payment gateway, notification service, and order repository.

A common approach is to create these dependencies directly inside each class:

class OrderService {    public function process(): void    {        $repository = new OrderRepository();        $gateway    = new PaymentGateway();        // ...    } }

This works for small examples, but it creates tight coupling.

The OrderService now decides:

Which repository implementation to use

Which payment gateway to use

How those objects are constructed

How their configuration is provided

A better approach is Dependency Injection (DI).

Instead of creating dependencies inside a class, provide them from outside:

class OrderService {    public function __construct(        private OrderRepository $repository,        private PaymentGateway $gateway    ) {    } }

The architecture becomes:

Application     ↓ Dependency Injection     ↓ Service     ├── Repository     ├── Gateway     └── Logger

This makes dependencies explicit and can make WordPress plugins easier to test, replace, refactor, and scale.

In this guide, you'll learn what dependency injection is, why it matters in WordPress, how to implement constructor injection, how to use interfaces, how to connect services and repositories, when to use a container, how to test injected dependencies, common mistakes, and how to build a production-ready dependency architecture.

What Is Dependency Injection?

Dependency Injection is a design technique where an object receives the objects or services it depends on from an external source.

Without dependency injection:

class ReportService {    public function __construct()    {        $this->repository = new ReportRepository();        $this->logger     = new Logger();    } }

The class creates its own dependencies.

With dependency injection:

class ReportService {    public function __construct(        private ReportRepository $repository,        private Logger $logger    ) {    } }

The dependencies are supplied by the caller.

This creates a separation between:

Object

and:

Object Construction

What Is a Dependency?

A dependency is something a class needs to perform its responsibility.

For example:

OrderService   ├── OrderRepository   ├── PaymentGateway   └── NotificationService

All three are dependencies.

The OrderService depends on them to complete its workflow.

Why Dependency Injection Matters in WordPress

WordPress plugins often use a mixture of:

Global WordPress functions

Hooks

Database APIs

External APIs

Admin interfaces

Background jobs

REST endpoints

WooCommerce APIs

Cron events

Without a clear dependency strategy, classes can become tightly coupled to global state and concrete implementations.

Dependency injection can improve:

Testability

Modularity

Maintainability

Flexibility

Reusability

Separation of concerns

Refactoring

It becomes increasingly valuable as plugin complexity grows.

Dependency Injection vs Creating Dependencies Internally

Consider this:

class ProductService {    public function __construct()    {        $this->repository = new ProductRepository();    } }

The service is locked to ProductRepository.

Now compare:

class ProductService {    public function __construct(        private ProductRepositoryInterface $repository    ) {    } }

Now the service depends on an abstraction.

The implementation can be changed externally.

ProductService       ↓ ProductRepositoryInterface       ↓ ┌─────┴─────┐ ↓           ↓ MySQL      Test Fake

Types of Dependency Injection

Dependency injection can be implemented in several ways.

Constructor Injection

Dependencies are provided through the constructor.

class ProductService {    public function __construct(        private ProductRepository $repository    ) {    } }

This is usually the preferred approach for required dependencies.

Setter Injection

A dependency is provided through a setter:

class ProductService {    private LoggerInterface $logger;    public function setLogger(        LoggerInterface $logger    ): void {        $this->logger = $logger;    } }

This can be useful for optional dependencies, but required dependencies are generally clearer through constructors.

Method Injection

A dependency is passed only to the method that needs it:

public function export(    ExporterInterface $exporter ): string {    // ... }

This can be appropriate when a dependency is required for only one operation.

Constructor Injection in WordPress Plugins

For most service classes, constructor injection is the simplest pattern.

Example:

namespace Kaddora\MyPlugin\Services; use Kaddora\MyPlugin\Repositories\ProductRepository; final class ProductService {    public function __construct(        private ProductRepository $repository    ) {    }    public function find(int $productId): ?array    {        return $this->repository->find($productId);    } }

The dependency is visible immediately.

Why Constructor Injection Is Usually Best

Constructor injection provides several advantages.

Dependencies Are Explicit

A developer can see what the class requires.

Objects Are Valid After Construction

Required dependencies are available immediately.

Testing Is Easier

Tests can pass mocks or fakes directly.

Immutability Is Easier

Dependencies can often be stored in private or readonly properties.

Static Analysis Works Better

Tools can reason about constructor types.

Using Interfaces With Dependency Injection

The strongest form of decoupling often uses interfaces.

For example:

namespace Kaddora\MyPlugin\Contracts; interface CacheInterface {    public function get(string $key): mixed;    public function set(        string $key,        mixed $value,        int $ttl = 3600    ): void; }

Then:

namespace Kaddora\MyPlugin\Services; use Kaddora\MyPlugin\Contracts\CacheInterface; final class ProductService {    public function __construct(        private CacheInterface $cache    ) {    } }

The service doesn't need to know whether the cache is backed by:

WordPress object cache

Redis

An in-memory fake

Another implementation

Dependency Injection and Repositories

A service can depend on a repository interface:

final class OrderService {    public function __construct(        private OrderRepositoryInterface $repository    ) {    } }

Then the application wires:

OrderService      ↓ OrderRepositoryInterface      ↓ OrderRepository      ↓ Database

This keeps persistence implementation separate from application behavior.

Dependency Injection and Service Classes

A service can receive multiple dependencies:

final class OrderService {    public function __construct(        private OrderRepositoryInterface $repository,        private PaymentGatewayInterface $gateway,        private NotificationInterface $notification    ) {    } }

The service coordinates the workflow.

Its constructor also documents the architecture.

Example: Processing an Order

final class OrderService {    public function __construct(        private OrderRepositoryInterface $repository,        private PaymentGatewayInterface $paymentGateway,        private NotificationInterface $notification    ) {    }    public function process(        int $customerId,        int $amount,        string $currency    ): int {        if ($amount <= 0) {            throw new \InvalidArgumentException(                'Amount must be greater than zero.'            );        }        $success = $this->paymentGateway->charge(            $amount,            $currency        );        if (! $success) {            throw new \RuntimeException(                'Payment failed.'            );        }        $orderId = $this->repository->create([            'customer_id' => $customerId,            'amount'      => $amount,            'currency'    => $currency,        ]);        $this->notification->send(            $orderId        );        return $orderId;    } }

The service doesn't construct any dependency itself.

Who Creates the Dependencies?

This is the important question.

Dependency injection means dependencies come from somewhere else.

For a WordPress plugin, a bootstrapper can build the object graph:

Plugin Bootstrap       ↓ Create Repository       ↓ Create Gateway       ↓ Create Notification       ↓ Create OrderService       ↓ Register Hooks

This is called composition or wiring.

Manual Dependency Wiring

For smaller plugins, manual wiring is often enough.

$repository  = new OrderRepository(); $gateway     = new StripeGateway(); $notification = new EmailNotification(); $orderService = new OrderService(    $repository,    $gateway,    $notification );

This is simple and easy to understand.

You don't need a service container for every plugin.

A WordPress Plugin Bootstrap Example

defined( 'ABSPATH' ) || exit; require_once __DIR__ . '/vendor/autoload.php'; $repository = new \Kaddora\MyPlugin\Repositories\OrderRepository(); $gateway = new \Kaddora\MyPlugin\Integrations\StripeGateway(); $notification =    new \Kaddora\MyPlugin\Services\EmailNotification(); $orderService = new \Kaddora\MyPlugin\Services\OrderService(    $repository,    $gateway,    $notification );

This can then be passed to a REST controller, hook listener, or other entry point.

Dependency Injection With WordPress Hooks

Suppose a service handles product synchronization:

final class ProductSyncService {    public function sync(): void    {        // ...    } }

A hook handler can receive the service:

final class CronHooks {    public function __construct(        private ProductSyncService $service    ) {    }    public function register(): void    {        \add_action(            'myplugin_sync_products',            [ $this, 'sync' ]        );    }    public function sync(): void    {        $this->service->sync();    } }

The WordPress hook knows nothing about how ProductSyncService is constructed.

Dependency Injection and REST Controllers

REST controllers are excellent places to demonstrate DI.

final class ProductController {    public function __construct(        private ProductService $service    ) {    }    public function create(        \WP_REST_Request $request    ): \WP_REST_Response {        $data = $request->get_params();        $productId = $this->service->create(            $data        );        return new \WP_REST_Response(            [                'id' => $productId,            ],            201        );    } }

The controller handles the HTTP boundary.

The service handles the application operation.

Dependency Injection and AJAX

The same architecture can be used for AJAX callbacks.

AJAX Handler     ↓ Service     ↓ Repository

The handler doesn't need to instantiate the service repeatedly.

Instead, the application creates the service once and gives it to the handler.

Dependency Injection and WP-Cron

Cron jobs can receive services too.

final class SyncProductsJob {    public function __construct(        private ProductSyncService $service    ) {    }    public function run(): void    {        $this->service->sync();    } }

The job coordinates scheduling.

The service performs the application work.

Dependency Injection and WP-CLI

A WP-CLI command can reuse the same service:

REST AJAX Admin Cron WP-CLI   ↓ Shared Service

This prevents duplicated business logic.

Dependency Injection and WordPress Globals

WordPress provides global state such as:

global $wpdb;

You can still use WordPress globals inside infrastructure classes.

The important architectural goal is to avoid making every application service tightly dependent on them.

For example:

Service   ↓ Repository   ↓ $wpdb

is cleaner than:

Service   ↓ $wpdb   ↓ Raw SQL everywhere

Dependency Injection for $wpdb

Passing $wpdb through the entire application isn't always necessary.

Instead, isolate database access:

final class ProductRepository {    public function find(int $id): ?array    {        global $wpdb;        // Database access.        return null;    } }

Then services depend on the repository.

This keeps WordPress-specific infrastructure details localized.

Dependency Injection and HTTP Clients

External API calls should also be abstracted.

Define:

interface HttpClientInterface {    public function request(        string $method,        string $url,        array $options = []    ): mixed; }

Then:

final class OpenAIClient {    public function __construct(        private HttpClientInterface $http    ) {    } }

The API client now depends on an abstraction.

Dependency Injection and API Providers

For applications supporting multiple providers:

AIService    ↓ AIProviderInterface    ↓ ┌──────────────┬──────────────┐ ↓              ↓              ↓ Provider A   Provider B     Test Fake

The selected provider can be injected during application wiring.

This is especially useful for AI, payments, email, storage, analytics, and CRM integrations.

Dependency Injection and Configuration

Configuration can also be injected.

For example:

final class ApiClient {    public function __construct(        private string $apiKey,        private string $baseUrl    ) {    } }

Then the bootstrap provides values:

$client = new ApiClient(    $apiKey,    $baseUrl );

Avoid hard-coding sensitive credentials inside classes.

Configuration Objects

When configuration becomes complex, a dedicated configuration object can improve clarity.

final class ApiConfig {    public function __construct(        public readonly string $apiKey,        public readonly string $baseUrl,        public readonly int $timeout    ) {    } }

Then:

final class ApiClient {    public function __construct(        private ApiConfig $config    ) {    } }

This can reduce constructors with many primitive parameters.

Dependency Injection and Factories

Factories can create objects when construction itself contains meaningful logic.

For example:

final class PaymentGatewayFactory {    public function create(        string $provider    ): PaymentGatewayInterface {        // Select implementation.    } }

The application service can receive the resulting gateway.

Don't create factories just to hide a simple new statement.

Dependency Injection and the Factory Pattern

A useful architecture is:

Configuration     ↓ Factory     ↓ Concrete Implementation     ↓ Interface     ↓ Service

Factories become useful when construction varies according to configuration or runtime conditions.

Dependency Injection Containers

A dependency injection container manages object construction and relationships.

For example:

Container │ ├── OrderRepository ├── PaymentGateway ├── NotificationService └── OrderService

The container resolves:

OrderService      ↓ Repository      ↓ Payment Gateway      ↓ Notification

Containers can be valuable for large plugins.

They are not mandatory.

When Should You Use a Container?

Consider a container when:

Dependency graphs become complex

Many classes share dependencies

Multiple implementations exist

Configuration-driven resolution is needed

Manual wiring becomes difficult to maintain

The application has many modules

Avoid a container when manual wiring is clearer.

A container should reduce complexity, not introduce it.

A Simple Service Container

A minimal container can map interfaces to factories:

final class Container {    private array $factories = [];    public function set(        string $id,        callable $factory    ): void {        $this->factories[ $id ] = $factory;    }    public function get(string $id): mixed    {        if (! isset($this->factories[ $id ])) {            throw new \RuntimeException(                "Service not found: {$id}"            );        }        return ($this->factories[ $id ])($this);    } }

Then:

$container->set(    OrderService::class,    function (Container $container): OrderService {        return new OrderService(            $container->get(OrderRepositoryInterface::class),            $container->get(PaymentGatewayInterface::class),            $container->get(NotificationInterface::class)        );    } );

This is enough to demonstrate the concept without introducing a large framework.

Singleton vs Dependency Injection

These patterns are often confused.

A singleton provides global access to one instance.

Dependency injection provides required objects explicitly.

Prefer:

public function __construct(    private LoggerInterface $logger ) { }

over:

Logger::instance()->log();

The second approach introduces global state and hides the dependency.

Dependency Injection vs Service Locator

A service locator gives classes access to a central registry:

$container->get('logger');

inside the class itself.

This hides dependencies.

Dependency injection is generally clearer:

public function __construct(    LoggerInterface $logger ) { }

The class declares what it needs.

Dependency Injection and Interfaces

Interfaces become especially useful when multiple implementations are possible.

For example:

interface NotificationInterface {    public function send(int $orderId): void; }

Implementations:

EmailNotification SmsNotification WebhookNotification

The service can depend on:

NotificationInterface

instead of a specific provider.

Dependency Injection and Testing

This is one of DI's biggest advantages.

Without DI:

class ProductService {    public function __construct()    {        $this->repository =            new ProductRepository();    } }

Testing the service requires the real repository.

With DI:

class ProductService {    public function __construct(        private ProductRepositoryInterface $repository    ) {    } }

A test can inject a fake.

Using a PHPUnit Mock

$repository = $this->createMock(    ProductRepositoryInterface::class ); $repository    ->method('find')    ->willReturn([        'id' => 10,        'title' => 'Demo Product',    ]); $service = new ProductService(    $repository );

The test doesn't need a live database for this particular behavior.

Fake Implementations

Mocks aren't the only option.

A simple fake can be useful:

final class InMemoryProductRepository    implements ProductRepositoryInterface {    public function __construct(        private array $products = []    ) {    }    public function find(int $id): ?array    {        return $this->products[$id] ?? null;    } }

Then:

$service = new ProductService(    new InMemoryProductRepository([        10 => [            'id' => 10,            'title' => 'Demo Product',        ],    ]) );

Dependency Injection and Unit Testing

A typical unit-test structure is:

Test ↓ Service ↓ Mock / Fake Dependency

No WordPress database or external API is required unless the test specifically targets that integration.

This makes tests fast and focused.

Dependency Injection and Integration Testing

DI doesn't eliminate integration tests.

You still need to verify:

Service   ↓ Real Repository   ↓ WordPress Database

and:

Service   ↓ Real API Client   ↓ External System

Unit and integration tests serve different purposes.

Dependency Injection and Static Analysis

Strongly typed dependency injection works well with PHPStan.

For example:

public function __construct(    private PaymentGatewayInterface $gateway ) { }

Static analysis can help detect:

Incorrect implementations

Missing methods

Type mismatches

Invalid constructor arguments

Unsupported dependencies

Consistent types make large plugin architectures safer to refactor.

Dependency Injection and Composer

Composer provides autoloading and package management.

A typical structure is:

composer.json src/ tests/ vendor/

PSR-4 maps namespaces:

{    "autoload": {        "psr-4": {            "Kaddora\\MyPlugin\\": "src/"        }    } }

Dependency injection operates at the object-creation level, while Composer handles class loading.

They solve different problems but work well together.

Dependency Injection and Namespaces

Namespaces give dependencies unique identities.

For example:

use Kaddora\MyPlugin\Contracts\LoggerInterface; use Kaddora\MyPlugin\Repositories\OrderRepositoryInterface;

Then:

final class OrderService {    public function __construct(        private OrderRepositoryInterface $repository,        private LoggerInterface $logger    ) {    } }

Namespaces and DI complement each other.

Dependency Injection and WordPress Core Classes

You can inject WordPress-related objects where doing so is useful.

For example:

final class QueryService {    public function __construct(        private \WP_Query $query    ) {    } }

However, don't inject every WordPress object merely for abstraction.

Use DI where it improves architecture or testability.

Dependency Injection and Hooks

A useful pattern is to keep hook registration separate from the service.

Service   ↑ Hook Listener   ↑ WordPress

Example:

final class ProductHooks {    public function __construct(        private ProductService $service    ) {    }    public function register(): void    {        \add_action(            'save_post_product',            [ $this, 'handle' ],            10,            1        );    }    public function handle(int $postId): void    {        $this->service->sync($postId);    } }

The hook adapter depends on the service.

Dependency Injection and Admin Pages

An admin page can also receive services.

final class SettingsPage {    public function __construct(        private SettingsService $service    ) {    }    public function render(): void    {        $settings = $this->service->getAll();        // Render UI.    } }

The UI component remains separate from settings persistence.

Dependency Injection and Background Jobs

Background jobs should be constructed with their required service:

final class ReportJob {    public function __construct(        private ReportService $service    ) {    } }

This is more maintainable than looking up services through globals during execution.

Dependency Injection and Event Listeners

WordPress hooks can represent events.

For example:

Order Completed      ↓ OrderListener      ↓ NotificationService

The listener can receive the notification service through DI.

This creates loose coupling between modules.

Dependency Injection and AI Providers

AI plugins often support different model providers.

A clean design:

AIService      ↓ AIProviderInterface      ↓ ┌────────────┬────────────┬────────────┐ ↓            ↓            ↓ Provider A  Provider B  Test Provider

Configuration determines which concrete implementation is injected.

This allows provider changes without rewriting the main service.

Dependency Injection and Payment Providers

The same design applies to payments:

PaymentService      ↓ PaymentGatewayInterface      ↓ ┌─────────┬─────────┬─────────┐ ↓         ↓         ↓ Stripe   PayPal    Sandbox

The service remains provider-neutral.

Dependency Injection and Logging

A service can depend on:

interface LoggerInterface {    public function info(        string $message,        array $context = []    ): void;    public function error(        string $message,        array $context = []    ): void; }

Then a WordPress-specific logger can implement it.

This makes testing easier and keeps logging policy centralized.

Dependency Injection and Configuration Storage

For plugin settings, avoid passing raw get_option() calls everywhere.

Instead:

SettingsRepository       ↓ SettingsService       ↓ Application

Then inject SettingsService where needed.

This reduces repeated configuration access logic.

Dependency Injection and Caching

A service can depend on a cache abstraction:

final class ProductService {    public function __construct(        private ProductRepositoryInterface $repository,        private CacheInterface $cache    ) {    } }

The service can then coordinate:

Service  ↓ Cache  ├── Hit → Return  └── Miss       ↓   Repository       ↓    Database

Dependency Injection and Database Transactions

A complex service may coordinate several persistence operations:

OrderService     ↓ Begin Transaction     ↓ Repository A     ↓ Repository B     ↓ Commit / Rollback

Transaction management should be designed deliberately.

Do not assume that injecting repositories automatically provides transactional integrity.

The persistence layer and database engine must support the required behavior.

Dependency Injection and Multi-Module Plugins

A large plugin can have modules:

Plugin ├── Analytics ├── Automation ├── Reporting ├── Notifications └── Integrations

Each module can define its own services and dependencies.

A central composition root can wire them together.

This avoids one giant initialization function containing the entire application's behavior.

The Composition Root

The composition root is the place where dependencies are assembled.

For a WordPress plugin, it is usually near:

plugin.php Bootstrap Plugin class Container

A simplified example:

Composition Root     ↓ Repository Implementations     ↓ Integration Implementations     ↓ Services     ↓ Controllers / Hooks / Jobs

Application classes should generally consume dependencies rather than deciding how every dependency is constructed.

Recommended WordPress Plugin Dependency Flow

WordPress    │    ├── REST    ├── Admin    ├── AJAX    ├── Cron    └── Hooks           │           ▼      Controllers /      Listeners / Jobs           │           ▼         Services           │      ┌────┴─────┐      ▼          ▼ Repositories  Integrations      │          │      ▼          ▼ Database     External APIs

Dependencies flow downward toward technical boundaries.

How to Build Dependency Injection Step by Step

Step 1: Identify Responsibilities

List your services, repositories, integrations, and controllers.

Step 2: Identify Dependencies

For each class, ask:

What does this class require to perform its responsibility?

Step 3: Prefer Interfaces Where Valuable

Create contracts for components that need multiple implementations or easy substitution.

Step 4: Use Constructor Injection

Add required dependencies to constructors.

Step 5: Remove Internal new Calls

Move object creation to the composition root.

Step 6: Wire Dependencies

Manually create the object graph or use a container if complexity justifies it.

Step 7: Connect WordPress Entry Points

Inject services into:

REST controllers

Admin pages

AJAX handlers

Cron jobs

Hook listeners

WP-CLI commands

Step 8: Add Tests

Mock or fake dependencies in unit tests.

Step 9: Run Static Analysis

Use PHPStan or equivalent tooling.

Step 10: Test the Real WordPress Runtime

Verify hooks, APIs, databases, and integrations in integration testing.

Example Project Structure

A scalable plugin might look like:

my-plugin/ ├── my-plugin.php ├── composer.json │ ├── src/ │   ├── Admin/ │   ├── Contracts/ │   ├── Domain/ │   ├── Integrations/ │   ├── Repositories/ │   ├── Services/ │   ├── Hooks/ │   ├── Rest/ │   ├── Infrastructure/ │   └── Plugin.php │ ├── tests/ │   ├── Unit/ │   └── Integration/ │ └── vendor/

A composition root can wire the classes together.

Example Composition Root

$container = new Container(); $container->set(    OrderRepositoryInterface::class,    fn() => new OrderRepository() ); $container->set(    PaymentGatewayInterface::class,    fn() => new StripeGateway(        apiKey: get_option('myplugin_api_key')    ) ); $container->set(    NotificationInterface::class,    fn() => new EmailNotification() ); $container->set(    OrderService::class,    fn(Container $container) => new OrderService(        $container->get(            OrderRepositoryInterface::class        ),        $container->get(            PaymentGatewayInterface::class        ),        $container->get(            NotificationInterface::class        )    ) );

This demonstrates the relationship between contracts and implementations.

For smaller plugins, direct manual wiring may be easier.

Avoid Injecting Everything

Not every function or WordPress API needs to become an injected service.

For example, turning:

\sanitize_text_field( $value );

into:

SanitizationService

may add complexity without meaningful benefit.

Dependency injection should be used for meaningful dependencies, not every function call.

Avoid Constructor Overload

A constructor such as:

public function __construct(    A $a,    B $b,    C $c,    D $d,    E $e,    F $f,    G $g,    H $h ) { }

may indicate that the class has too many responsibilities.

Split the class or group coherent configuration into a dedicated object.

Don't solve every large constructor with a service container.

Avoid Service Locator Abuse

This:

$logger = $container->get(    LoggerInterface::class );

inside every class hides dependencies.

Prefer:

public function __construct(    private LoggerInterface $logger ) { }

Dependencies should be visible in the class definition.

Avoid Hidden Global State

This:

global $plugin; $service = $plugin->get('service');

creates hidden coupling.

Prefer explicit construction and injection.

Avoid Static Dependency Injection

Static properties can make dependencies difficult to replace and test:

MyService::$repository;

Instance-based objects with constructor injection are generally easier to reason about.

Dependency Injection and Backward Compatibility

Changing constructor signatures can affect code that creates your classes directly.

For public or extension-facing classes, consider:

Existing third-party integrations

Public APIs

Constructor compatibility

Factory methods

Adapters

Deprecation paths

Internal classes can usually evolve more freely.

Architecture changes should be planned around the plugin's actual public surface.

Dependency Injection and Plugin Activation

Dependency injection should not interfere with WordPress activation and deactivation hooks.

For example:

Plugin Bootstrap       ↓ Activation Handler       ↓ Register Services       ↓ Runtime Hooks

Activation-specific database setup and runtime service construction are separate concerns.

Dependency Injection and Multisite

For multisite-compatible plugins, injected services should understand the relevant site context.

Avoid assuming a single global site when operations depend on:

Current blog

Network settings

Site-specific configuration

User context

Dependency injection does not automatically solve multisite concerns; your services and repositories still need correct WordPress context handling.

Dependency Injection and WooCommerce

A WooCommerce plugin might use:

OrderService ├── OrderRepository ├── PaymentGateway ├── CustomerService └── NotificationService

WooCommerce-specific APIs can remain behind the appropriate boundary.

The service coordinates application behavior without hard-coding every implementation.

Dependency Injection and AI WordPress Plugins

An AI plugin may use:

ContentService ├── AIProviderInterface ├── PromptRepository ├── UsageRepository ├── LoggerInterface └── CacheInterface

This architecture supports:

Multiple AI providers

Testing without external API calls

Usage tracking

Caching

Logging

Provider switching

The external API client should remain isolated from the application's core logic.

Dependency Injection and Security

Security dependencies can also be injected where appropriate.

For example:

final class AdminService {    public function __construct(        private CapabilityChecker $capabilities    ) {    } }

However, don't hide basic WordPress capability or nonce checks behind unnecessary abstractions.

Use DI where a component represents meaningful reusable behavior.

Dependency Injection and CI/CD

Dependency injection itself doesn't require a specific CI system.

But a mature plugin pipeline should verify:

Composer Validate      ↓ Autoload Generation      ↓ PHPStan      ↓ PHPCS      ↓ PHPUnit      ↓ WordPress Integration Tests      ↓ Build

Constructor and interface changes are then checked automatically.

How AI Can Help Implement Dependency Injection

AI can assist in converting tightly coupled plugin code into injected architecture.

For example:

Legacy Class     ↓ Find `new` Dependencies     ↓ Classify Responsibilities     ↓ Suggest Interfaces     ↓ Add Constructor Injection     ↓ Build Composition Root     ↓ Generate Tests     ↓ Developer Review

AI can help detect:

Classes constructing too many dependencies

Hidden dependencies

Static state

Service locator usage

Repeated object creation

Candidate interfaces

Testability problems

But automated refactoring should be reviewed carefully.

Changing constructors or interfaces can break:

Third-party integrations

Existing hooks

Direct class instantiation

Backward compatibility

Serialization

Plugin initialization

AI should assist the refactoring process rather than blindly rewrite the entire plugin.

Common Dependency Injection Mistakes

Creating Dependencies Inside Classes

$this->repository = new Repository();

This creates tight coupling.

Using a Service Locator Everywhere

It hides dependencies.

Building a Container Too Early

Small plugins don't always need one.

Injecting Every Function

This creates unnecessary abstraction.

Giant Constructors

Too many dependencies may indicate poor class boundaries.

Depending on Concrete Implementations

Interfaces can be useful where substitution matters.

Hidden Global State

Globals make dependencies difficult to see.

Static Singletons

They make testing and replacement harder.

No Composition Root

Without a clear wiring location, dependency setup becomes scattered.

Dependency Injection Checklist

Architecture

 Classes have focused responsibilities

 Dependencies are explicit

 Constructor injection is used for required dependencies

 Interfaces are introduced where they provide value

Wiring

 Object creation is centralized

 Manual wiring is used where sufficient

 Container introduced only when justified

 Composition root is identifiable

WordPress

 Hooks remain thin

 REST controllers remain thin

 AJAX handlers remain thin

 Cron jobs delegate to services

 WordPress globals are isolated where practical

Testing

 Dependencies can be mocked

 Fake implementations are possible

 Unit tests cover service behavior

 Integration tests cover real persistence

Quality

 PHPStan configured

 PHPCS configured

 Composer validated

 CI runs tests

Security

 Authorization remains explicit

 Request validation remains at boundaries

 API credentials aren't hard-coded

 External dependencies are handled safely

Recommended Dependency Injection Architecture

A practical production architecture is:

                    WordPress                        │        ┌───────────────┼───────────────┐        ↓               ↓               ↓      REST            Admin           Hooks        │               │               │        └───────────────┼───────────────┘                        ↓                    Services                        ↓              Contracts / Interfaces                   ┌────┴────┐                   ↓         ↓             Repositories  Integrations                   ↓         ↓                Database   External APIs                        │                        ↓                   Infrastructure

The composition root wires the concrete implementations.

Manual DI vs Container

Approach

Best For

Advantages

Trade-Offs

Manual Wiring

Small to medium plugins

Simple, explicit

More setup as the graph grows

Basic Container

Medium to large plugins

Centralized resolution

More infrastructure

Full DI Framework

Complex applications

Powerful resolution and lifecycle features

Higher complexity and dependency overhead

Start with manual wiring.

Introduce a container when object construction itself becomes a recurring source of complexity.

Step-by-Step Refactoring Example

Start with tightly coupled code:

class ReportService {    public function generate(): array    {        $repository = new ReportRepository();        $logger     = new Logger();        $report = $repository->getData();        $logger->info(            'Report generated.'        );        return $report;    } }

Refactor the dependencies:

class ReportService {    public function __construct(        private ReportRepositoryInterface $repository,        private LoggerInterface $logger    ) {    }    public function generate(): array    {        $report = $this->repository->getData();        $this->logger->info(            'Report generated.'        );        return $report;    } }

Then wire it externally:

$service = new ReportService(    new ReportRepository(),    new Logger() );

Finally, test it with a mock:

$service = new ReportService(    $mockRepository,    $mockLogger );

The business class is now easier to test and replace.

Dependency Injection for Large WordPress Plugins

For large plugins, consider organizing dependencies by module:

Plugin │ ├── Admin Module │   ├── SettingsPage │   └── AdminService │ ├── Analytics Module │   ├── ReportService │   └── AnalyticsRepository │ ├── Commerce Module │   ├── OrderService │   └── ProductService │ └── Integration Module    ├── PaymentGateway    └── CRMClient

Each module can have a clear composition boundary.

This reduces the need for a single massive container configuration.

Why Choose ThemeKaddora?

ThemeKaddora develops WordPress themes, plugins, WooCommerce solutions, AI products, HTML templates, UI kits, SaaS-oriented products, and business-focused digital solutions.

As products grow, dependency injection can support a consistent architecture:

WordPress Entry Point        ↓ Controller / Listener / Job        ↓ Service        ↓ Interface   ┌────┴─────┐   ↓          ↓ Repository  Integration   ↓          ↓ Database    External API

This architecture can help support:

Modular plugin development

Composer and PSR-4

Service classes

Repository patterns

API integrations

WooCommerce workflows

AI provider abstraction

Unit testing

Integration testing

Static analysis

CI/CD

For a growing ThemeKaddora product ecosystem, standardizing dependency-injection conventions can make teams more productive while still allowing each plugin to use an appropriate level of architectural complexity.

A small plugin may use straightforward constructor injection.

A larger application may benefit from contracts, module-specific composition, and a service container.

The key is to introduce abstraction only where it solves a real engineering problem.

Conclusion

Dependency Injection is one of the most useful techniques for building maintainable object-oriented WordPress plugins.

The core principle is simple:

A class should receive the dependencies it needs instead of deciding how to construct them.

Instead of:

Service  ↓ new Repository() new Logger() new API Client()

use:

Composition Root      ↓ Inject Dependencies      ↓ Service

A practical dependency-injection architecture should:

Make dependencies explicit

Prefer constructor injection for required dependencies

Use interfaces when substitution provides value

Separate application services from infrastructure

Keep WordPress hooks and controllers thin

Isolate database and external API details

Support unit and integration testing

Centralize dependency wiring

Use a container only when complexity justifies it

Preserve compatibility when refactoring public APIs

Dependency injection works especially well with:

Namespaces

Composer

PSR-4

Service classes

Repository patterns

Interfaces

PHPUnit

PHPStan

PHPCS

CI/CD

The architecture becomes:

WordPress    ↓ Entry Point    ↓ Service    ↓ Interface    ↓ Implementation

The most important rule is:

Inject meaningful dependencies, not every possible function or API.

For small plugins, manual dependency wiring may be the best solution.

For larger plugins, dependency graphs can become complex enough to justify a container.

The goal is not to create the most sophisticated dependency system.

The goal is to make dependencies visible, replaceable, testable, and easy to manage.

When implemented thoughtfully, dependency injection gives WordPress developers a cleaner way to build software that can evolve as features, integrations, teams, and business requirements grow.

Frequently Asked Questions

What is dependency injection in WordPress?

Dependency injection is a design technique where WordPress plugin classes receive the objects and services they depend on instead of constructing those dependencies internally.

Why use dependency injection in WordPress plugins?

DI can improve testability, maintainability, modularity, flexibility, and separation of concerns, particularly in larger plugins.

What is constructor injection?

Constructor injection passes required dependencies through the class constructor:

public function __construct(    private ProductRepositoryInterface $repository ) { }

What is dependency injection used for?

It can be used for repositories, services, API clients, payment gateways, loggers, cache systems, configuration objects, and other meaningful components.

Is dependency injection necessary for every WordPress plugin?

No. Small plugins can often remain simple. DI becomes more useful as dependencies and application complexity increase.

What is the difference between dependency injection and a service container?

Dependency injection is the design technique of providing dependencies externally. A service container is a tool that can automate the construction and resolution of those dependencies.

Can dependency injection work with WordPress hooks?

Yes. Hook listeners can receive services through their constructors and then delegate WordPress events to those services.

Can dependency injection work with REST API controllers?

Yes. REST controllers can receive application services through dependency injection while the services handle business operations.

Can WP-Cron jobs use dependency injection?

Yes. Scheduled jobs can receive services and execute them when WordPress triggers the scheduled event.

Can WP-CLI commands use dependency injection?

Yes. WP-CLI commands can reuse injected application services to avoid duplicating business logic.

Should I inject $wpdb into every class?

Usually not. Isolating $wpdb inside repositories or infrastructure components often produces a cleaner architecture.

Can WordPress global functions be used with dependency injection?

Yes. Dependency injection does not prevent the use of WordPress functions. It helps organize meaningful object dependencies.

Does dependency injection improve performance?

Not automatically. DI primarily improves architecture and testability. Performance still depends on database access, caching, API requests, hooks, and implementation details.

Can dependency injection improve security?

DI itself is not a security mechanism. It can make security-related components easier to isolate and test, but authorization, validation, nonce protection, escaping, and secure API handling are still required.

Can dependency injection work with WooCommerce?

Yes. WooCommerce extensions can inject order repositories, payment gateways, customer services, analytics services, and other application components.

Can AI WordPress plugins use dependency injection?

Yes. AI plugins can inject provider interfaces, API clients, prompt services, usage repositories, caches, loggers, and other components.

Can multiple AI providers be supported through dependency injection?

Yes. A service can depend on an AIProviderInterface, while different provider implementations are injected according to configuration.

Can payment gateways use dependency injection?

Yes. A PaymentGatewayInterface can support multiple implementations such as production gateways and test fakes.

Why is constructor injection preferred?

It makes required dependencies explicit, ensures objects receive them during construction, and generally improves testability and static analysis.

Why shouldn't every class use a service container directly?

Doing so hides dependencies and can effectively turn the container into a global service locator. Prefer injecting the dependencies a class actually needs.

Can dependency injection replace global state?

It can reduce unnecessary application-level global state, but WordPress itself uses global APIs and runtime state. The goal is to isolate those dependencies rather than pretend they don't exist.

What should I do when a constructor has too many dependencies?

Review the class responsibilities. Too many dependencies can indicate that the class should be split or that related configuration and behavior should be grouped into more focused components.

Should I inject every WordPress function as a service?

No. Inject meaningful components and external boundaries. Wrapping every simple WordPress function can create unnecessary complexity.

How do I test a dependency-injected WordPress service?

Inject mocks, stubs, or fake implementations into the service and test its behavior independently from real infrastructure.

Do I still need integration tests when using dependency injection?

Yes. DI improves unit testing, but integration tests are still needed to verify WordPress APIs, database behavior, WooCommerce integrations, and external boundaries.

Can PHPStan validate dependency injection?

Yes. Strongly typed constructors, interfaces, and return types allow static-analysis tools such as PHPStan to detect many dependency and type-related problems.

How does Composer relate to dependency injection?

Composer provides package management and autoloading. Dependency injection controls how application objects receive and use their dependencies.

How does PSR-4 relate to dependency injection?

PSR-4 provides predictable class autoloading based on namespaces and directories. It does not perform dependency injection itself, but it makes namespaced DI-based architectures easier to organize.

Can AI help refactor a WordPress plugin to dependency injection?

Yes. AI can identify internal new calls, hidden dependencies, candidate interfaces, and potential service boundaries. Developers should review all constructor and compatibility changes before merging them.

Why choose ThemeKaddora?

ThemeKaddora develops WordPress themes, plugins, WooCommerce solutions, AI products, HTML templates, UI kits, SaaS-oriented solutions, and business-focused digital products using maintainable architecture, modern PHP practices, API integrations, testing, performance considerations, and scalable engineering workflows.

Comments (0)
Login or create account to leave comments

We use cookies to personalize your experience. By continuing to visit this website you agree to our use of cookies

More