Refactor: Component based PHP rendering#673
Refactor: Component based PHP rendering#673abhishekxix wants to merge 7 commits intorefactor/frontend-source-reorganizationfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a component-based PHP rendering mechanism for the theme, centered around a single ComponentLoader API that resolves and renders component partials from configurable source paths with a controllable priority order (theme vs plugin).
Changes:
- Added
ComponentLoaderwithrender()(echo) andget()(return string) APIs plus filter-based path + priority resolution. - Added global convenience wrappers (
elementary_theme_component(),elementary_theme_get_component()) and PoCButton+Cardcomponents undersrc/Components/. - Added PHPUnit coverage for core resolution behavior and adjusted PHPCS rules to accommodate component partials.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
inc/Framework/ComponentLoader.php |
New resolver/renderer for PHP component partials with filter-driven paths and priority handling. |
inc/helpers/custom-functions.php |
Adds thin global wrapper functions delegating to ComponentLoader. |
src/Components/Button/Button.php |
PoC render-only Button component (link or button). |
src/Components/Card/Card.php |
PoC render-only Card component demonstrating composition by rendering Button. |
tests/php/inc/Framework/ComponentLoaderTest.php |
Adds PHPUnit tests for component rendering, filters, and priority behavior. |
phpcs.xml.dist |
Excludes component partials from specific sniffs that don’t work well with required partial scoping. |
inc/Framework/Traits/AssetLoaderTrait.php |
Small refactor to make asset meta target fallback explicit. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $file = trailingslashit( $paths[ $source ] ) . $name . '/' . $name . '.php'; | ||
|
|
||
| if ( file_exists( $file ) && is_readable( $file ) ) { | ||
| return $file; | ||
| } |
| $paths = apply_filters( | ||
| 'elementary_theme_component_paths', | ||
| [ | ||
| 'theme' => ELEMENTARY_THEME_TEMP_DIR . '/src/Components', | ||
| ], | ||
| $name, | ||
| $options | ||
| ); | ||
|
|
||
| // Order sources based on priority. | ||
| $order = self::get_source_order( $priority, $paths ); | ||
|
|
| // Create a temporary plugin component directory with a custom Button. | ||
| $tmp_dir = sys_get_temp_dir() . '/elementary-test-plugin-components'; | ||
|
|
||
| if ( ! is_dir( $tmp_dir . '/Button' ) ) { | ||
| mkdir( $tmp_dir . '/Button', 0755, true ); // phpcs:ignore | ||
| } | ||
|
|
||
| file_put_contents( // phpcs:ignore | ||
| $tmp_dir . '/Button/Button.php', | ||
| '<?php echo "plugin-button";' | ||
| ); | ||
|
|
||
| $callback = function ( $paths ) use ( $tmp_dir ) { | ||
| $paths['plugin'] = $tmp_dir; | ||
| return $paths; | ||
| }; | ||
|
|
||
| add_filter( 'elementary_theme_component_paths', $callback ); | ||
|
|
||
| // With priority='plugin', the plugin Button should be used. | ||
| ob_start(); | ||
| ComponentLoader::render( 'Button', [ 'label' => 'Test' ], [ 'priority' => 'plugin' ] ); | ||
| $plugin_output = ob_get_clean(); | ||
|
|
||
| // With priority='theme', the theme Button should be used. | ||
| ob_start(); | ||
| ComponentLoader::render( 'Button', [ 'label' => 'Test' ], [ 'priority' => 'theme' ] ); | ||
| $theme_output = ob_get_clean(); | ||
|
|
||
| remove_filter( 'elementary_theme_component_paths', $callback ); | ||
|
|
||
| // Clean up. | ||
| unlink( $tmp_dir . '/Button/Button.php' ); // phpcs:ignore | ||
| rmdir( $tmp_dir . '/Button' ); // phpcs:ignore | ||
| rmdir( $tmp_dir ); // phpcs:ignore | ||
|
|
| /** | ||
| * Test that the global wrapper delegates to ComponentLoader. | ||
| */ | ||
| public function test_global_wrapper_renders_component(): void { | ||
| ob_start(); |
Adi-ty
left a comment
There was a problem hiding this comment.
- Copilot's review points on directory traversal validation in
get_component_file()and missing test coverage forget()/elementary_theme_get_component()can be addressed. - CI is flagging an alignment issue in
AssetLoaderTrait.phpline 90 — equals sign spacing needs to match the surrounding assignments.
There was a problem hiding this comment.
Noticed the spec and upcoming TASK-007 / TASK-009 refer to rtcamp_* naming, while this PR introduces elementary_theme_*. Not a major issue, but worth deciding on a direction early so we can update TASK-007 accordingly if needed.
There was a problem hiding this comment.
@Adi-ty, I have mentioned the same in the task for this PR as well. elementary_theme_* was kept to make it compatible with the auto rename thing that we have going on in theme setup.
| /** | ||
| * Test the elementary_theme_component_default_priority filter is applied. | ||
| */ | ||
| public function test_default_priority_filter_is_applied(): void { |
There was a problem hiding this comment.
test_default_priority_filter_is_applied only asserts the filter fires — it doesn't verify that returning 'plugin' actually changes resolution order. A regression where the return value is silently ignored would pass this test.
Description
Introduces a component-based PHP rendering system that allows theme and plugin components to be resolved and rendered through a single API with configurable priority.
Technical Details
New class:
ComponentLoader(inc/Framework/ComponentLoader.php)render()method echoes component HTML; staticget()returns it as a string via output buffering.{source_path}/{Name}/{Name}.php.'theme'or'plugin') is configurable per call site via$options['priority'], or project-wide via theelementary_theme_component_default_priorityfilter (defaults to'theme').elementary_theme_component_pathsfilter. Theme path (src/Components/) is registered by default; plugins register their own path through the filter.priority'theme'(default)'plugin'Global wrappers (
inc/helpers/custom-functions.php)elementary_theme_component()→ delegates toComponentLoader::render()elementary_theme_get_component()→ delegates toComponentLoader::get()function_exists().PoC components (
src/Components/)Button/Button.php— renders<a>or<button>based on args.Card/Card.php— renders a card; demonstrates composability by renderingButtoninternally.Components are render-only PHP partials. They receive a
$argsarray and output HTML — no business logic, no data fetching.Checklist
ComponentLoaderclass exists ininc/Framework/ComponentLoader.phpComponentLoader::render()collects paths viaelementary_theme_component_pathsfilter and resolves by prioritypriorityoption accepted ('theme'|'plugin'); defaults to'theme'elementary_theme_component_default_priorityfilter allows project-wide overrideelementary_theme_component()wrapper exists withfunction_exists()guard; delegates entirely toComponentLoader::render()Screenshots
N/A — no visual changes. This is an infrastructure/API addition.
To-do
Fixes/Covers issue
Fixes #637