Jake Vanderwerf
5 days ago 266aa37c48222993bf7bdad6834e31bd08736f5e
inc/registrar/config/seo/Schema.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\registrar\config\seo;
use JVBase\base\SchemaHelper;
use JVBase\managers\Cache;
use JVBase\managers\SEO\render;
use JVBase\meta\Meta;
@@ -35,7 +36,7 @@
   ];
   protected array $defaultArchive = [
      'type'   => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
      'type'   => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
      'title' => '{{registrar.plural}}'
   ];
@@ -58,7 +59,7 @@
         switch ($registrar->getType()) {
            case 'term':
               $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
               $this->defaultSchema['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
               break;
            case 'user':
               $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage';
@@ -79,7 +80,7 @@
      $registrar = Registrar::getInstance($this->slug);
      $this->defaultArchive = [
         'type'   => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage',
         'type'   => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
         'name'   => $registrar->getPlural(),
         'description' => $registrar->getDescription()
      ];
@@ -148,6 +149,7 @@
   public function outputSchema():void
   {
      $registrar = Registrar::getInstance($this->slug);
      if (is_singular()) {
         $this->outputSingularSchema();
      } elseif (is_post_type_archive(jvbCheckBase($this->slug) || is_tax(jvbCheckBase($this->slug)))) {
@@ -163,15 +165,25 @@
            $this->cache->flush();
         }
         $registrar = Registrar::getInstance($this->slug);
         return $this->cache->remember(
            $ID,
            function () use ($ID) {
            function () use ($ID, $registrar) {
               $meta = Meta::forPost($ID);
               if ($registrar->hasFeature('is_faq')) {
                  return $this->outputQASchema($ID, $meta);
               }
               if ($registrar->hasFeature('is_timeline')) {
                  return $this->outputTimelineSchema($ID, $meta);
               }
               $config = $this->getConfig();
               $class = $this->classFromConfig($config, $meta);
               $class = JVB()->schemaHelper()::classFromConfig($config, $meta);
               $class->setAuthor(JVB()->seo()->getCreator(true));
               $class = apply_filters('jvb_single_'.$this->slug.'_schema_output', $class, $ID);
               return $class->outputSchema();
            }
         );
@@ -188,15 +200,16 @@
            $ID,
            function() use ($ID) {
               $action = BASE.ucfirst($this->slug).'Schema';
               $config = get_option($action, apply_filters($action, $this->defaultSchema));
               $config = JVB()->schemaHelper()::schema($action);
               if (!array_key_exists('type', $config)) {
                  $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
                  $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
               }
               if (!class_exists($config['type'])) {
                  error_log('No class found for archive schema output: '.$config['type']);
                  return [];
               }
               $class = $this->classFromConfig($config);
               $class = JVB()->schemaHelper()::classFromConfig($config);
               $class->setIsPartOf(get_home_url().'/#website');
               $itemList = new render\Thing\Intangible\ItemList\ItemList();
@@ -235,18 +248,28 @@
            $this->slug,
            function() {
               $action = BASE.ucfirst($this->slug).'Archive';
               $config = get_option($action, apply_filters($action, $this->defaultArchive));
               $config = JVB()->schemaHelper()->archive($this->slug);
               if (!array_key_exists('type', $config)) {
                  $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage';
                  $config['type'] = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage';
               }
               if (!class_exists($config['type'])) {
                  error_log('No class found for archive schema output: '.$config['type']);
                  return [];
               }
               $registrar = Registrar::getInstance($this->slug);
               if ($registrar->hasFeature('is_glossary')) {
                  return $this->outputGlossarySchema();
               } else if ($registrar->hasFeature('is_faq')) {
                  return $this->outputFAQSchema();
               }
               if ($registrar->hasFeature('is_timeline')) {
                  return $this->outputTimelineArchiveSchema();
               }
               $obj = get_queried_object();
               $meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null;
               $class = $this->classFromConfig($config, $meta);
               $class = JVB()->schemaHelper()::classFromConfig($config, $meta);
               $class->setIsPartOf(get_home_url().'/#website');
               $itemList = new render\Thing\Intangible\ItemList\ItemList();
@@ -261,6 +284,7 @@
               foreach ($items->posts as $ID) {
                  $item = $this->outputReferenceSchema($ID, 'post',false);
                  $listItem = new render\Thing\Intangible\ListItem();
                  $listItem->setId('listitem-'.$ID);
                  $listItem->setPosition($pos);
                  $listItem->setItem($item);
                  $itemListItems[] = $listItem;
@@ -271,7 +295,10 @@
               $class->setMainEntity($itemList);
               $schema = $class->outputSchema();
               error_log('Generated archive schema: '.print_r($schema, true));
               if (JVB_TESTING) {
//                error_log('Generated archive schema: '.print_r($schema, true));
               }
               return $schema;
            }
         );
@@ -300,8 +327,8 @@
                  error_log('Invalid type used for reference: '.print_r($type, true));
                  $meta = null;
            }
            $config = $this->getConfig('archive');
            $class = $this->classFromConfig($config, $meta);
            $config = $this->getConfig();
            $class = JVB()->schemaHelper()::classFromConfig($config, $meta);
            $class->delete('about');
            switch ($type) {
@@ -327,9 +354,29 @@
         error_log('[SEO]Schema::getConfig Invalid type: '.$type);
         return [];
      }
      $action = BASE.ucfirst($this->slug).ucfirst($type);
      $default = 'default'.ucfirst($type);
      return get_option($action, apply_filters($action, $this->$default));
      return JVB()->schemaHelper()::getConfig($this->slug, $type);
   }
   public function resolveMeta(array $config, Meta $meta):array
   {
      foreach ($config as $property => $value) {
         if (is_array($value)) {
            $config[$property] = $this->resolveMeta($value, $meta);
            if ($property === 'additionalProperty' && (!array_key_exists('value', $config[$property]) || empty($config[$property]['value']))) {
               unset($config[$property]);
            }
         }
         if (is_string($value) && str_contains($value, '{{')) {
            $value = Resolver::resolve($value, $meta);
            if (empty($value)){
               unset($config[$property]);
            } else {
               $config[$property] = $value;
            }
         }
      }
      return $config;
   }
   public function define(string $property, string $value):void
@@ -356,7 +403,7 @@
         $config['type'] = $this->defaultSchema['type'];
         update_option(BASE.ucfirst($this->slug).'Schema', $config);
      }
      $class = $this->getConfig()['type'];
      $class = $config['type'];
      if (!class_exists($class)) {
         error_log('[SEO]Schema::defineReference Class not found: '.$class);
         return;
@@ -393,40 +440,223 @@
      if (is_singular($based)){
         $config = $this->getConfig('meta');
         $meta = Meta::forPost(get_the_ID());
         $title = Resolver::resolve($config['name'], $meta);
      } elseif (is_post_type_archive($based) || is_tax($based)) {
         $title = Resolver::resolve($config['name']??$config['title']??'', $meta);
      } elseif (is_post_type_archive($based) ) {
         $config = $this->getConfig('archive');
         $title = $config['name'];
         $title = Resolver::resolve($config['name'], null);
      } elseif (is_tax($based)) {
         $config = $this->getConfig('archive');
         $meta = Meta::forTerm(get_queried_object_id());
         $title = Resolver::resolve($config['name'], $meta);
      } else {
         error_log('[Schema]::filterTSFOGTitle Unmatched condition: '.$this->slug);
      }
      return $title;
   }
   protected function classFromConfig(array $config, ?Meta $meta = null):mixed
   public function outputQASchema(int $ID, Meta $meta):array
   {
      if (!array_key_exists('type', $config)) {
         error_log('[Schema]::classFromConfig No class defined in config: '.print_r($config, true));
         return false;
      }
      $className = $config['type'];
      unset($config['type']);
      $class = new $className();
      $registrar = Registrar::getInstance($this->slug);
      global $wp;
      $current = get_home_url(null, $wp->request).'/';
      $config = $this->getConfig();
      foreach ($config as $property=>$value) {
         if (is_array ($value)) {
            $value = $this->classFromConfig($value, $meta);
      $config['type']   = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\QAPage';
      $page = SchemaHelper::classFromConfig($config, $meta);
      $post = get_queried_object();
      $question = [
         'id'  => $current.'#question-'.$post->post_name,
         'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question',
         'name'   => $meta->get('post_title'),
         'acceptedAnswer' => [
            'id'  => $current.'#answer-'.$post->post_name,
            'type'   => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer',
            'text'   => wp_strip_all_tags(str_replace("\n", '', $meta->get('post_content'))),
         ],
      ];
      $question = SchemaHelper::classFromConfig($question);
      $page->setMainEntity($question);
      $page->setAuthor(JVB()->seo()->getCreator(true));
      return $page->outputSchema();
   }
   public function outputTimelineSchema(int $ID, Meta $meta):array
   {
      $registrar = Registrar::getInstance($this->slug);
      global $wp;
      $current = get_home_url(null, $wp->request).'/';
      $config = $this->getConfig();
      $config['type']   = 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\ImageGallery';
      $this->resolveMeta($config, $meta);
      $page = SchemaHelper::classFromConfig($config, $meta);
      $post = get_queried_object();
      $images = [];
      $children = new WP_Query([
         'post_type'=> jvbCheckBase($this->slug),
         'post_status'  => 'publish',
         'post_parent'  => $post->ID,
      ]);
      $children = $children->posts;
      array_unshift($children, $post);
      foreach ($children as $index => $child) {
         $meta = Meta::forPost($child->ID);
         $image = render\Thing\Thing::createImageFromID($meta->get('post_thumbnail'));
         $image->setId(($index === 0) ? '#before-treatment' : '#treatment-'.$index);
         $image->setPosition($index+1);
         $image->setName(($index === 0) ? 'Before Laser Tattoo Removal' : 'After '.$index.' Laser Tattoo Removal Treatments');
         if (!empty ($meta->get('post_excerpt'))){
            $image->setDescription($meta->get('post_excerpt'));
         }
         $method = 'set'.ucfirst($property);
         if (!method_exists($class, $method)) {
            error_log('[Schema]::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);
         }
         $images[] = $image;
      }
      return $class;
      $page->setImage($images);
      $page->setAuthor(JVB()->seo()->getCreator(true));
      return $page->outputSchema();
   }
   /*********************************************
    * Archive Presets
   *********************************************/
   public function outputFAQSchema():array
   {
      $registrar = Registrar::getInstance($this->slug);
      global $wp;
      $current = get_home_url(null, $wp->request).'/';
      $config = $this->getConfig('archive');
      $page = [
         'id'  => $current.'#'.$registrar->getSlug(),
         'type'   => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\FAQPage',
         'name'   => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(),
         'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(),
         'url' => $current,
      ];
      $page = SchemaHelper::classFromConfig($page);
      $args = [
         'post_type'    => $registrar->getBased(),
         'posts_per_page'=> -1,
         'post_status'  => 'publish',
      ];
      $obj = get_queried_object();
      if (property_exists($obj, 'taxonomy')) {
         $page->setName('FAQ on '.$obj->name);
         $args['post_type'] = array_map('jvbCheckBase', $registrar->registrar->for);
         $args['tax_query'] = [
            [
               'taxonomy'  => $obj->taxonomy,
               'terms'     => $obj->term_id,
            ]
         ];
      }
      $questions = [];
      $posts = new WP_Query($args);
      foreach ($posts->posts as $post) {
         $meta = Meta::forPost($post->ID);
         $question = [
            'id'  => $current.'#question-'.$post->post_name,
            'type' => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Question',
            'name'   => $meta->get('post_title'),
            'acceptedAnswer' => [
               'id'  => $current.'#answer-'.$post->post_name,
               'type'   => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\Comment\Answer',
               'text'   => $meta->get('post_excerpt'),
            ],
            'url' => get_the_permalink($post->ID),
         ];
         $questions[] = SchemaHelper::classFromConfig($question);
      }
      wp_reset_postdata();
      $page->setMainEntity($questions);
      return $page->outputSchema();
   }
   public function outputTimelineArchiveSchema():array
   {
      $registrar = Registrar::getInstance($this->slug);
      global $wp;
      $current = get_home_url(null, $wp->request).'/';
      $config = $this->getConfig('archive');
      $page = array_merge($config, [
         'id'  => $current.'#'.$registrar->getSlug(),
         'type'   => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
         'name'   => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(),
         'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(),
         'url' => $current,
      ]);
      $page = SchemaHelper::classFromConfig($page);
      $parts = [];
      $timelines = new WP_Query([
         'post_type'    => $registrar->getBased(),
         'posts_per_page'=> 50,
         'post_status'  => 'publish',
         'post_parent'  => 0,
      ]);
      foreach ($timelines->posts as $post) {
         $item = $this->outputReferenceSchema($post->ID, 'post', false);
         $item->setId($current.'#'.$post->post_name);
         $item->setName($post->post_title);
         $item->setUrl(get_the_permalink($post->ID));
         $parts[] = $item;
      }
      $page->setHasPart($parts);
      return $page->outputSchema();
   }
   public function outputGlossarySchema():array
   {
      $registrar = Registrar::getInstance($this->slug);
      global $wp;
      $current = get_home_url(null, $wp->request).'/';
      $config = $this->getConfig('archive');
      $page = [
         'id'  => $current.'#'.$registrar->getSlug(),
         'type'   => 'JVBase\inc\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage\CollectionPage',
         'name'   => array_key_exists('name', $config) ? $config['name'] : $registrar->getPlural(),
         'description' => array_key_exists('description', $config) ? $config['description'] : $registrar->getDescription(),
         'url' => $current,
      ];
      $page = SchemaHelper::classFromConfig($page);
      //Defined Termset
      $termset = [
         'type'   => 'JVBase\managers\SEO\render\Thing\CreativeWork\DefinedTermSet',
         'id'  => $current.'#definedtermset',
         'name'   => $registrar->getPlural(),
         'description' => $registrar->getDescription(),
      ];
      $termset = SchemaHelper::classFromConfig($termset);
      $terms = new WP_Query([
         'post_type' => $registrar->getBased(),
         'posts_per_page' => -1,
         'post_status' => 'publish',
      ]);
      $outputTerms = [];
      foreach ($terms->posts as $post) {
         $item = $this->outputReferenceSchema($post->ID, 'post', false);
         $item->setId($current.'#'.$post->post_name);
         $item->setName($post->post_title);
         $outputTerms[] = $item;
      }
      $termset->setHasDefinedTerm($outputTerms);
      $page->setMainEntity($termset);
      return $page->outputSchema();
   }
}