>can_create would be: * [ * 'piercer' => ['piercings', 'artwork', 'events'], * 'tattoo_artist'=> ['tattoos', 'artwork', 'events'], * 'artist' => ['artwork'] * ] */ protected string $user_subtype; /** Configs **/ protected Breadcrumbs $breadcrumbs; protected Dashboard $dashboard; protected Directory|false $directory; protected Feed|false $feed; // protected Management $management; // protected Responses $responses; protected ?SEO $seo = null; /** Helpers **/ protected MakeCalendarType|false $calendar = false; protected array $integrationConfigs = []; protected array $integrationFields = []; protected MakeTrackChanges $trackChanges; protected MakeVerification $verification; private static array $instances = []; protected string $based; private function __construct(string $slug, string $singular, string $plural, string $type) { $this->slug = $slug; $this->based = jvbCheckBase($slug); $this->type = $type; $this->singular = $singular; $this->plural = $plural; // $this->init(); // $this->initClasses(); $this->setFields(); add_action('init', [$this, 'register'], 2); add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 3); } protected function initRegistrar():void { $this->registrar = match ($this->type) { 'post' => new Posts($this->slug, $this->singular, $this->plural), 'term' => new Terms($this->slug, $this->singular, $this->plural), default => false, }; } protected function initClasses():void { $this->breadcrumbs = new Breadcrumbs($this->slug); $this->dashboard = new Dashboard($this->slug); $this->directory = new Directory($this->singular); $this->feed = new Feed($this->slug); // $this->management = new Management($this->slug); // $this->responses = new Responses($this->slug); $this->seo = new SEO($this->slug); // $this->trackChanges = new TrackChanges($this->slug); // $this->Verification = new Verification($this->slug); } /** * Instantiates for post types * @param string $slug * @return self */ public static function forPost(string $slug, string $singular, string $plural):self { if (!isset(self::$instances[$slug])) { self::$instances[$slug] = new self($slug, $singular, $plural,'post'); } return self::$instances[$slug]; } /** * Instantiates for term types * @param string $slug * @return self */ public static function forTerm(string $slug, string $singular, string $plural):self { if (!isset(self::$instances[$slug])) { self::$instances[$slug] = new self($slug, $singular, $plural,'term'); } return self::$instances[$slug]; } /** * Instantiates for user types * @param string $slug * @return self */ public static function forUser(string $slug, string $singular, string $plural):self { if (!isset(self::$instances[$slug])) { self::$instances[$slug] = new self($slug, $singular, $plural, 'user'); } return self::$instances[$slug]; } /** * Adds the properties for register_post_type or register_taxonomy * @param array $args * @return $this */ public function make(array $args):self { $this->initRegistrar(); if (!$this->registrar) { return $this; } $this->args = $args; foreach ($args as $property => $value) { if (property_exists($this->registrar, $property)) { $this->registrar->$property = $value; } if (property_exists($this, $property)) { $this->$property = $value; } } if (isset($this->icon) && !str_contains($this->icon, 'dashicons')){ $this->registrar->menu_icon = IconsManager::for()->getCSSIcon($this->icon); } return $this; } public function fields():Fields { return $this->fields; } public function args():array { return $this->args; } public function setFields():void { $this->fields = new Fields($this->type, $this); } public function getFields():array { return array_map(function ($field) { return $field->getConfig(); }, $this->fields->getFields() ); } public function setIcon(string $icon):self { $this->icon = $icon; return $this; } public function getIcon(?string $default = null):string { if (!$default) { $default = jvbDefaultIcon(); } return $this->icon ?: $default; } public function getSingular():string { return $this->singular; } public function getPlural():string { return $this->plural; } public function getDescription():string { return $this->description; } public function setDescription(string $description):self { $this->description = $description; return $this; } public function getType():string { return $this->type; } public function setIntegration(string $integration):self { if (!Site::hasIntegration($integration)){ error_log('Integration not available for '.$this->slug.': '.$integration); return $this; } $this->integrationConfigs[$integration] = new Integration($integration, $this); $this->integrationFields[$integration] = new AddIntegrationFields($integration, $this); return $this; } public function getIntegrationFields(string $integration):AddIntegrationFields|false { if (!Site::has($integration)){ error_log('Integration not available for '.$this->slug.': '.$integration); return false; } if (!in_array($integration, $this->integrationFields)) { error_log('No integration fields intitialized for '.$integration); return false; } return $this->integrationFields[$integration]; } public function getIntegrations():array { return $this->integrationConfigs; } public function hasIntegration(string $integration) { return in_array($integration, $this->integrationConfigs); } public function hasAnyIntegrations(array $integrations = []):bool { if (empty($integrations)) { $integrations = array_keys($this->integrationConfigs); return !empty($integrations); } return !empty(array_intersect($integrations, array_keys($this->integrationConfigs))); } public function setUploadTitle(string $title):self { $this->upload_title = $title; return $this; } public function getUploadTitle():string { return ($this->upload_title) ?: 'Upload '.$this->plural; } public function getSlug():string { return $this->slug; } public function getBased():string { return $this->based; } public function setGlossary(bool $set):self { $this->is_glossary = $set; // if ($set) { // $this->timeline = new MakeTimelineType($this->slug, $this); // } return $this; } public function isGlossary():bool { return $this->is_glossary; } public function setFaq(bool $set):self { $this->is_faq = $set; // if ($set) { // $this->timeline = new MakeTimelineType($this->slug, $this); // } return $this; } public function isFaq():bool { return $this->is_faq; } public function setTimeline(bool $set):self { $this->is_timeline = $set; // if ($set) { // $this->timeline = new MakeTimelineType($this->slug, $this); // } return $this; } public function isTimeline():bool { return $this->is_timeline; } public function setCalendar(bool $set):self { $this->is_calendar = $set; if ($set) { $this->calendar = new MakeCalendarType($this->slug, $this); } return $this; } public function setAll(array $flags):self { $flags = array_filter($flags, function($flag) { return in_array($flag, static::$allFlags); }); foreach ($flags as $flag) { $this->$flag = true; switch ($flag) { case 'is_content': add_action('init', [$this, 'setupContent'], 20); break; case 'is_glossary': $this->hide_single = true; break; } } return $this; } public function removeAll(array $flags):self { $flags = array_filter($flags, function($flag) { return in_array($flag, static::$allFlags); }); foreach ($flags as $flag) { $this->$flag = false; } return $this; } public function hasFeature(string $feature):bool { if (!in_array($feature, static::$allFlags)) { return false; } return isset($this->$feature) && $this->$feature === true; } public static function getFeatured(string $feature, ?string $type = null):array { self::ensureInstanced(); if (!in_array($feature, static::$allFlags)) { error_log('Feature requested not found: '.$feature); return []; } return array_map(function($inst) { return $inst->slug; },array_filter(self::$instances, function ($inst) use ($feature, $type){ if (!is_null($type) && $inst->type !== $type) { return false; } return property_exists($inst, $feature) && isset($inst->$feature) && $inst->$feature === true; })); } public function config(string $config):mixed { $allowed = ['breadcrumbs','calendar','dashboard','directory','feed','management','has_responses','seo','trackchanges','verification']; if (!in_array(strtolower($config), $allowed)) { error_log('Invalid config requested from Registrar: '.$config); return []; } return match(strtolower($config)) { 'breadcrumbs' => $this->getBreadcrumbs(), 'dashboard' => $this->getDashboard(), 'directory' => $this->getDirectory(), 'feed' => $this->getFeed(), 'management' => $this->getManagement(), 'has_responses' => $this->getResponses(), 'seo' => $this->getSEO(), 'trackchanges' => $this->getTrackChanges(), 'verification' => $this->getVerification() }; } protected function getBreadcrumbs():Breadcrumbs { if (!isset($this->breadcrumbs)) { $this->breadcrumbs = new Breadcrumbs($this->slug, $this); } return $this->breadcrumbs; } protected function getCalendar():MakeCalendarType|false { if ($this->is_calendar && !isset($this->calendar)){ $this->calendar = new MakeCalendarType($this->slug, $this); } else { $this->calendar = false; } return $this->calendar; } protected function getDashboard():Dashboard { if (!isset($this->dashboard)) { $this->dashboard = new Dashboard($this->plural, $this); } return $this->dashboard; } protected function getDirectory():Directory|false { if (!isset($this->directory)) { $this->directory = new Directory($this->singular); } return $this->directory; } protected function getFeed():Feed|false { if (!isset($this->feed)) { $this->feed = new Feed($this->slug); } return $this->feed; } public function getSEO():SEO { if (!isset($this->seo)){ $this->seo = new SEO($this->slug); } return $this->seo; } public function getSections():array { $allSections = array_map(function($section) { return $section->getConfig; }, $this->sections); if (!empty($this->sectionOrder)) { $allSections['order'] = $this->sectionOrder; } return $allSections; } public function addSection(string $title):Section { $section = new Section($title, $this); $this->sections[] = $section; return $section; } public function setSectionOrder(array $sections):self { $allSections = array_map(function($section) { return $section->getSlug; }, $this->sections); $this->sectionOrder = array_intersect($allSections, $sections); return $this; } public function getSectionOrder():array { return $this->sectionOrder; } public function getConfig(string $config):array { $allowed = ['breadcrumbs','calendar','dashboard','directory','feed','management','has_responses','seo','trackchanges','verification']; if (!in_array(strtolower($config), $allowed)) { error_log('Invalid config requested from Registrar: '.$config); return []; } $config = $this->config($config); return ($config) ? $config->getConfig() : []; } public function getIntegration(string $service_name):Integration|false { if (array_key_exists($service_name, $this->integrationConfigs)) { return $this->integrationConfigs[$service_name]; } return false; } public function getIntegrationConfig(string $service_name):array|false { $integration = $this->getIntegration($service_name); if ($integration){ return $integration->getConfig(); } return false; } public function register():void { if ($this->type === 'post') { if ($this->hide_single) { $this->hideSingleHandler = new HideSingle($this->slug, $this); } if ($this->is_timeline) { $this->isTimelineHandler = new MakeTimelineType($this->slug, $this); $this->registrar->hierarchical = true; } if ($this->is_calendar) { $this->isCalendarHandler = new MakeCalendarType($this->slug, $this); } if (!is_null($this->rewrite_taxonomy)) { $this->registrar->addTaxonomyRewrite($this->rewrite_taxonomy); } if ($this->registrar) { $this->registrar->register(); } if ($this->add_image_column) { add_filter("manage_{$this->based}_posts_columns", [$this, 'addImageColumn']); add_action("manage_{$this->based}_posts_custom_column", [$this, 'showImageColumn'], 10, 2); } } elseif ($this->type === 'term') { if ($this->is_content) { if ($this->verify_entry) { $this->verifyEntryHandler = new MakeVerification(); } if ($this->track_changes) { $this->trackChangesHandler = new MakeTrackChanges($this->slug); } } if ($this->registrar) { $this->registrar->register(); } } if ($this->karma) { $this->karmaManager = KarmaManager::for($this->slug, $this->type); } } public static function getInstance(string $slug):Registrar|false { self::ensureInstanced(); $slug = jvbNoBase($slug); if (array_key_exists($slug, static::$instances)) { return static::$instances[$slug]; } return false; } public static function getFieldsFor(string $slug):array { self::ensureInstanced(); if (!array_key_exists($slug, static::$instances)) { return []; } $instance = static::$instances[$slug]; return $instance->getFields(); } public static function getRegistered(?string $type = null):array { self::ensureInstanced(); $instances = ($type) ? array_filter(static::$instances, function($instance) use ($type) { return $instance->type === $type; }) : static::$instances; return array_keys($instances); } public static function getLabels():array { self::ensureInstanced(); return array_map(function ($instance) { return ['singular' => $instance->getSingular(), 'plural' => $instance->getPlural()]; }, static::$instances); } public function getCreatable(bool $based = false):array { if ($this->type !== 'user') { return []; } return $based ? array_map(function ($item) { return jvbCheckBase($item); },$this->can_create) : $this->can_create; } public function setCreatable(string|array $creatable):self { $this->can_create = is_string($creatable) ? [jvbNoBase($creatable)] : array_map(function ($type) { return jvbNoBase($type); }, $creatable); return $this; } public function getManageOthers():array { if ($this->type !== 'user'){ return []; } return $this->manage_others; } public function setManageOthers(array $manageable):self { $this->manage_others = $manageable; return $this; } public function renderDashPage(string $content, string $page, string $slug):string { if ($slug === $this->slug) { ob_start(); $crud = new CRUD($slug); $crud->render(); return ob_get_clean(); } return $content; } public function setupContent():void { if (!$this->is_content) return; //We need a pseudo-archive page for this content taxonomy. We create a post with the plural name $this->page = get_option(BASE.$this->slug.'_archive', false); if (!$this->page || !(bool)get_post((int)$this->page)) { $exists = new WP_Query([ 'post_type' => 'page', 'title'=> $this->plural, 'posts_per_page' => 1, 'post_status' => 'publish', 'fields' => 'ids', ]); if ($exists->have_posts()) { $page = $exists->posts[0]; } else { $page = wp_insert_post([ 'post_type' => 'page', 'post_title' => $this->plural, 'post_content' => '', 'post_status' => 'publish', ]); } if ($page && !is_wp_error($page)) { update_post_meta($page, BASE.'for_type', $this->slug); update_option(BASE.$this->slug.'_archive', $page); $this->page = $page; } wp_reset_postdata(); } add_filter('jvb_post_content_output', [$this, 'renderContent'], 20, 2); //Add a date published and date modified fields, and auto-update them on term creation/modification $this->fields()->addField('date_published', [ 'type' => 'datetime', 'label' => 'Published', 'hidden' => true, ]); $this->fields()->addField('date_modified', [ 'type' => 'datetime', 'label' => 'Modified', 'hidden' => true, ]); add_action('created_'.$this->based, [$this, 'addTermCreatedMeta']); add_action('edited_'.$this->based, [$this, 'addTermUpdatedMeta']); } public function addTermCreatedMeta(int $termId):void { update_term_meta($termId, BASE . 'date_published', date('Y-m-d H:i:s')); update_term_meta($termId, BASE . 'date_modified', date('Y-m-d H:i:s')); } public function handleContentTermMetaChange(int $meta_id, int $term_id, string $meta_key, $meta_value):void { $term = get_term($term_id); $taxonomy = $term->taxonomy; if ($taxonomy === $this->based && $meta_key !== BASE . 'date_modified') { $meta = Meta::forTerm($term_id); $meta->set('date_modified', date('Y-m-d H:i:s')); } } public function addTermUpdatedMeta(int $termId):void { static $processing = []; if (isset($processing[$termId])) return; $processing[$termId] = true; update_term_meta($termId, BASE . 'date_modified', date('Y-m-d H:i:s')); unset($processing[$termId]); } public function renderContent(string $content, array $block):string { if (!is_page($this->page)) { return $content; } if ($block['blockName'] !== 'core/post-content') { return $content; } if (JVB_TESTING) { Cache::for($this->slug)->flush(); } $per_page = 10; $page = $_GET['tp']??1; $args = apply_filters('jvb_content_tax_args_'.$this->slug, [ 'taxonomy' => $this->based, // 'hide_empty' => true, 'fields' => 'ids', 'number' => $per_page, 'offset' => ($page - 1) * $per_page, 'meta_key' => BASE.'date_modified', 'meta_type' => 'DATETIME', 'orderby' => 'meta_value' ]); $cache = Cache::for($this->slug); $max = get_terms([ 'taxonomy' => $this->based, 'fields' => 'ids', 'number' => 0, 'hide_empty' => true ]); $max = count($max??[]); $totalPages = floor($max/$per_page); global $wp; $current = get_home_url(null, '/'.$wp->request); $pages = ''; for ($i = 1; $i<=$totalPages; $i++) { $pages .= (int)$page === $i ? sprintf( '
  • %s
  • ', $i ): sprintf( '
  • %s
  • ', add_query_arg('tp', $i, $current), $i ); } $nav = sprintf( '', $page > 1 ? ''.jvbIcon('arrow-circle-left').'Previous Page' : '', $pages, $page < $totalPages ? ''.jvbIcon('arrow-circle-right').'Next Page' : '', ); $out = $nav. Cache::for($this->slug)->remember( $cache->generateKey(['type' =>'contentArchive', ... $args]), function() use ($args) { $items = get_terms($args); $out = []; $method = BASE.'render_'.$this->slug.'_content'; if ($items && !is_wp_error($items)) { foreach ($items as $termID) { if (function_exists($method)) { $out[] = $method($termID); continue; } $meta = Meta::forTerm($termID); $slug = sanitize_title($meta->get('name')); $item = sprintf( '
  • %s

    %s

  • '; $out[] = $item; } } $before = apply_filters(BASE.'before_'.$this->slug.'_content',''); $out = empty($out) ? '' : ''; $after = apply_filters(BASE.'after_'.$this->slug.'_content', ''); return $before.$out.$after; } ).$nav; error_log('Built the '.$this->slug.' page content.'); return $content . $out; } public static function ensureInstanced():void { if (empty(self::$instances)) { do_action('jvbDefineRegistrar'); do_action('jvbDefineRegistrarFields'); } } /***************************************************************** * FLAGGED FEATURES *****************************************************************/ public function profile(?string $slug = null, ?string $singular = null, ?string $plural = null):self { if (!$slug) { $slug = $this->slug.'_profile'; } if (!$singular) { $singular = $this->singular; } if (!$plural) { $plural = $this->plural; } $this->profile_link = true; $this->profile = $slug; return Registrar::forPost($slug, $singular, $plural); } public function getProfile():self|false { if (!$this->profile_link) { return false; } return self::getInstance($this->profile); } public static function getProfileTypes():array { $hasProfiles = self::getFeatured('profile_link'); if (empty($hasProfiles)) { return []; } return array_filter(array_map(function($profile) { $instance = self::getInstance($profile); return $instance->getProfile()->based??false; }, $hasProfiles)); } public function setUserSubtype(string $type):self { $this->user_subtype = sanitize_text_field($type); return $this; } public function getUserSubtype():string|false { return $this->user_subtype??false; } public function addImageColumn(array $columns):array { $keys = array_keys($columns); $index = array_search('cb', $keys); if ($index !== false) { $pos = $index+1; $columns = array_slice($columns, 0, $pos, true) + ['jvb_featured_image' => 'Image'] + array_slice($columns, $pos, null, true); } return $columns; } public function showImageColumn(string $column, int $postID):void { if ($column === 'jvb_featured_image') { echo get_the_post_thumbnail($postID, 'tiny'); } } }