Major improvements to WooNooW Page Editor system: Schema & Architecture: - Canonical section schema with unified sectionSchema.ts - Normalized feature-grid to use items (not features) - Standardized default values across all section types - Schema versioning with automatic migration on read Backend (PHP): - Enhanced PlaceholderRenderer with typed output contracts - Added fallback behavior for empty/invalid dynamic sources - Added caching support for post data resolution - New SchemaMigration class for backward compatibility - New Features class for feature flags - Enhanced PageSSR with full style support - Removed controller-level special-casing for related_posts Frontend (Admin SPA): - Updated CanvasRenderer with schema-aware transformation - Enhanced InspectorPanel with canonical schema metadata - Added new section renderers Frontend (Customer SPA): - New section components: BentoCategoryGrid, MarqueeBanner, ProductCarousel, ShoppableImage - Updated FeatureGridSection for items prop contract Testing: - Add PHP tests: SchemaMigrationTest, PlaceholderRendererTest, PageSSRTest - Add TypeScript tests: schema-integration, feature-grid-regression - Add parity tests for React vs SSR content matching - Add CI script: check-schema-drift.mjs - Add VERIFICATION_CHECKLIST.md Documentation: - RELEASE_NOTES-v1.0.md with full release notes - docs/PAGE_EDITOR_SECTION_SCHEMA_V1.md - docs/PAGE_EDITOR_SSR_COVERAGE_AUDIT.md
253 lines
6.8 KiB
PHP
253 lines
6.8 KiB
PHP
<?php
|
|
namespace WooNooW\Frontend;
|
|
|
|
/**
|
|
* Schema Migration Handler
|
|
* Handles backward compatibility for legacy saved structures
|
|
*/
|
|
class SchemaMigration
|
|
{
|
|
/**
|
|
* Current schema version
|
|
*/
|
|
const CURRENT_VERSION = 1;
|
|
|
|
/**
|
|
* Migration functions per version
|
|
*/
|
|
private static $migrations = [
|
|
1 => 'migrate_to_v1',
|
|
];
|
|
|
|
/**
|
|
* Migrate a page/template structure to current schema
|
|
*
|
|
* @param array $structure Page or template structure
|
|
* @return array Migrated structure
|
|
*/
|
|
public static function migrate($structure)
|
|
{
|
|
$version = $structure['schemaVersion'] ?? 0;
|
|
|
|
// Already at current version
|
|
if ($version >= self::CURRENT_VERSION) {
|
|
return $structure;
|
|
}
|
|
|
|
// Apply migrations in order
|
|
for ($v = $version + 1; $v <= self::CURRENT_VERSION; $v++) {
|
|
if (isset(self::$migrations[$v])) {
|
|
$method = self::$migrations[$v];
|
|
$structure = self::$method($structure);
|
|
}
|
|
}
|
|
|
|
// Set current version
|
|
$structure['schemaVersion'] = self::CURRENT_VERSION;
|
|
|
|
return $structure;
|
|
}
|
|
|
|
/**
|
|
* Migrate to version 1
|
|
* - Normalize feature-grid items/features
|
|
* - Normalize style keys
|
|
* - Add missing defaults
|
|
*
|
|
* @param array $structure
|
|
* @return array
|
|
*/
|
|
private static function migrate_to_v1($structure)
|
|
{
|
|
if (!isset($structure['sections']) || !is_array($structure['sections'])) {
|
|
return $structure;
|
|
}
|
|
|
|
foreach ($structure['sections'] as &$section) {
|
|
// Migrate section type
|
|
if (!isset($section['type'])) {
|
|
continue;
|
|
}
|
|
|
|
// Feature grid: normalize items/features
|
|
if ($section['type'] === 'feature-grid') {
|
|
$section = self::migrate_feature_grid($section);
|
|
}
|
|
|
|
// Normalize section styles
|
|
$section = self::migrate_section_styles($section);
|
|
|
|
// Normalize element styles
|
|
$section = self::migrate_element_styles($section);
|
|
}
|
|
|
|
return $structure;
|
|
}
|
|
|
|
/**
|
|
* Migrate feature-grid section
|
|
* - Ensure items array exists
|
|
* - Copy features to items if needed
|
|
*
|
|
* @param array $section
|
|
* @return array
|
|
*/
|
|
private static function migrate_feature_grid($section)
|
|
{
|
|
$props = $section['props'] ?? [];
|
|
|
|
// Handle items/features normalization
|
|
if (!isset($props['items']) && isset($props['features'])) {
|
|
// features exists but items doesn't - copy features to items
|
|
$props['items'] = $props['features'];
|
|
|
|
// If features was an empty string, convert to empty array
|
|
if ($props['items'] === '') {
|
|
$props['items'] = [];
|
|
}
|
|
|
|
// Keep features for backward compat but prefer items
|
|
// Don't remove it yet - some old code may still reference it
|
|
}
|
|
|
|
// Ensure items is always an array
|
|
if (!isset($props['items'])) {
|
|
$props['items'] = [];
|
|
} elseif (!is_array($props['items'])) {
|
|
$props['items'] = [];
|
|
}
|
|
|
|
$section['props'] = $props;
|
|
|
|
return $section;
|
|
}
|
|
|
|
/**
|
|
* Migrate section styles
|
|
* - Normalize key names
|
|
* - Add missing defaults
|
|
*
|
|
* @param array $section
|
|
* @return array
|
|
*/
|
|
private static function migrate_section_styles($section)
|
|
{
|
|
$styles = $section['styles'] ?? [];
|
|
|
|
// Normalize contentWidth if missing
|
|
if (!isset($styles['contentWidth']) && isset($styles['container_width'])) {
|
|
// Old key name
|
|
$styles['contentWidth'] = $styles['container_width'];
|
|
unset($styles['container_width']);
|
|
}
|
|
|
|
// Normalize background type
|
|
if (isset($styles['backgroundImage']) && !isset($styles['backgroundType'])) {
|
|
// If there's a background image but no type, assume image type
|
|
$styles['backgroundType'] = 'image';
|
|
}
|
|
|
|
// Normalize height preset
|
|
if (isset($styles['height']) && !isset($styles['heightPreset'])) {
|
|
$height = $styles['height'];
|
|
$map = [
|
|
'small' => 'small',
|
|
'medium' => 'medium',
|
|
'large' => 'large',
|
|
'fullscreen' => 'fullscreen',
|
|
'screen' => 'fullscreen', // Old naming
|
|
'default' => 'default',
|
|
];
|
|
$styles['heightPreset'] = $map[$height] ?? 'default';
|
|
unset($styles['height']);
|
|
}
|
|
|
|
// Add default background settings if missing
|
|
if (!isset($styles['backgroundType'])) {
|
|
$styles['backgroundType'] = 'solid';
|
|
}
|
|
|
|
$section['styles'] = $styles;
|
|
|
|
return $section;
|
|
}
|
|
|
|
/**
|
|
* Migrate element styles
|
|
* - Normalize key names
|
|
* - Add missing defaults
|
|
*
|
|
* @param array $section
|
|
* @return array
|
|
*/
|
|
private static function migrate_element_styles($section)
|
|
{
|
|
$elementStyles = $section['elementStyles'] ?? [];
|
|
|
|
// Normalize common element style keys
|
|
$normalizations = [
|
|
'cta' => 'cta_text', // Old key
|
|
'button' => 'cta_text', // Alias for button
|
|
'heading' => 'heading', // Standardize
|
|
'text' => 'text', // Standardize
|
|
'subtitle' => 'subtitle', // Standardize
|
|
];
|
|
|
|
$normalized = [];
|
|
foreach ($elementStyles as $key => $style) {
|
|
$normalized_key = $normalizations[$key] ?? $key;
|
|
$normalized[$normalized_key] = $style;
|
|
}
|
|
|
|
// Add default styling keys if missing
|
|
$defaults = [
|
|
'title' => [],
|
|
'subtitle' => [],
|
|
'text' => [],
|
|
'cta_text' => [],
|
|
'feature_item' => [],
|
|
];
|
|
|
|
foreach ($defaults as $key => $default) {
|
|
if (!isset($normalized[$key])) {
|
|
$normalized[$key] = $default;
|
|
}
|
|
}
|
|
|
|
$section['elementStyles'] = $normalized;
|
|
|
|
return $section;
|
|
}
|
|
|
|
/**
|
|
* Check if a structure needs migration
|
|
*
|
|
* @param array $structure
|
|
* @return bool
|
|
*/
|
|
public static function needs_migration($structure)
|
|
{
|
|
return ($structure['schemaVersion'] ?? 0) < self::CURRENT_VERSION;
|
|
}
|
|
|
|
/**
|
|
* Batch migrate multiple structures
|
|
*
|
|
* @param array $structures Array of structures
|
|
* @return array Migrated structures
|
|
*/
|
|
public static function migrate_all($structures)
|
|
{
|
|
return array_map([self::class, 'migrate'], $structures);
|
|
}
|
|
|
|
/**
|
|
* Get current schema version
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function get_current_version()
|
|
{
|
|
return self::CURRENT_VERSION;
|
|
}
|
|
} |