WordPress registration. * * This is the "universal engine" itself: every site runs the same code and all the difference comes from the * configuration in Store. The era of hand-written `register_post_type` ends here. * * Note that it does not exclude hand-writing: a site mu-plugin can still call register_post_type, both paths * run on the same runtime, and the editor treats them identically (SiteData scans WordPress's registry, not this * configuration). The manager is the convenient path, not the only entrance — whether a system becomes a * limitation comes down to whether it left that road open. * * @package Aumframe */ defined('ABSPATH') || exit; class Registrar { public static function boot(): void { /* * Priority 9: it has to run before the default 10. * * Site mu-plugins and other plugins register at 10, and a taxonomy attaching to a content type needs that * type to exist first. Running ahead of them leaves anyone wanting to * register_taxonomy_for_object_type afterwards plenty of room. */ add_filter('aumframe/register_types', [self::class, 'declare_types']); add_action('init', [self::class, 'register_all_fields'], 9); add_action('init', [self::class, 'maybe_flush'], 99); } public static function declare_types($decl) { $decl = is_array($decl) ? $decl : ['post_types' => [], 'taxonomies' => []]; $model = Store::all(); foreach ($model['postTypes'] as $pt) { if (!empty($pt['extends'])) { continue; } $decl['post_types'][$pt['name']] = self::post_type_args($pt); } foreach ($model['taxonomies'] as $tx) { $decl['taxonomies'][$tx['name']] = [ 'objects' => $tx['postTypes'], 'args' => self::taxonomy_args($tx), ]; } return $decl; } public static function register_all_fields(): void { $model = Store::all(); foreach ($model['postTypes'] as $pt) { self::register_fields('post', $pt['name'], $pt['fields'] ?? []); } foreach ($model['taxonomies'] as $tx) { self::register_fields('term', $tx['name'], $tx['fields'] ?? []); } } private static function post_type_args(array $pt): array { $singular = $pt['labels']['singular']; $plural = $pt['labels']['plural']; $public = !empty($pt['public']); return [ 'labels' => self::post_type_labels($singular, $plural), 'public' => $public, 'show_ui' => true, 'show_in_menu' => true, // REST must be on: the block editor, Aumframe's binding panel and the future AI layer all go // through it 'show_in_rest' => true, 'publicly_queryable' => $public, 'exclude_from_search' => !$public, 'has_archive' => !empty($pt['hasArchive']) ? $pt['slug'] : false, 'rewrite' => $public ? ['slug' => $pt['slug'], 'with_front' => false] : false, 'menu_icon' => $pt['menuIcon'] !== '' ? $pt['menuIcon'] : 'dashicons-portfolio', 'supports' => $pt['supports'], 'taxonomies' => $pt['taxonomies'], ]; } private static function taxonomy_args(array $tx): array { return [ 'labels' => self::taxonomy_labels($tx['labels']['singular'], $tx['labels']['plural']), 'public' => !empty($tx['public']), 'show_ui' => true, 'show_in_rest' => true, 'hierarchical' => !empty($tx['hierarchical']), 'show_admin_column' => true, 'rewrite' => !empty($tx['public']) ? ['slug' => $tx['slug'], 'with_front' => false] : false, ]; } /** * Field registration. * * register_post_meta rather than update_post_meta on the fly: only registered meta appears in REST and * only registered meta is found by `get_registered_meta_keys()` — which is precisely where the editor's * binding panel gets its field list. An unregistered field is "fillable, unbindable". * * Always `type => string`: post meta is a string table. Storing an array means PHP serialise, and that data * is unreadable by anything but PHP — locking the client's data inside this codebase. * A repeater stores JSON — readable by people and machines, so export, migration and AI generation all stay * possible. */ private static function register_fields(string $kind, string $object, array $fields): void { foreach ($fields as $f) { $args = [ 'type' => 'string', 'description' => $f['label'], 'single' => true, 'show_in_rest' => true, 'auth_callback' => static function () use ($kind) { return current_user_can($kind === 'term' ? 'manage_categories' : 'edit_posts'); }, ]; if ($kind === 'term') { register_term_meta($object, $f['key'], $args); } else { register_post_meta($object, $f['key'], $args); } } } /** * Flush the rewrite rules once after the configuration changes. * * Without it the symptom is `/bikes/` 404ing — and it looks fine at the moment of saving (the admin list * is entirely normal); it only surfaces when somebody clicks a front-end link. So flushing once more is the * safer trade: this is a one-off after saving a configuration, not something that runs per request. */ public static function maybe_flush(): void { if (!get_option(Store::FLUSH_OPTION)) { return; } delete_option(Store::FLUSH_OPTION); flush_rewrite_rules(false); } // ------------------------------------------------------------------ Labels /** * The admin's set of labels. * * In the hand-written era only name and singular_name were ever filled in, so the admin was full of * "Add new post" and "Search posts" — a client looking at their "Bikes" list read the word "posts" * everywhere. Generating them in code means none is ever missed. */ private static function post_type_labels(string $singular, string $plural): array { return [ 'name' => $plural, 'singular_name' => $singular, 'menu_name' => $plural, /* translators: %s: Plural name of the content type, e.g. "Projects". */ 'all_items' => sprintf(__('All %s', 'aumframe'), $plural), 'add_new' => __('Add New', 'aumframe'), /* translators: %s: Singular name of the content type, e.g. "Project". */ 'add_new_item' => sprintf(__('Add %s', 'aumframe'), $singular), /* translators: %s: Singular name of the content type, e.g. "Project". */ 'edit_item' => sprintf(__('Edit %s', 'aumframe'), $singular), /* translators: %s: Singular name of the content type, e.g. "Project". */ 'new_item' => sprintf(__('New %s', 'aumframe'), $singular), /* translators: %s: Singular name of the content type, e.g. "Project". */ 'view_item' => sprintf(__('View %s', 'aumframe'), $singular), /* translators: %s: Plural name of the content type, e.g. "Projects". */ 'view_items' => sprintf(__('View %s', 'aumframe'), $plural), /* translators: %s: Plural name of the content type, e.g. "Projects". */ 'search_items' => sprintf(__('Search %s', 'aumframe'), $plural), /* translators: %s: Plural name of the content type, e.g. "Projects". */ 'not_found' => sprintf(__('No %s found', 'aumframe'), $plural), /* translators: %s: Plural name of the content type, e.g. "Projects". */ 'not_found_in_trash' => sprintf(__('No %s found in Trash', 'aumframe'), $plural), /* translators: %s: Singular name of the content type, e.g. "Project". */ 'archives' => sprintf(__('%s archives', 'aumframe'), $singular), 'featured_image' => __('Featured image', 'aumframe'), 'set_featured_image' => __('Set featured image', 'aumframe'), ]; } private static function taxonomy_labels(string $singular, string $plural): array { return [ 'name' => $plural, 'singular_name' => $singular, 'menu_name' => $plural, /* translators: %s: Plural name of the taxonomy, e.g. "Topics". */ 'all_items' => sprintf(__('All %s', 'aumframe'), $plural), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'edit_item' => sprintf(__('Edit %s', 'aumframe'), $singular), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'view_item' => sprintf(__('View %s', 'aumframe'), $singular), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'update_item' => sprintf(__('Update %s', 'aumframe'), $singular), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'add_new_item' => sprintf(__('Add %s', 'aumframe'), $singular), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'new_item_name' => sprintf(__('New %s name', 'aumframe'), $singular), /* translators: %s: Plural name of the taxonomy, e.g. "Topics". */ 'search_items' => sprintf(__('Search %s', 'aumframe'), $plural), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'parent_item' => sprintf(__('Parent %s', 'aumframe'), $singular), /* translators: %s: Singular name of the taxonomy, e.g. "Topic". */ 'parent_item_colon' => sprintf(__('Parent %s:', 'aumframe'), $singular), /* translators: %s: Plural name of the taxonomy, e.g. "Topics". */ 'not_found' => sprintf(__('No %s found', 'aumframe'), $plural), /* translators: %s: Plural name of the taxonomy, e.g. "Topics". */ 'back_to_items' => sprintf(__('← Back to %s', 'aumframe'), $plural), ]; } }