Skip to content

Refactor: Component based PHP rendering#673

Open
abhishekxix wants to merge 7 commits intorefactor/frontend-source-reorganizationfrom
refactor/component-based-php-rendering
Open

Refactor: Component based PHP rendering#673
abhishekxix wants to merge 7 commits intorefactor/frontend-source-reorganizationfrom
refactor/component-based-php-rendering

Conversation

@abhishekxix
Copy link
Copy Markdown
Member

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)

  • Static render() method echoes component HTML; static get() returns it as a string via output buffering.
  • Resolves component files using the pattern {source_path}/{Name}/{Name}.php.
  • Resolution priority ('theme' or 'plugin') is configurable per call site via $options['priority'], or project-wide via the elementary_theme_component_default_priority filter (defaults to 'theme').
  • Component source paths are collected via the elementary_theme_component_paths filter. Theme path (src/Components/) is registered by default; plugins register their own path through the filter.
priority Order checked
'theme' (default) theme → plugin → other sources
'plugin' plugin → theme → other sources

Global wrappers (inc/helpers/custom-functions.php)

  • elementary_theme_component() → delegates to ComponentLoader::render()
  • elementary_theme_get_component() → delegates to ComponentLoader::get()
  • Both guarded with 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 rendering Button internally.

Components are render-only PHP partials. They receive a $args array and output HTML — no business logic, no data fetching.

Checklist

  • ComponentLoader class exists in inc/Framework/ComponentLoader.php
  • ComponentLoader::render() collects paths via elementary_theme_component_paths filter and resolves by priority
  • priority option accepted ('theme' | 'plugin'); defaults to 'theme'
  • elementary_theme_component_default_priority filter allows project-wide override
  • Resolution works correctly in all three project configurations: theme-only, plugin-only, both
  • Global elementary_theme_component() wrapper exists with function_exists() guard; delegates entirely to ComponentLoader::render()
  • Components are render-only — no business logic, no data fetching inside component files
  • Existing partials migrated to the new component structure as PoC (Button + Card)
  • No regressions in existing templates

Screenshots

N/A — no visual changes. This is an infrastructure/API addition.

To-do

  • Component library build-out (TASK-007)
  • Scaffold CLI for generating new components (TASK-009)

Fixes/Covers issue

Fixes #637

@abhishekxix abhishekxix self-assigned this Apr 29, 2026
@abhishekxix abhishekxix requested a review from aryanjasala May 4, 2026 09:04
@abhishekxix abhishekxix marked this pull request as ready for review May 4, 2026 09:04
@aryanjasala aryanjasala requested review from Adi-ty and Copilot May 6, 2026 08:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ComponentLoader with render() (echo) and get() (return string) APIs plus filter-based path + priority resolution.
  • Added global convenience wrappers (elementary_theme_component(), elementary_theme_get_component()) and PoC Button + Card components under src/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.

Comment thread inc/Framework/ComponentLoader.php Outdated
Comment on lines +118 to +122
$file = trailingslashit( $paths[ $source ] ) . $name . '/' . $name . '.php';

if ( file_exists( $file ) && is_readable( $file ) ) {
return $file;
}
Comment on lines +100 to +111
$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 );

Comment on lines +190 to +225
// 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

Comment on lines +237 to +241
/**
* Test that the global wrapper delegates to ComponentLoader.
*/
public function test_global_wrapper_renders_component(): void {
ob_start();
Copy link
Copy Markdown

@Adi-ty Adi-ty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Copilot's review points on directory traversal validation in get_component_file() and missing test coverage for get() / elementary_theme_get_component() can be addressed.
  • CI is flagging an alignment issue in AssetLoaderTrait.php line 90 — equals sign spacing needs to match the surrounding assignments.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@abhishekxix abhishekxix requested a review from Adi-ty May 7, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants