<?php
|
namespace JVBase\utility;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
/**
|
* FeatureFlags - Advanced feature detection and management
|
*
|
* Replaces repetitive jvbCheck() calls with a more elegant API
|
* Supports caching, bulk checks, and feature dependencies
|
*/
|
class Features
|
{
|
private array $config;
|
private string $type;
|
private string $slug;
|
private array $cache = [];
|
private static array $globalCache = [];
|
|
// Common feature groups for quick reference
|
const CONTENT_FEATURES = [
|
'hide_single', 'show_feed', 'show_directory', 'karma',
|
'favouritable', 'responses', 'is_calendar', 'single_image',
|
'redirectToAuthor', 'syncWithSquare', 'approve_new'
|
];
|
|
const TAXONOMY_FEATURES = [
|
'show_feed', 'show_directory', 'is_content', 'is_ownable',
|
'karma', 'verify_entry', 'approve_new', 'track_changes',
|
'invitable', 'associate_user_content', 'is_favouritable'
|
];
|
|
const USER_FEATURES = [
|
'has_dashboard', 'can_register', 'invitable', 'approve_new',
|
'keep_stats', 'can_favourite', 'member_verified'
|
];
|
|
const SITE_FEATURES = [
|
'dashboard', 'favourites', 'enthusiast', 'forum', 'notifications',
|
'has_support', 'has_membership', 'use_feed_block', 'square',
|
'gmb', 'umami', 'cloudflare', 'facebook', 'instagram', 'bluesky',
|
'social', 'limit_hours'
|
];
|
|
/**
|
* Constructor for type-specific feature flags
|
*/
|
public function __construct(array $config = [], string $type = '', string $slug = '')
|
{
|
$this->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)
|
};
|
}
|
|
/**
|
* 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");
|
}
|
}
|