type = $type; $this->schemaKey = BASE.'schema_for_'.$type; $this->metaKey = BASE.'meta_for_'.$type; $this->registry = SchemaBuilder::getInstance(); $this->schema = $this->getConfigFor($type); $this->meta = $this->getMetaFor($type); } /** * Factory method - returns singleton instance per type */ public static function for(string $type): self { $key = jvbNoBase($type); if (!isset(self::$instances[$key])) { self::$instances[$key] = new self($type); } return self::$instances[$key]; } public function meta():array { return $this->meta ?? []; } public function schema():array { return $this->schema ?? []; } public function archive(): array { return $this->archive ?? []; } public function setupArchive() { $this->hasArchive = true; $this->archiveKey = BASE.'archive_for_'.$this->type; $this->archive = $this->getArchiveFor($this->type); } /** * Get default meta configuration for a type */ protected function getMetaFor(string $type): array { $default = $this->registry->getDefaultMetaValues(); return get_option($this->metaKey, $default); } /** * Get default schema configuration for a type */ protected function getConfigFor(string $type): array { $default = $this->getDefaultConfig($type, 'schema'); return get_option($this->schemaKey, $default); } /** * Get default schema configuration for a type */ protected function getArchiveFor(string $type): array { $default = $this->getDefaultConfig($type, 'archive'); return get_option($this->archiveKey, $default); } /** * Get default configuration from constants */ private function getDefaultConfig(string $type, string $configType): array { switch ($type) { case 'website': // Try actual schema type first, then semantic key if (defined('JVB_SCHEMA')) { if (array_key_exists('website', JVB_SCHEMA)) { return JVB_SCHEMA['website']; } } return []; case 'organization': // Try actual schema types first, then semantic keys if (defined('JVB_SCHEMA')) { if (array_key_exists('organization', JVB_SCHEMA)) { return JVB_SCHEMA['organization']; } } return []; default: // Try to find in content, taxonomy, or user configs $config = $this->findInConstants($type); if (array_key_exists('seo', $config) && is_array($config['seo'])) { $config = $config['seo']; } // If asking for archive config and none exists, provide default if ($configType === 'archive' && !isset($config['archive'])) { return [ 'type' => 'CollectionPage', 'name' => '{{archive_title}}', 'description' => '{{archive_description}}', 'url' => '{{archive_url}}' ]; } return $config[$configType] ?? []; } } /** * Find configuration in JVB constants */ private function findInConstants(string $type): array { if (defined('JVB_CONTENT') && isset(JVB_CONTENT[$type])) { return JVB_CONTENT[$type]; } if (defined('JVB_TAXONOMY') && isset(JVB_TAXONOMY[$type])) { return JVB_TAXONOMY[$type]; } if (defined('JVB_USER') && isset(JVB_USER[$type])) { return JVB_USER[$type]; } return []; } public function resetConfig(): bool { $result = delete_option($this->schemaKey); if ($result) { $this->schema = $this->getConfigFor($this->type); } return $result; } /** * Reset meta configuration to defaults */ public function resetMeta(): bool { $result = delete_option($this->metaKey); if ($result) { $this->meta = $this->getMetaFor($this->type); } return $result; } public function resetArchive():bool { $result = delete_option($this->archiveKey); if ($result) { $this->archive = $this->getArchiveFor($this->type); } return $result; } /** * Reset both configurations to defaults */ public function resetAll(): bool { return !($this->resetConfig() && $this->resetMeta() && ($this->hasArchive)) || $this->resetArchive(); } /** * Validate and update schema configuration * * @param array $config Schema configuration to save * @return bool|\WP_Error True on success, WP_Error on failure */ public function updateConfig(array $config): bool|\WP_Error { // Validate type is provided if (!isset($config['type'])) { return new \WP_Error('missing_type', 'Schema type is required'); } // Validate type exists in registry if (!$this->registry->getTypeDefinition($config['type'])) { return new \WP_Error('invalid_type', sprintf('Schema type "%s" is not registered', $config['type'])); } // Get allowed fields for this type $allowedFields = $this->registry->getFieldsForType($config['type']); // Filter to only allowed fields $validated = array_filter($config, function($key) use ($allowedFields) { return in_array($key, $allowedFields); }, ARRAY_FILTER_USE_KEY); // Validate template syntax for field values $fieldErrors = []; foreach ($validated as $field => $value) { if (is_string($value) && $field !== 'type') { $validationResult = $this->validateTemplate($value, $field); if (is_wp_error($validationResult)) { $fieldErrors[$field] = $validationResult->get_error_message(); } } } if (!empty($fieldErrors)) { return new \WP_Error('validation_failed', 'Template validation failed', $fieldErrors); } // Remove completely empty values (but keep false/0) $validated = array_filter($validated, function($value) { return $value !== '' && $value !== null && $value !== []; }); // Update option $result = update_option($this->schemaKey, $validated); if ($result) { // Update instance cache $this->schema = $validated; } return $result; } /** * Validate and update meta configuration * * @param array $meta Meta configuration to save * @return bool|\WP_Error True on success, WP_Error on failure */ public function updateMeta(array $meta): bool|\WP_Error { // Validate template syntax $errors = []; foreach ($meta as $field => $value) { if (is_string($value)) { $validationResult = $this->validateTemplate($value, $field); if (is_wp_error($validationResult)) { $errors[$field] = $validationResult->get_error_message(); } } } if (!empty($errors)) { return new \WP_Error('validation_failed', 'Template validation failed', $errors); } // Update option $result = update_option($this->metaKey, $meta); if ($result) { // Update instance cache $this->meta = $meta; } return $result; } /** * Validate and update archive configuration * * @param array $archive Archive configuration to save * @return bool|\WP_Error True on success, WP_Error on failure */ public function updateArchive(array $archive): bool|\WP_Error { if (!$this->hasArchive) { return new \WP_Error('no_archive', 'This type does not support archives'); } // Validate type is provided if (!isset($archive['type'])) { return new \WP_Error('missing_type', 'Schema type is required'); } // Validate type exists in registry if (!$this->registry->getTypeDefinition($archive['type'])) { return new \WP_Error('invalid_type', sprintf('Schema type "%s" is not registered', $archive['type'])); } // Get allowed fields for this type $allowedFields = $this->registry->getFieldsForType($archive['type']); // Filter to only allowed fields $validated = array_filter($archive, function($key) use ($allowedFields) { return in_array($key, $allowedFields); }, ARRAY_FILTER_USE_KEY); // Validate template syntax $fieldErrors = []; foreach ($validated as $field => $value) { if (is_string($value) && $field !== 'type') { $validationResult = $this->validateTemplate($value, $field); if (is_wp_error($validationResult)) { $fieldErrors[$field] = $validationResult->get_error_message(); } } } if (!empty($fieldErrors)) { return new \WP_Error('validation_failed', 'Template validation failed', $fieldErrors); } // Remove completely empty values $validated = array_filter($validated, function($value) { return $value !== '' && $value !== null && $value !== []; }); // Update option $result = update_option($this->archiveKey, $validated); if ($result) { $this->archive = $validated; } return $result; } /** * Validate template syntax * * @param string $template Template string to validate * @param string $field Field name (for error messages) * @return bool|\WP_Error True if valid, WP_Error if invalid */ private function validateTemplate(string $template, string $field): bool|\WP_Error { // Check for unclosed template tags $openCount = substr_count($template, '{{'); $closeCount = substr_count($template, '}}'); if ($openCount !== $closeCount) { return new \WP_Error( 'malformed_template', sprintf('Unclosed template tag in field "%s"', $field) ); } // Extract all template variables preg_match_all('/\{\{([^}]+)\}\}/', $template, $matches); if (!empty($matches[1])) { foreach ($matches[1] as $variable) { $variable = trim($variable); // Check for empty variables if (empty($variable)) { return new \WP_Error( 'empty_variable', sprintf('Empty template variable in field "%s"', $field) ); } // Check for invalid characters (basic validation) // Allows: field_name, field_name|filter, nested.field if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_.]*(?:\|[a-zA-Z_][a-zA-Z0-9_]*)*$/', $variable)) { return new \WP_Error( 'invalid_variable', sprintf('Invalid template variable "%s" in field "%s"', $variable, $field) ); } } } return true; } }