Skip to content

Conversation

@jasonbahl
Copy link
Collaborator

@jasonbahl jasonbahl commented Oct 17, 2025

What does this implement/fix? Explain your changes.

This PR implements a formal, namespaced API for storing and retrieving data in the AppContext class while maintaining full backward compatibility with existing dynamic property usage.

New API Methods

Adds six new methods to AppContext for managing request-scoped state with namespace isolation:

  • set(string $namespace, string $key, $value): void - Store a value
  • get(string $namespace, string $key, $default = null): mixed - Retrieve a value with optional default
  • has(string $namespace, string $key): bool - Check if a key exists
  • remove(string $namespace, string $key): void - Remove a specific key
  • clear(string $namespace): void - Clear all data in a namespace
  • all(string $namespace): array - Get all data in a namespace

Implementation Details

  • Namespace Isolation: Data is stored in a private $store property as nested arrays ($store[$namespace][$key]) to prevent collisions between plugins/extensions
  • Deprecation Notice: Adds __set() magic method that triggers _doing_it_wrong() when setting dynamic properties (not existing properties)
  • Backward Compatibility: Dynamic properties still work as before - they just trigger a deprecation notice to encourage migration
  • Documentation: Comprehensive PHPDoc with usage examples and guidance on intended use cases

Testing

  • Baseline Tests: 14 new tests covering existing AppContext functionality to ensure no regressions
  • New API Tests: 11 tests covering the new get/set API with various data types, edge cases, and namespace isolation
  • Integration Test: Demonstrates real-world directive-like pattern for state management across resolver chains
  • All tests passing: 25 tests, 86 assertions ✅

Code Quality

  • Fixed PHPStan return type issue in get_loader() method
  • Removed 11 unnecessary @phpstan-ignore comments across the codebase
  • Added appropriate phpcs:ignore comments for semantic parameter names ($namespace, $default)
  • ✅ PHPCS: No errors
  • ✅ PHPStan: No errors
  • ✅ All tests passing

Does this close any currently open issues?

Closes #3393

Any other comments?

Why Namespace Isolation?

Without namespaces, different plugins could accidentally overwrite each other's context data. By requiring a namespace (recommended to use plugin text domain), we ensure:

  • No collisions between plugins
  • Clear ownership of data
  • Better debugging (you know which plugin set what data)

Migration Path

Existing code using dynamic properties will continue to work but will see deprecation notices in debug mode:

// Old way (deprecated, but still works)
$context->my_custom_data = 'value';

// New way (recommended)
$context->set('my-plugin', 'custom_data', 'value');

Use Cases

This API is perfect for request-scoped state management patterns, including, but not limited to:

🌍 Internationalization (i18n)

Store user's preferred locale, language settings, or translation context to localize GraphQL responses on a per-request basis.

$context->set('my-plugin', 'user-locale', 'fr_FR');
$context->set('my-plugin', 'currency', 'EUR');

🧪 A/B Testing & Feature Flags

Determine variant assignments or feature flags at the request start, then use consistently across all resolvers in that request.

$context->set('my-plugin', 'ab-test-variant', 'B');
$context->set('my-plugin', 'feature-new-checkout', true);

🔒 Private/Protected Content Access

Store password verification state or content access tokens to avoid re-checking permissions for every field.

$context->set('my-plugin', 'post-password-verified', $post_id);
$context->set('my-plugin', 'member-tier', 'premium');

👤 User Preferences & Personalization

Cache user preferences fetched once, then reuse across multiple resolvers without repeated database queries.

$context->set('my-plugin', 'user-preferences', $preferences);
$context->set('my-plugin', 'theme', 'dark');

🔐 External API Authentication

Store API tokens or authentication state for third-party services used during the request.

$context->set('my-plugin', 'stripe-api-key', $api_key);
$context->set('my-plugin', 'oauth-token', $token);

📊 Request Tracing & Debugging

Add trace IDs, performance markers, or debug context for logging and monitoring.

$context->set('my-plugin', 'trace-id', $trace_id);
$context->set('my-plugin', 'start-time', microtime(true));

