<?php
|
namespace JVBase\managers\SEO;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Interface for options for schema and meta, defaulting to what is defined in the constants
|
*/
|
class ConfigManager
|
{
|
private ?string $type = null;
|
private ?string $metaKey = null;
|
private ?string $schemaKey = null;
|
private ?string $archiveKey = null;
|
protected bool $hasArchive = false;
|
private static array $instances = [];
|
protected ?array $schema = null;
|
protected ?array $meta = null;
|
protected ?array $archive = null;
|
protected SchemaBuilder $registry;
|
|
/**
|
* Private constructor; use for() factory method instead
|
*/
|
private function __construct(string $type) {
|
$this->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();
|
|
// Check if content/taxonomy config has SEO meta defined
|
$configMeta = $this->getDefaultConfig($type, 'meta');
|
if (!empty($configMeta)) {
|
$configMeta = $this->normalizeMetaKeys($configMeta);
|
$default = array_merge($default, $configMeta);
|
}
|
|
return get_option($this->metaKey, $default);
|
}
|
|
/**
|
* Normalize content-defined meta keys to system keys
|
*/
|
private function normalizeMetaKeys(array $meta): array
|
{
|
$map = [
|
'title' => 'metaTitle',
|
'description' => 'metaDescription',
|
];
|
|
$normalized = [];
|
foreach ($meta as $key => $value) {
|
$normalized[$map[$key] ?? $key] = $value;
|
}
|
return $normalized;
|
}
|
|
/**
|
* 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;
|
}
|
}
|