self::schema($type), 'archive' => self::archive($type), 'meta' => self::meta($type), 'reference' => self::reference($type), }; } public static function schema(string $type): array { $type = self::checkType($type, '[SchemaHelper]::schema'); if (!$type) { return []; } if (!array_key_exists($type, self::$schemas)) { self::$schemas[$type] = get_option(BASE.ucfirst($type).'Schema', self::getDefault($type, 'schema')); } return self::$schemas[$type]; } public static function meta(string $type): array { $type = self::checkType($type, '[SchemaHelper]::meta'); if (!$type) { return []; } if (!array_key_exists($type, self::$metas)) { self::$metas[$type] = get_option(BASE.ucfirst($type).'Meta', self::getDefault($type, 'meta')); } return self::$metas[$type]; } public static function archive(string $type): array { $type = self::checkType($type, '[SchemaHelper]::archive'); if (!$type) { return []; } if (!array_key_exists($type, self::$archives)) { self::$archives[$type] = get_option(BASE.ucfirst($type).'Archive', self::getDefault($type, 'archive')); } return self::$archives[$type]; } public static function reference(string $type): array { $type = self::checkType($type, '[SchemaHelper]::reference'); if (!$type) { return []; } if (!array_key_exists($type, self::$references)) { self::$references[$type] = get_option(BASE.ucfirst($type).'Reference', self::getDefault($type, 'reference')); } return self::$references[$type]; } public static function getDefault(string $type, string $format):array { $reference = '[SchemaHelper]::getDefault'; $type = self::checkType($type, $reference); $format = self::checkFormat($format, $reference); if (!$type || !$format) { return []; } $defaults = match ($format) { 'schema' => match ($type) { 'website' => [ 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebSite', 'name' => get_bloginfo('name'), 'url' => get_home_url(), 'id' => get_home_url() . '/#website', 'description' => get_bloginfo('description'), 'inLanguage' => 'en-CA' ], default => [] }, 'archive' => [ 'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage', 'name' => '{{name}}' ], default => [], }; error_log('Getting defaults for: '.BASE.ucfirst($type).ucfirst($format).'Default'); $result = apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults); if ($format === 'reference' && empty($result)) { $full = self::getDefault($type, 'schema'); if (!empty($full)) { $result = [ 'type' => $full['type'], 'name' => $full['name'], 'description'=> $full['description'] ]; $check = ['id', 'url']; foreach ($check as $ch) { if (array_key_exists($ch, $full)) { $result[$ch] = $full[$ch]; } } } } return $result; } public static function updateHistory(string $type, string $format, array $newest):bool { $reference = '[SchemaHelper]::updateHistory'; $type = self::checkType($type, $reference); $format = self::checkFormat($format, $reference); if (!$type || !$format) { return false; } $historyOption = BASE.ucfirst($type).ucfirst($format).'History'; $history = get_option($historyOption, []); array_unshift($history, $newest); if (count($history) > 5) { array_pop($history); } return update_option($historyOption, $history); } public static function update(string $type, string $format, array $config, ?Meta $meta = null):bool { $reference = '[SchemaHelper]::update'; $type = self::checkType($type, $reference); $format = self::checkFormat($format, $reference); if (!$type || !$format) { return false; } $method = 'update'.ucfirst($type); return self::$method($config, $meta); } public static function updateSchema(string $type, array $config):bool { $reference = '[SchemaHelper]::updateSchema'; $type = self::checkType($type, $reference); if (!$type) { return false; } if (!class_exists($config['type'])){ error_log('[SchemaHelper]::updateSchema Config must be a valid schema type: '.$config['type']); return false; } if (!in_array($type, self::$allowedTypes)) { error_log('[SchemaHelper]::updateSchema Config must have a schema type'); } return self::updateClassConfig($type, 'schema', $config); } public static function updateClassConfig(string $type, string $format, array $config):bool { $reference = '[SchemaHelper]::updateClassConfig'; $type = self::checkType($type, $reference); if (!$type) { return false; } if (!class_exists($config['type'])){ error_log($reference.' Config must be a valid schema type: '.$config['type']); return false; } if (!in_array($type, self::$allowedTypes)) { error_log($reference.' Config must have a schema type'); } //Merge stored config with updates $stored = self::schema($type); $update = array_merge_recursive($stored, $config); //Validate Properties $className = $update['type']; unset($update['type']); foreach ($update as $property => $value) { if (!property_exists($className, $property)) { error_log($reference.' invalid property attempted: '.$property.', with value: '.print_r($value, true).' for class: '.$className); unset($update[$property]); } } $update['type'] = $className; //Add changes to history (keeps last 5 changes) self::updateHistory($type, $format, $update); self::$schemas[$type] = $update; return update_option(BASE.ucfirst($type).ucfirst($format), $update); } public static function updateMeta(string $type, array $config):bool { $type = self::checkType($type, '[SchemaHelper]::updateMeta'); $allowed = array_filter($config, function($key) { $allowed = in_array($key, ['name', 'description']); if (!$allowed) { error_log('[SchemaHelper]::updateMeta invalid property attempted: '.$key); } return $allowed; }); if (empty($allowed)) { error_log('[SchemaHelper]::updateMeta Name or Description must be set'); return false; } $config = array_map('sanitize_text_field', $config); self::updateHistory($type, 'meta', $config); self::$metas[$type] = $config; return update_option(BASE.ucfirst($type).'Meta', $config); } public static function updateArchive(string $type, array $config):bool { $reference = '[SchemaHelper]::updateArchive'; $type = self::checkType($type, $reference); if (!$type) { return false; } if (!class_exists($config['type'])){ error_log('[SchemaHelper]::updateSchema Config must be a valid schema type: '.$config['type']); return false; } if (!in_array($type, self::$allowedTypes)) { error_log('[SchemaHelper]::updateSchema Config must have a schema type'); } return self::updateClassConfig($type, 'schema', $config); } public static function classFromConfig(array $config, ?Meta $meta = null):mixed { if (!array_key_exists('type', $config)) { error_log('[SchemaHelper]::classFromConfig No class defined in config: '.print_r($config, true)); return false; } $className = $config['type']; unset($config['type']); $class = new $className(); foreach ($config as $property => $value) { if (is_array($value) && array_key_exists('type', $value)) { $value = self::classFromConfig($value, $meta); } $method = 'set'.ucfirst($property); if (!method_exists($class, $method)) { error_log('[SchemaHelper]::classFromConfig - method: '.$method.' does not exist in class: '.$className); continue; } if (is_string($value) && str_contains($value, '{{')) { $value = Resolver::resolveForSchema($property, $value, $config, $meta); } if (!empty($value)) { $class->$method($value); } } return $class; } } /** * JVB_SCHEMA: Site-wide schema configuration * * Structure: * - business: LocalBusiness/Organization for home page * - website: WebSite schema configuration * - actions: PotentialAction definitions * - attribution: Developer/maintainer info */ /** JVB_CONTENT['artwork'] = [ 'singular' => 'Artwork', 'plural' => 'Artworks', // ... other config 'seo' => [ 'meta' => [ 'title' => '{{post_title}} by {{linked_user.display_name}} | {{site_title}}', 'description' => '{{style.primary.name}} artwork by {{linked_user.display_name}}. {{short_bio|default:View this piece and more.}}', 'archive_title' => 'Artwork Gallery', 'archive_description' => 'Browse our collection of tattoo artwork and designs.', ], 'schema' => [ 'type' => 'VisualArtwork', 'mappings' => [ 'artform' => 'style.primary', // DefinedTerm from taxonomy 'creator' => 'linked_user', // Person from linked user 'image' => 'featured_image', // ImageObject 'artMedium' => 'medium.names:3', // Comma-separated term names ], 'overrides' => [ 'inLanguage' => 'en', ] ] ] ]; JVB_CONTENT['artist'] = [ 'singular' => 'Artist', 'plural' => 'Artists', 'seo' => [ 'meta' => [ 'title' => '{{post_title}} - {{artist_type|default:Tattoo Artist}} in {{city.primary.name|default:Edmonton}}', 'description' => '{{short_bio|truncate:155}}', ], 'schema' => [ 'type' => 'Person', 'mappings' => [ 'image' => 'image_portrait', 'jobTitle' => 'artist_type', 'worksFor' => 'shop.primary', // LocalBusiness reference 'knowsAbout' => 'style.names', // Array of style names 'areaServed' => 'city.primary.name', ] ] ] ]; JVB_TAXONOMY['shop'] = [ 'singular' => 'Shop', 'plural' => 'Shops', 'seo' => [ 'meta' => [ 'title' => '{{term_name}} - Tattoo Shop in {{city.primary.name|default:Edmonton}}', 'description' => '{{tagline|default:Visit}} {{term_name}}. {{short_bio|truncate:120}}', ], 'schema' => [ 'type' => 'TattooParlor', // or LocalBusiness 'mappings' => [ 'address' => 'location', 'telephone' => 'phone', 'email' => 'email', 'openingHoursSpecification' => 'hours', 'image' => 'image', 'priceRange' => 'price_range', 'paymentAccepted' => 'payment_accepted', ], 'overrides' => [ 'additionalTypeTrait' => 'https://schema.org/TattooParlor', ] ] ] ]; JVB_TAXONOMY['style'] = [ 'singular' => 'Style', 'plural' => 'Styles', 'seo' => [ 'meta' => [ 'title' => '{{term_name}} Tattoos in Edmonton | {{site_title}}', 'description' => '{{tagline|default:Explore}} {{term_name}} tattoo artists and designs. {{characteristics|strip|truncate:100}}', ], 'schema' => [ 'type' => 'DefinedTerm', 'mappings' => [ 'alternateName' => 'alternate_name', ] ] ] ]; **/