LESSON 2 โฑ๏ธ 15 min read

OOP Foundation: Main Class & Autoloading

Setting Up Composer

Composer handles autoloading so you never write require statements manually. Navigate to your plugin folder and initialize:

cd wp-content/plugins/task-manager
composer init

Answer the prompts:

  • Package name: yourname/task-manager
  • Type: wordpress-plugin
  • License: GPL-2.0-or-later

Configuring PSR-4 Autoloading

Edit composer.json to add autoloading:

{
    "name": "yourname/task-manager",
    "description": "A modern task management plugin",
    "type": "wordpress-plugin",
    "license": "GPL-2.0-or-later",
    "autoload": {
        "psr-4": {
            "TaskManager\": "src/"
        }
    },
    "require": {
        "php": ">=8.0"
    }
}

Generate the autoloader:

composer dump-autoload

This creates vendor/autoload.php which loads any class in the TaskManager namespace from the src/ directory.

How PSR-4 Works:
TaskManagerAdminSettings โ†’ src/Admin/Settings.php
TaskManagerDatabaseTasksTable โ†’ src/Database/TasksTable.php

The Singleton Pattern

For our main Plugin class, we'll use the Singleton pattern to ensure only one instance exists:

<?php
// src/Plugin.php

namespace TaskManager;

class Plugin {
    
    /**
     * Single instance of the plugin.
     */
    private static ?Plugin $instance = null;
    
    /**
     * Plugin version.
     */
    public const VERSION = '1.0.0';
    
    /**
     * Get plugin instance.
     */
    public static function get_instance(): Plugin {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    /**
     * Private constructor - use get_instance().
     */
    private function __construct() {
        $this->init_hooks();
    }
    
    /**
     * Prevent cloning.
     */
    private function __clone() {}
    
    /**
     * Prevent unserialization.
     */
    public function __wakeup() {
        throw new Exception( 'Cannot unserialize singleton' );
    }
    
    /**
     * Initialize WordPress hooks.
     */
    private function init_hooks(): void {
        // Admin hooks
        if ( is_admin() ) {
            add_action( 'admin_menu', [ $this, 'register_admin_menu' ] );
            add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
        }
        
        // REST API
        add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
        
        // Load translations
        add_action( 'init', [ $this, 'load_textdomain' ] );
    }
    
    /**
     * Register admin menu pages.
     */
    public function register_admin_menu(): void {
        $admin_menu = new AdminAdminMenu();
        $admin_menu->register();
    }
    
    /**
     * Enqueue admin CSS and JS.
     */
    public function enqueue_admin_assets( string $hook ): void {
        // Only load on our plugin pages
        if ( ! str_contains( $hook, 'task-manager' ) ) {
            return;
        }
        
        wp_enqueue_style(
            'task-manager-admin',
            TM_PLUGIN_URL . 'assets/css/admin.css',
            [],
            self::VERSION
        );
        
        wp_enqueue_script(
            'task-manager-admin',
            TM_PLUGIN_URL . 'assets/js/admin.js',
            [ 'jquery' ],
            self::VERSION,
            true
        );
    }
    
    /**
     * Register REST API routes.
     */
    public function register_rest_routes(): void {
        $controller = new RESTTasksController();
        $controller->register_routes();
    }
    
    /**
     * Load plugin translations.
     */
    public function load_textdomain(): void {
        load_plugin_textdomain(
            'task-manager',
            false,
            dirname( plugin_basename( TM_PLUGIN_FILE ) ) . '/languages'
        );
    }
}

Activation & Deactivation Classes

Create separate classes for activation/deactivation logic:

<?php
// src/Activator.php

namespace TaskManager;

class Activator {
    
    /**
     * Run on plugin activation.
     */
    public static function activate(): void {
        // Create database tables
        DatabaseTasksTable::create_table();
        
        // Set default options
        if ( false === get_option( 'task_manager_settings' ) ) {
            update_option( 'task_manager_settings', [
                'tasks_per_page' => 20,
                'enable_api'     => true,
            ]);
        }
        
        // Store version for upgrades
        update_option( 'task_manager_version', Plugin::VERSION );
        
        // Flush rewrite rules for custom endpoints
        flush_rewrite_rules();
    }
}
<?php
// src/Deactivator.php

namespace TaskManager;

class Deactivator {
    
    /**
     * Run on plugin deactivation.
     */
    public static function deactivate(): void {
        // Clear scheduled events
        wp_clear_scheduled_hook( 'task_manager_daily_cleanup' );
        
        // Flush rewrite rules
        flush_rewrite_rules();
    }
}
Don't Delete Data on Deactivation! Users may reactivate later. Only remove data in uninstall.php.

The Uninstall File

For complete cleanup when the plugin is deleted:

<?php
// uninstall.php

// Exit if not called by WordPress
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

global $wpdb;

// Delete options
delete_option( 'task_manager_settings' );
delete_option( 'task_manager_version' );

// Drop custom table
$table_name = $wpdb->prefix . 'task_manager_tasks';
$wpdb->query( "DROP TABLE IF EXISTS {$table_name}" );

// Clear any transients
delete_transient( 'task_manager_cache' );

Dependency Injection (Advanced)

For larger plugins, use dependency injection instead of static calls:

<?php
// src/Container.php

namespace TaskManager;

class Container {
    
    private array $services = [];
    
    public function register( string $key, callable $factory ): void {
        $this->services[ $key ] = $factory;
    }
    
    public function get( string $key ): mixed {
        if ( ! isset( $this->services[ $key ] ) ) {
            throw new Exception( "Service not found: {$key}" );
        }
        return $this->services[ $key ]( $this );
    }
}

// Usage in Plugin class
private function register_services(): void {
    $this->container = new Container();
    
    $this->container->register( 'database', function() {
        return new DatabaseTasksTable();
    });
    
    $this->container->register( 'admin', function( $c ) {
        return new AdminAdminMenu( $c->get( 'database' ) );
    });
}

Testing Your Setup

Verify everything works:

  1. Activate the plugin in WordPress admin
  2. Check for PHP errors in wp-content/debug.log
  3. Confirm the menu item appears (we'll create it next lesson)
// Quick test - add to Plugin::init_hooks() temporarily
add_action( 'admin_notices', function() {
    echo '<div class="notice notice-success"><p>Task Manager loaded!</p></div>';
});

Next Steps

In Lesson 3, we'll dive deep into WordPress hooks โ€“ actions and filters โ€“ and learn how to create custom hooks for extensibility.

๐ŸŽฏ Lesson Complete! You've set up modern PHP autoloading and created a solid plugin foundation.