Skip to content

Latest commit

 

History

History
302 lines (239 loc) · 7.31 KB

File metadata and controls

302 lines (239 loc) · 7.31 KB

in # Dependency Injection vs Global State

The Problem

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

Bad Practice (bad.php)

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 $wpdb global
  • 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

Good Practice (good.php)

Constructor Injection

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

How to Test

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"}');
}

Common WordPress Patterns

Pattern 1: Wrapping $wpdb

// 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();

Pattern 2: Avoiding global $post

// 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;
}

Pattern 3: Service Container (Advanced)

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');

Singleton Anti-Pattern

BAD: Singleton

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

GOOD: Dependency Injection

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);

Key Takeaways

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

Migration Strategy

You don't have to convert everything at once:

  1. Start with new code - Use DI for all new features
  2. Create adapters - Wrap WordPress globals in repositories
  3. Refactor incrementally - Convert one class at a time
  4. Test as you go - New code should be testable

Example Adapter

// 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);

The Bottom Line

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.