Jake Vanderwerf
2026-05-01 48721c85ebcfa973ee81719d2467ca80e4253dc9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
<?php
namespace JVBase\registrar;
 
use JVBase\forms\TaxonomySelector;
 
if (!defined('ABSPATH')) {
    exit;
}
final class Posts {
    public string $postType;
    public string $singular;
    public string $plural;
    public array $labels;
    /**
     * Whether a post type is intended for use publicly either via the admin interface or by front-end users.
     * @var bool
     */
    public bool $public = true;
    /**
     * A short descriptive summary of what the post type is.
     * @var string
     */
    public string $description;
    /**
     * Whether the post type is hierarchical (e.g. page)
     * @var bool
     */
    public bool $hierarchical = false;
    /**
     * Whether to exclude posts with this post type from front end search results.
     * Default is the opposite value of $public
     * @var bool
     */
    public bool $exclude_from_search;
    /**
     * Whether queries can be performed on the front end for the post type as part of parse_request().
     * Endpoints would include:
            * ?post_type={post_type_key}
            * ?{post_type_key}={single_post_slug}
            * ?{post_type_query_var}={single_post_slug}
     * If not set, the default is inherited from $public
     * @var bool
     */
    public bool $publicly_queryable;
    /**
     * Whether to generate and allow a UI for managing this post type in the admin.
     * Default is value of $public.
     * @var bool
     */
    public bool $show_ui;
    /**
     * Where to show the post type in the admin menu.
     * To work, $show_ui must be true.
     * If true, the post type is shown in its own top level menu.
     * If false, no menu is shown.
     * If a string of an existing top level menu ('tools.php' or 'edit.php?post_type=page', for example), the post type will be placed as a sub-menu of that.
     * Default is value of $show_ui.
     * @var bool
     */
    public bool|string $show_in_menu;
    /**
     * Makes this post type available for selection in navigation menus
     * Default is value of $public.
     * @var bool
     */
    public bool $show_in_nav_menus;
    /**
     * Makes this post type available via the admin bar.
     * Default is value of $show_in_menu.
     * @var bool
     */
    public bool $show_in_admin_bar;
    /**
     * Whether to include the post type in the REST API.
     * Set this to true for the post type to be available in the block editor.
     * @var bool
     */
    public bool $show_in_rest = true;
    /**
     * To change the base URL of REST API route. Default is $post_type.
     * @var string
     */
    public string $rest_base;
    /**
     * To change the namespace URL of REST API route. Default is wp/v2.
     * @var string
     */
    public string $rest_namespace;
    /**
     * REST API controller class name. Default is ‘WP_REST_Posts_Controller‘.
     * @var string
     */
    public string $rest_controller_class;
    /**
     * REST API controller class name. Default is ‘WP_REST_Autosaves_Controller‘.
     * @var string
     */
    public string $autosave_rest_controller_class;
    /**
     * REST API controller class name. Default is ‘WP_REST_Revisions_Controller‘.
     * @var string
     */
    public string $revisions_rest_controller_class;
    /**
     * A flag to direct the REST API controllers for autosave / revisions should be registered before/after the post type controller.
     * @var bool
     */
    public bool $late_route_registration;
    /**
     * The position in the menu order the post type should appear.
     * To work, $show_in_menu must be true. Default null (at the bottom).
     * @var int
     */
    public int $menu_position;
    /**
     * The URL to the icon to be used for this menu. Pass a base64-encoded SVG using a data URI, which will be colored to match the color scheme — this should begin with 'data:image/svg+xml;base64,'. Pass the name of a Dashicons helper class to use a font icon, e.g.
     * 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS. Defaults to use the posts icon.
     * @var string
     */
    public string $menu_icon;
    /**
     * The string to use to build the read, edit, and delete capabilities.
     * May be passed as an array to allow for alternative plurals when using this argument as a base to construct the capabilities, e.g.
     * array('story', 'stories'). Default 'post'.
     * @var string|array
     */
    public string|array $capability_type;
    /**
     * Array of capabilities for this post type. $capability_type is used as a base to construct capabilities by default.
     * See get_post_type_capabilities() .
     * @var array
     */
    public array $capabilities;
    /**
     * Whether to use the internal default meta capability handling.
     * Default false.
     * @var bool
     */
    public bool $map_meta_cap = false;
    /**
     * Core feature(s) the post type supports. Serves as an alias for calling add_post_type_support() directly. Core features include 'title', 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes', 'thumbnail', 'custom-fields', and 'post-formats'.
     * Additionally, the 'revisions' feature dictates whether the post type will store revisions, the 'autosave' feature dictates whether the post type will be autosaved, and the 'comments' feature dictates whether the comments count will show on the edit screen. For backward compatibility reasons, adding 'editor' support implies 'autosave' support too. A feature can also be specified as an array of arguments to provide additional information about supporting that feature.
     * Example: array( 'my_feature', array( 'field' => 'value' ) ).
     * If false, no features will be added.
     * Default is an array containing 'title' and 'editor'.
     * @var false|array|string[]
     */
    public false|array $supports = ['title', 'author', 'thumbnail', 'editor', 'revisions', 'custom-fields', 'excerpt', 'content'];
    /**
     * Provide a callback function that sets up the meta boxes for the edit form.
     * Do remove_meta_box() and add_meta_box() calls in the callback. Default null.
     * @var mixed|null
     */
    public mixed $register_meta_box_cb = null;
    /**
     * An array of taxonomy identifiers that will be registered for the post type. Taxonomies can be registered later with register_taxonomy() or register_taxonomy_for_object_type() .
     * @var array
     */
    public array $taxonomies;
    /**
     * Whether there should be post type archives, or if a string, the archive slug to use.
     * Will generate the proper rewrite rules if $rewrite is enabled. Default false.
     * @var bool|string
     */
    public bool|string $has_archive = true;
    /**
     * Triggers the handling of rewrites for this post type. To prevent rewrite, set to false.
     * Defaults to true, using $post_type as slug. To specify rewrite rules, an array can be passed with any of these keys:
     * slug {string} - Customize the permastruct slug. Defaults to $post_type key.
     * with_front {bool} - Whether the permastruct should be prepended with WP_Rewrite::$front.
            * Default true.
     * feeds {bool} - Whether the feed permastruct should be built for this post type. Default is value of $has_archive.
     * pages {bool} - Whether the permastruct should provide for pagination. Default true.
     * ep_mask {int} -  Endpoint mask to assign. If not specified and permalink_epmask is set, inherits from $permalink_epmask. If not specified and permalink_epmask is not set, defaults to EP_PERMALINK.
     * @var bool|array
     */
    public bool|array $rewrite;
    /**
     * Sets the query_var key for this post type.
     * Defaults to $post_type key.
     * If false, a post type cannot be loaded at ?{query_var}={post_slug}.
     * If specified as a string, the query ?{query_var_string}={post_slug} will be valid.
     * @var string|bool
     */
    public string|bool $query_var;
    /**
     * Whether to allow this post type to be exported. Default true.
     * @var bool
     */
    public bool $can_export = true;
    /**
     * Whether to delete posts of this type when deleting a user.
     * If true, posts of this type belonging to the user will be moved to Trash when the user is deleted.
     * If false, posts of this type belonging to the user will *not* be trashed or deleted.
     * If not set (the default), posts are trashed if post type supports the 'author' feature. Otherwise posts are not trashed or deleted.
     * Default null.
     * @var bool
     */
    public bool $delete_with_user;
    /**
     * Array of blocks to use as the default initial state for an editor session. Each item should be an array containing block name and optional attributes.
     * @var array
     */
    public array $template;
    /**
     * Whether the block template should be locked if $template is set.
     * If set to 'all', the user is unable to insert new blocks, move existing blocks and delete blocks.
     * If set to 'insert', the user is able to move existing blocks but is unable to insert new blocks and delete blocks.
     * Default false.
     * @var string|false
     */
    public string|false $template_lock;
    protected string $unbased;
    protected ?string $taxonomyRewrite = null;
 
