| | |
| | | { |
| | | private array $errors = []; |
| | | private array $warnings = []; |
| | | protected array $validSchemaTypes = [ |
| | | 'content' => [ |
| | | 'Article', 'NewsArticle', 'BlogPosting', 'VisualArtwork', |
| | | 'Product', 'Service', 'Event', 'Person', 'CreativeWork', |
| | | 'MedicalProcedure', 'HowTo', 'Recipe', 'Review', |
| | | ], |
| | | 'taxonomy' => [ |
| | | 'CollectionPage', 'DefinedTerm', 'ItemList', |
| | | ], |
| | | 'user' => [ |
| | | 'Person', |
| | | ], |
| | | ]; |
| | | |
| | | protected array $validModifiers = [ |
| | | 'first', 'last', 'join', 'truncate', 'strip', 'lower', 'upper', |
| | | 'title', 'count', 'get', 'default', 'date', 'image_url', 'excerpt', 'plural' |
| | | ]; |
| | | |
| | | public function validateAll():array |
| | | { |
| | |
| | | $success['terms'] = $this->validateTaxonomyConfig(JVB_TAXONOMY); |
| | | $success['user'] = $this->validateUserConfig(JVB_USER); |
| | | $success['crossReference'] = $this->validateCrossReferences(JVB_CONTENT, JVB_TAXONOMY, JVB_USER); |
| | | $success['seo'] = $this->validateSEOConfig(); |
| | | $success['schema'] = $this->validateSchemaConfig(JVB_SCHEMA ?? []); |
| | | return $success; |
| | | } |
| | | /** |
| | |
| | | $validTypes = [ |
| | | 'text', 'textarea', 'number', 'email', 'url', 'select', |
| | | 'radio', 'checkbox', 'true_false', 'date', 'time', |
| | | 'datetime', 'color', 'image', 'file', 'gallery', |
| | | 'datetime', 'color', 'upload', 'image', 'file', 'gallery', |
| | | 'repeater', 'location', 'user', 'taxonomy', 'set' |
| | | ]; |
| | | |
| | |
| | | break; |
| | | |
| | | case 'repeater': |
| | | if (empty($config['sub_fields'])) { |
| | | if (empty($config['fields'])) { |
| | | $this->addError( |
| | | "{$path}.fields.{$fieldName}", |
| | | "Repeater field requires 'sub_fields' array" |
| | | "Repeater field requires 'fields' definition array" |
| | | ); |
| | | } |
| | | break; |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Validate SEO configurations across all types |
| | | */ |
| | | public function validateSEOConfig(): bool |
| | | { |
| | | $this->errors = []; |
| | | $this->warnings = []; |
| | | |
| | | foreach (JVB_CONTENT ?? [] as $slug => $config) { |
| | | if (isset($config['seo'])) { |
| | | $this->validateTypeSEOConfig($slug, $config['seo'], 'content', $config); |
| | | } |
| | | } |
| | | |
| | | foreach (JVB_TAXONOMY ?? [] as $slug => $config) { |
| | | if (isset($config['seo'])) { |
| | | $this->validateTypeSEOConfig($slug, $config['seo'], 'taxonomy', $config); |
| | | } |
| | | } |
| | | |
| | | foreach (JVB_USER ?? [] as $slug => $config) { |
| | | if (isset($config['seo'])) { |
| | | $this->validateTypeSEOConfig($slug, $config['seo'], 'user', $config); |
| | | } |
| | | } |
| | | |
| | | $this->logResults(); |
| | | return empty($this->errors); |
| | | } |
| | | |
| | | /** |
| | | * Validate SEO config for a specific type |
| | | */ |
| | | private function validateTypeSEOConfig(string $slug, array $seo, string $objectType, array $fullConfig): void |
| | | { |
| | | $path = "{$objectType}.{$slug}.seo"; |
| | | $availableFields = $this->getAvailableSEOFields($slug, $objectType, $fullConfig); |
| | | |
| | | if (isset($seo['schema_type'])) { |
| | | $validTypes = $this->validSchemaTypes[$objectType] ?? $this->validSchemaTypes['content']; |
| | | if (!in_array($seo['schema_type'], $validTypes)) { |
| | | $this->addWarning("{$path}.schema_type", "'{$seo['schema_type']}' may not be valid. Common types: " . implode(', ', array_slice($validTypes, 0, 5))); |
| | | } |
| | | } |
| | | |
| | | if (isset($seo['field_map'])) { |
| | | foreach ($seo['field_map'] as $prop => $source) { |
| | | $this->validateFieldSource($source, $availableFields, "{$path}.field_map.{$prop}"); |
| | | } |
| | | } |
| | | |
| | | if (isset($seo['meta']['title'])) { |
| | | $this->validatePatternString($seo['meta']['title'], $availableFields, "{$path}.meta.title"); |
| | | } |
| | | |
| | | if (isset($seo['meta']['description'])) { |
| | | $this->validatePatternString($seo['meta']['description'], $availableFields, "{$path}.meta.description"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Validate a field source reference |
| | | */ |
| | | private function validateFieldSource(string $source, array $availableFields, string $path): void |
| | | { |
| | | if (empty($source)) { |
| | | return; |
| | | } |
| | | |
| | | if (str_contains($source, '{{')) { |
| | | $this->validatePatternString($source, $availableFields, $path); |
| | | return; |
| | | } |
| | | |
| | | $field = explode('|', $source)[0]; |
| | | $field = explode('.', $field)[0]; |
| | | |
| | | if (!in_array($field, $availableFields) && !in_array($field, ['site', 'author', 'meta', 'terms'])) { |
| | | $this->addWarning($path, "Field '{$field}' may not exist"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Validate pattern string syntax |
| | | */ |
| | | private function validatePatternString(string $pattern, array $availableFields, string $path): void |
| | | { |
| | | preg_match_all('/\{\{([^}]+)\}\}/', $pattern, $matches); |
| | | |
| | | foreach ($matches[1] as $token) { |
| | | $token = trim($token); |
| | | |
| | | if (empty($token)) { |
| | | $this->addError($path, "Empty placeholder {{}} found"); |
| | | continue; |
| | | } |
| | | |
| | | $parts = explode('|', $token); |
| | | $field = trim(explode('.', $parts[0])[0]); |
| | | |
| | | if (!in_array($field, $availableFields) && !in_array($field, ['site', 'author', 'meta', 'terms'])) { |
| | | $this->addWarning($path, "Field '{$field}' in pattern may not exist"); |
| | | } |
| | | |
| | | if (isset($parts[1])) { |
| | | $modifier = trim(explode(':', $parts[1])[0]); |
| | | if (!in_array($modifier, $this->validModifiers)) { |
| | | $this->addWarning($path, "Unknown modifier '|{$modifier}'"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Validate JVB_SCHEMA configuration |
| | | */ |
| | | public function validateSchemaConfig(array $schema): bool |
| | | { |
| | | $this->errors = []; |
| | | $this->warnings = []; |
| | | |
| | | if (isset($schema['business'])) { |
| | | $this->validateBusinessSchema($schema['business']); |
| | | } |
| | | |
| | | if (isset($schema['faqs']['items'])) { |
| | | foreach ($schema['faqs']['items'] as $i => $faq) { |
| | | if (empty($faq['question'])) { |
| | | $this->addError("schema.faqs.items[{$i}].question", "FAQ question required"); |
| | | } |
| | | if (empty($faq['answer'])) { |
| | | $this->addError("schema.faqs.items[{$i}].answer", "FAQ answer required"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $this->logResults(); |
| | | return empty($this->errors); |
| | | } |
| | | |
| | | /** |
| | | * Validate business schema |
| | | */ |
| | | private function validateBusinessSchema(array $config): void |
| | | { |
| | | $path = 'schema.business'; |
| | | |
| | | if (empty($config['name'])) { |
| | | $this->addError("{$path}.name", "Business name required"); |
| | | } |
| | | |
| | | if (isset($config['url']) && !filter_var($config['url'], FILTER_VALIDATE_URL)) { |
| | | $this->addError("{$path}.url", "Invalid URL"); |
| | | } |
| | | |
| | | if (isset($config['email']) && !filter_var($config['email'], FILTER_VALIDATE_EMAIL)) { |
| | | $this->addError("{$path}.email", "Invalid email"); |
| | | } |
| | | |
| | | if (isset($config['geo'])) { |
| | | $lat = $config['geo']['lat'] ?? null; |
| | | $lng = $config['geo']['lng'] ?? null; |
| | | |
| | | if ($lat !== null && (!is_numeric($lat) || $lat < -90 || $lat > 90)) { |
| | | $this->addError("{$path}.geo.lat", "Latitude must be -90 to 90"); |
| | | } |
| | | if ($lng !== null && (!is_numeric($lng) || $lng < -180 || $lng > 180)) { |
| | | $this->addError("{$path}.geo.lng", "Longitude must be -180 to 180"); |
| | | } |
| | | } |
| | | |
| | | if (isset($config['opening_hours'])) { |
| | | $days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; |
| | | foreach ($config['opening_hours'] as $day => $data) { |
| | | if (!in_array(strtolower($day), $days)) { |
| | | $this->addWarning("{$path}.opening_hours.{$day}", "Invalid day"); |
| | | } |
| | | if (is_array($data) && empty($data['closed'])) { |
| | | if (isset($data['open']) && !preg_match('/^\d{2}:\d{2}$/', $data['open'])) { |
| | | $this->addWarning("{$path}.opening_hours.{$day}.open", "Use HH:MM format"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (isset($config['aggregate_rating'])) { |
| | | $value = $config['aggregate_rating']['value'] ?? null; |
| | | if ($value !== null && (!is_numeric($value) || $value < 0 || $value > 5)) { |
| | | $this->addError("{$path}.aggregate_rating.value", "Rating must be 0-5"); |
| | | } |
| | | } |
| | | |
| | | if (isset($config['same_as'])) { |
| | | foreach ($config['same_as'] as $i => $link) { |
| | | $url = is_array($link) ? ($link['url'] ?? '') : $link; |
| | | if (!empty($url) && !filter_var($url, FILTER_VALIDATE_URL)) { |
| | | $this->addError("{$path}.same_as[{$i}]", "Invalid URL: {$url}"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get available fields for SEO validation |
| | | */ |
| | | private function getAvailableSEOFields(string $slug, string $objectType, array $config): array |
| | | { |
| | | $fields = match($objectType) { |
| | | 'content' => ['post_title', 'post_excerpt', 'post_content', 'post_date', 'post_modified', 'post_thumbnail', 'permalink'], |
| | | 'taxonomy' => ['term_name', 'term_description', 'term_slug', 'permalink', 'count'], |
| | | 'user' => ['display_name', 'first_name', 'last_name', 'user_email', 'description', 'permalink'], |
| | | default => [] |
| | | }; |
| | | |
| | | if (!empty($config['fields'])) { |
| | | $fields = array_merge($fields, array_keys($config['fields'])); |
| | | } |
| | | |
| | | return $fields; |
| | | } |
| | | } |