config = $config; $this->type = $type; $this->slug = $slug; } /** * Create from a specific content type */ public static function forContent(string $slug): self { $slug = jvbNoBase($slug); if (!isset(JVB_CONTENT[$slug])) { return new self([], 'content', $slug); } return new self(JVB_CONTENT[$slug], 'content', $slug); } public static function hasIntegration(string $integration, string $type = 'site', ?string $subType = null):bool { $allowedTypes = ['site', 'content', 'taxonomy', 'user']; if (!in_array($type, $allowedTypes)) { return false; } if (in_array($type, ['content', 'taxonomy', 'user']) && !$subType) { return false; } switch ($type) { case 'site': $feature = (array_key_exists('integrations', JVB_SITE)) ? new self(JVB_SITE['integrations'], 'integrations', 'site-integrations') : new self([], 'integrations', 'site-integrations'); break; case 'content': $feature = (!isset(JVB_CONTENT[$subType])|| !array_key_exists('integrations', JVB_CONTENT[$subType])) ? new self([], 'integrations', 'content-integrations') : new self(JVB_CONTENT[$subType]['integrations'], 'integrations', 'content-integrations'); break; case 'taxonomy': $feature = (!isset(JVB_TAXONOMY[$subType])|| !array_key_exists('integrations', JVB_TAXONOMY[$subType])) ? new self([], 'integrations', 'taxonomy-integrations') : new self(JVB_TAXONOMY[$subType]['integrations'], 'integrations', 'taxonomy-integrations'); break; case 'user': $feature = (!isset(JVB_USER[$subType])|| !array_key_exists('integrations', JVB_USER[$subType])) ? new self([], 'integrations', 'user-integrations') : new self(JVB_USER[$subType]['integrations'], 'integrations', 'user-integrations'); break; default: return false; } return $feature->has($integration); } public static function hasAnyIntegration(string $type = 'site', ?string $subType = null):bool { $allowedTypes = ['site', 'content', 'taxonomy', 'user']; if (!in_array($type, $allowedTypes)) { return false; } if (in_array($type, ['content', 'taxonomy', 'user']) && !$subType) { return false; } switch ($type) { case 'site': return (array_key_exists('integrations', JVB_SITE) && !empty(JVB_SITE['integrations'])); case 'content': return (array_key_exists($subType, JVB_CONTENT) && array_key_exists('integrations', JVB_CONTENT[$subType]) && !empty(JVB_CONTENT[$subType]['integrations'])); case 'taxonomy': return (array_key_exists($subType, JVB_TAXONOMY) && array_key_exists('integrations', JVB_TAXONOMY[$subType]) && !empty(JVB_TAXONOMY[$subType]['integrations'])); case 'user': return (array_key_exists($subType, JVB_USER) && array_key_exists('integrations', JVB_USER[$subType]) && !empty(JVB_USER[$subType]['integrations'])); default: return false; } } /** * Create from a specific taxonomy */ public static function forTaxonomy(string $slug): self { $slug = jvbNoBase($slug); if (!isset(JVB_TAXONOMY[$slug])) { return new self([], 'taxonomy', $slug); } return new self(JVB_TAXONOMY[$slug], 'taxonomy', $slug); } /** * Create from a specific user role */ public static function forUser(string $slug): self { $slug = jvbNoBase($slug); if (!isset(JVB_USER[$slug])) { return new self([], 'user', $slug); } return new self(JVB_USER[$slug], 'user', $slug); } /** * Create for site-wide features */ public static function forSite(): self { return new self(JVB_SITE ?? [], 'site', 'site'); } /** * Create for membership features */ public static function forMembership(): self { return new self(JVB_MEMBERSHIP ?? [], 'membership', 'membership'); } /** * Check if a single feature is enabled */ public function has(string $feature): bool { // Check cache first $cacheKey = $this->getCacheKey($feature); if (isset($this->cache[$cacheKey])) { return $this->cache[$cacheKey]; } // Perform the check $result = isset($this->config[$feature]) && $this->config[$feature] === true; // Cache the result $this->cache[$cacheKey] = $result; return $result; } /** * Check if ALL specified features are enabled */ public function hasAll(array $features): bool { foreach ($features as $feature) { if (!$this->has($feature)) { return false; } } return true; } /** * Check if ANY of the specified features are enabled */ public function hasAny(array $features): bool { foreach ($features as $feature) { if ($this->has($feature)) { return true; } } return false; } /** * Check if NONE of the specified features are enabled */ public function hasNone(array $features): bool { return !$this->hasAny($features); } /** * Get all enabled features */ public function getEnabled(): array { $enabled = []; // Determine which features to check based on type $featuresToCheck = $this->getFeatureList(); foreach ($featuresToCheck as $feature) { if ($this->has($feature)) { $enabled[] = $feature; } } return $enabled; } /** * Get all disabled features */ public function getDisabled(): array { $disabled = []; $featuresToCheck = $this->getFeatureList(); foreach ($featuresToCheck as $feature) { if (!$this->has($feature)) { $disabled[] = $feature; } } return $disabled; } /** * Check feature with a default value if not set */ public function get(string $feature, bool $default = false): bool { if (!isset($this->config[$feature])) { return $default; } return $this->has($feature); } /** * Check if a feature is explicitly set (regardless of value) */ public function isSet(string $feature): bool { return isset($this->config[$feature]); } /** * Get the raw value of a feature (not just boolean) */ public function getValue(string $feature, $default = null) { return $this->config[$feature] ?? $default; } /** * Check features with dependencies */ public function hasWithDependencies(string $feature): bool { if (!$this->has($feature)) { return false; } // Check dependencies based on feature $dependencies = $this->getFeatureDependencies($feature); return $this->hasAll($dependencies); } /** * Global feature checks across all types */ public static function anyContentHas(string $feature): bool { $cacheKey = "content_any_{$feature}"; if (isset(self::$globalCache[$cacheKey])) { return self::$globalCache[$cacheKey]; } foreach (JVB_CONTENT as $slug => $config) { $flags = new self($config, 'content', $slug); if ($flags->has($feature)) { self::$globalCache[$cacheKey] = true; return true; } } self::$globalCache[$cacheKey] = false; return false; } /** * Check if any taxonomy has a feature */ public static function anyTaxonomyHas(string $feature): bool { $cacheKey = "taxonomy_any_{$feature}"; if (isset(self::$globalCache[$cacheKey])) { return self::$globalCache[$cacheKey]; } foreach (JVB_TAXONOMY as $slug => $config) { $flags = new self($config, 'taxonomy', $slug); if ($flags->has($feature)) { self::$globalCache[$cacheKey] = true; return true; } } self::$globalCache[$cacheKey] = false; return false; } /** * Check if any user role has a feature */ public static function anyUserHas(string $feature): bool { $cacheKey = "user_any_{$feature}"; if (isset(self::$globalCache[$cacheKey])) { return self::$globalCache[$cacheKey]; } foreach (JVB_USER as $slug => $config) { $flags = new self($config, 'user', $slug); if ($flags->has($feature)) { self::$globalCache[$cacheKey] = true; return true; } } self::$globalCache[$cacheKey] = false; return false; } /** * Get all types with a specific feature */ public static function getTypesWithFeature(string $feature, string $type = 'content'): array { $types = []; $source = match ($type) { 'content' => JVB_CONTENT, 'taxonomy' => JVB_TAXONOMY, 'user' => JVB_USER, default => [] }; foreach ($source as $slug => $config) { $flags = new self($config, $type, $slug); if ($flags->has($feature)) { $types[] = $slug; } } return $types; } /** * Count types with a feature */ public static function countWithFeature(string $feature, string $type = 'content'): int { return count(self::getTypesWithFeature($feature, $type)); } /** * Check complex feature combinations */ public function meetsRequirements(array $requirements): bool { foreach ($requirements as $requirement => $expected) { if (is_array($expected)) { // Handle OR conditions if (!$this->hasAny($expected)) { return false; } } elseif (is_bool($expected)) { // Handle boolean requirements if ($this->has($requirement) !== $expected) { return false; } } elseif (is_callable($expected)) { // Handle custom validation if (!$expected($this->getValue($requirement))) { return false; } } } return true; } /** * Get feature statistics */ public function getStats(): array { $all = $this->getFeatureList(); $enabled = $this->getEnabled(); return [ 'total' => count($all), 'enabled' => count($enabled), 'disabled' => count($all) - count($enabled), 'percentage' => count($all) > 0 ? round((count($enabled) / count($all)) * 100, 2) : 0 ]; } /** * Export configuration for debugging */ public function export(): array { return [ 'type' => $this->type, 'slug' => $this->slug, 'features' => $this->getEnabled(), 'config' => $this->config ]; } /** * Check if configuration supports a specific workflow */ public function supportsWorkflow(string $workflow): bool { $workflows = [ 'moderation' => ['approve_new', 'member_verified'], 'social' => ['karma', 'responses', 'favouritable'], 'calendar' => ['is_calendar'], 'directory' => ['show_directory'], 'ownership' => ['is_ownable', 'is_owned_by'], 'dashboard' => ['has_dashboard'], 'public_profile' => ['has_dashboard', 'profile'], 'content_creation' => ['can_create'], 'invitation' => ['invitable', 'can_invite'], 'feed' => ['show_feed', 'use_feed_block'], 'ecommerce' => ['square', 'syncWithSquare'], 'analytics' => ['keep_stats', 'umami'], 'forum' => ['forum', 'responses'] ]; if (!isset($workflows[$workflow])) { return false; } return $this->hasAny($workflows[$workflow]); } /** * Get feature dependencies */ private function getFeatureDependencies(string $feature): array { $dependencies = [ 'responses' => ['forum'], 'karma' => ['member_verified'], 'invitable' => ['can_invite'], 'keep_stats' => ['has_dashboard'], 'syncWithSquare' => ['square'] ]; return $dependencies[$feature] ?? []; } /** * Get list of features based on type */ private function getFeatureList(): array { return match ($this->type) { 'content' => self::CONTENT_FEATURES, 'taxonomy' => self::TAXONOMY_FEATURES, 'user' => self::USER_FEATURES, 'site' => self::SITE_FEATURES, 'membership' => array_keys($this->config), default => array_keys($this->config) }; } /***************************************************************** * Dashboard Utilitiies *****************************************************************/ /** * Get content types that a user role can create * Extracts and flattens from 'can_create' config * * @return array Array of content type slugs * * Usage: * Features::forUser('artist')->getCreatableContent() * // Returns: ['tattoo', 'piercing', 'artwork'] */ public function getCreatableContent(): array { if ($this->type !== 'user') { return []; } $canCreate = $this->getValue('can_create', []); if (empty($canCreate)) { return []; } $content = []; foreach ($canCreate as $item) { if (is_array($item)) { // Handle nested arrays like ['shop' => ['tattoo', 'piercing']] foreach ($item as $type => $contents) { $content = array_merge($content, $contents); } } else { // Handle simple strings $content[] = $item; } } return array_unique($content); } /** * Get all dashboard pages for a user role * Includes profile, creatable content, and settings * * @return array Array of page slugs * * Usage: * Features::forUser('artist')->getDashboardPages() * // Returns: ['artist-profile', 'tattoo', 'piercing', 'settings'] */ public function getDashboardPages(): array { if ($this->type !== 'user') { return []; } $pages = []; // Add profile page if configured $profile = $this->getValue('profile'); if ($profile) { $pages[] = $profile; } // Add creatable content types $pages = array_merge($pages, $this->getCreatableContent()); // Add settings if user has dashboard if ($this->has('has_dashboard')) { $pages[] = 'settings'; } return array_unique($pages); } /** * Check if user role can create a specific content type * * @param string $contentType * @return bool * * Usage: * Features::forUser('artist')->canCreate('tattoo') // true/false */ public function canCreate(string $contentType): bool { return in_array($contentType, $this->getCreatableContent()); } /** * Get the profile type for a user role * * @return string|null Profile slug or null if none * * Usage: * Features::forUser('artist')->getProfile() // 'artist-profile' */ public function getProfile(): ?string { if ($this->type !== 'user') { return null; } return $this->getValue('profile'); } /** * Check if user role has a profile page * * @return bool * * Usage: * Features::forUser('artist')->hasProfile() // true/false */ public function hasProfile(): bool { return $this->getProfile() !== null; } /** * Get content types grouped by parent type (if nested) * * @return array Associative array with parent types as keys * * Usage: * Features::forUser('artist')->getGroupedContent() * // Returns: ['shop' => ['tattoo', 'piercing'], 'standalone' => ['artwork']] */ public function getGroupedContent(): array { if ($this->type !== 'user') { return []; } $canCreate = $this->getValue('can_create', []); if (empty($canCreate)) { return []; } $grouped = []; foreach ($canCreate as $item) { if (is_array($item)) { // Handle nested arrays like ['shop' => ['tattoo', 'piercing']] foreach ($item as $parent => $contents) { if (!isset($grouped[$parent])) { $grouped[$parent] = []; } $grouped[$parent] = array_merge($grouped[$parent], $contents); } } else { // Handle simple strings - add to 'standalone' if (!isset($grouped['standalone'])) { $grouped['standalone'] = []; } $grouped['standalone'][] = $item; } } return $grouped; } /** * Static method to get all content types across all user roles * * @return array Array of unique content type slugs * * Usage: * Features::getAllUserContent() * // Returns: ['tattoo', 'piercing', 'artwork', 'event', ...] */ public static function getAllUserContent(): array { $allContent = []; foreach (JVB_USER as $slug => $config) { $features = self::forUser($slug); $allContent = array_merge($allContent, $features->getCreatableContent()); } return array_unique($allContent); } /** * Static method to get all user roles that can create specific content * * @param string $contentType * @return array Array of role slugs * * Usage: * Features::getRolesForContent('tattoo') * // Returns: ['artist', 'shop'] */ public static function getRolesForContent(string $contentType): array { $roles = []; foreach (JVB_USER as $slug => $config) { $features = self::forUser($slug); if ($features->canCreate($contentType)) { $roles[] = $slug; } } return $roles; } /** * Get all dashboard pages across all user roles * * @return array Array of unique page slugs * * Usage: * Features::getAllDashboardPages() * // Returns: ['artist-profile', 'shop-profile', 'tattoo', 'piercing', ...] */ public static function getAllDashboardPages(): array { $allPages = []; foreach (JVB_USER as $slug => $config) { $features = self::forUser($slug); $allPages = array_merge($allPages, $features->getDashboardPages()); } return array_unique($allPages); } public static function getType(string $slug):?string { if (array_key_exists($slug, JVB_CONTENT)) { return 'content'; } if (array_key_exists($slug, JVB_USER)) { return 'user'; } if (array_key_exists($slug, JVB_TAXONOMY)) { return 'taxonomy'; } if (array_key_exists($slug, JVB_OPTIONS)) { return 'option'; } return null; } public static function getConfig(string $slug, ?string $type = null): array { $all = ['post', 'content', 'taxonomy', 'user']; $types = (!$type || !in_array($type, $all)) ? $all : [$type]; foreach($types as $type) { switch($type) { case 'post': case 'content': if (array_key_exists($slug, JVB_CONTENT)) { return JVB_CONTENT[$slug]; } break; case 'taxonomy': if (array_key_exists($slug, JVB_TAXONOMY)) { return JVB_TAXONOMY[$slug]; } break; case 'user': if (array_key_exists($slug, JVB_USER)) { return JVB_USER[$slug]; } break; default: return []; } } error_log('No config found for: '.$slug); return []; } /** * Generate cache key */ private function getCacheKey(string $feature): string { return "{$this->type}_{$this->slug}_{$feature}"; } /** * Clear cache */ public function clearCache(): void { $this->cache = []; } /** * Clear global cache */ public static function clearGlobalCache(): void { self::$globalCache = []; } /** * Magic method for property-style access */ public function __get(string $name): bool { return $this->has($name); } /** * Magic method for method-style checks */ public function __call(string $name, array $arguments): bool { // Support is*, has*, can* method calls if (preg_match('/^(is|has|can)(.+)$/', $name, $matches)) { $feature = lcfirst($matches[2]); // Convert camelCase to snake_case $feature = strtolower(preg_replace('/([A-Z])/', '_$1', $feature)); return $this->has($feature); } throw new \BadMethodCallException("Method {$name} does not exist"); } }