    public function __construct(string $postType, string $singular, string $plural)
    {
        $this->unbased = jvbNoBase($postType);
        $this->postType = jvbCheckBase($postType);
        $this->labels = $this->buildLabels($singular, $plural);
    }
 
    public function register():void
    {
        $args = array_filter(get_object_vars($this));
//      error_log('Got Object Vars: '.print_r($args, true));
//      error_log('Filtered: '.print_r(array_filter($args), true));
//      $args = [
//          'labels'        => $this->labels,
//          'public'        => $this->public,
//          'hierarchical'  => $this->hierarchical,
//          'has_archive'   => $this->has_archive,
//          'can_export'    => $this->can_export,
//      ];
//      if (isset($this->exclude_from_search)) {
//          $args['exclude_from_search'] = $this->exclude_from_search;
//      }
//      if (isset($this->publicly_queryable)) {
//          $args['publicly_queryable'] = $this->publicly_queryable;
//      }
//      if (isset($this->show_ui)) {
//          $args['show_ui'] = $this->show_ui;
//      }
//      if (isset($this->show_in_menu)) {
//          $args['show_in_menu'] = $this->show_in_menu;
//      }
//      if (isset($this->show_in_nav_menus)) {
//          $args['show_in_nav_menus'] = $this->show_in_nav_menus;
//      }
//      if (isset($this->show_in_admin_bar)) {
//          $args['show_in_admin_bar'] = $this->show_in_admin_bar;
//      }
//      if (isset($this->show_in_rest)) {
//          $args['show_in_rest'] = $this->show_in_rest;
//      }
//      if (isset($this->rest_base)) {
//          $args['rest_base'] = $this->rest_base;
//      }
//      if (isset($this->rest_namespace)) {
//          $args['rest_namespace'] = $this->rest_namespace;
//      }
//      if (isset($this->rest_controller_class)) {
//          $args['rest_controller_class'] = $this->rest_controller_class;
//      }
//      if (isset($this->autosave_rest_controller_class)) {
//          $args['autosave_rest_controller_class'] = $this->autosave_rest_controller_class;
//      }
//      if (isset($this->revisions_rest_controller_class)) {
//          $args['revisions_rest_controller_class'] = $this->revisions_rest_controller_class;
//      }
//      if (isset($this->late_route_registration)) {
//          $args['late_route_registration'] = $this->late_route_registration;
//      }
//      if (isset($this->menu_position)) {
//          $args['menu_position'] = $this->menu_position;
//      }
//      if (isset($this->menu_icon)) {
//          $args['menu_icon'] = $this->menu_icon;
//      }
//      if (isset($this->capability_type)) {
//          $args['capability_type'] = $this->capability_type;
//      }
//      if (isset($this->capabilities)) {
//          $args['capabilities'] = $this->capabilities;
//      }
//      if (isset($this->map_meta_cap)) {
//          $args['map_meta_cap'] = $this->map_meta_cap;
//      }
//      if (isset($this->supports)) {
//          if ($this->supports) {
//              $allowed = ['title','editor', 'comments', 'revisions','trackbacks','author','excerpt','page-attributes','thumbnail','custom-fields','post-formats'];
//              $this->supports = array_intersect($allowed, $this->supports);
//          }
//          $args['supports'] = $this->supports;
//      }
//      if (isset($this->register_meta_box_cb)) {
//          $args['register_meta_box_cb'] = $this->register_meta_box_cb;
//      }
//      if (isset($this->taxonomies)) {
//          $args['taxonomies'] = $this->taxonomies;
//      }
//      if (isset($this->rewrite)) {
//          $args['rewrite'] = $this->rewrite;
//      }
//      if (isset($this->query_var)) {
//          $args['query_var'] = $this->query_var;
//      }
//      if (isset($this->delete_with_user)) {
//          $args['delete_with_user'] = $this->delete_with_user;
//      }
//      if (isset($this->template)) {
//          $args['template'] = $this->template;
//      }
//      if (isset($this->template_lock)) {
//          $args['template_lock'] = $this->template_lock;
//      }
        unset ($args['postType']);
 
//      error_log('Registering PostType '.$this->postType.', with args: '.print_r($args, true));
        $registrar = Registrar::getInstance($this->postType);
        $rewrite = $args['rewrite']['slug']??'';
        if ($registrar) {
 
            $hasSlug = array_key_exists('rewrite', $args) && array_key_exists('slug', $args['rewrite']);
 
            if ($registrar->rewrite_taxonomy && !str_contains($rewrite, '%')) {
                if (!$hasSlug && !array_key_exists('rewrite', $args)) {
                    $args['rewrite'] = [];
                }
                $tax = is_array($registrar->rewrite_taxonomy) ? $registrar->rewrite_taxonomy[array_key_first($registrar->rewrite_taxonomy)] : $registrar->rewrite_taxonomy;
                $args['rewrite']['slug'] = $hasSlug ? $rewrite."/%{$tax}%" : $this->unbased."/%{$tax}%";
            }
 
            if ($registrar->hasFeature('is_calendar')) {
                if (!$hasSlug && !array_key_exists('rewrite', $args)) {
                    $args['rewrite'] = [];
                }
                $args['rewrite']['slug'] = $hasSlug ? $rewrite."/%eyear%/%emonth%/%eday%" : $this->unbased."/%eyear%/%emonth%/%eday%";
            }
        }
 
 
        $registered = register_post_type($this->postType, $args);
        if (is_wp_error($registered)) {
            JVB()->error()->log('JVBase\registrar\Posts', 'Could not register post type', $registered->get_error_messages());
        }
    }
 
