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)