Plugin Context

The PluginContext is the foundation of WPZylos's isolation strategy. It encapsulates a plugin's identity and provides prefixed access to WordPress resources.

What is Plugin Context?

PluginContext is an immutable object that holds:

  • Plugin file path
  • Slug (URL-safe identifier)
  • Prefix (for hooks, options, transients)
  • Text domain (for translations)
  • Version

Every WPZylos plugin creates exactly one PluginContext at boot time.

Creating a Plugin Context

<?php
// In your main plugin file

use WPZylos\Framework\Core\PluginContext;

$context = new PluginContext(
    __FILE__,              // Plugin main file path
    'my-awesome-plugin',   // Slug
    'map_',                // Prefix (include trailing underscore)
    'my-awesome-plugin',   // Text domain
    '1.0.0'                // Version
);

Accessing Context Values

// File paths
$context->file();        // /path/to/my-plugin/my-plugin.php
$context->path();        // /path/to/my-plugin/
$context->path('src');   // /path/to/my-plugin/src

// URLs
$context->url();         // https://example.com/wp-content/plugins/my-plugin/
$context->url('assets'); // https://example.com/wp-content/plugins/my-plugin/assets

// Identity
$context->slug();        // my-awesome-plugin
$context->prefix();      // map_
$context->textDomain();  // my-awesome-plugin
$context->version();     // 1.0.0

Prefixed Resources

Custom Hooks

// Create prefixed hook names
$hookName = $context->hook('settings_saved');
// Returns: map_settings_saved

do_action($hookName, $settings);

// Other plugins using the same hook name won't conflict
// Plugin B's context would return: pb_settings_saved

Options

// Create prefixed option keys
$optionKey = $context->option('settings');
// Returns: map_settings

update_option($optionKey, $value);
$value = get_option($optionKey, []);

Transients

// Create prefixed transient keys
$transientKey = $context->transientKey('api_cache');
// Returns: map_api_cache

set_transient($transientKey, $data, 3600);
$data = get_transient($transientKey);

User Meta

// Create prefixed meta keys
$metaKey = $context->metaKey('preferences');
// Returns: map_preferences

update_user_meta($userId, $metaKey, $preferences);

The ContextInterface

PluginContext implements ContextInterface, allowing for testing with mock contexts:

<?php

namespace WPZylos\Framework\Core\Contracts;

interface ContextInterface
{
    public function file(): string;
    public function path(string $relative = ''): string;
    public function url(string $relative = ''): string;
    public function slug(): string;
    public function prefix(): string;
    public function textDomain(): string;
    public function version(): string;
    public function hook(string $name): string;
    public function option(string $name): string;
    public function transientKey(string $name): string;
    public function metaKey(string $name): string;
}

Why We Did It This Way

No Hardcoded Text Domains

Problem: Hardcoded text domains in translation functions make plugins non-portable and cause issues with PHP-Scoper.

Solution: Text domain comes from context, used consistently throughout.

// Bad: Hardcoded text domain
__('Hello', 'my-plugin');

// Good: Dynamic text domain from context
__('Hello', $context->textDomain());

// Good: Or via I18n service
$i18n->translate('Hello');

No Global Constants for Identity

Problem: define('MY_PLUGIN_SLUG', 'my-plugin') pollutes global namespace and can conflict.

Solution: Identity lives in PluginContext, passed via dependency injection.

// Bad: Global constants (can conflict)
define('MY_PLUGIN_PREFIX', 'mp_');
$hook = MY_PLUGIN_PREFIX . 'init';

// Good: Context-based (isolated)
$hook = $context->hook('init');

Immutable Identity

Problem: Mutable plugin identity could lead to inconsistent state.

Solution: PluginContext is immutable. Once created, identity cannot change.

// PluginContext has no setters
$context = new PluginContext(...);

// This is guaranteed to always return the same value
$context->prefix(); // Always 'map_'

Trailing Underscore Convention

Problem: Inconsistent prefix usage leads to ugly keys like mp_settings vs mpsettings.

Solution: Prefix includes trailing underscore. Methods don't add extra underscores.

$context = new PluginContext(
    __FILE__,
    'my-plugin',
    'mp_',  // Trailing underscore included
    'my-plugin',
    '1.0.0'
);

$context->hook('init');     // mp_init (not mp__init)
$context->option('cache');  // mp_cache (not mp__cache)

Using Context in Services

Inject ContextInterface into services that need plugin identity:

<?php

namespace Vendor\MyPlugin\Services;

use WPZylos\Framework\Core\Contracts\ContextInterface;

class CacheService
{
    public function __construct(private ContextInterface $context)
    {
    }

    public function get(string $key): mixed
    {
        return get_transient($this->context->transientKey($key));
    }

    public function put(string $key, mixed $value, int $ttl = 3600): bool
    {
        return set_transient($this->context->transientKey($key), $value, $ttl);
    }
}

Register in the container:

// In a service provider
$this->container->singleton(ContextInterface::class, fn() => $this->context);
$this->container->singleton(CacheService::class);

Testing with Mock Context

<?php

use PHPUnit\Framework\TestCase;
use WPZylos\Framework\Core\Contracts\ContextInterface;

class CacheServiceTest extends TestCase
{
    public function testGetReturnsTransient(): void
    {
        $context = $this->createMock(ContextInterface::class);
        $context->method('transientKey')
            ->with('data')
            ->willReturn('test_data');

        // Mock get_transient in your test bootstrap
        $service = new CacheService($context);

        // Test behavior
    }
}

Next Steps