Files
WooNooW/includes/Frontend/SchemaMigration.php
Dwindi Ramadhana 396ca25be4 feat: Page Editor v1.0 - canonical schema, SSR parity, and migration
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
2026-05-30 13:02:08 +07:00

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;
}
}