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/ # i18nMain 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 install → vendor/ 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 dependenciesPHP-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.