arraydev/wptoolkit

WP Tool Kit is library for create wordpress plugin.

Maintainers

Package info

github.com/bogdanovandreycode/WPToolkit

pkg:composer/arraydev/wptoolkit

Transparency log

Statistics

Installs: 364

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.16-beta 2026-07-22 13:14 UTC

README

WPToolKit is a small toolkit for building WordPress plugins with OOP, attributes, and a lightweight DI container.

The library helps you register:

  • REST routes
  • actions
  • ajax handlers
  • cron jobs
  • filters
  • admin pages
  • meta boxes
  • shortcodes
  • widgets
  • WPBakery elements
  • roles
  • options
  • transients
  • settings API
  • nonces
  • capabilities
  • notices
  • PHP-file translations
  • HTTP client
  • lifecycle callbacks
  • views

It is designed to avoid global helper state and internal static service locators, which makes runtime behavior more predictable when multiple plugins are active.

Requirements

  • PHP ^8.1
  • WordPress
  • Composer

Installation

composer require arraydev/wptoolkit

Philosophy

  • No global app() helper
  • No internal static container state
  • Controllers can be instantiated through a local ServiceFactory
  • Attribute-based bootstrapping for template-method style base controllers

Quick Start

Minimal plugin bootstrap:

<?php
/**
 * Plugin Name: Demo Toolkit Plugin
 */

declare(strict_types=1);

use WpToolKit\Loader\AttributeLoader;

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

add_action('plugins_loaded', function (): void {
    $loader = new AttributeLoader(
        'Vendor\\DemoPlugin',
        __DIR__ . '/src'
    );

    $loader->loadControllers();
});

Your plugin classes can then be initialized through PHP attributes.

Attribute-Based Controllers

AttributeLoader scans a directory, finds classes with supported attributes, validates their parent controller, and creates them through ServiceFactory.

Supported attributes:

  • #[Route(...)]
  • #[Action(...)]
  • #[Ajax(...)]
  • #[Cron(...)]
  • #[Filter(...)]
  • #[Page(...)]
  • #[MetaBox(...)]
  • #[Shortcode(...)]
  • #[Widget(...)]
  • #[WpBakeryElement(...)]

Important rules:

  • One controller class can have only one controller attribute.
  • A class with #[Route] must extend RouteController, #[Action] must extend ActionController, and so on.
  • Attribute-loaded classes must be constructible from attribute arguments and container-resolved dependencies.
  • Your current wpkit Boot.template still loads only the route directory, so you will need to expand your plugin bootstrap later if you want Ajax, Cron, Page, Widget, and other attribute controllers to load automatically.

REST Routes

Base controller: WpToolKit\Controller\RouteController

Attribute: WpToolKit\Attribute\Route

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Http;

use WP_REST_Request;
use WpToolKit\Attribute\Route;
use WpToolKit\Controller\RouteController;

#[Route('demo-plugin/v1', '/ping', methods: 'GET')]
final class PingRoute extends RouteController
{
    public function callback(WP_REST_Request $request): mixed
    {
        return ['message' => 'pong'];
    }

    public function checkPermission(WP_REST_Request $request): bool
    {
        return current_user_can('read');
    }
}

Route Params

If you need route params, pass param classes in the attribute.

Example:

#[Route(
    'demo-plugin/v1',
    '/sync',
    params: [
        \Vendor\DemoPlugin\Http\Params\PostIdParam::class,
    ],
    methods: 'POST'
)]

Param classes must implement WpToolKit\Interface\ParamRoureInterface, usually by extending WpToolKit\Controller\ParamRoute.

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Http\Params;

use WP_Error;
use WpToolKit\Controller\ParamRoute;

final class PostIdParam extends ParamRoute
{
    public function __construct()
    {
        parent::__construct('post_id', required: true);
    }

    public function validate($param, $request, $key): bool|WP_Error
    {
        return is_numeric($param);
    }

