prefix . self::TABLE; } public static function maybe_install(): void { if ( (int) get_option('aumframe_lib_version') === self::DB_VERSION && Storage::table_exists(self::table()) ) { return; } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::table(); $collate = $wpdb->get_charset_collate(); dbDelta("CREATE TABLE {$table} ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, slug VARCHAR(191) NOT NULL DEFAULT '', name VARCHAR(191) NOT NULL DEFAULT '', category VARCHAR(64) NOT NULL DEFAULT 'general', def LONGTEXT NULL, baked_html LONGTEXT NULL, baked_css LONGTEXT NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY slug (slug), KEY category (category) ) {$collate};"); update_option('aumframe_lib_version', self::DB_VERSION); self::seed(); } /** * Seed the plugin's own components on first install. * * Kept in files rather than hard-coded in PHP: components are data, not code, and this way * adding one means adding a JSON file rather than changing any logic. */ private static function seed(): void { $dir = AUMFRAME_DIR . 'library'; if (!is_dir($dir)) { return; } foreach (glob($dir . '/*.json') as $file) { $def = json_decode((string) file_get_contents($file), true); if (!is_array($def) || empty($def['root']) || empty($def['nodes'])) { continue; } self::save([ 'slug' => basename($file, '.json'), 'name' => $def['name'] ?? basename($file, '.json'), 'category' => $def['category'] ?? 'general', 'def' => $def, ]); } } /** * @param string[] $slugs * @return array */ public static function baked(array $slugs): array { global $wpdb; $slugs = array_values(array_unique(array_filter(array_map('sanitize_key', $slugs)))); if (!$slugs) { return []; } $in = implode(',', array_fill(0, count($slugs), '%s')); $rows = $wpdb->get_results( $wpdb->prepare("SELECT slug, baked_html, baked_css FROM %i WHERE slug IN ({$in})", self::table(), ...$slugs), ARRAY_A ) ?: []; $out = []; foreach ($rows as $r) { $out[(string) $r['slug']] = [ 'html' => (string) ($r['baked_html'] ?? ''), 'css' => (string) ($r['baked_css'] ?? ''), ]; } return $out; } /** @return array */ public static function all(): array { global $wpdb; self::maybe_install(); $rows = $wpdb->get_results( $wpdb->prepare('SELECT * FROM %i ORDER BY category ASC, name ASC', self::table()), ARRAY_A ) ?: []; $out = []; foreach ($rows as $r) { $def = json_decode((string) $r['def'], true); if (!is_array($def)) { continue; } $out[] = [ 'id' => (int) $r['id'], 'slug' => $r['slug'], 'name' => $r['name'], 'category' => $r['category'], 'def' => $def, ]; } return $out; } /** One row by slug (def as stored), or null. */ public static function find(string $slug): ?array { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare('SELECT * FROM %i WHERE slug = %s', self::table(), sanitize_key($slug)), ARRAY_A ); return $row ?: null; } public static function save(array $row): int { global $wpdb; self::maybe_install(); $slug = sanitize_key($row['slug'] ?? ''); if ($slug === '') { $slug = 'cmp-' . wp_generate_password(6, false); } $data = [ 'slug' => $slug, 'name' => sanitize_text_field($row['name'] ?? $slug), 'category' => sanitize_key($row['category'] ?? 'general') ?: 'general', // Store the client's structure verbatim — never round-trip it through a PHP array, where // an empty object becomes an empty array (the same trap as in Rest::save) 'def' => is_string($row['def']) ? $row['def'] : wp_json_encode($row['def'], JSON_UNESCAPED_UNICODE), 'baked_html' => (string) ($row['baked_html'] ?? ''), 'baked_css' => (string) ($row['baked_css'] ?? ''), 'updated_at' => current_time('mysql'), ]; $existing = $wpdb->get_var( $wpdb->prepare('SELECT id FROM %i WHERE slug = %s', self::table(), $slug) ); if ($existing) { $wpdb->update(self::table(), $data, ['id' => (int) $existing]); $id = (int) $existing; } else { $wpdb->insert(self::table(), $data); $id = (int) $wpdb->insert_id; } /* * Since the library became the source of truth (1.7.65) a template's baked HTML carries only * `` placeholders — the words of every section head and CTA live here. Anything * derived from rendered text (the theme's search index, and through it the AumViso score) goes * stale when a component changes, not only when a template does. Same contract as * `aumframe_template_saved`: fired, not acted on — the theme decides what to rebuild. */ if ($id > 0) { do_action('aumframe_component_saved', $slug, $id); } return $id; } public static function delete(int $id): bool { global $wpdb; return (bool) $wpdb->delete(self::table(), ['id' => $id]); } }