<?php
|
namespace JVBase\base;
|
use JVBase\meta\Meta;
|
use JVBase\registrar\config\seo\Resolver;
|
use JVBase\registrar\Registrar;
|
|
class SchemaHelper
|
{
|
protected static array $allowedTypes;
|
protected static array $allowedFormats = ['schema', 'archive', 'meta', 'reference'];
|
protected static array $schemas = [];
|
protected static array $metas = [];
|
protected static array $archives = [];
|
protected static array $references = [];
|
public function __construct()
|
{
|
self::$allowedTypes = array_merge(['website', 'organization'], Registrar::getRegistered());
|
}
|
public static function checkType(string $type, string $reference = '[SchemaHelper] Invalid type'):string|false
|
{
|
$type = strtolower($type);
|
if (!in_array($type, self::$allowedTypes)) {
|
error_log($reference.': '.$type);
|
return false;
|
}
|
return $type;
|
}
|
public static function checkFormat(string $format, string $reference = '[SchemaHelper] Invalid format'):string|false
|
{
|
$format = strtolower($format);
|
if (!in_array($format, self::$allowedFormats)) {
|
error_log($reference.': '.$format);
|
return false;
|
}
|
return $format;
|
}
|
public static function getConfig(string $type, string $format):array
|
{
|
$reference = '[SchemaHelper]::getConfig';
|
$type = self::checkType($type, $reference);
|
$format = self::checkFormat($format, $reference);
|
if (!$type || !$format) {
|
return [];
|
}
|
return match($format) {
|
'schema' => 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');
|
error_log('[SchemaHelper]::archive type: '.print_r($type, true));
|
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\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
|
],
|
default => [],
|
};
|
return apply_filters(BASE.ucfirst($type).ucfirst($format).'Default', $defaults);
|
}
|
|
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)) {
|
$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',
|
]
|
]
|
]
|
];
|
|
**/
|