    public function sanitize($param, $request, $key): mixed
    {
        return (int) $param;
    }
}

Actions

Base controller: WpToolKit\Controller\ActionController

Attribute: WpToolKit\Attribute\Action

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Hooks;

use WpToolKit\Attribute\Action;
use WpToolKit\Controller\ActionController;

#[Action('init', priority: 20)]
final class RegisterSomethingAction extends ActionController
{
    public function handle(...$args): void
    {
        // Your logic here
    }
}

AJAX

Base controller: WpToolKit\Controller\AjaxController

Attribute: WpToolKit\Attribute\Ajax

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Http;

use WpToolKit\Attribute\Ajax;
use WpToolKit\Controller\AjaxController;

#[Ajax('demo_sync')]
final class SyncAjax extends AjaxController
{
    public function handle(): void
    {
        wp_send_json_success([
            'message' => 'Sync completed',
        ]);
    }
}

Guest access example:

#[Ajax('public_lookup', allowGuests: true)]

Cron

Base controller: WpToolKit\Controller\CronController

Attribute: WpToolKit\Attribute\Cron

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Hooks;

use WpToolKit\Attribute\Cron;
use WpToolKit\Controller\CronController;

#[Cron('demo_hourly_sync', 'hourly')]
final class HourlySyncCron extends CronController
{
    public function handle(): void
    {
        // Scheduled job logic
    }
}

Filters

Base controller: WpToolKit\Controller\FilterController

Attribute: WpToolKit\Attribute\Filter

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Hooks;

use WpToolKit\Attribute\Filter;
use WpToolKit\Controller\FilterController;

#[Filter('the_title', priority: 10, acceptedArgs: 2)]
final class TitleFilter extends FilterController
{
    public function handle(...$args): mixed
    {
        $title = $args[0] ?? '';

        return '[Demo] ' . $title;
    }
}

Admin Pages

Base controller: WpToolKit\Controller\AdminPage

Attribute: WpToolKit\Attribute\Page

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Admin;

use WpToolKit\Attribute\Page;
use WpToolKit\Controller\AdminPage;

#[Page(
    'Demo Settings',
    'Demo Settings',
    'manage_options',
    'demo-settings',
    25,
    icon: 'dashicons-admin-generic'
)]
final class SettingsPage extends AdminPage
{
    public function render(): void
    {
        echo '<div class="wrap"><h1>Demo Settings</h1></div>';
    }

    public function callback(): void
    {
        // Handle POST request for this page
    }
}

Submenu example:

#[Page(
    'Logs',
    'Logs',
    'manage_options',
    'demo-logs',
    30,
    isSubManuItem: true,
    parentUrl: 'demo-settings'
)]

Meta Boxes

Base controller: WpToolKit\Controller\MetaBoxController

Attribute: WpToolKit\Attribute\MetaBox

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Admin;

use WpToolKit\Attribute\MetaBox;
use WpToolKit\Controller\MetaBoxController;
use WpToolKit\Entity\MetaBoxContext;
use WpToolKit\Entity\MetaBoxPriority;

#[MetaBox(
    'demo_box',
    'Demo Box',
    'post',
    MetaBoxContext::SIDE,
    MetaBoxPriority::HIGH
)]
final class DemoMetaBox extends MetaBoxController
{
    public function render($post): void
    {
        echo '<p>Meta box content</p>';
    }

    public function callback($postId): void
    {
        // Save logic
    }
}

Shortcodes

Base controller: WpToolKit\Controller\ShortcodeController

Attribute: WpToolKit\Attribute\Shortcode

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Shortcodes;

use WpToolKit\Attribute\Shortcode;
use WpToolKit\Controller\ShortcodeController;

#[Shortcode('demo_badge', ['label' => 'Default Badge'])]
final class BadgeShortcode extends ShortcodeController
{
    public function render($atts, $content): string
    {
        $atts = $this->getAtts($atts);

        return '<span class="demo-badge">' . esc_html($atts['label']) . '</span>';
    }
}

