diff --git a/docs/Core-Modules.md b/docs/Core-Modules.md new file mode 100644 index 0000000..cac4359 --- /dev/null +++ b/docs/Core-Modules.md @@ -0,0 +1,180 @@ +# Core Modules + +WP Framework provides several Core modules that handle common WordPress functionality modifications. These modules are designed to be explicitly opted into by consuming applications (themes/plugins) rather than being automatically initialized. + +## Available Core Modules + +### HeadOverrides + +The `HeadOverrides` module removes unwanted WordPress head elements that are typically not needed in production sites. + +**Location**: `TenupFramework\Core\HeadOverrides` + +**Functionality**: +- Removes WordPress generator meta tag (`wp_generator`) +- Removes Windows Live Writer manifest link (`wlwmanifest_link`) +- Removes Really Simple Discovery service endpoint link (`rsd_link`) + +**Usage**: +```php +// Recommended: Array-based approach +$core_modules = [ \TenupFramework\Core\HeadOverrides::class ]; +foreach ( $core_modules as $module_class ) { + $module = new $module_class(); + if ( $module->can_register() ) { + $module->register(); + } +} +``` + +### Emoji + +The `Emoji` module disables WordPress core emoji functionality, which can improve performance by removing unnecessary scripts and styles. + +**Location**: `TenupFramework\Core\Emoji` + +**Functionality**: +- Removes emoji detection scripts from `wp_head` and admin +- Removes emoji-related styles from front-end and back-end +- Removes emoji-to-static-img conversion from feeds and email +- Disables TinyMCE emoji plugin +- Removes emoji CDN from DNS prefetching hints + +**Usage**: +```php +// Recommended: Array-based approach +$core_modules = [ \TenupFramework\Core\Emoji::class ]; +foreach ( $core_modules as $module_class ) { + $module = new $module_class(); + if ( $module->can_register() ) { + $module->register(); + } +} +``` + +## Core Module Characteristics + +All Core modules follow these patterns: + +### Module Interface Compliance +- Implement `TenupFramework\ModuleInterface` +- Use the `TenupFramework\Module` trait +- Provide `load_order()`, `can_register()`, and `register()` methods + +### Load Order +Core modules typically use a load order of `5`, ensuring they initialize early in the module lifecycle. + +### Explicit Opt-In +Core modules are **not automatically initialized** by the framework. Consuming applications must explicitly instantiate and register them. + +## Integration Patterns + +### Recommended: Array-Based Approach +The cleanest way to initialize Core modules is using an array and foreach loop: + +```php +// Define the Core modules you want to use +$core_modules = [ + \TenupFramework\Core\HeadOverrides::class, + \TenupFramework\Core\Emoji::class, +]; + +// Initialize each module +foreach ( $core_modules as $module_class ) { + $module = new $module_class(); + if ( $module->can_register() ) { + $module->register(); + } +} +``` + +### Manual Instantiation (Alternative) +```php +// Initialize specific Core modules individually +$head_overrides = new \TenupFramework\Core\HeadOverrides(); +if ( $head_overrides->can_register() ) { + $head_overrides->register(); +} + +$emoji = new \TenupFramework\Core\Emoji(); +if ( $emoji->can_register() ) { + $emoji->register(); +} +``` + +### Configuration-Based Approach (Future Enhancement) +If using the configuration-based initialization pattern: + +```php +// Using ModuleInitialization::init_specific_modules() +ModuleInitialization::instance()->init_specific_modules([ + \TenupFramework\Core\HeadOverrides::class, + \TenupFramework\Core\Emoji::class, +]); +``` + +## Best Practices + +### When to Use Core Modules +- **HeadOverrides**: Use when you want to remove WordPress generator meta and other unnecessary head elements +- **Emoji**: Use when you want to disable WordPress emoji functionality for performance reasons + +### Conditional Loading +Consider loading Core modules conditionally based on your application's needs: + +```php +// Build array of Core modules based on conditions +$core_modules = [ \TenupFramework\Core\HeadOverrides::class ]; + +// Only load emoji module in production +if ( wp_get_environment_type() === 'production' ) { + $core_modules[] = \TenupFramework\Core\Emoji::class; +} + +// Initialize all selected modules +foreach ( $core_modules as $module_class ) { + $module = new $module_class(); + if ( $module->can_register() ) { + $module->register(); + } +} +``` + +### Testing +Core modules include comprehensive test suites that verify: +- Interface implementation +- Method existence and callability +- Source code verification of WordPress function calls +- Functional behavior testing + +## Migration from Scaffold + +If migrating from the WP Scaffold plugin's Core modules: + +1. **Remove** the old Core module classes from your scaffold +2. **Add explicit opt-in** to the framework's Core modules +3. **Test** that functionality works as expected +4. **Verify** no regressions in your application + +## Future Enhancements + +### Framework Helper Method +A future enhancement could add a helper method to the `ModuleInitialization` class: + +```php +// Potential future API +ModuleInitialization::init_core_modules([ + \TenupFramework\Core\HeadOverrides::class, + \TenupFramework\Core\Emoji::class, +]); +``` + +This would internally handle the instantiation and registration logic, making the array-based approach even cleaner. + +## Future Core Modules + +The framework is designed to accommodate additional Core modules as needed. New modules should follow the same patterns: +- Implement `ModuleInterface` +- Use the `Module` trait +- Provide comprehensive test coverage +- Require explicit opt-in from consuming applications diff --git a/docs/README.md b/docs/README.md index 2cb283c..2fb7f8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,6 +5,7 @@ WP Framework is a lightweight set of building blocks for structuring WordPress p - Base classes for custom and core post types - Base class for taxonomies - Asset helpers that read modern build sidecars (`.asset.php`) +- Core modules for common WordPress functionality modifications ## Who is this for? - External engineers — Start here: follow the Quick Start and then read Autoloading and Modules → Modules and Initialization → Post Types/Taxonomies → Asset Loading. @@ -17,6 +18,7 @@ WP Framework is a lightweight set of building blocks for structuring WordPress p 3) Initialize modules: `TenupFramework\ModuleInitialization::instance()->init_classes( YOUR_PLUGIN_INC )`. 4) Implement small classes that implement `ModuleInterface` (use the `Module` trait) and optionally extend `AbstractPostType` / `AbstractTaxonomy`. 5) Load assets via the `GetAssetInfo` trait using dist/.asset.php sidecars. +6) Optionally opt into Core modules for common WordPress functionality modifications. ## Table of Contents - [Autoloading and Modules](Autoloading.md) — how classes are discovered and initialized @@ -24,6 +26,7 @@ WP Framework is a lightweight set of building blocks for structuring WordPress p - [Post Types](Post-Types.md) — building custom and core post type integrations - [Taxonomies](Taxonomies.md) — registering and configuring taxonomies - [Asset Loading](Asset-Loading.md) — working with dist/.asset.php for dependencies and versioning +- [Core Modules](Core-Modules.md) — WordPress functionality modification modules ## Conventions - Namespaces: use your project namespace (e.g., `YourVendor\\YourPlugin`) for app code; reference framework classes via the TenupFramework namespace. diff --git a/src/Core/Emoji.php b/src/Core/Emoji.php new file mode 100644 index 0000000..ca90706 --- /dev/null +++ b/src/Core/Emoji.php @@ -0,0 +1,102 @@ +assertInstanceOf( \TenupFramework\ModuleInterface::class, $emoji ); + } + + /** + * Test that Emoji can be registered. + * + * @return void + */ + public function test_can_register() { + $emoji = new Emoji(); + + $this->assertTrue( $emoji->can_register() ); + } + + /** + * Test that Emoji has correct load order. + * + * @return void + */ + public function test_load_order() { + $emoji = new Emoji(); + + $this->assertEquals( 5, $emoji->load_order() ); + } + + /** + * Test that register method can be called without errors. + * + * @return void + */ + public function test_register_can_be_called() { + $emoji = new Emoji(); + + // Mock WordPress functions to prevent actual function calls + \Brain\Monkey\Functions\when( 'remove_action' )->justReturn( true ); + \Brain\Monkey\Functions\when( 'remove_filter' )->justReturn( true ); + \Brain\Monkey\Functions\when( 'add_filter' )->justReturn( true ); + + // This should not throw any exceptions + $emoji->register(); + + // If we get here, the method executed successfully + $this->assertTrue( true ); + } + + /** + * Test that register method exists and is callable. + * + * @return void + */ + public function test_register_method_exists() { + $emoji = new Emoji(); + + $this->assertTrue( method_exists( $emoji, 'register' ) ); + $this->assertTrue( is_callable( [ $emoji, 'register' ] ) ); + } + + /** + * Test that Emoji has the expected WordPress function calls in register method. + * + * @return void + */ + public function test_register_method_contains_expected_calls() { + $reflection = new \ReflectionClass( Emoji::class ); + $method = $reflection->getMethod( 'register' ); + $filename = $method->getFileName(); + $start_line = $method->getStartLine(); + $end_line = $method->getEndLine(); + + // Read the method source code + $lines = file( $filename ); + $method_source = implode( '', array_slice( $lines, $start_line - 1, $end_line - $start_line + 1 ) ); + + // Verify the method contains the expected remove_action calls + $this->assertStringContainsString( "remove_action( 'wp_head', 'print_emoji_detection_script', 7 )", $method_source ); + $this->assertStringContainsString( "remove_action( 'admin_print_scripts', 'print_emoji_detection_script' )", $method_source ); + $this->assertStringContainsString( "remove_action( 'wp_print_styles', 'print_emoji_styles' )", $method_source ); + $this->assertStringContainsString( "remove_action( 'admin_print_styles', 'print_emoji_styles' )", $method_source ); + + // Verify the method contains the expected remove_filter calls + $this->assertStringContainsString( "remove_filter( 'the_content_feed', 'wp_staticize_emoji' )", $method_source ); + $this->assertStringContainsString( "remove_filter( 'comment_text_rss', 'wp_staticize_emoji' )", $method_source ); + $this->assertStringContainsString( "remove_filter( 'wp_mail', 'wp_staticize_emoji_for_email' )", $method_source ); + + // Verify the method contains the expected add_filter calls + $this->assertStringContainsString( "add_filter( 'tiny_mce_plugins', [ \$this, 'disable_emojis_tinymce' ] )", $method_source ); + $this->assertStringContainsString( "add_filter( 'wp_resource_hints', [ \$this, 'disable_emoji_dns_prefetch' ], 10, 2 )", $method_source ); + } + + /** + * Test that disable_emojis_tinymce method exists and is callable. + * + * @return void + */ + public function test_disable_emojis_tinymce_method_exists() { + $emoji = new Emoji(); + + $this->assertTrue( method_exists( $emoji, 'disable_emojis_tinymce' ) ); + $this->assertTrue( is_callable( [ $emoji, 'disable_emojis_tinymce' ] ) ); + } + + /** + * Test that disable_emoji_dns_prefetch method exists and is callable. + * + * @return void + */ + public function test_disable_emoji_dns_prefetch_method_exists() { + $emoji = new Emoji(); + + $this->assertTrue( method_exists( $emoji, 'disable_emoji_dns_prefetch' ) ); + $this->assertTrue( is_callable( [ $emoji, 'disable_emoji_dns_prefetch' ] ) ); + } + + /** + * Test disable_emojis_tinymce method functionality. + * + * @return void + */ + public function test_disable_emojis_tinymce_functionality() { + $emoji = new Emoji(); + + // Test with wpemoji plugin present + $plugins_with_emoji = [ 'wordpress', 'wpemoji', 'media' ]; + $result = $emoji->disable_emojis_tinymce( $plugins_with_emoji ); + + $this->assertNotContains( 'wpemoji', $result ); + $this->assertContains( 'wordpress', $result ); + $this->assertContains( 'media', $result ); + + // Test with wpemoji plugin not present + $plugins_without_emoji = [ 'wordpress', 'media' ]; + $result = $emoji->disable_emojis_tinymce( $plugins_without_emoji ); + + $this->assertEquals( $plugins_without_emoji, $result ); + } + + /** + * Test disable_emoji_dns_prefetch method functionality. + * + * @return void + */ + public function test_disable_emoji_dns_prefetch_functionality() { + $emoji = new Emoji(); + + // Mock apply_filters for emoji_svg_url + \Brain\Monkey\Filters\expectApplied( 'emoji_svg_url' ) + ->once() + ->andReturn( 'https://s.w.org/images/core/emoji/2/svg/' ); + + $urls = [ + 'https://fonts.googleapis.com', + 'https://s.w.org/images/core/emoji/2/svg/', + 'https://example.com', + ]; + + $result = $emoji->disable_emoji_dns_prefetch( $urls, 'dns-prefetch' ); + + $this->assertNotContains( 'https://s.w.org/images/core/emoji/2/svg/', $result ); + $this->assertContains( 'https://fonts.googleapis.com', $result ); + $this->assertContains( 'https://example.com', $result ); + + // Test with different relation type + $result = $emoji->disable_emoji_dns_prefetch( $urls, 'preconnect' ); + $this->assertEquals( $urls, $result ); + } + + /** + * Test that Emoji can be instantiated multiple times. + * + * @return void + */ + public function test_multiple_instances() { + $emoji_1 = new Emoji(); + $emoji_2 = new Emoji(); + + $this->assertInstanceOf( Emoji::class, $emoji_1 ); + $this->assertInstanceOf( Emoji::class, $emoji_2 ); + $this->assertNotSame( $emoji_1, $emoji_2 ); + } + + /** + * Test that Emoji uses the Module trait. + * + * @return void + */ + public function test_uses_module_trait() { + $emoji = new Emoji(); + + // Check that the class has the methods from the Module trait + $this->assertTrue( method_exists( $emoji, 'load_order' ) ); + $this->assertTrue( method_exists( $emoji, 'can_register' ) ); + $this->assertTrue( method_exists( $emoji, 'register' ) ); + } +} diff --git a/tests/Core/HeadOverridesTest.php b/tests/Core/HeadOverridesTest.php new file mode 100644 index 0000000..b15d940 --- /dev/null +++ b/tests/Core/HeadOverridesTest.php @@ -0,0 +1,137 @@ +assertInstanceOf( \TenupFramework\ModuleInterface::class, $head_overrides ); + } + + /** + * Test that HeadOverrides can be registered. + * + * @return void + */ + public function test_can_register() { + $head_overrides = new HeadOverrides(); + + $this->assertTrue( $head_overrides->can_register() ); + } + + /** + * Test that HeadOverrides has correct load order. + * + * @return void + */ + public function test_load_order() { + $head_overrides = new HeadOverrides(); + + $this->assertEquals( 5, $head_overrides->load_order() ); + } + + /** + * Test that register method can be called without errors. + * + * @return void + */ + public function test_register_can_be_called() { + $head_overrides = new HeadOverrides(); + + // Mock remove_action to prevent actual WordPress function calls + \Brain\Monkey\Functions\when( 'remove_action' )->justReturn( true ); + + // This should not throw any exceptions + $head_overrides->register(); + + // If we get here, the method executed successfully + $this->assertTrue( true ); + } + + /** + * Test that register method exists and is callable. + * + * @return void + */ + public function test_register_method_exists() { + $head_overrides = new HeadOverrides(); + + $this->assertTrue( method_exists( $head_overrides, 'register' ) ); + $this->assertTrue( is_callable( [ $head_overrides, 'register' ] ) ); + } + + /** + * Test that HeadOverrides has the expected WordPress function calls in register method. + * + * @return void + */ + public function test_register_method_contains_expected_calls() { + $reflection = new \ReflectionClass( HeadOverrides::class ); + $method = $reflection->getMethod( 'register' ); + $filename = $method->getFileName(); + $start_line = $method->getStartLine(); + $end_line = $method->getEndLine(); + + // Read the method source code + $lines = file( $filename ); + $method_source = implode( '', array_slice( $lines, $start_line - 1, $end_line - $start_line + 1 ) ); + + // Verify the method contains the expected remove_action calls + $this->assertStringContainsString( "remove_action( 'wp_head', 'wp_generator' )", $method_source ); + $this->assertStringContainsString( "remove_action( 'wp_head', 'wlwmanifest_link' )", $method_source ); + $this->assertStringContainsString( "remove_action( 'wp_head', 'rsd_link' )", $method_source ); + } + + /** + * Test that HeadOverrides can be instantiated multiple times. + * + * @return void + */ + public function test_multiple_instances() { + $head_overrides_1 = new HeadOverrides(); + $head_overrides_2 = new HeadOverrides(); + + $this->assertInstanceOf( HeadOverrides::class, $head_overrides_1 ); + $this->assertInstanceOf( HeadOverrides::class, $head_overrides_2 ); + $this->assertNotSame( $head_overrides_1, $head_overrides_2 ); + } + + /** + * Test that HeadOverrides uses the Module trait. + * + * @return void + */ + public function test_uses_module_trait() { + $head_overrides = new HeadOverrides(); + + // Check that the class has the methods from the Module trait + $this->assertTrue( method_exists( $head_overrides, 'load_order' ) ); + $this->assertTrue( method_exists( $head_overrides, 'can_register' ) ); + $this->assertTrue( method_exists( $head_overrides, 'register' ) ); + } +}