This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the vonAffenfels WordPress Framework - a PHP framework that simplifies WordPress plugin and theme development using Symfony components and modern PHP practices.
- Run all tests:
composer testor./vendor/bin/pest - Run specific test file:
./vendor/bin/pest tests/Unit/ExampleTest.php - The framework uses Pest PHP testing framework
- Check code style:
composer codestyle - Fix code style:
composer fixstyle - Code standards: PSR-12 for src/, custom rules for tests/
- Install dependencies:
composer install - The framework requires PHP >= 8.1
The framework is built around a Symfony-based kernel system that provides dependency injection and service container functionality for WordPress:
-
Kernel System (
src/Kernel/)WordpressKernel- Base kernel that registers all framework servicesPluginKernel- For WordPress pluginsThemeKernel- For WordPress themes- Services configured via
config/services.yaml
-
Attribute-Based Registration The framework uses PHP 8 attributes for service discovery and registration:
#[AsHookContainer]- Register WordPress hooks#[AsMetaboxContainer]- Register metaboxes#[AsDynamicBlock]- Register Gutenberg blocks#[AsRestContainer]- Register REST API routes#[AsAdminAjaxContainer]- Register admin AJAX handlers#[PostType]- Register custom post types#[AsFacade]- Register facades for static access to services
-
Component Loaders Each major feature has a Loader class and CompilerPass:
- Hooks:
HookLoader+HookLoaderCompilerPass - Metaboxes:
MetaboxLoader+MetaboxLoaderCompilerPass - REST API:
RestAPILoader+RestAPILoaderCompilerPass - Facades:
FacadeLoader+FacadeLoaderCompilerPass - etc.
- Hooks:
-
Template System (
src/TemplateRenderer/)- Supports both Twig and PHTML templates
- Templates stored in
templates/directory - Global context and function handlers for template data
-
Post Objects (
src/PostObjects/)- Object-oriented wrapper for WordPress posts
- Extensible via
#[PostTypeExtension]attribute - Built-in support for Pages, Posts, and Nav Menu Items
-
Facade System (
src/Facade/)- Laravel-style facades for static access to container services
- Lazy loading - services are instantiated only when first accessed
- Automatic class aliasing for clean syntax
- Cache resolved instances for performance
-
Testing System (
src/Wordpress/)Wordpresswrapper class for mocking WordPress global functions- Enables testable code without loading WordPress environment
- Uses
__callStaticmagic method to proxy function calls - Provides
fake(),resetFake(),mock()methods for easy mocking
- Service Registration: Services are registered using Symfony DI container with attribute-based autoconfiguration
- WordPress Integration: The framework boots during WordPress initialization and registers all components via appropriate WordPress hooks
- Template Rendering: Use
TemplateRendererservice to render templates with proper context - Admin AJAX: Admin AJAX actions are registered via attributes and handled through a unified loader
- Settings: Framework provides a
Settingbase class with conversion support for handling WordPress options- Uses
Wordpresswrapper class for all WordPress function calls (get_option, update_option) - Provides built-in fake system for high-level testing with
Setting::fakeSetting()andSetting::clearFakes() - Supports conversions between database and application formats
- Fake values are treated as database values and go through the same conversion pipeline
- Uses
- Facades: Use
#[AsFacade(ServiceClass::class)]attribute on facade classes extendingFacadefor static access to services - Testing: Use
Wordpress::function_name()instead of direct WordPress function calls to enable mocking in tests
// Define a service
class UserService {
public function __construct(
private readonly DatabaseConnection $db,
private readonly CacheInterface $cache
) {}
public function getUser(int $id): ?User {
// Implementation
}
}
// Create a facade for the service
use VAF\WP\Framework\Facade\Facade;
use VAF\WP\Framework\Facade\Attribute\AsFacade;
#[AsFacade(UserService::class)]
class UserServiceFacade extends Facade {
}
// Use the facade statically
$user = UserServiceFacade::getUser(123);The facade will automatically resolve UserService from the container with all its dependencies when first accessed.
// Instead of direct WordPress function calls (untestable):
if (is_admin()) {
add_action('admin_init', $callback);
}
// Use the Wordpress wrapper (testable):
use VAF\WP\Framework\Wordpress\Wordpress;
if (Wordpress::is_admin()) {
Wordpress::add_action('admin_init', $callback);
}
// In tests:
Wordpress::fake();
Wordpress::mock()->shouldReceive('is_admin')->andReturn(true);The wrapper automatically forwards calls to WordPress functions in production but allows easy mocking during testing.
The Settings system provides two complementary testing approaches:
// 1. High-level testing with Setting fakes (easy for simple scenarios):
// IMPORTANT: Fake values should be in DATABASE FORMAT (as stored in WordPress)
// They will be converted using fromDb when retrieved and toDb when saved
Setting::fakeSetting('my_option', 'fake_value'); // String as stored in DB
$setting = new MySetting('my_option', 'plugin_name');
expect($setting->getValue())->toBe('fake_value');
// For settings with conversions (e.g., JSON):
Setting::fakeSetting('json_option', '{"key":"value"}'); // JSON string (DB format)
$setting = new JsonSetting('json_option', 'plugin_name');
expect($setting->getValue())->toBe(['key' => 'value']); // Converted to array
// 2. Lower-level WordPress function mocking (for integration testing):
Wordpress::fake();
Wordpress::mock()->shouldReceive('get_option')
->with('plugin_name_my_option', 'default')
->andReturn('db_value');
$setting = new MySetting('my_option', 'plugin_name', 'default');
expect($setting->getValue())->toBe('db_value');
// Always clean up in tests:
beforeEach(function () {
Setting::clearFakes();
Wordpress::resetFake();
});The Setting fake system respects conversion functions, treating faked values as database values:
- When faking: Provide the value in database format (e.g., JSON string, 'true'/'false' for booleans)
- When getting: The
fromDbconversion is applied to the faked value - When saving: The
toDbconversion is applied before updating the fake - This ensures conversion logic is tested even when using fakes
Example with boolean conversion:
// Fake with database format string
Setting::fakeSetting('bool_setting', 'true'); // Database stores as string
$setting = new BooleanSetting('bool_setting', 'plugin');
expect($setting->getValue())->toBe(true); // Converted to boolean
$setting->setValue(false);
// Fake is now 'false' (string) after toDb conversion- Plugins extend
Pluginclass and callPlugin::registerPlugin($file) - Themes extend
Themeclass and callTheme::registerTheme($path) - Container building:
Plugin::buildContainer()for development
- When writing a new system add a new docs/{system name}.md file similar to the files already in the docs directory and add a link to this file in the README.md
- When changing an existing system, update the corresponding docs/{system name}.md file
- Always use the Wordpress wrapper class for WordPress functions to ensure testability
- Follow TDD cycle: Write test first (red), implement functionality (green), then refactor
- Use appropriate testing level:
- Setting fakes for high-level behavior testing
- Wordpress mocks for WordPress integration testing
- Clean up after tests: Always reset fakes and mocks in beforeEach/afterEach hooks
- Mock at the right level: Don't over-mock; use the simplest approach that tests your code properly