Widgets

Base controller: WpToolKit\Controller\WidgetsController

Attribute: WpToolKit\Attribute\Widget

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\Widgets;

use WpToolKit\Attribute\Widget;
use WpToolKit\Controller\WidgetsController;

#[Widget('demo_widget', 'Demo Widget', 'Simple demo widget')]
final class DemoWidget extends WidgetsController
{
    public function widget($args, $instance): void
    {
        echo $args['before_widget'] ?? '';
        echo '<p>Demo widget output</p>';
        echo $args['after_widget'] ?? '';
    }

    public function form($instance): void
    {
        echo '<p>No settings</p>';
    }

    public function update($new_instance, $old_instance): array
    {
        return $old_instance;
    }
}

WPBakery Elements

Base controller: WpToolKit\Controller\WpBakeryElementController

Attribute: WpToolKit\Attribute\WpBakeryElement

WpBakeryElementController registers both the shortcode and the vc_map() configuration. Every element gets a Design tab by default. The design value is stored as URL-encoded JSON with responsive keys:

  • default
  • laptops
  • tablets
  • mobiles

Example:

<?php

declare(strict_types=1);

namespace Vendor\DemoPlugin\WpBakery;

use WpToolKit\Attribute\WpBakeryElement;
use WpToolKit\Controller\WpBakeryElementController;

#[WpBakeryElement(
    base: 'demo_title',
    name: 'Demo Title',
    category: 'Demo',
    description: 'Styled demo title'
)]
final class DemoTitleElement extends WpBakeryElementController
{
    protected function params(): array
    {
        return [
            [
                'type' => 'textfield',
                'heading' => 'Title',
                'param_name' => 'title',
                'value' => 'Hello',
            ],
        ];
    }

    protected function renderElement(array $atts, ?string $content = null): string
    {
        return '<h2>' . esc_html($atts['title']) . '</h2>';
    }
}

The parent controller wraps rendered HTML with a design class and prints the generated CSS automatically. Override wrapDesignElement() if the element needs to put the design class on its own root node:

protected function wrapDesignElement(): bool
{
    return false;
}

Then use $this->getDesignClass($atts) inside renderElement().

ServiceFactory

WpToolKit\Factory\ServiceFactory is a lightweight container for local plugin runtime.

Supported methods:

  • bind()
  • singleton()
  • instance()
  • make()
  • get()
  • has()
  • call()

Example:

<?php

declare(strict_types=1);

use Vendor\DemoPlugin\Service\Mailer;
use WpToolKit\Factory\ServiceFactory;

$container = new ServiceFactory();

$container->singleton(Mailer::class);

$mailer = $container->make(Mailer::class);

Passing runtime arguments:

$postController = $container->make(
    \WpToolKit\Controller\PostController::class,
    ['post' => $postEntity]
);

Use a shared container with AttributeLoader:

$container = new ServiceFactory();

$loader = new AttributeLoader(
    'Vendor\\DemoPlugin',
    __DIR__ . '/src',
    $container
);

$loader->loadControllers();

Options

Manager: WpToolKit\Manager\OptionManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\OptionManager;

$options = new OptionManager();

$options->update('demo_plugin_enabled', true);
$enabled = $options->get('demo_plugin_enabled', false);

Site options:

$options->updateSite('demo_network_enabled', true);
$enabled = $options->getSite('demo_network_enabled', false);

Transients

Manager: WpToolKit\Manager\TransientManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\TransientManager;

$transients = new TransientManager();

$transients->set('demo_cache', ['ok' => true], HOUR_IN_SECONDS);
$cached = $transients->get('demo_cache');

Settings API

Manager: WpToolKit\Manager\SettingsManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Field\TextField;
use WpToolKit\Manager\SettingsManager;