🏢 Multi-tenancy

Store tenant-specific context, database connections, or configuration for multi-tenant applications.

$context->set('my-plugin', 'tenant-id', $tenant_id);
$context->set('my-plugin', 'tenant-db', $db_connection);

⚡ Custom Caching Strategies

Store cache keys, bust flags, or other cache-related state specific to the request.

$context->set('my-plugin', 'cache-version', $version);
$context->set('my-plugin', 'skip-cache', true);

What This API is NOT For:

  • Permanent configuration: Use graphql_app_context_config filter instead
  • Replacing existing properties: Don't override $viewer, $request, $loaders, etc.
  • Cross-request state: This is request-scoped only - data is cleared after each request
  • Session storage: Use WordPress sessions or transients for persistent data

Example Usage

// Set up locale data at the start of the request
add_filter('pre_graphql_execute_request', function($response, $request) {
    $context = $request->app_context;
    
    // Determine and store user's preferred locale
    $user_locale = determine_user_locale();
    $context->set('my-plugin', 'user-locale', $user_locale);
    $context->set('my-plugin', 'original-locale', get_locale());
    
    // Optionally switch locale for the request
    switch_to_locale($user_locale);
    
    return $response;
}, 10, 2);

// Use the stored locale in field resolvers
add_filter('graphql_resolve_field', function($result, $source, $args, $context, $info, $type_name, $field_key) {
    // Check if custom locale is set
    if ($context->has('my-plugin', 'user-locale')) {
        $locale = $context->get('my-plugin', 'user-locale');
        
        // Use the locale to localize the result
        if (is_string($result)) {
            $result = translate_string_for_locale($result, $locale);
        }
    }
    
    return $result;
}, 10, 7);

// Clean up after the request completes
add_action('graphql_after_execute', function($response, $request) {
    $context = $request->app_context;
    
    // Restore original locale if it was changed
    if ($context->has('my-plugin', 'original-locale')) {
        $original_locale = $context->get('my-plugin', 'original-locale');
        restore_current_locale();
    }
    
    // Clear all plugin data from context
    $context->clear('my-plugin');
}, 10, 2);

This pattern ensures:

  • Clean setup/teardown lifecycle
  • Request-scoped state management
  • No pollution of the global scope
  • Automatic cleanup after each request

…ynamic properties

- Add formal get(), set(), has(), remove(), clear(), and all() methods to AppContext
- Implement namespace isolation to prevent collisions between plugins/extensions
- Add __set() magic method with deprecation notice for dynamic property usage
- Store namespaced data in private $store property with nested array structure
- Maintain full backward compatibility for existing dynamic property access

Testing:
- Add comprehensive baseline tests for existing AppContext functionality (14 tests)
- Add new tests for get/set API covering various data types and edge cases (11 tests)
- Add integration test demonstrating directive-like state management pattern
- All 25 tests passing with 86 assertions

Code Quality:
- Add phpcs:ignore comments for $namespace and $default parameter names
- Fix PHPStan return type issue in get_loader() with @var annotation
- Remove 11 unnecessary @phpstan-ignore comments for requireOnce.fileNotFound
- All PHPCS and PHPStan checks passing with no errors

Breaking Changes: None - fully backward compatible

Related: wp-graphql#3393
@jasonbahl jasonbahl requested a review from justlevine October 17, 2025 20:45
@jasonbahl jasonbahl self-assigned this Oct 17, 2025
@jasonbahl jasonbahl added the type: enhancement Improvements to existing functionality label Oct 17, 2025
@coveralls
Copy link

Coverage Status

coverage: 84.639% (+0.06%) from 84.584%
when pulling 99bbb19 on jasonbahl:feat/3393-formalize-app-context
into 80ff199 on wp-graphql:develop.

@jasonbahl jasonbahl merged commit e13bdab into wp-graphql:develop Oct 23, 2025
40 checks passed
jasonbahl pushed a commit to jasonbahl/wp-graphql that referenced this pull request Oct 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: enhancement Improvements to existing functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RFC: Formalize AppContext Custom Data with a get/set API

2 participants