in # Dependency Injection vs Global State
WordPress heavily relies on global variables ($wpdb, $post, $wp_query, etc.), which creates:
- Hidden dependencies - Functions don't declare what they need
- Impossible to test - Can't mock globals easily
- Mysterious bugs - State changes unexpectedly
- Tight coupling - Code depends on WordPress being fully loaded
- No reusability - Can't use code outside WordPress context
class PostExporter {
public function export($post_id) {
global $wpdb;
// Where does $wpdb come from? Good luck testing this!
$post = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE ID = %d",
$post_id
));
return json_encode($post);
}
}Problems:
- Hidden dependency on
$wpdbglobal - Must have WordPress loaded to use this class
- Impossible to test without database
- Can't swap database implementation
- No way to see what this class needs
class PostExporter {
public function __construct(
private PostRepository $repository,
private PostFormatterInterface $formatter
) {}
public function export(int $post_id): ?string {
$posts = $this->repository->get_user_posts($post_id);
if (empty($posts)) {
return null;
}
return $this->formatter->format($posts[0]);
}
}Benefits:
- All dependencies declared in constructor
- Easy to see what the class needs
- Testable - inject mocks
- Flexible - swap implementations
- Reusable - no WordPress coupling
Bad (global state):
function test_export() {
// Have to set up entire WordPress + database
// Hope globals are in right state
$exporter = new PostExporter();
$result = $exporter->export(123);
// Did it actually work? Who knows!
}Good (dependency injection):
function test_export() {
// Create mocks - no database needed!
$mock_repository = new class implements PostRepository {
public function get_user_posts(int $id): array {
return [(object)['id' => 123, 'title' => 'Test']];
}
};
$mock_formatter = new class implements PostFormatterInterface {
public function format(object $post): string {
return json_encode($post);
}
};
$exporter = new PostExporter($mock_repository, $mock_formatter);
$result = $exporter->export(123);
// Verify exact behavior without database
assert($result === '{"id":123,"title":"Test"}');
}// BAD: Use global everywhere
function get_posts_by_author($author_id) {
global $wpdb;
return $wpdb->get_results(/* ... */);
}
// GOOD: Repository pattern
class PostRepository {
public function __construct() {}
public function get_by_author(int $author_id): array {
global $wpdb;
return $wpdb->get_results(/* ... */);
}
}
// Usage: No need to inject wpdb, use global directly
$repository = new PostRepository();// BAD: Hidden dependency
function get_current_post_title() {
global $post;
return $post ? $post->post_title : '';
}
// GOOD: Explicit parameter
function get_post_title(WP_Post $post): string {
return $post->post_title;
}class Container {
private array $services = [];
public function set(string $id, callable $factory): void {
$this->services[$id] = $factory;
}
public function get(string $id): mixed {
if (!isset($this->services[$id])) {
throw new Exception("Service {$id} not found");
}
// Create service using factory
return $this->services[$id]($this);
}
}
// Setup
$container = new Container();
$container->set('wpdb', function() {
global $wpdb;
return $wpdb;
});
$container->set('post.repository', function($c) {
return new PostRepository($c->get('wpdb'));
});
$container->set('post.exporter', function($c) {
return new PostExporter(
$c->get('post.repository'),
new JsonPostFormatter()
);
});
// Usage
$exporter = $container->get('post.exporter');class Logger {
private static $instance = null;
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}
// Usage - global state in disguise
Logger::getInstance()->log('message');Problems:
- Global state hidden as static property
- Hard to test - can't inject mock
- Can't have multiple instances when needed
- Tight coupling - every caller depends on Logger class
interface LoggerInterface {
public function log(string $message): void;
}
class FileLogger implements LoggerInterface {
public function __construct(private string $file) {}
public function log(string $message): void {
error_log($message, 3, $this->file);
}
}
class SomeService {
public function __construct(
private LoggerInterface $logger
) {}
public function do_work(): void {
$this->logger->log('Working...');
}
}
// Usage - inject logger
$logger = new FileLogger('/tmp/app.log');
$service = new SomeService($logger);
// Testing - inject mock
$mock = new NullLogger(); // Doesn't actually log
$service = new SomeService($mock);✅ Inject dependencies via constructor
✅ Program to interfaces, not implementations
✅ Use repository pattern for $wpdb
✅ Pass objects explicitly, not via globals
✅ Use container for complex dependency graphs
❌ Don't use global inside classes
❌ Don't use Singleton pattern
❌ Don't access $post, $wpdb directly in business logic
❌ Don't hide dependencies
❌ Don't make testing require full WordPress setup
You don't have to convert everything at once:
- Start with new code - Use DI for all new features
- Create adapters - Wrap WordPress globals in repositories
- Refactor incrementally - Convert one class at a time
- Test as you go - New code should be testable
// Adapter: WordPress globals → Clean API
class WordPressPostRepository implements PostRepositoryInterface {
public function __construct(private wpdb $wpdb) {}
public function find(int $id): ?WP_Post {
$post = get_post($id);
return $post instanceof WP_Post ? $post : null;
}
}
// Your clean business logic
class PostService {
public function __construct(
private PostRepositoryInterface $repository
) {}
// No WordPress coupling here!
}
// Wire it up at the edge
global $wpdb;
$repository = new WordPressPostRepository($wpdb);
$service = new PostService($repository);Global state is convenient... until it's not.
The moment you want to:
- ✅ Write a unit test
- ✅ Reuse code in different context
- ✅ Understand what a class does
- ✅ Swap an implementation
...you'll wish you had used dependency injection.