Jake Vanderwerf
2026-04-26 86c6cd3cc099d2480932ede03c12cea01e625c94
base/seo.php
@@ -1,4 +1,306 @@
<?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');
      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 => [],
      };
      $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
 *
@@ -9,44 +311,6 @@
 *   - attribution: Developer/maintainer info
 */
use JVBase\managers\SEO\SchemaBuilder;
$schema = apply_filters('jvb_schema', []);
$registry = SchemaBuilder::getInstance();
$checked = [];
foreach ($schema as $key => $config) {
   if (array_key_exists('type', $config)) {
      $type = $config['type'];
   } elseif ($key === 'website') {
      $type = 'WebSite';
   }
   $exists = !is_null($registry->getTypeDefinition($type));
   if (!$exists) {
//    error_log('[JVB_SCHEMA] No definitions for: '.print_r($type, true));
      continue;
   }
   $allowed = $registry->getFieldsForType($type);
   $filtered = array_filter($config, function ($item) use ($allowed) {
      return in_array($item, $allowed);
   }, ARRAY_FILTER_USE_KEY);
   if (empty($filtered)) {
//    error_log('[JVB_SCHEMA] No valid filters for '.$type.'.');
      continue;
   }
   $removed = array_filter($config, function ($item) use ($allowed) {
      return !in_array($item, $allowed);
   }, ARRAY_FILTER_USE_KEY);
   if (!empty($removed)) {
//    error_log('[JVB_SCHEMA] Invalid fields detected for '.$type.': '.print_r($removed, true));
   }
   $checked[$key] = $filtered;
}
define('JVB_SCHEMA', $checked);
/**
JVB_CONTENT['artwork'] = [