$settings = new SettingsManager();

$settings->addSetting('demo_options', 'demo_option_name');
$settings->addSection('demo_main', 'Main Settings', 'demo-page');
$settings->addField(
    'demo-page',
    'demo_main',
    new TextField(
        'demo_option_name',
        'Option Name',
        (string) get_option('demo_option_name', '')
    )
);

Render form in your admin page:

$settings->renderForm('demo_options', 'demo-page');

Notices

Manager: WpToolKit\Manager\NoticeManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\NoticeManager;

$notices = new NoticeManager();
$notices->success('Settings saved successfully.');
$notices->warning('This action will affect all items.');

Nonces

Manager: WpToolKit\Manager\NonceManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\NonceManager;

$nonces = new NonceManager();

$nonce = $nonces->create('save_demo_settings');
$isValid = $nonces->verify($nonce, 'save_demo_settings');

Render nonce field in a form:

echo $nonces->field('save_demo_settings');

Check nonce in admin postback:

$nonces->checkAdminReferer('save_demo_settings');

Check nonce in AJAX:

$nonces->checkAjaxReferer('demo_sync');

Translations

Manager: WpToolKit\Manager\LocaleManager

This is an optional alternative to WordPress gettext helpers. It loads PHP dictionaries from locale folders and works through normal dependency injection instead of a static facade.

Recommended structure:

my-plugin/
├─ languages/
│  ├─ en/
│  │  └─ payment.php
│  ├─ ro/
│  │  └─ payment.php
│  └─ ru/
│     └─ payment.php

Example dictionary:

<?php

return [
    'maib_payment' => 'Payment via MAIB',
    'maib_description' => 'Payment for booking #:booking_id',
];

Configure it in your plugin bootstrap:

<?php

use WpToolKit\Factory\ServiceFactory;
use WpToolKit\Manager\LocaleManager;

$container = new ServiceFactory();

$container->instance(
    LocaleManager::class,
    new LocaleManager(__DIR__ . '/languages', 'en', 'en')
);

Inject it into your services or controllers:

<?php

use WpToolKit\Manager\LocaleManager;

final class PaymentService
{
    public function __construct(private LocaleManager $locale)
    {
    }

    public function title(string $language): string
    {
        return $this->locale->get('payment.maib_payment', $language);
    }

    public function description(int $bookingId, string $language): string
    {
        return $this->locale->get('payment.maib_description', $language, [
            'booking_id' => $bookingId,
        ]);
    }
}

Load a whole dictionary by dotted path:

$locale = $this->locale->getDictionary('request.entity.passenger', $language);

return [
    'first_name.required' => $locale['first_name_required'] ?? 'first_name.required',
    'first_name.string' => $locale['first_name_string'] ?? 'first_name.string',
];

For nested dictionaries, dotted keys are supported too:

$message = $this->locale->get('validation.passenger.first_name.required', 'ro');

Capabilities

Manager: WpToolKit\Manager\CapabilityManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\CapabilityManager;

$capabilities = new CapabilityManager();

if (!$capabilities->currentUserCan('manage_options')) {
    wp_die('Access denied');
}

Role capability management:

$capabilities->addCapabilityToRole('editor', 'manage_demo_plugin');
$capabilities->removeCapabilityFromRole('editor', 'manage_demo_plugin');

HTTP API

Controller: WpToolKit\Controller\HttpClient

Example:

<?php

declare(strict_types=1);

use WpToolKit\Controller\HttpClient;

$http = new HttpClient();
$response = $http->get('https://example.com/wp-json/');
$body = $http->getBody($response);

Fetch JSON:

$json = $http->getJson('https://example.com/wp-json/');

Lifecycle

Manager: WpToolKit\Manager\LifecycleManager

This manager does not replace your Boot.php. It is intended to organize activation, deactivation, and uninstall callbacks inside your existing boot flow.

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\LifecycleManager;