    private function buildLabels(string $singular, string $plural): array
    {
        return [
            'name'               => $plural,
            'singular_name'      => $singular,
            'menu_name'          => $plural,
            'name_admin_bar'     => $singular,
            'add_new'            => "Add New",
            'add_new_item'       => "Add New {$singular}",
            'new_item'           => "New {$singular}",
            'edit_item'          => "Edit {$singular}",
            'view_item'          => "View {$singular}",
            'all_items'          => "All {$plural}",
            'search_items'       => "Search {$plural}",
            'parent_item_colon'  => "Parent {$plural}:",
            'not_found'          => "No {$plural} found.",
            'not_found_in_trash' => "No {$plural} found in Trash.",
        ];
    }
 
    public function addTaxonomyRewrite(string $taxonomy):void
    {
        $exists = Registrar::getInstance($taxonomy);
        if (!$exists) return;
 
        $this->taxonomyRewrite = $taxonomy;
 
        if (!isset($this->rewrite)) {
            $this->rewrite = [];
        }
        if (array_key_exists('slug', $this->rewrite)) {
            if (str_contains($this->rewrite['slug'], '%')) {
                return;
            }
            $this->rewrite['slug'] = $this->rewrite['slug'].'/%'.$taxonomy.'%';
        } else {
            $this->rewrite['slug'] = $this->unbased.'/%'.$taxonomy.'%';
        }
 
        $this->addTaxonomyRewriteRules();
        add_action('post_type_link', [$this, 'rewriteTaxonomySingle'], 15, 2);
        add_action('post_type_archive_link', [$this, 'rewriteTaxonomyArchive'], 15, 2);
    }
    public function addTaxonomyRewriteRules(): void
    {
        if (!$this->taxonomyRewrite) return;
        $tax = jvbCheckBase($this->taxonomyRewrite);
 
        // Rule 1: Post type archive - /faq/
        add_rewrite_rule(
            "{$this->unbased}/?$",
            "index.php?post_type={$this->postType}",
            'top'
        );
 
        // Rule 2: Single posts with taxonomy - /faq/section/post/
        add_rewrite_rule(
            "{$this->unbased}/([^/]+)/([^/]+)/?$",
            "index.php?post_type={$this->postType}&name=\$matches[2]&{$tax}=\$matches[1]",
            'top'
        );
 
        // Rule 3: Un-sectioned posts - /faq/post/
        // Use 'bottom' priority so taxonomy rules match first
        add_rewrite_rule(
            "{$this->unbased}/([^/]+)/?$",
            "index.php?post_type={$this->postType}&name=\$matches[1]",
            'bottom'
        );
    }
 
    /**
     * Set $this->rewrite_taxonomy to a valid taxonomy
     * @param string $url
     * @param \WP_Post $post
     * @return string
     */
    public function rewriteTaxonomySingle(string $url, \WP_Post $post): string
    {
        if ($post->post_type === $this->postType && !is_null($this->taxonomyRewrite)) {
            $type = $this->taxonomyRewrite;
            $taxonomy = jvbCheckBase($type);
            $terms = wp_get_post_terms($post->ID, $taxonomy);
            if (!empty($terms) && !is_wp_error($terms)) {
                $path = TaxonomySelector::getTermPath($terms[0], true);
                $path = implode('/', array_map(function($term) {
                    return sanitize_title($term);
                }, $path));
                return str_replace("%{$type}%", $path, $url);
            }
            return str_replace("/%{$type}%", '', $url);
        }
        return $url;
    }
 
    /**
     * Set $this->rewrite_taxonomy to a valid taxonomy
     * @param string $url
     * @param string $post_type
     * @return string
     */
    public function rewriteTaxonomyArchive(string $url, string $post_type):string
    {
        if ($post_type === $this->postType && !is_null($this->taxonomyRewrite)) {
            $url = get_home_url(null, "/{$this->postType}/");
        }
        return $url;
    }
 
}