Ana Sayfa / Blog / WP plugin development best practices: PSR-4, Composer, autoload

WP plugin development best practices: PSR-4, Composer, autoload

WordPress plugin development modern PHP development standartlarına gelmeli. PSR-4, Composer, namespace - pratik bir rehber.

WordPress plugin geliştirmenin eski hali: single .php file, hundreds of function’lar, wp_xxx_ prefix’li naming conventions. 2025’te bu yaklaşım sürdürülebilir değil.

19 yıllık WP development deneyimim var. Son 5 yıldır tüm plugin’lerimi modern PHP standartlarına göre geliştiriyorum. Bu yazıda kullandığım pattern’leri paylaşacağım.

Neden modern approach?

Eski plugin pattern’i (single file, function prefix):

Problem’ler:
– Code organization zor, her şey global function
– Class/interface/trait kullanamıyorsun (veya loading manual)
– Dependency management primitive
– Unit testing challenge
– IDE autocomplete limited
– Team development conflict-prone

Modern PHP standardları (PSR-4, Composer, namespace) bu sorunları çözüyor.

Basic setup

Yeni plugin için base structure:

my-awesome-plugin/
├── my-awesome-plugin.php    # Main file
├── composer.json             # Dependencies
├── vendor/                   # Composer autoload
├── src/                      # PSR-4 classes
│   ├── Plugin.php
│   ├── Admin/
│   │   └── SettingsPage.php
│   ├── Frontend/
│   │   └── Renderer.php
│   └── Services/
│       └── ApiClient.php
├── templates/                # PHP templates
├── assets/                   # CSS, JS, images
├── tests/                    # Unit tests
└── languages/                # i18n

Main plugin file

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Version: 1.0.0
 * Author: Ali Çınaroğlu
 * Text Domain: my-awesome-plugin
 */

if (!defined('ABSPATH')) exit;

require_once __DIR__ . '/vendor/autoload.php';

use MyPlugin\Plugin;

Plugin::boot(__FILE__);

Main file minimal. Plugin constants define eder, autoload’u yükler, Plugin class’ını start eder.

Composer setup

composer.json:

{
    "name": "alicinaroglu/my-awesome-plugin",
    "description": "My awesome WordPress plugin",
    "type": "wordpress-plugin",
    "require": {
        "php": ">=7.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0",
        "wp-phpunit/wp-phpunit": "^6.0"
    },
    "autoload": {
        "psr-4": {
            "MyPlugin\\": "src/"
        }
    }
}

composer installvendor/ oluşuyor, autoload ready.

Plugin class pattern

namespace MyPlugin;

class Plugin {
    private static ?Plugin $instance = null;
    
    public static function boot(string $mainFile): void {
        if (self::$instance === null) {
            self::$instance = new self($mainFile);
            self::$instance->init();
        }
    }
    
    private function __construct(private string $mainFile) {}
    
    private function init(): void {
        add_action('init', [$this, 'onInit']);
        add_action('admin_menu', [new Admin\SettingsPage(), 'register']);
        add_action('wp_enqueue_scripts', [new Frontend\Assets(), 'enqueue']);
    }
    
    public function onInit(): void {
        // Plugin boot logic
    }
}

Singleton pattern, dependency injection, clear entry points.

Namespace discipline

Every class kendi namespace’inde:

// src/Admin/SettingsPage.php
namespace MyPlugin\Admin;

class SettingsPage {
    public function register(): void { /* ... */ }
}

// src/Services/ApiClient.php
namespace MyPlugin\Services;

class ApiClient {
    public function fetch(string $url): array { /* ... */ }
}

Namespace conflict yok, IDE autocomplete işe yarıyor.

Dependency injection

Basit DI pattern:

namespace MyPlugin\Services;

class ApiClient {
    public function __construct(
        private string $apiKey,
        private LoggerInterface $logger
    ) {}
    
    public function fetch(string $endpoint): array {
        $this->logger->info("Fetching: $endpoint");
        // ... HTTP call ...
    }
}

// Usage
$client = new ApiClient(
    apiKey: get_option('my_api_key'),
    logger: new FileLogger(WP_CONTENT_DIR . '/my-plugin.log')
);

Constructor’da dependencies. Easy to test (mock dependencies).

Service container (optional)

Büyük plugin’lerde service container:

use DI\Container;

$container = new Container();

$container->set(LoggerInterface::class, \DI\create(FileLogger::class));
$container->set(ApiClient::class, \DI\autowire());

// Usage
$client = $container->get(ApiClient::class);  // Auto-resolved dependencies

PHP-DI, Pimple, Symfony Container. Overkill for small plugins.

Hook registration

Eski pattern:

add_action('init', 'my_plugin_init');
function my_plugin_init() { /* ... */ }

Modern pattern:

