Automating CI/CD Workflows for Enterprise Full Site Editing (FSE) Block Themes and theme.json Using Modern PHP 8.x Features
Leveraging PHP 8.x for Robust FSE Theme CI/CD Pipelines
Enterprise-grade WordPress Full Site Editing (FSE) themes, particularly those heavily reliant on the `theme.json` configuration and complex block structures, demand a sophisticated CI/CD strategy. Traditional approaches often fall short when dealing with the nuances of FSE, such as dynamic template generation, style variations, and the intricate interplay between PHP and JavaScript. This post outlines an advanced CI/CD workflow, emphasizing modern PHP 8.x features to enhance reliability, performance, and maintainability for FSE block themes.
Automated Theme Linting and Validation with PHP 8.x Attributes
A critical first step in any CI pipeline is rigorous code validation. For FSE themes, this extends beyond standard PHP linting to include validation of `theme.json` structure and adherence to WordPress coding standards. PHP 8.x attributes provide a powerful, declarative way to annotate code, enabling custom linters and static analysis tools to enforce theme-specific rules more effectively.
Consider a scenario where we want to enforce specific naming conventions or required properties within custom block classes. We can define an attribute to mark these classes and then use a static analysis tool (or a custom PHP script) to check for compliance.
Defining Custom Validation Attributes
First, let’s define a simple attribute to mark blocks that require a specific configuration key in their `block.json` or `theme.json` context.
namespace MyTheme\Attributes;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
class RequiresThemeJsonConfig {
public function __construct(public string $configKey) {}
}
Now, let’s apply this attribute to a hypothetical custom block class.
namespace MyTheme\Blocks;
use MyTheme\Attributes\RequiresThemeJsonConfig;
use WP_Block_Type_Registry;
#[RequiresThemeJsonConfig('my_custom_block_settings')]
class CustomFeaturedPostBlock {
public static function register() {
register_block_type( __DIR__ . '/custom-featured-post' );
}
// ... other block methods
}
Implementing a Custom Linter/Validator
We can then create a PHP script that uses reflection to find classes with this attribute and check their corresponding `block.json` or `theme.json` configurations. This script can be integrated into the CI pipeline.
namespace MyTheme\CI;
use ReflectionClass;
use MyTheme\Attributes\RequiresThemeJsonConfig;
use Exception;
class ThemeValidator {
public static function validateBlocks() {
$blockClasses = [
\MyTheme\Blocks\CustomFeaturedPostBlock::class,
// ... other block classes
];
foreach ($blockClasses as $blockClass) {
$reflectionClass = new ReflectionClass($blockClass);
$attributes = $reflectionClass->getAttributes(RequiresThemeJsonConfig::class);
if (!empty($attributes)) {
$attribute = $attributes[0]->newInstance();
$configKey = $attribute->configKey;
// In a real scenario, this would involve parsing theme.json or block.json
// and checking for the presence of $configKey.
// For demonstration, we'll simulate a check.
if (!self::themeJsonContainsConfig($configKey)) {
throw new Exception("Block '{$blockClass}' requires theme.json config key '{$configKey}' which is missing.");
}
}
}
}
private static function themeJsonContainsConfig(string $key): bool {
// Simulate checking theme.json. In production, load and parse theme.json.
$mockThemeJson = [
'version' => 2,
'settings' => [
'color' => [],
'typography' => [],
'layout' => [],
'my_custom_block_settings' => [ // This key is present
'enabled' => true,
'default_style' => 'modern'
]
]
];
return isset($mockThemeJson['settings'][$key]);
}
}
// Example usage within a CI script:
try {
ThemeValidator::validateBlocks();
echo "Block validation passed.\n";
} catch (Exception $e) {
echo "Block validation failed: " . $e->getMessage() . "\n";
exit(1); // Indicate failure
}
Optimizing `theme.json` Processing with PHP 8.x Typed Properties and Union Types
The `theme.json` file is the cornerstone of FSE. Efficiently parsing and validating its structure, especially when dealing with complex nested configurations for styles, typography, and layout, is crucial. PHP 8.x’s typed properties and union types can significantly improve the robustness and readability of the code that handles this data.
Structured Data Representation
Instead of relying on associative arrays, we can define PHP classes that mirror the structure of `theme.json`. This allows for strong typing, autocompletion, and compile-time checks.
namespace MyTheme\Config;
class ThemeJsonSettings {
public array $color = [];
public array $typography = [];
public array $layout = [];
public ?array $spacing = null; // Optional property
public array $custom = []; // For custom block settings
// PHP 8.1+ constructor property promotion
public function __construct(
array $color = [],
array $typography = [],
array $layout = [],
?array $spacing = null,
array $custom = []
) {
$this->color = $color;
$this->typography = $typography;
$this->layout = $layout;
$this->spacing = $spacing;
$this->custom = $custom;
}
}
class ThemeJson {
public int $version;
public ThemeJsonSettings $settings;
public function __construct(int $version, ThemeJsonSettings $settings) {
$this->version = $version;
$this->settings = $settings;
}
public static function fromJson(string $jsonString): self {
$data = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR);
// Basic validation and type casting
if (!isset($data['version']) || !is_int($data['version'])) {
throw new \InvalidArgumentException("Invalid theme.json: 'version' is missing or not an integer.");
}
$settingsData = $data['settings'] ?? [];
$settings = new ThemeJsonSettings(
$settingsData['color'] ?? [],
$settingsData['typography'] ?? [],
$settingsData['layout'] ?? [],
$settingsData['spacing'] ?? null,
$settingsData['custom'] ?? []
);
return new self($data['version'], $settings);
}
}
This structured approach makes it easier to access and manipulate theme settings within PHP, reducing the likelihood of runtime errors due to unexpected data types or missing keys. The `JSON_THROW_ON_ERROR` flag in `json_decode` is a PHP 7.3+ feature that simplifies error handling for JSON parsing.
Advanced Asset Management and Enqueueing with PHP 8.x Match Expressions
Managing JavaScript and CSS assets for FSE themes can become complex, especially when different blocks or template parts require specific dependencies or conditional loading. PHP 8.x’s `match` expression offers a more concise and readable alternative to long `if/elseif/else` chains for handling these scenarios.
Conditional Asset Enqueuing Based on Block Usage
Imagine a scenario where a specific JavaScript module is only needed if a custom block is present on the current page. We can use `has_block()` to detect this and then enqueue the asset.
function mytheme_enqueue_block_assets() {
// Example: Enqueue a custom JS for a specific block
if ( has_block( 'mytheme/custom-gallery' ) ) {
wp_enqueue_script(
'mytheme-custom-gallery-js',
get_template_directory_uri() . '/assets/js/custom-gallery.js',
['wp-blocks', 'wp-element', 'wp-editor'],
filemtime( get_template_directory() . '/assets/js/custom-gallery.js' )
);
}
// Example: Enqueue a global CSS for FSE features
wp_enqueue_style(
'mytheme-fse-global-styles',
get_template_directory_uri() . '/assets/css/fse-global.css',
[],
filemtime( get_template_directory() . '/assets/css/fse-global.css' )
);
}
add_action( 'enqueue_block_assets', 'mytheme_enqueue_block_assets' );
Now, let’s consider a more complex scenario where we need to enqueue different sets of assets based on the *type* of FSE template being rendered (e.g., single post, page, archive). PHP 8.x’s `match` expression shines here.
function mytheme_conditional_template_assets() {
$template_type = get_page_template_slug(); // Or other methods to determine template type
match ($template_type) {
'templates/single-post.html' => {
wp_enqueue_script('mytheme-single-post-enhancements', get_template_directory_uri() . '/assets/js/single-post.js', ['wp-jquery'], '1.0', true);
wp_enqueue_style('mytheme-single-post-styles', get_template_directory_uri() . '/assets/css/single-post.css', [], '1.0');
},
'templates/page.html' => {
wp_enqueue_script('mytheme-page-layout-js', get_template_directory_uri() . '/assets/js/page-layout.js', ['wp-element'], '1.0', true);
},
'templates/archive.html' => {
wp_enqueue_script('mytheme-archive-filter-js', get_template_directory_uri() . '/assets/js/archive-filter.js', ['wp-jquery-ui-core'], '1.0', true);
},
default => {
// Default assets or no specific assets for other templates
}
};
}
add_action( 'template_redirect', 'mytheme_conditional_template_assets' );
The `match` expression is exhaustive, meaning all possible cases must be handled, or a `default` case must be provided, preventing subtle bugs that can arise from incomplete `if/elseif` structures. The `filemtime()` function is used to automatically version assets based on their modification time, ensuring cache busting during development and deployment.
Automating `theme.json` Style Variations and Theme Building
FSE themes often define multiple style variations within `theme.json`. Automating the generation and testing of these variations is crucial for maintaining consistency and preventing regressions. This can involve PHP scripts that programmatically modify `theme.json` or generate CSS based on its settings.
Programmatic `theme.json` Modification for Variations
During the build process, we might want to generate different `theme.json` files for each style variation, or perhaps inject specific variables into a base `theme.json`.
namespace MyTheme\Build;
use MyTheme\Config\ThemeJson;
use Exception;
class ThemeJsonGenerator {
private string $baseThemeJsonPath;
private string $outputDir;
public function __construct(string $baseThemeJsonPath, string $outputDir) {
$this->baseThemeJsonPath = $baseThemeJsonPath;
$this->outputDir = $outputDir;
}
public function generateVariations(array $variations): void {
if (!is_dir($this->outputDir)) {
mkdir($this->outputDir, 0755, true);
}
$baseJsonContent = file_get_contents($this->baseThemeJsonPath);
if ($baseJsonContent === false) {
throw new Exception("Could not read base theme.json from: {$this->baseThemeJsonPath}");
}
$baseThemeJson = ThemeJson::fromJson($baseJsonContent);
foreach ($variations as $variationName => $variationSettings) {
// Deep merge variation settings into base settings
$mergedSettings = $this->deepMerge(
['version' => $baseThemeJson->version, 'settings' => $baseThemeJson->settings],
['settings' => $variationSettings]
);
$outputFilePath = "{$this->outputDir}/theme-{$variationName}.json";
file_put_contents($outputFilePath, json_encode($mergedSettings, JSON_PRETTY_PRINT));
echo "Generated: {$outputFilePath}\n";
}
}
// Simple deep merge for demonstration. A more robust solution might be needed.
private function deepMerge(array $base, array $override): array {
foreach ($override as $key => $value) {
if (is_array($value) && isset($base[$key]) && is_array($base[$key])) {
$base[$key] = $this->deepMerge($base[$key], $value);
} else {
$base[$key] = $value;
}
}
return $base;
}
}
// Example usage in a build script (e.g., a Makefile or a custom PHP build tool)
$variations = [
'default' => [
'color' => [
'primary' => '#0073aa',
'secondary' => '#d3d3d3',
],
'typography' => [
'fontSizes' => [
['size' => '1rem', 'slug' => 'base'],
['size' => '1.5rem', 'slug' => 'large'],
],
],
],
'dark' => [
'color' => [
'background' => '#1a1a1a',
'text' => '#ffffff',
'primary' => '#00bcd4',
],
'typography' => [
'fontSizes' => [
['size' => '0.9rem', 'slug' => 'small'],
['size' => '1.2rem', 'slug' => 'medium'],
],
],
],
];
$generator = new ThemeJsonGenerator('theme.json', './build/theme-json/');
try {
$generator->generateVariations($variations);
} catch (Exception $e) {
echo "Error generating theme.json variations: " . $e->getMessage() . "\n";
exit(1);
}
This script leverages the structured `ThemeJson` class and performs a deep merge of variation-specific settings into a base configuration. The output can then be used by WordPress or other build tools.
Integrating with CI/CD Platforms (GitHub Actions Example)
These PHP-based validation and build steps can be seamlessly integrated into popular CI/CD platforms like GitHub Actions. A typical workflow would involve:
- Checking out the repository.
- Setting up a PHP environment (e.g., using `shivammathur/setup-php`).
- Installing Composer dependencies.
- Running PHP linting (`php -l`).
- Executing the custom theme validator script (e.g., `php build/validate-theme.php`).
- Running automated tests (unit, integration, E2E).
- Building theme assets (e.g., compiling SCSS, JS).
- Generating `theme.json` variations.
- Deploying to staging or production environments upon successful builds.
Here’s a simplified example of a GitHub Actions workflow file (`.github/workflows/ci.yml`):
name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1' # Use a specific PHP 8.x version
extensions: mbstring, xml, json, zip # Required extensions
coverage: none
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: PHP Linting
run: find . -name "*.php" -print0 | xargs -0 php -l
- name: Run Custom Theme Validator
run: php build/validate-theme.php # Assumes your validator script is here
- name: Run Theme JSON Generator
run: php build/generate-theme-json.php # Assumes your generator script is here
# Add steps for running PHPUnit tests, WP-CLI commands, JS linters, etc.
# - name: Run PHPUnit Tests
# run: vendor/bin/phpunit tests/
# - name: Build theme assets (example)
# run: npm install && npm run build
# - name: Deploy to staging (example)
# if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# run: ./deploy.sh staging
By integrating these advanced PHP 8.x features and robust validation steps into your CI/CD pipeline, you can significantly improve the quality, stability, and maintainability of your enterprise FSE block themes.