$lifecycle = new LifecycleManager();

$lifecycle->onActivate(function (): void {
    flush_rewrite_rules();
});

$lifecycle->onDeactivate(function (): void {
    flush_rewrite_rules();
});

Then call it from your Boot.php methods:

public function activate_demo_plugin(): void
{
    $this->lifecycle->activate();
}

public function deactivate_demo_plugin(): void
{
    $this->lifecycle->deactivate();
}

Classic Controllers Without Attributes

You can still use controllers manually if that fits your plugin better.

Example:

<?php

declare(strict_types=1);

use WpToolKit\Controller\ScriptController;
use WpToolKit\Entity\ScriptType;

$scripts = new ScriptController();
$scripts->addStyle('demo-style', '/assets/style.css', ScriptType::ADMIN);

Roles

Manager: WpToolKit\Manager\RoleManager

Example:

<?php

declare(strict_types=1);

use WpToolKit\Manager\RoleManager;

$roles = new RoleManager();

$roles->addRole('demo_manager', 'Demo Manager', [
    'read' => true,
    'edit_posts' => true,
]);

Loading roles from YAML:

$roles->loadFromYaml(__DIR__ . '/config/roles.yaml');

Example YAML:

demo_manager:
  display_name: Demo Manager
  capabilities:
    read: true
    edit_posts: true

Views

Controller: WpToolKit\Controller\ViewLoader

Example:

<?php

declare(strict_types=1);

use WpToolKit\Controller\ViewLoader;
use WpToolKit\Entity\View;

$views = new ViewLoader();

$views->add(new View(
    'settings',
    __DIR__ . '/views/settings.php',
    ['title' => 'Demo Settings']
));

$views->load('settings');

Loading views from YAML:

$views->loadFromYaml(
    __DIR__ . '/config/views.yaml',
    plugin_dir_path(__FILE__)
);

Example YAML:

settings: /views/settings.php
logs: /views/logs.php

Menu and Script Controllers

MenuController

use WpToolKit\Controller\MenuController;

$menu = new MenuController();
$menu->addItem(
    'Demo',
    'Demo',
    'manage_options',
    'demo-page',
    fn () => print 'Demo page',
    'dashicons-admin-generic',
    25
);

ScriptController

use WpToolKit\Controller\ScriptController;
use WpToolKit\Entity\ScriptType;

$scripts = new ScriptController();

$scripts->addStyle('demo-style', '/assets/style.css', ScriptType::ADMIN);
$scripts->addScript('demo-script', '/assets/app.js', ScriptType::FRONT);

Recommended Project Structure

my-plugin/
├─ my-plugin.php
├─ composer.json
├─ vendor/
├─ src/
│  ├─ Admin/
│  ├─ Controllers/
│  ├─ Hooks/
│  ├─ Http/
│  │  ├─ Params/
│  │  ├─ Routes/
│  │  └─ Requests/
│  ├─ Shortcodes/
│  ├─ Widgets/
│  └─ Service/
├─ views/
└─ config/

Notes About Multiple Plugins and Vendors

WPToolKit no longer relies on internal static service state, which reduces conflicts between plugins.

However, WordPress still has a common Composer problem:

  • if two plugins load different copies of the same PHP library
  • and both copies use the same namespace
  • the first loaded copy may win

If your plugin ships its own vendor directory, the safest production approach is to isolate namespaces with a tool such as PHP-Scoper or Mozart.

Best Practices

  • Create one ServiceFactory per plugin runtime.
  • Pass the same container into AttributeLoader.
  • Keep attribute-loaded controllers thin and focused.
  • Put business logic in your own services and inject them through the container.
  • Prefer namespaced classes inside your plugin.

Limitations

  • AttributeLoader expects classes to live under the namespace and directory you pass in.
  • Controller attributes can only describe one base controller per class.
  • Route params in attributes should be passed as class names, not arbitrary runtime objects.

License

MIT