namespace MyPlugin\Hooks;

class Initializer {
    public function __construct(private ApiClient $client) {}
    
    public function register(): void {
        add_action('init', [$this, 'onInit']);
    }
    
    public function onInit(): void {
        $this->client->setup();
    }
}

// Plugin boot'ta
(new Initializer($apiClient))->register();

Class method’lar hook’a bağlanıyor. Testable, organized.

Database schema management

Plugin activation’da table create:

namespace MyPlugin\Database;

class Schema {
    public static function install(): void {
        global $wpdb;
        
        $charset = $wpdb->get_charset_collate();
        
        $sql = "CREATE TABLE {$wpdb->prefix}my_plugin_data (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            user_id BIGINT UNSIGNED NOT NULL,
            data LONGTEXT,
            created_at DATETIME NOT NULL,
            PRIMARY KEY (id),
            INDEX idx_user_id (user_id)
        ) $charset;";
        
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }
}

register_activation_hook(__FILE__, [Schema::class, 'install']);

Migration’lar için: versioning + dbDelta. Schema evolution managed.

Settings API kullanımı

namespace MyPlugin\Admin;

class Settings {
    private const OPTION_NAME = 'my_plugin_settings';
    
    public function register(): void {
        add_action('admin_init', [$this, 'registerSettings']);
    }
    
    public function registerSettings(): void {
        register_setting('my_plugin_group', self::OPTION_NAME);
        
        add_settings_section(
            'general_section',
            __('General Settings', 'my-awesome-plugin'),
            null,
            'my_plugin_settings'
        );
        
        add_settings_field(
            'api_key',
            __('API Key', 'my-awesome-plugin'),
            [$this, 'renderApiKeyField'],
            'my_plugin_settings',
            'general_section'
        );
    }
    
    public function renderApiKeyField(): void {
        $settings = get_option(self::OPTION_NAME, []);
        $value = esc_attr($settings['api_key'] ?? '');
        echo "<input type='text' name=\"" . self::OPTION_NAME . "[api_key]\" value=\"$value\">";
    }
}

REST API endpoints

Modern WP plugin genelde REST API expose ediyor:

namespace MyPlugin\Api;

class RestController {
    public function register(): void {
        add_action('rest_api_init', [$this, 'registerRoutes']);
    }
    
    public function registerRoutes(): void {
        register_rest_route('my-plugin/v1', '/data/(?P<id>\d+)', [
            'methods' => 'GET',
            'callback' => [$this, 'getData'],
            'permission_callback' => [$this, 'canAccess'],
            'args' => [
                'id' => [
                    'required' => true,
                    'validate_callback' => fn($v) => is_numeric($v)
                ]
            ]
        ]);
    }
    
    public function getData(\WP_REST_Request $request): \WP_REST_Response {
        $id = $request['id'];
        return new \WP_REST_Response(['data' => $data], 200);
    }
    
    public function canAccess(): bool {
        return current_user_can('manage_options');
    }
}

Testing

PHPUnit setup:

// tests/bootstrap.php
require_once __DIR__ . '/../vendor/autoload.php';
require_once WP_PHPUNIT_DIR . '/wordpress/wp-load.php';

// tests/Unit/ApiClientTest.php
use PHPUnit\Framework\TestCase;
use MyPlugin\Services\ApiClient;

class ApiClientTest extends TestCase {
    public function testFetch(): void {
        $client = new ApiClient('test_key', $this->createMock(LoggerInterface::class));
        $result = $client->fetch('/endpoint');
        $this->assertIsArray($result);
    }
}
./vendor/bin/phpunit tests/

Security basics

Every plugin’de:

  • Nonce verification: form submission’larda
  • Capability check: admin action’larda
  • Data sanitization: user input
  • Output escaping: HTML output
  • Prepared statements: DB query’leri

Bu 5 discipline security’nin temeli.

i18n (localization)

__('Save Changes', 'my-awesome-plugin')
_e('Saved', 'my-awesome-plugin')
_n('item', 'items', $count, 'my-awesome-plugin')

Text domain plugin slug’ına eşit. load_plugin_textdomain() in init action.

Build ve distribution

Dev vs production:

  • Dev: composer install with dev dependencies
  • Production: composer install --no-dev --optimize-autoloader

Build script (Gulp, Webpack) assets’i compile, minify. Release zip’i dev files içermez.

Sonuç

WordPress plugin development modern PHP standartlarına geldiğinde sürdürülebilirlik dramatically artıyor. PSR-4 autoload, namespace, dependency injection, testing.

İlk plugin’de bu disciplini kurmak 1-2 gün. Ondan sonraki her plugin template’ten generate. Scale iyi.

Eski plugin’leri retrofit ederken incremental migration. Yeni feature’lar modern pattern’de, eski kod zaman içinde refactor.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç