Prepare Beta Version

This commit is contained in:
2025-05-29 10:05:27 -06:00
parent a7002701f5
commit ea6b04f3f4
254 changed files with 5653 additions and 6569 deletions

View File

@ -9,8 +9,8 @@ use Illuminate\Support\Facades\Log;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
use Koneko\VuexyApisAndIntegrations\Models\ExternalApi;
use Illuminate\Support\Collection;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoComponentContextRegistrar;
class ApiModuleRegistry
{

View File

@ -5,7 +5,11 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Bootstrap;
use Illuminate\Support\Str;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
/**
* Clase representativa de un módulo del sistema Vuexy Admin
*/
class KonekoModule
{
// === Identidad del Módulo ===
@ -23,10 +27,10 @@ class KonekoModule
public string $minimumStability;
public string $buildVersion;
// === Namespace de configuraciones ===
// === Namespace del componente para el sistema de configuración ===
public string $componentNamespace;
// === Datos de composer/autoload ===
// === Composer & Autoload ===
public string $composerName;
public string $namespace;
public ?string $provider;
@ -34,126 +38,50 @@ class KonekoModule
public string $composerPath;
public array $dependencies;
// === Metadatos visuales para UI del gestor ===
// === Metadatos visuales UI ===
public array $ui;
// === Archivos de configuraciones ===
// === Definiciones técnicas del módulo ===
public array $configs;
// === Providers, Middleware y Aliases (runtime) ===
public array $providers;
public array $middleware;
public array $aliases;
// === Singleton *Nuevo Prototipo ===
public array $singletons;
// === Bindings de interfaces a servicios ===
public array $bindings;
// === Macro ===
public array $macros;
// === Observadores y Auditable ===
public array $observers;
public array $listeners;
public array $auditable;
// === Migraciones ===
public array $migrations;
// === Rutas ===
public array $routes;
// === Vistas y traducciones ===
public array $views;
public array $translations;
// === Livewire, Blade, Vistas ===
public array $bladeComponents;
public array $livewire;
// === Archivos publicables ===
public array $publishedFiles;
// === Comandos Artisan ===
public array $commands;
public array $schedules;
// === Roles y permisos ===
public array $rbac;
// === APIs ===
public array $apis;
// === Catálogos ===
public array $catalogs;
// === Extensiones del ecosistema ===
public array $extensions;
public array $scopeModels;
public array $configBlocks;
public function __construct(array $overrides = [])
{
$this->vendor = $overrides['vendor'] ?? null;
$this->name = $overrides['name'] ?? '';
$this->slug = $overrides['slug'] ?? '';
$this->description = $overrides['description'] ?? '';
$this->type = $overrides['type'] ?? 'plugin';
$this->tags = $overrides['tags'] ?? [];
$this->version = $overrides['version'] ?? '1.0.0';
$this->keywords = $overrides['keywords'] ?? [];
$this->authors = $overrides['authors'] ?? [];
$this->support = $overrides['support'] ?? [];
$this->license = $overrides['license'] ?? null;
$this->minimumStability= $overrides['minimumStability']?? 'stable';
$this->buildVersion = $overrides['buildVersion'] ?? now()->format('YmdHis');
foreach ((new \ReflectionClass($this))->getProperties() as $property) {
$name = $property->getName();
$this->$name = $overrides[$name] ?? $this->$name ?? ($property->hasType() && $property->getType()->getName() === 'array' ? [] : null);
}
$this->componentNamespace = $overrides['componentNamespace'] ?? '';
$this->composerName = $overrides['composerName'] ?? '';
$this->namespace = $overrides['namespace'] ?? '';
$this->provider = $overrides['provider'] ?? null;
$this->basePath = $overrides['basePath'] ?? '';
$this->composerPath = $overrides['composerPath'] ?? '';
$this->dependencies = $overrides['dependencies'] ?? [];
$this->ui = $overrides['ui'] ?? [];
$this->configs = $overrides['configs'] ?? [];
$this->providers = $overrides['providers'] ?? [];
$this->middleware = $overrides['middleware'] ?? [];
$this->aliases = $overrides['aliases'] ?? [];
$this->singletons = $overrides['singletons'] ?? [];
$this->bindings = $overrides['bindings'] ?? [];
$this->macros = $overrides['macros'] ?? [];
$this->observers = $overrides['observers'] ?? [];
$this->listeners = $overrides['listeners'] ?? [];
$this->auditable = $overrides['auditable'] ?? [];
$this->migrations = $overrides['migrations'] ?? [];
$this->routes = $overrides['routes'] ?? [];
$this->views = $overrides['views'] ?? [];
$this->translations = $overrides['translations'] ?? [];
$this->bladeComponents = $overrides['bladeComponents'] ?? [];
$this->livewire = $overrides['livewire'] ?? [];
$this->publishedFiles = $overrides['publishedFiles'] ?? [];
$this->commands = $overrides['commands'] ?? [];
$this->schedules = $overrides['schedules'] ?? [];
$this->rbac = $overrides['rbac'] ?? [];
$this->apis = $overrides['apis'] ?? [];
$this->catalogs = $overrides['catalogs'] ?? [];
$this->extensions = $overrides['extensions'] ?? [];
// Defaults no cubiertos
$this->type ??= 'plugin';
$this->version ??= '1.0.0';
$this->minimumStability??= 'stable';
$this->buildVersion ??= now()->format('YmdHis');
$this->description ??= '';
$this->componentNamespace ??= $overrides['componentNamespace'] ?? '';
}
public function getId(): string
@ -208,9 +136,9 @@ class KonekoModule
'version' => $this->version,
'buildVersion' => $this->buildVersion,
'type' => $this->type,
'vendor' => $this->vendor ?? null,
'vendor' => $this->vendor,
'license' => $this->license ?? 'proprietary',
'minimumStability' => $this->minimumStability ?? 'stable',
'minimumStability' => $this->minimumStability,
'tags' => $this->tags,
'authors' => $this->authors,
'support' => $this->support,
@ -221,92 +149,42 @@ class KonekoModule
];
}
private static function buildAttributesFromComposer(array $composer, string $basePath, array $custom = []): array
public static function fromComposerJson(array $composer, string $basePath, array $custom = []): self
{
$nameParts = explode('/', $composer['name'] ?? 'unknown/module');
$vendor = $nameParts[0] ?? 'unknown';
$slug = $nameParts[1] ?? 'module';
return [
'vendor' => $vendor,
'slug' => $slug,
'name' => $custom['name'] ?? 'unknown/module',
'description' => $custom['description'] ?? ($composer['description'] ?? ''),
'type' => $custom['type'] ?? ($composer['type'] ?? 'plugin'),
'tags' => $custom['tags'] ?? ($composer['keywords'] ?? []),
'composerName' => $composer['name'] ?? 'unknown/module',
'version' => $composer['version'] ?? '1.0.0',
'keywords' => $composer['keywords'] ?? [],
'authors' => $composer['authors'] ?? [],
'support' => $composer['support'] ?? [],
'license' => $composer['license'] ?? 'proprietary',
'minimumStability' => $composer['minimum-stability'] ?? 'stable',
'buildVersion' => now()->format('YmdHis'), // Versión de compilación en timestamp
'namespace' => array_key_first($composer['autoload']['psr-4'] ?? []) ?? '',
'provider' => $composer['extra']['laravel']['providers'][0] ?? null,
'basePath' => rtrim($basePath, '/'),
'composerPath' => rtrim($basePath, '/') . '/composer.json',
'dependencies' => array_keys($composer['require'] ?? []),
'ui' => $custom['ui'] ?? [],
'configs' => $custom['configs'] ?? [],
'componentNamespace' => $custom['componentNamespace'] ?? '',
'providers' => $custom['providers'] ?? [],
'middleware' => $custom['middleware'] ?? [],
'aliases' => $custom['aliases'] ?? [],
'singletons' => $custom['singletons'] ?? [],
'bindings' => $custom['bindings'] ?? [],
'macros' => $custom['macros'] ?? [],
'observers' => $custom['observers'] ?? [],
'listeners' => $custom['listeners'] ?? [],
'auditable' => $custom['auditable'] ?? [],
'migrations' => $custom['migrations'] ?? [],
'routes' => $custom['routes'] ?? [],
'views' => $custom['views'] ?? [],
'translations' => $custom['translations'] ?? [],
'bladeComponents' => $custom['bladeComponents'] ?? [],
'livewire' => $custom['livewire'] ?? [],
'publishedFiles' => $custom['publishedFiles'] ?? [],
'commands' => $custom['commands'] ?? [],
'schedules' => $custom['schedules'] ?? [],
'rbac' => $custom['rbac'] ?? [],
'apis' => $custom['apis'] ?? [],
'catalogs' => $custom['catalogs'] ?? [],
'extensions' => $custom['extensions'] ?? [],
];
return new self(
static::buildAttributesFromComposer($composer, $basePath, $custom)
);
}
public static function fromModuleDirectory(string $dirPath): ?KonekoModule
public static function fromModuleFile(string $file): ?self
{
$file = null;
if (file_exists($dirPath . '/vuexy-admin.module.php')){
$file = $dirPath . '/vuexy-admin.module.php';
}
if (file_exists($dirPath . '/koneko-vuexy.module.php')){
$file = $dirPath . '/koneko-vuexy.module.php';
}
if (!file_exists($file)) return null;
$result = require $file;
if ($result instanceof KonekoModule) {
return $result;
}
if ($result instanceof self) return $result;
if (is_array($result)) {
// Inferir basePath desde el archivo
$basePath = dirname($file, 2);
return self::fromModuleDefinition($basePath, $result);
return self::fromModuleDefinition(dirname($file, 2), $result);
}
throw new \RuntimeException("El archivo [$file] no contiene una instancia ni un array válido para definir el módulo.");
}
public static function fromModuleDirectory(string $dirPath): ?self
{
foreach (['vuexy-admin.module.php', 'koneko-vuexy.module.php'] as $fileName) {
$file = $dirPath . '/' . $fileName;
if (file_exists($file)) {
return self::fromModuleFile($file);
}
}
return null;
}
public static function fromModuleDefinition(string $basePath, array $custom = []): self
{
$composerPath = rtrim($basePath, '/') . '/composer.json';
if (!file_exists($composerPath)) {
throw new \RuntimeException("No se encontró composer.json en: {$composerPath}");
}
@ -317,78 +195,48 @@ class KonekoModule
throw new \RuntimeException("composer.json inválido en: {$composerPath}");
}
$attributes = static::buildAttributesFromComposer($composer, realpath($basePath), $custom);
$module = new self($attributes);
return $module;
}
public static function fromComposerJson(array $composer, string $basePath, array $custom = []): self
{
return new self(
static::buildAttributesFromComposer($composer, $basePath, $custom)
static::buildAttributesFromComposer($composer, realpath($basePath), $custom)
);
}
/**
* Registra desde archivo PHP que retorna un KonekoModule.
*/
public static function fromModuleFile(string $file): ?KonekoModule
private static function buildAttributesFromComposer(array $composer, string $basePath, array $custom = []): array
{
if (!file_exists($file)) return null;
$nameParts = explode('/', $composer['name'] ?? 'unknown/module');
$vendor = $nameParts[0] ?? 'unknown';
$slug = $nameParts[1] ?? 'module';
$result = require $file;
if ($result instanceof KonekoModule) {
return $result;
}
if (is_array($result)) {
// Inferir basePath desde el archivo
$basePath = dirname($file, 2);
return self::fromModuleDefinition($basePath, $result);
}
throw new \RuntimeException("El archivo [$file] no contiene una instancia ni un array válido para definir el módulo.");
return array_merge([
'vendor' => $vendor,
'slug' => $slug,
'name' => $custom['name'] ?? $composer['name'] ?? 'unknown/module',
'description' => $composer['description'] ?? '',
'type' => $composer['type'] ?? 'plugin',
'tags' => $composer['keywords'] ?? [],
'composerName' => $composer['name'] ?? 'unknown/module',
'version' => $composer['version'] ?? '1.0.0',
'keywords' => $composer['keywords'] ?? [],
'authors' => $composer['authors'] ?? [],
'support' => $composer['support'] ?? [],
'license' => $composer['license'] ?? 'proprietary',
'minimumStability' => $composer['minimum-stability'] ?? 'stable',
'buildVersion' => now()->format('YmdHis'),
'namespace' => array_key_first($composer['autoload']['psr-4'] ?? []) ?? '',
'provider' => $composer['extra']['laravel']['providers'][0] ?? null,
'basePath' => rtrim($basePath, '/'),
'composerPath' => rtrim($basePath, '/') . '/composer.json',
'dependencies' => array_keys($composer['require'] ?? []),
], $custom);
}
/**
* Obtiene el slug del módulo actualmente activo, basado en la ruta.
*/
public static function currentSlug(): string
{
$module = static::resolveCurrent();
return $module?->slug ?? 'unknown';
return static::resolveCurrent()?->slug ?? 'unknown';
}
/**
* Obtiene el módulo actual, basado en la ruta.
*
* @return static|null
*/
public static function resolveCurrent(): ?self
{
$modules = KonekoModuleRegistry::enabled();
if (empty($modules)) {
return null;
}
$currentRoute = request()?->route()?->getName();
foreach ($modules as $module) {
@ -397,7 +245,6 @@ class KonekoModule
}
}
// Fallback: el primero que esté activo
return reset($modules) ?: null;
}
}

View File

@ -2,15 +2,19 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Bootstrap;
namespace Koneko\VuexyAdmin\Application\Bootstrap\Manager;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\{App,Blade,Event,Lang,Route,View};
use Illuminate\Support\Facades\{App, Blade, Event, Lang, Route, View};
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Catalog\CatalogModuleRegistry;
use Koneko\VuexyAdmin\Application\Factories\FactoryExtensionRegistry;
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Model\ModelExtensionRegistry;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Application\Config\Registry\ConfigBlockRegistry;
use Koneko\VuexyAdmin\Application\Settings\Registry\ScopeRegistry;
use Livewire\Livewire;
class KonekoModuleBootManager
@ -27,58 +31,56 @@ class KonekoModuleBootManager
public static function boot(KonekoModule $module): void
{
// ⚙️ Archivos de configuración del módulo
// =========================================
// 1⃣ CONFIGURACIONES DEL MÓDULO
// =========================================
foreach ($module->configs ?? [] as $namespace => $relativePath) {
$fullPath = $module->basePath . DIRECTORY_SEPARATOR . $relativePath;
$filename = basename($relativePath);
$projectPath = config_path($filename);
$modulePath = $module->basePath . DIRECTORY_SEPARATOR . $relativePath;
if (file_exists($fullPath)) {
$config = require $fullPath;
$config = null;
if (is_array($config)) {
config()->set($namespace, array_merge(
config($namespace, []),
$config
));
}
if (file_exists($projectPath)) {
$config = require $projectPath;
} elseif (file_exists($modulePath)) {
$config = require $modulePath;
}
if (is_array($config)) {
config()->set($namespace, $config);
}
}
// 🛡️ Middleware
// =========================================
// 2⃣ INFRAESTRUCTURA BASE (App Bindings)
// =========================================
foreach ($module->middleware ?? [] as $alias => $middlewareClass) {
// @var Router $router
$router = app(Router::class);
$router->aliasMiddleware($alias, $middlewareClass);
app(Router::class)->aliasMiddleware($alias, $middlewareClass);
}
// 🏭 Proveedores de servicio
foreach ($module->providers ?? [] as $provider) {
App::register($provider);
}
// 🧩 Alias de clases
foreach ($module->aliases ?? [] as $alias => $class) {
AliasLoader::getInstance()->alias($alias, $class);
}
// 🔩 Singletons
foreach ($model->Singletons?? [] as $singletone) {
app()->singleton($singletone);
foreach ($module->Singletons ?? [] as $singleton) {
app()->singleton($singleton);
}
// 🔗 Bindings de interfaces a servicios
foreach ($module->bindings ?? [] as $abstract => $concrete) {
app()->singleton($abstract, $concrete);
}
// ⚙️ Namespace de componentes
if (!empty($module->componentNamespace)) {
KonekoComponentContextRegistrar::registerComponent($module->componentNamespace);
}
// 📜 Macros
// =========================================
// 3⃣ FUNCIONALIDADES COMPLEMENTARIAS
// =========================================
foreach ($module->macros ?? [] as $macroPath) {
$fullPath = static::resolvePath($module, $macroPath);
if (file_exists($fullPath)) {
require_once $fullPath;
@ -87,12 +89,10 @@ class KonekoModuleBootManager
}
}
// 🔊 Eventos
foreach ($module->listeners ?? [] as $event => $listener) {
Event::listen($event, $listener);
}
// 🔍 Observadores
foreach ($module->observers ?? [] as $model => $observers) {
if (!class_exists($model)) {
logger()->warning("🔍 Modelo no encontrado para observers: $model");
@ -102,30 +102,29 @@ class KonekoModuleBootManager
foreach ((array) $observers as $observer) {
if (class_exists($observer)) {
$model::observe($observer);
} else {
logger()->warning("⚠️ Observer no válido: {$observer}");
}
}
}
// 🧪 Modelos auditables
foreach ($module->auditable ?? [] as $model) {
if (class_exists($model)) {
$model::observe(\OwenIt\Auditing\AuditableObserver::class);
}
}
// 🧬 Migraciones
// =========================================
// 4⃣ DEFINICIONES TÉCNICAS Y RUTAS
// =========================================
foreach ($module->migrations ?? [] as $relativePath) {
$fullPath = static::resolvePath($module, $relativePath);
if (is_dir($fullPath)) {
app()->make('migrator')->path($fullPath);
}
}
// 🗺️ Rutas (después de setModule)
foreach ($module->routes ?? [] as $routeGroup) {
$middleware = $routeGroup['middleware'] ?? [];
$paths = $routeGroup['paths'] ?? [];
@ -141,65 +140,80 @@ class KonekoModuleBootManager
}
}
// 🗂️ Vistas
// =========================================
// 5⃣ RECURSOS: VISTAS, TRADUCCIONES Y COMPONENTES
// =========================================
foreach ($module->views ?? [] as $namespace => $path) {
View::addNamespace($namespace, static::resolvePath($module, $path));
}
// 🌍 Traducciones
foreach ($module->translations ?? [] as $namespace => $path) {
Lang::addNamespace($namespace, static::resolvePath($module, $path));
}
// 🧩 Componentes Blade
foreach ($module->bladeComponents ?? [] as $prefix => $namespace) {
Blade::componentNamespace($namespace, $prefix);
}
// ⚡ Livewire
foreach ($module->livewire ?? [] as $namespace => $components) {
foreach ($components as $alias => $class) {
Livewire::component("{$namespace}::{$alias}", $class);
}
}
// 🛠 Comandos Artisan
// =========================================
// 6⃣ CONSOLA Y DEFINICIONES EXTERNAS
// =========================================
if (App::runningInConsole() && !empty($module->commands)) {
static::registerCommands($module->commands);
}
// 🔗 Registro de APIs disponibles en el módulo
if (!empty($module->apis['catalog_path'])) {
$apiCatalogPath = static::resolvePath($module, $module->apis['catalog_path']);
if (file_exists($apiCatalogPath)) {
$apiDefinitions = require $apiCatalogPath;
config()->set("apis.{$module->slug}", $apiDefinitions);
}
}
// 📑 Registro de catálogos
// =========================================
// 7⃣ REGISTROS INTERNOS
// =========================================
foreach ($module->catalogs ?? [] as $catalog => $service) {
CatalogModuleRegistry::register($catalog, $service);
}
// 🧠 Extensiones de Modelos
foreach ($module->extensions['config'] ?? [] as $builder => $extensions) {
ModelExtensionRegistry::registerConfigExtensions($builder, $extensions);
}
// 🧪 Extensiones de Flags de Modelos
foreach ($module->extensions['model_flags'] ?? [] as $model => $flags) {
ModelExtensionRegistry::registerModelFlags($model, $flags);
}
// 🧬 Extensiones de Traits de Factory
foreach ($module->extensions['factory_traits'] ?? [] as $factory => $traits) {
foreach ((array) $traits as $trait) {
FactoryExtensionRegistry::registerFactoryTrait($factory, $trait);
}
}
foreach ($module->scopeModels ?? [] as $scope => $modelClass) {
try {
ScopeRegistry::register($scope, $modelClass);
} catch (\Throwable $e) {
logger()->error("[ScopeRegistry] No se pudo registrar el scope '{$scope}' del módulo '{$module->slug}': {$e->getMessage()}");
}
}
foreach ($module->configBlocks ?? [] as $configKey => $blockDefinition) {
try {
ConfigBlockRegistry::register($configKey, $blockDefinition);
} catch (\Throwable $e) {
logger()->warning("[ConfigBlockRegistry] No se pudo registrar el bloque '$configKey': {$e->getMessage()}");
}
}
}
/**
@ -250,6 +264,7 @@ class KonekoModuleBootManager
// Intenta aplicar el método al evento, no al schedule
if ($method && method_exists($event, $method)) {
$event = $event->{$method}(...$params);
} else {
logger()->warning("[VuexySchedule] ❌ Método inválido para evento: {$method}");
continue;
@ -259,6 +274,7 @@ class KonekoModuleBootManager
foreach ($chain as $chainMethod) {
if (method_exists($event, $chainMethod)) {
$event = $event->{$chainMethod}();
} else {
logger()->warning("[VuexySchedule] ⚠️ Método de chain no válido: {$chainMethod} en {$jobClass}");
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Bootstrap;
namespace Koneko\VuexyAdmin\Application\Bootstrap\Registry;
use Illuminate\Support\Collection;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
@ -11,6 +11,7 @@ class KonekoModuleRegistry
{
/** @var KonekoModule[] */
protected static array $modules = [];
protected static ?string $currentComponent = null;
/**
* Registra un módulo completo.
@ -130,6 +131,18 @@ class KonekoModuleRegistry
return null;
}
public static function setCurrent(string $component): void
{
static::$currentComponent = $component;
}
public static function current(): ?KonekoModule
{
return static::$currentComponent
? static::get(static::$currentComponent)
: null;
}
/**
* Mapa resumido para tarjetas en UI.
*/

View File

@ -2,14 +2,14 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Bootstrap;
namespace Koneko\VuexyAdmin\Application\Bootstrap\Registry;
use Illuminate\Support\Facades\App;
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Cache\KonekoCacheManager;
use Koneko\VuexyAdmin\Application\Loggers\{KonekoSystemLogger, KonekoSecurityLogger, KonekoUserInteractionLogger};
class KonekoComponentContextRegistrar
class ___KonekoComponentContextRegistrar
{
protected static ?string $currentComponent = null;
protected static ?string $currentSlug = null;
@ -24,11 +24,11 @@ class KonekoComponentContextRegistrar
App::make(SettingsRepositoryInterface::class)
->setNamespaceByComponent($componentNamespace);
}
*/
if (class_exists(KonekoCacheManager::class)) {
try {
cache_manager($componentNamespace)->registerDefaults();
cache_m($componentNamespace)->registerDefaults();
} catch (\Throwable $e) {
logger()->warning("[KonekoContext] No se pudo registrar defaults para CacheManager: {$e->getMessage()}");
}
@ -45,6 +45,7 @@ class KonekoComponentContextRegistrar
if (class_exists(KonekoUserInteractionLogger::class)) {
KonekoUserInteractionLogger::setComponent($componentNamespace, $slug);
}
*/
// Futuro: API Manager y Event Dispatcher
// api_manager()->setComponent($componentNamespace);

View File

@ -0,0 +1,104 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Builders;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\Cache\Services\KonekoVarsService;
/**
* 🎛️ Builder de variables administrativas para el layout de administración.
* - Fuente primaria: settings globales (namespace 'koneko.core.layout.admin')
* - Permite override explícito por usuario autenticado.
*/
class KonekoAdminVarsBuilder
{
public function __construct(
protected KonekoVarsService $vars
) {
$this->vars->context('layout', 'admin')->setKeyName('meta');
}
/**
* Devuelve las variables visuales del layout administrativo.
* Incluye título, autor, logos, favicon, etc.
*/
public function get(): array
{
return $this->vars->remember('meta', fn () => $this->resolveAdminVars());
}
/**
* Limpia el caché asociado al layout administrativo.
*/
public function clear(): void
{
$this->vars->setKeyName('meta')->clear();
}
/**
* Devuelve metainformación del contexto del builder (debug, auditoría).
*/
public function info(): array
{
return [
'context' => $this->vars->getContext(),
'cache_key' => $this->vars->cacheKey('meta'),
'source' => 'config + settings + optional user override',
];
}
/**
* Construye el array de variables administrativas del layout.
* Aplica override por usuario si está autenticado.
*/
protected function resolveAdminVars(): array
{
$base = settings()->context('layout', 'admin')->asArray()->getSubGroup();
return [
'title' => $base['title'] ?? config_m()->get('layout.admin.title', 'Koneko Admin'),
'author' => $base['author'] ?? config_m()->get('layout.admin.author', 'Default Author'),
'description' => $base['description'] ?? config_m()->get('layout.admin.description', 'Default Description'),
'favicon' => $this->buildFaviconPaths($base),
'app_name' => $base['app_name'] ?? config_m()->get('app_name'),
'image_logo' => $this->buildImageLogoPaths($base),
];
}
/**
* Construye el arreglo de rutas de favicon según configuración.
*/
protected function buildFaviconPaths(array $settings): array
{
$ns = $settings['favicon_ns'] ?? null;
$default = config_m()->get('favicon', 'favicon.ico');
return [
'namespace' => $ns,
'16x16' => $ns ? "{$ns}_16x16.png" : $default,
'76x76' => $ns ? "{$ns}_76x76.png" : $default,
'120x120' => $ns ? "{$ns}_120x120.png" : $default,
'152x152' => $ns ? "{$ns}_152x152.png" : $default,
'180x180' => $ns ? "{$ns}_180x180.png" : $default,
'192x192' => $ns ? "{$ns}_192x192.png" : $default,
];
}
/**
* Construye el arreglo de rutas de logos según configuración.
*/
protected function buildImageLogoPaths(array $settings): array
{
$default = config_m()->get('app_logo', 'logo-default.png');
return [
'small' => $settings['image_logo_small'] ?? $default,
'medium' => $settings['image_logo_medium'] ?? $default,
'large' => $settings['image_logo'] ?? $default,
'small_dark' => $settings['image_logo_small_dark'] ?? $default,
'medium_dark' => $settings['image_logo_medium_dark'] ?? $default,
'large_dark' => $settings['image_logo_dark'] ?? $default,
];
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Builders;
use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Settings\Manager\KonekoSettingManager;
/**
* 🎛️ Builder de variables administrativas para el layout de administración.
* - Fuente primaria: settings globales (namespace 'koneko.core.layout.admin')
* - Permite override explícito por usuario autenticado.
*/
class KonekoAdminVarsBuilder
{
public function __construct(
protected SettingsRepositoryInterface $settings
) {
$this->settings->setGroup('layout');
}
/**
* Devuelve las variables visuales del layout administrativo.
* Incluye título, autor, logos, favicon, etc.
*/
public function get(): array
{
return $this->settings
->setSection('admin')
->setKeyName('meta')
->remember(fn () => $this->resolveAdminVars());
}
/**
* Limpia el caché asociado al layout administrativo.
*/
public function clear(): void
{
$this->settings
->setSection('admin')
->forgetCache('meta');
}
/**
* Devuelve metainformación del contexto del builder (debug, auditoría).
*/
/*
public function info(): array
{
return [
'context' => $this->settings->getContext(),
'cache_key' => $this->settings->cacheKey('meta'),
'source' => 'config + settings + optional user override',
];
}
*/
/**
* Construye el array de variables administrativas del layout.
* Aplica override por usuario si está autenticado.
*/
protected function resolveAdminVars(): array
{
$base = $this->settings
->setSection('admin')
->asArray()
->getSubGroup();
return [
'title' => $base['title'] ?? config_m()->get('layout.admin.title', 'Koneko Admin'),
'author' => $base['author'] ?? config_m()->get('layout.admin.author', 'Default Author'),
'description' => $base['description'] ?? config_m()->get('layout.admin.description', 'Default Description'),
'favicon' => $this->buildFaviconPaths($base),
'app_name' => $base['app_name'] ?? config_m()->get('app_name', 'Koneko Admin'),
'image_logo' => $this->buildImageLogoPaths($base),
];
}
/**
* Construye el arreglo de rutas de favicon según configuración.
*/
protected function buildFaviconPaths(array $settings): array
{
$ns = $settings['favicon_ns'] ?? null;
$default = config_m()->get('favicon', 'favicon.ico');
return [
'namespace' => $ns,
'16x16' => $ns ? "{$ns}_16x16.png" : $default,
'76x76' => $ns ? "{$ns}_76x76.png" : $default,
'120x120' => $ns ? "{$ns}_120x120.png" : $default,
'152x152' => $ns ? "{$ns}_152x152.png" : $default,
'180x180' => $ns ? "{$ns}_180x180.png" : $default,
'192x192' => $ns ? "{$ns}_192x192.png" : $default,
];
}
/**
* Construye el arreglo de rutas de logos según configuración.
*/
protected function buildImageLogoPaths(array $settings): array
{
$default = config_m()->get('app_logo', 'logo-default.png');
return [
'small' => $settings['image_logo_small'] ?? $default,
'medium' => $settings['image_logo_medium'] ?? $default,
'large' => $settings['image_logo'] ?? $default,
'small_dark' => $settings['image_logo_small_dark'] ?? $default,
'medium_dark' => $settings['image_logo_medium_dark'] ?? $default,
'large_dark' => $settings['image_logo_dark'] ?? $default,
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Builders;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\Cache\Services\KonekoVarsService;
/**
* 🎛️ Builder de variables personalizadas para el layout de administración.
* - Fuente primaria: settings globales (namespace 'koneko.core.layout.admin')
* - Permite override explícito por usuario autenticado.
*/
class KonekoVuexyCustomizerVarsBuilder extends KonekoAdminVarsBuilder
{
public function build(): array
{
$this->setContext([
'component' => CoreModule::COMPONENT,
'group' => 'layout',
'sub_group' => 'vuexy',
]);
$this->setScope('customizer');
return parent::build();
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Builders;
use Illuminate\Contracts\Auth\Authenticatable;
use Koneko\VuexyAdmin\Application\Settings\SettingDefaults;
use Koneko\VuexyAdmin\Application\CoreModule;
final class SettingCacheKeyBuilder
{
private const MAX_KEY_LENGTH = 120;
/**
* Construye una clave canónica de cache para un setting.
*/
public static function build(
string $namespace,
string $environment = 'local',
?string $scope = null,
int|string|null $scopeId = null,
string $component = CoreModule::COMPONENT,
string $group = SettingDefaults::DEFAULT_GROUP,
string $section = SettingDefaults::DEFAULT_SECTION,
string $subGroup = SettingDefaults::DEFAULT_SUB_GROUP,
string $keyName
): string {
if (empty($keyName)) {
throw new \InvalidArgumentException("El parámetro 'keyName' no puede estar vacío.");
}
$scopeSegment = $scopeId
? "{$scope}:{$scopeId}"
: ($scope ?? '');
$segments = array_filter([
$namespace,
$environment,
$scopeSegment,
$component,
$group,
$section,
$subGroup,
$keyName,
]);
$baseKey = implode('.', $segments);
return strlen($baseKey) > self::MAX_KEY_LENGTH
? 'h:' . hash('sha1', $baseKey)
: $baseKey;
}
/**
* Verifica si una clave generada es calificada (tiene estructura válida).
*/
public static function isQualified(string $key): bool
{
return str_starts_with($key, 'h:') || substr_count($key, '.') >= 5;
}
/**
* Construye una clave específica basada en un usuario autenticado.
*/
public static function forUser(
string $namespace,
Authenticatable|int|null $user,
string $environment = 'prod',
string $component = CoreModule::COMPONENT,
string $group = SettingDefaults::DEFAULT_GROUP,
string $section = SettingDefaults::DEFAULT_SECTION,
string $subGroup = SettingDefaults::DEFAULT_SUB_GROUP,
string $keyName,
): string {
if (empty($keyName)) {
throw new \InvalidArgumentException("El parámetro 'keyName' no puede estar vacío.");
}
$userId = $user instanceof Authenticatable ? $user->getAuthIdentifier() : $user;
return self::build(
namespace: $namespace,
environment: $environment,
scope: 'user',
scopeId: $userId,
component: $component,
group: $group,
section: $section,
subGroup: $subGroup,
keyName: $keyName,
);
}
/**
* Genera una clave hash predecible para valores extremadamente largos.
*/
public static function hashed(string ...$segments): string
{
return 'h:' . hash('sha1', implode('.', $segments));
}
}

View File

@ -1,301 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Cache;
use Illuminate\Support\Facades\{Config,DB,Redis};
use Memcached;
/**
* Servicio para gestionar y obtener información de configuración del sistema de caché.
*
* Esta clase proporciona métodos para obtener información detallada sobre las configuraciones
* de caché, sesión, base de datos y drivers del sistema. Permite consultar versiones,
* estados y configuraciones de diferentes servicios como Redis, Memcached y bases de datos.
*/
class CacheConfigService
{
/**
* Obtiene la configuración completa del sistema de caché y servicios relacionados.
*
* @return array Configuración completa que incluye caché, sesión, base de datos y drivers
*/
public function getConfig(): array
{
return [
'cache' => $this->getCacheConfig(),
'session' => $this->getSessionConfig(),
'database' => $this->getDatabaseConfig(),
'driver' => $this->getDriverVersion(),
'memcachedInUse' => $this->isDriverInUse('memcached'),
'redisInUse' => $this->isDriverInUse('redis'),
];
}
/**
* Obtiene la configuración específica del sistema de caché.
*
* @return array Configuración del caché incluyendo driver, host y base de datos
*/
private function getCacheConfig(): array
{
$cacheConfig = Config::get('cache');
$driver = $cacheConfig['default'];
switch ($driver) {
case 'redis':
$connection = config('database.redis.cache');
$cacheConfig['host'] = $connection['host'] ?? 'localhost';
$cacheConfig['database'] = $connection['database'] ?? 'N/A';
break;
case 'database':
$connection = config('database.connections.' . config('cache.stores.database.connection'));
$cacheConfig['host'] = $connection['host'] ?? 'localhost';
$cacheConfig['database'] = $connection['database'] ?? 'N/A';
break;
case 'memcached':
$servers = config('cache.stores.memcached.servers');
$cacheConfig['host'] = $servers[0]['host'] ?? 'localhost';
$cacheConfig['database'] = 'N/A';
break;
case 'file':
$cacheConfig['host'] = storage_path('framework/cache/data');
$cacheConfig['database'] = 'N/A';
break;
default:
$cacheConfig['host'] = 'N/A';
$cacheConfig['database'] = 'N/A';
break;
}
return $cacheConfig;
}
/**
* Obtiene la configuración del sistema de sesiones.
*
* @return array Configuración de sesiones incluyendo driver, host y base de datos
*/
private function getSessionConfig(): array
{
$sessionConfig = Config::get('session');
$driver = $sessionConfig['driver'];
switch ($driver) {
case 'redis':
$connection = config('database.redis.sessions');
$sessionConfig['host'] = $connection['host'] ?? 'localhost';
$sessionConfig['database'] = $connection['database'] ?? 'N/A';
break;
case 'database':
$connection = config('database.connections.' . $sessionConfig['connection']);
$sessionConfig['host'] = $connection['host'] ?? 'localhost';
$sessionConfig['database'] = $connection['database'] ?? 'N/A';
break;
case 'memcached':
$servers = config('cache.stores.memcached.servers');
$sessionConfig['host'] = $servers[0]['host'] ?? 'localhost';
$sessionConfig['database'] = 'N/A';
break;
case 'file':
$sessionConfig['host'] = storage_path('framework/sessions');
$sessionConfig['database'] = 'N/A';
break;
default:
$sessionConfig['host'] = 'N/A';
$sessionConfig['database'] = 'N/A';
break;
}
return $sessionConfig;
}
/**
* Obtiene la configuración de la base de datos principal.
*
* @return array Configuración de la base de datos incluyendo host y nombre de la base de datos
*/
private function getDatabaseConfig(): array
{
$databaseConfig = Config::get('database');
$connection = $databaseConfig['default'];
$connectionConfig = config('database.connections.' . $connection);
$databaseConfig['host'] = $connectionConfig['host'] ?? 'localhost';
$databaseConfig['database'] = $connectionConfig['database'] ?? 'N/A';
return $databaseConfig;
}
/**
* Obtiene información sobre las versiones de los drivers en uso.
*
* Recopila información detallada sobre las versiones de los drivers de base de datos,
* Redis y Memcached si están en uso en el sistema.
*
* @return array Información de versiones de los drivers activos
*/
private function getDriverVersion(): array
{
$drivers = [];
$defaultDatabaseDriver = config('database.default'); // Obtén el driver predeterminado
switch ($defaultDatabaseDriver) {
case 'mysql':
case 'mariadb':
$drivers['mysql'] = [
'version' => $this->getMySqlVersion(),
'details' => config("database.connections.$defaultDatabaseDriver"),
];
$drivers['mariadb'] = $drivers['mysql'];
case 'pgsql':
$drivers['pgsql'] = [
'version' => $this->getPgSqlVersion(),
'details' => config("database.connections.pgsql"),
];
break;
case 'sqlsrv':
$drivers['sqlsrv'] = [
'version' => $this->getSqlSrvVersion(),
'details' => config("database.connections.sqlsrv"),
];
break;
default:
$drivers['unknown'] = [
'version' => 'No disponible',
'details' => 'Driver no identificado',
];
break;
}
// Opcional: Agrega detalles de Redis y Memcached si están en uso
if ($this->isDriverInUse('redis')) {
$drivers['redis'] = [
'version' => $this->getRedisVersion(),
];
}
if ($this->isDriverInUse('memcached')) {
$drivers['memcached'] = [
'version' => $this->getMemcachedVersion(),
];
}
return $drivers;
}
/**
* Obtiene la versión del servidor MySQL.
*
* @return string Versión del servidor MySQL o mensaje de error
*/
private function getMySqlVersion(): string
{
try {
$version = DB::selectOne('SELECT VERSION() as version');
return $version->version ?? 'No disponible';
} catch (\Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Obtiene la versión del servidor PostgreSQL.
*
* @return string Versión del servidor PostgreSQL o mensaje de error
*/
private function getPgSqlVersion(): string
{
try {
$version = DB::selectOne("SHOW server_version");
return $version->server_version ?? 'No disponible';
} catch (\Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Obtiene la versión del servidor SQL Server.
*
* @return string Versión del servidor SQL Server o mensaje de error
*/
private function getSqlSrvVersion(): string
{
try {
$version = DB::selectOne("SELECT @@VERSION as version");
return $version->version ?? 'No disponible';
} catch (\Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Obtiene la versión del servidor Memcached.
*
* @return string Versión del servidor Memcached o mensaje de error
*/
private function getMemcachedVersion(): string
{
try {
$memcached = new Memcached();
$memcached->addServer(
Config::get('cache.stores.memcached.servers.0.host'),
Config::get('cache.stores.memcached.servers.0.port')
);
$stats = $memcached->getStats();
foreach ($stats as $serverStats) {
return $serverStats['version'] ?? 'No disponible';
}
return 'No disponible';
} catch (\Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Obtiene la versión del servidor Redis.
*
* @return string Versión del servidor Redis o mensaje de error
*/
private function getRedisVersion(): string
{
try {
$info = Redis::info();
return $info['redis_version'] ?? 'No disponible';
} catch (\Exception $e) {
return 'Error: ' . $e->getMessage();
}
}
/**
* Verifica si un driver específico está en uso en el sistema.
*
* Comprueba si el driver está siendo utilizado en caché, sesiones o colas.
*
* @param string $driver Nombre del driver a verificar
* @return bool True si el driver está en uso, false en caso contrario
*/
protected function isDriverInUse(string $driver): bool
{
return in_array($driver, [
Config::get('cache.default'),
Config::get('session.driver'),
Config::get('queue.default'),
]);
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Contracts;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
interface CacheRepositoryInterface
{
// ==================== Factory ====================
public static function make(): static;
public static function fromArray(array $context): static;
public static function fromRequest(?Request $request = null): static;
// ==================== Context ====================
public function setNamespace(string $namespace): static;
public function setEnvironment(?string $environment = null): static;
public function setComponent(string $component): static;
public function context(string $group, ?string $section = null, ?string $subGroup = null): static;
public function setContextArray(array $context): static;
public function setScope(Model|string|false $scope, int|null|false $scopeId = false): static;
public function setScopeId(?int $scopeId): static;
public function setUser(Authenticatable|int|null|false $user): static;
public function withScopeFromModel(Model $model): static;
public function setGroup(string $group): static;
public function setSection(string $section): static;
public function setSubGroup(string $subGroup): static;
public function setKeyName(string $keyName): static;
// ==================== Config ====================
public function isEnabled(): bool;
public function resolveTTL(): int;
public function driver(): string;
// ==================== Cache Operations ====================
public function get(mixed $default = null): mixed;
public function put(mixed $value, ?int $ttl = null): void;
public function forget(): void;
public function remember(?callable $resolver = null, ?int $ttl = null): mixed;
public function rememberWithTTLResolution(callable $resolver, ?int $ttl = null): mixed;
// ==================== Getters ====================
public function qualifiedKey(?string $key = null): string;
public function getScopeModel(): ?Model;
// ==================== Utils ====================
public function has(string $qualifiedKey): bool;
public function hasContext(): bool;
public function reset(): static;
// ==================== Diagnostics ====================
public function info(): array;
public function infoWithCacheLayers(): array;
}

View File

@ -0,0 +1,34 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Driver;
use Illuminate\Support\Facades\Cache;
final class KonekoCacheDriver
{
public static function get(string $key, mixed $default = null): mixed
{
return Cache::get($key, $default);
}
public static function put(string $key, mixed $value, int $ttl): void
{
Cache::put($key, $value, now()->addMinutes($ttl));
}
public static function forget(string $key): void
{
Cache::forget($key);
}
public static function remember(string $key, int $ttl, \Closure $callback): mixed
{
return Cache::remember($key, now()->addMinutes($ttl), $callback);
}
public static function has(string $key): bool
{
return Cache::has($key);
}
}

View File

@ -1,186 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache;
/**
* 📊 Gestor de Cache del Ecosistema Koneko
* Soporte para múltiples niveles (core, componente, grupo), drivers mixtos y tagging.
* Compatible con redis, memcached, file y database.
*/
class KonekoCacheManager
{
private string $namespace;
private string $component;
private string $group;
private string $subGroup;
public function __construct(string $namespace, string $component = 'core')
{
$this->namespace = $namespace;
$this->component = $component;
}
public function setContext(string $component, string $group, string $subGroup): static
{
return $this
->setComponent($component)
->setGroup($group)
->setSubGroup($subGroup);
}
public function setNamespace(string $namespace): static
{
$this->validateSlug('namespace', $namespace);
$this->namespace = strtolower($namespace);
return $this;
}
public function setComponent(string $component): static
{
$this->validateSlug('component', $component);
$this->component = strtolower($component);
return $this;
}
public function setGroup(string $group): static
{
$this->validateSlug('group', $group);
$this->group = strtolower($group);
return $this;
}
public function setSubGroup(string $subGroup): static
{
$this->validateSlug('subGroup', $subGroup);
$this->subGroup = strtolower($subGroup);
return $this;
}
private function ensureContext(): void
{
foreach (['component', 'group', 'subGroup'] as $context) {
if (empty($this->$context)) {
throw new \LogicException("Debe establecer {$context} antes de generar una clave de caché.");
}
}
}
public function currentNamespace(): string
{
return $this->namespace;
}
public function currentComponent(): string
{
return $this->component;
}
public function currentGroup(): string
{
return $this->group;
}
public function currentSubGroup(): string
{
return $this->subGroup;
}
public function fullKey(string $suffix): string
{
return "{$this->path()}.{$suffix}";
}
public function key(string $suffix): string
{
$this->ensureContext();
return "{$this->path()}.{$suffix}";
}
public function config(string $key, mixed $default = null): mixed
{
return config($this->key($key), $default);
}
public function ttl(): int
{
return (int) (
config("{$this->namespace}.{$this->component}.{$this->group}.{$this->subGroup}.ttl")
?? config("{$this->namespace}.{$this->component}.{$this->group}.ttl")
?? config("{$this->namespace}.{$this->component}.cache.ttl")
?? config("{$this->namespace}.cache.ttl", 3600)
);
}
public function enabled(): bool
{
return (bool) (
config("{$this->namespace}.{$this->component}.{$this->group}.{$this->subGroup}.enabled")
?? config("{$this->namespace}.{$this->component}.{$this->group}.enabled")
?? config("{$this->namespace}.{$this->component}.cache.enabled")
?? config("{$this->namespace}.cache.enabled", true)
);
}
public function shouldDebug(): bool
{
return (bool) $this->config('debug', false);
}
public function driver(): string
{
return config('cache.default');
}
public function path(): string
{
return "{$this->namespace}.{$this->component}.{$this->group}.{$this->subGroup}";
}
public function info(): array
{
return [
'namespace' => $this->namespace,
'component' => $this->component,
'group' => $this->group,
'subGroup' => $this->subGroup,
'enabled' => $this->enabled(),
'ttl' => $this->ttl(),
'driver' => $this->driver(),
'debug' => $this->shouldDebug(),
];
}
private function validateSlug(string $field, string $value): void
{
if (!preg_match('/^[a-z0-9\-]+$/', $value)) {
throw new \InvalidArgumentException("El valor de '{$field}' debe ser un slug válido.");
}
}
private function ensureContext(): void
{
if (empty($this->component) || empty($this->group)) {
throw new \LogicException("Debe establecer component y group antes de generar una clave de caché.");
}
}
}

View File

@ -1,185 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache;
/**
* 📊 Gestor de Cache del Ecosistema Koneko
* Soporte para múltiples niveles (core, componente, grupo), drivers mixtos y tagging.
* Compatible con redis, memcached, file y database.
*/
class KonekoCacheManager
{
private string $namespace;
private string $component;
private string $group;
public function __construct(string $namespace)
{
$this->namespace = $namespace;
}
/**
* Establece el contexto de la caché.
*/
public function setContext(string $component, string $group): static
{
return $this
->setNamespace($this->namespace)
->setComponent($component)
->setGroup($group);
}
/**
* Establece el namespace de la caché.
*/
public function setNamespace(string $namespace): static
{
if (!preg_match('/^[a-z0-9\-]+$/', $namespace)) {
throw new \InvalidArgumentException("El namespace '{$namespace}' debe ser un slug válido.");
}
$this->namespace = strtolower($namespace);
return $this;
}
/**
* Establece el componente de la caché.
*/
public function setComponent(string $component): static
{
if (!preg_match('/^[a-z0-9\-]+$/', $component)) {
throw new \InvalidArgumentException("El componente '{$component}' debe ser un slug válido.");
}
$this->component = strtolower($component);
return $this;
}
/**
* Establece el grupo de la caché.
*/
public function setGroup(string $group): static
{
if (!preg_match('/^[a-z0-9\-]+$/', $group)) {
throw new \InvalidArgumentException("El grupo '{$group}' debe ser un slug válido.");
}
$this->group = strtolower($group);
return $this;
}
public function currentNamespace(): string
{
return $this->namespace;
}
public function currentComponent(): string
{
return $this->component;
}
public function currentGroup(): string
{
return $this->group;
}
/**
* Genera una clave calificada para la caché.
*/
public function key(string $suffix): string
{
if (empty($this->component) || empty($this->group)) {
throw new \LogicException("Component and group must be set before generating a cache key.");
}
return "{$this->path()}.{$suffix}";
}
/**
* Obtiene un valor de configuración.
*/
public function config(string $key, mixed $default = null): mixed
{
return config($this->key($key), $default);
}
/**
* Obtiene el tiempo de vida (TTL) de la caché.
*/
public function ttl(): int
{
return (int) (
config("{$this->namespace}.{$this->component}.{$this->group}.ttl") ??
config("{$this->namespace}.{$this->component}.cache.ttl") ??
config("{$this->namespace}.cache.ttl", 3600)
);
}
/**
* Obtiene el estado de habilitación de la caché.
*/
public function enabled(): bool
{
return (bool) (
config("{$this->namespace}.{$this->component}.{$this->group}.enabled") ??
config("{$this->namespace}.{$this->component}.cache.enabled") ??
config("{$this->namespace}.cache.enabled", true)
);
}
/**
* Determina si se debe depurar la caché.
*/
public function shouldDebug(): bool
{
return (bool) $this->config('debug', false);
}
/**
* Obtiene el driver de caché.
*/
public function driver(): string
{
return config('cache.default');
}
/**
* Registra los valores por defecto en la configuración.
*/
public function registerDefaults(): void
{
if (! config()->has($this->key('ttl'))) {
config()->set($this->key('ttl'), 3600);
}
if (! config()->has($this->key('enabled'))) {
config()->set($this->key('enabled'), true);
}
}
/**
* Obtiene la ruta de la caché.
*/
public function path(): string
{
return "{$this->namespace}.{$this->component}.{$this->group}";
}
/**
* Información extendida de depuración.
*/
public function info(): array
{
return [
'component' => $this->component,
'group' => $this->group,
'enabled' => $this->enabled(),
'ttl' => $this->ttl(),
'driver' => $this->driver(),
'debug' => $this->shouldDebug(),
];
}
}

View File

@ -1,232 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Support\Facades\{Auth, Config};
use Illuminate\Support\Str;
use Koneko\VuexyAdmin\Application\Enums\Settings\SettingScope;
use Koneko\VuexyAdmin\Models\User;
class KonekoCacheManager
{
public const MAX_KEY_LENGTH = 120; // Friendly limit
private const DEFAULT_SCOPE = SettingScope::GLOBAL->value;
private const USER_GUEST_ALIAS = SettingScope::GUEST->value;
private string $namespace;
private string $scope;
private string $component;
private string $group;
private string $subGroup;
protected bool $isUserScoped = false;
protected ?Authenticatable $user = null;
public function __construct(string $namespace)
{
if (empty($namespace) || !preg_match('/^[a-z0-9\-]+$/', $namespace)) {
throw new \InvalidArgumentException("El namespace '{$namespace}' no es válido.");
}
$this->namespace = $this->truncate('namespace', $namespace, 8);
$this->scope = self::DEFAULT_SCOPE;
}
// ========= CONTEXT MANAGEMENT =========
public function setContext(
string $component,
string $group,
string $subGroup,
string $scope = self::DEFAULT_SCOPE
): static {
return $this
->setComponent($component)
->setGroup($group)
->setSubGroup($subGroup)
->setScope($scope);
}
public function setScope(SettingScope|string $scope): static
{
if (is_string($scope) && !SettingScope::isValid($scope)) {
throw new \InvalidArgumentException("Scope '{$scope}' no es válido.");
}
$this->scope = is_string($scope)
? SettingScope::from($scope)->value
: $scope->value;
return $this;
}
public function setNamespace(string $namespace): static
{
$this->namespace = $this->truncate('namespace', $namespace, 8);
return $this;
}
public function setComponent(string $component): static
{
$this->component = $this->truncate('component', $component, 16);
return $this;
}
public function setGroup(string $group): static
{
$this->group = $this->truncate('group', $group, 16);
return $this;
}
public function setSubGroup(string $subGroup): static
{
$this->subGroup = $this->truncate('subGroup', $subGroup, 16);
return $this;
}
public function setUser(int|Authenticatable|null|false $user = null): static
{
match (true) {
$user === false => $this->resetUserScope(), // Visitante explícito
is_int($user) => $this->assignUser(User::findOrFail($user)),
$user instanceof Authenticatable => $this->assignUser($user),
default => $this->assignUser(Auth::user()) // Usuario autenticado o null (visitante)
};
return $this;
}
protected function assignUser(?Authenticatable $user): void
{
$this->user = $user;
$this->isUserScoped = !is_null($user);
}
protected function resetUserScope(): void
{
$this->user = null;
$this->isUserScoped = false;
}
public function isUserScoped(): bool
{
return $this->isUserScoped;
}
// ========= ACCESSORS =========
public function currentNamespace(): string { return $this->namespace; }
public function currentComponent(): string { return $this->component; }
public function currentGroup(): string { return $this->group; }
public function currentSubGroup(): string { return $this->subGroup; }
public function currentScope(): string { return $this->scope; }
// ========= CACHE CONFIGURATION =========
public function key(string $suffix): string
{
$this->ensureContext();
$userSegment = $this->isUserScoped
? 'u.' . ($this->user?->getAuthIdentifier() ?? self::USER_GUEST_ALIAS)
: self::DEFAULT_SCOPE;
$scopeSegment = $this->scope === self::DEFAULT_SCOPE
? null
: "scope." . $this->scope;
$base = implode('.', array_filter([
$this->namespace,
app()->environment(),
$this->component,
$this->group,
$this->subGroup,
$scopeSegment,
$userSegment,
$suffix
]));
return strlen($base) > self::MAX_KEY_LENGTH
? 'h:' . crc32($base)
: $base;
}
public function ttl(): int
{
return (int) (
Config::get("{$this->path()}.ttl") ??
Config::get("{$this->namespace}.{$this->component}.{$this->group}.ttl") ??
Config::get("{$this->namespace}.{$this->component}.cache.ttl") ??
Config::get("{$this->namespace}.cache.ttl", 3600)
);
}
public function enabled(): bool
{
return (bool) (
Config::get("{$this->path()}.enabled") ??
Config::get("{$this->namespace}.{$this->component}.{$this->group}.enabled") ??
Config::get("{$this->namespace}.{$this->component}.cache.enabled") ??
Config::get("{$this->namespace}.cache.enabled", true)
);
}
public function driver(): string
{
return Config::get('cache.default');
}
public function path(): string
{
return "{$this->namespace}." . app()->environment() . ".{$this->component}.{$this->group}.{$this->subGroup}";
}
public function info(): array
{
return [
'environment' => app()->environment(),
'namespace' => $this->namespace,
'component' => $this->component,
'group' => $this->group,
'subGroup' => $this->subGroup,
'scope' => $this->scope,
'enabled' => $this->enabled(),
'ttl' => $this->ttl(),
'driver' => $this->driver(),
];
}
// ========= VALIDATION & SANITIZATION =========
private function validateSlug(string $field, string $value): void
{
if (!preg_match('/^[a-z0-9\-]+$/', $value)) {
throw new \InvalidArgumentException("El valor de '{$field}' debe ser un slug válido.");
}
}
private function ensureContext(): void
{
foreach (['namespace', 'component', 'group', 'subGroup'] as $prop) {
if (empty($this->$prop) || !preg_match('/^[a-z0-9\-]+$/', $this->$prop)) {
throw new \InvalidArgumentException("El valor de '{$prop}' es obligatorio y debe ser un slug válido.");
}
}
}
private function truncate(string $field, string $value, int $maxLength): string
{
$this->validateSlug($field, $value);
return Str::limit(strtolower($value), $maxLength, '');
}
}

View File

@ -1,155 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Cache;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
class KonekoSessionManager
{
private string $driver;
public function __construct(mixed $driver = null)
{
$this->driver = $driver ?? config('session.driver');
}
public function getSessionStats(mixed $driver = null): array
{
$driver = $driver ?? $this->driver;
if (!$this->isSupportedDriver($driver))
return $this->response('warning', 'Driver no soportado o no configurado.', ['session_count' => 0]);
try {
switch ($driver) {
case 'redis':
return $this->getRedisStats();
case 'database':
return $this->getDatabaseStats();
case 'file':
return $this->getFileStats();
default:
return $this->response('warning', 'Driver no reconocido.', ['session_count' => 0]);
}
} catch (\Exception $e) {
return $this->response('danger', 'Error al obtener estadísticas: ' . $e->getMessage(), ['session_count' => 0]);
}
}
public function clearSessions(mixed $driver = null): array
{
$driver = $driver ?? $this->driver;
if (!$this->isSupportedDriver($driver)) {
return $this->response('warning', 'Driver no soportado o no configurado.');
}
try {
switch ($driver) {
case 'redis':
return $this->clearRedisSessions();
case 'memcached':
Cache::getStore()->flush();
return $this->response('success', 'Se eliminó la memoria caché de sesiones en Memcached.');
case 'database':
DB::table('sessions')->truncate();
return $this->response('success', 'Se eliminó la memoria caché de sesiones en la base de datos.');
case 'file':
return $this->clearFileSessions();
default:
return $this->response('warning', 'Driver no reconocido.');
}
} catch (\Exception $e) {
return $this->response('danger', 'Error al limpiar las sesiones: ' . $e->getMessage());
}
}
private function getRedisStats()
{
$prefix = config('cache.prefix'); // Asegúrate de agregar el sufijo correcto si es necesario
$keys = Redis::connection('sessions')->keys($prefix . '*');
return $this->response('success', 'Se ha recargado la información de la caché de Redis.', ['session_count' => count($keys)]);
}
private function getDatabaseStats(): array
{
$sessionCount = DB::table('sessions')->count();
return $this->response('success', 'Se ha recargado la información de la base de datos.', ['session_count' => $sessionCount]);
}
private function getFileStats(): array
{
$cachePath = config('session.files');
$files = glob($cachePath . '/*');
return $this->response('success', 'Se ha recargado la información de sesiones de archivos.', ['session_count' => count($files)]);
}
/**
* Limpia sesiones en Redis.
*/
private function clearRedisSessions(): array
{
$prefix = config('cache.prefix', '');
$keys = Redis::connection('sessions')->keys($prefix . '*');
if (!empty($keys)) {
Redis::connection('sessions')->flushdb();
// Simulate cache clearing delay
sleep(1);
return $this->response('success', 'Se eliminó la memoria caché de sesiones en Redis.');
}
return $this->response('info', 'No se encontraron claves para eliminar en Redis.');
}
/**
* Limpia sesiones en archivos.
*/
private function clearFileSessions(): array
{
$cachePath = config('session.files');
$files = glob($cachePath . '/*');
if (!empty($files)) {
foreach ($files as $file) {
unlink($file);
}
return $this->response('success', 'Se eliminó la memoria caché de sesiones en archivos.');
}
return $this->response('info', 'No se encontraron sesiones en archivos para eliminar.');
}
private function isSupportedDriver(string $driver): bool
{
return in_array($driver, ['redis', 'memcached', 'database', 'file']);
}
/**
* Genera una respuesta estandarizada.
*/
private function response(string $status, string $message, array $data = []): array
{
return array_merge(compact('status', 'message'), $data);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Manager\Concerns;
use Koneko\VuexyAdmin\Application\Traits\System\Context\HasBaseContext;
use Koneko\VuexyAdmin\Application\Traits\System\Context\HasCacheContextValidation;
trait ___HasCacheContext
{
use HasBaseContext;
use HasCacheContextValidation;
// ======================= HELPERS =========================
public function reset(): static
{
return $this;
}
}

View File

@ -0,0 +1,181 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Manager;
use Illuminate\Support\Facades\Config;
use Koneko\VuexyAdmin\Application\Cache\Driver\KonekoCacheDriver;
use Koneko\VuexyAdmin\Application\Cache\Contracts\CacheRepositoryInterface;
use Koneko\VuexyAdmin\Application\Traits\System\Context\{HasBaseContext, HasCacheContextValidation};
use Koneko\VuexyAdmin\Application\CoreModule;
final class KonekoCacheManager implements CacheRepositoryInterface
{
use HasBaseContext;
use HasCacheContextValidation;
public function __construct()
{
$this->setNamespace(CoreModule::NAMESPACE)
->setEnvironment()
->setComponent(CoreModule::COMPONENT);
}
// ==================== Factory ====================
public static function make(): static
{
return new static();
}
// ======================= ⚙️ Configuración de Cache =========================
public function isEnabled(): bool
{
foreach ($this->enabledCandidateKeys() as $key) {
$value = Config::get($key);
if (!is_null($value)) {
return (bool) $value;
}
}
return true;
}
public function resolveEnabledSourceKey(): string
{
foreach ($this->enabledCandidateKeys() as $key) {
if (Config::has($key)) {
return $key;
}
}
return 'default';
}
public function resolveTTL(): int
{
foreach ($this->ttlCandidateKeys() as $key) {
if (Config::has($key)) {
return (int) Config::get($key);
}
}
return 3600;
}
public function resolveTtlSourceKey(): string
{
foreach ($this->ttlCandidateKeys() as $key) {
if (Config::has($key)) {
return $key;
}
}
return 'default';
}
public function driver(): string
{
return config('cache.default');
}
protected function enabledCandidateKeys(): array
{
$base = "{$this->context['namespace']}.{$this->context['component']}.{$this->context['group']}";
return [
"{$this->context['namespace']}.cache.enabled",
"{$this->context['namespace']}.{$this->context['component']}.cache.enabled",
"{$base}.cache.enabled",
"{$base}.{$this->context['sub_group']}.cache.enabled",
"{$base}.{$this->context['section']}.{$this->context['sub_group']}.cache.enabled",
];
}
protected function ttlCandidateKeys(): array
{
$base = "{$this->context['namespace']}.{$this->context['component']}.{$this->context['group']}";
return [
"{$base}.{$this->context['sub_group']}.ttl",
"{$base}.ttl",
"{$this->context['namespace']}.{$this->context['component']}.cache.ttl",
"{$this->context['namespace']}.cache.ttl",
];
}
// ======================= 🧠 Operaciones =========================
public function get(mixed $default = null): mixed
{
return KonekoCacheDriver::get($this->qualifiedKey(), $default);
}
public function put(mixed $value, ?int $ttl = null): void
{
KonekoCacheDriver::put($this->qualifiedKey(), $value, $ttl ?? $this->resolveTTL());
}
public function forget(): void
{
KonekoCacheDriver::forget($this->qualifiedKey());
}
public function remember(?callable $resolver = null, ?int $ttl = null): mixed
{
return $this->rememberWithTTLResolution($resolver ?? fn () => null, $ttl);
}
public function rememberWithTTLResolution(callable $resolver, ?int $ttl = null): mixed
{
return KonekoCacheDriver::remember(
$this->qualifiedKey(),
$ttl ?? $this->resolveTTL(),
$resolver
);
}
// ======================= 🧩 Contexto =========================
public function has(string $qualifiedKey): bool
{
return KonekoCacheDriver::has($qualifiedKey);
}
public function hasContext(): bool
{
return isset($this->context['component'], $this->context['group'], $this->context['sub_group'], $this->context['key_name']);
}
public function reset(): static
{
return $this;
}
// ======================= 🧪 Diagnóstico =========================
public function info(): array
{
return [
'key' => $this->qualifiedKey(),
'context' => $this->context,
'enabled' => $this->isEnabled(),
'ttl' => $this->resolveTTL(),
'driver' => $this->driver(),
];
}
public function infoWithCacheLayers(): array
{
return [
'context' => $this->context,
'qualified_key' => $this->qualifiedKey(),
'enabled' => $this->isEnabled(),
'enabled_source' => $this->resolveEnabledSourceKey(),
'ttl' => $this->resolveTTL(),
'ttl_source' => $this->resolveTtlSourceKey(),
'driver' => $this->driver(),
'has' => $this->has($this->qualifiedKey()),
];
}
}

View File

@ -2,9 +2,9 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Cache;
namespace Koneko\VuexyAdmin\Application\Cache\Manager;
use Illuminate\Support\Facades\{Cache,DB,Redis,File};
use Illuminate\Support\Facades\{Cache, DB, Redis, File};
use Memcached;
/**

View File

@ -0,0 +1,131 @@
<?php
namespace Koneko\VuexyAdmin\Application\Cache\Services;
use Closure;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\Cache\Manager\KonekoCacheManager;
/**
* 🎛️ Servicio central para construcción de variables visuales Koneko.
* Cachea resultados en memoria, permite resolución flexible desde settings + config.
*
* @method self context(string $group, string $section, string|null $subGroup = null)
* @method self setKeyName(string $key)
* @method self forScope(string $scope, int $scope_id)
* @method self forModel(Model $model)
* @method self forUser(Authenticatable|int|null $user)
*/
class KonekoVarsService
{
protected string $namespace = CoreModule::NAMESPACE;
protected string $component = CoreModule::COMPONENT;
protected string $group;
protected string $section;
protected ?string $subGroup = null;
protected ?string $keyName = null;
protected ?string $scope = null;
protected ?int $scope_id = null;
public function context(string $group, string $section, ?string $subGroup = null): static
{
$this->group = $group;
$this->section = $section;
$this->subGroup = $subGroup;
return $this;
}
public function setKeyName(string $key): static
{
$this->keyName = $key;
return $this;
}
public function forScope(string $scope, int $scope_id): static
{
$this->scope = $scope;
$this->scope_id = $scope_id;
return $this;
}
public function forModel(Model $model): static
{
$this->scope = strtolower(class_basename($model));
$this->scope_id = $model->getKey();
return $this;
}
public function forUser(Authenticatable|int|null $user): static
{
$this->scope = 'user';
$this->scope_id = is_int($user) ? $user : ($user?->getAuthIdentifier());
return $this;
}
/**
* Ejecuta y cachea un resultado asociado al key/contexto actual
*
* @param string $key
* @param Closure(): mixed $resolver
* @param int|null $ttl
* @return mixed
*/
public function remember(string $key, Closure $resolver, ?int $ttl = null): mixed
{
return $this->getCacheManager(['key_name' => $key])->remember($resolver, $ttl);
}
/**
* Limpia el valor de caché actual según el contexto y clave
*/
public function clear(): void
{
$this->getCacheManager()->forget();
}
/**
* Devuelve el contexto actual aplicado
*/
public function getContext(): array
{
return [
'namespace' => $this->namespace,
'component' => $this->component,
'group' => $this->group,
'section' => $this->section,
'sub_group' => $this->subGroup,
'scope' => $this->scope,
'scope_id' => $this->scope_id,
'key_name' => $this->keyName,
];
}
/**
* Devuelve la clave completa de caché calificada
*/
public function cacheKey(?string $key = null): string
{
return $this->getCacheManager(['key_name' => $key ?? $this->keyName])->qualifiedKey();
}
/**
* Obtiene el gestor de caché con el contexto aplicado
*/
protected function getCacheManager(array $overrides = []): KonekoCacheManager
{
return cache_manager([
'namespace' => $this->namespace,
'component' => $this->component,
'group' => $this->group,
'section' => $this->section,
'sub_group' => $this->subGroup,
'scope' => $this->scope,
'scope_id' => $this->scope_id,
'key_name' => $overrides['key_name'] ?? $this->keyName,
]);
}
}

View File

@ -1,191 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Cache;
use Illuminate\Support\Facades\{Cache,Config,Schema};
use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
/**
* 🎛️ Servicio de gestión de variables Vuexy Admin y Customizer.
*/
class VuexyVarsBuilderService extends AbstractKeyValueCacheBuilder
{
// Namespace base
private const COMPONENT = 'core';
private const GROUP = 'layout';
//protected string $group = 'layout-builder';
// Cache scope
protected bool $isUserScoped = true;
/** @var string Settings & Cache key */
private const SETTINGS_ADMIN_VARS_KEY = 'admin-vars';
public const SETTINGS_CUSTOMIZER_VARS_KEY = 'customizer-vars';
public function __construct()
{
parent::__construct(self::COMPONENT, self::GROUP);
}
/**
* Obtiene las variables administrativas principales.
*/
public function getAdminVars(?string $key = null): mixed
{
/*
if (!Schema::hasTable('settings')) {
return $this->getDefaultAdminVars($key);
}
*/
$vars = $this->rememberCache(self::SETTINGS_ADMIN_VARS_KEY, function () {
$settings = settings()->setContext($this->component, $this->group)->getGroup(self::SETTINGS_ADMIN_VARS_KEY);
return $this->buildAdminVarsArray($settings);
});
return $key ? ($vars[$key] ?? null) : $vars;
}
/**
* Obtiene las configuraciones del customizador Vuexy.
*/
public function getVuexyCustomizerVars(): array
{
/*
if (!Schema::hasTable('settings')) {
return $this->getDefaultVuexyVars();
}
*/
return $this->rememberCache(self::SETTINGS_CUSTOMIZER_VARS_KEY, function () {
$settings = settings()->setContext($this->component, $this->group)->getGroup(self::SETTINGS_CUSTOMIZER_VARS_KEY);
return $this->buildVuexyCustomizerVars($settings);
});
}
/**
* Elimina las configuraciones del customizador Vuexy.
*/
public static function deleteVuexyCustomizerVars(): void
{
$instance = new static();
$instance->setContext($instance->component, $instance->group);
settings()->setContext($instance->component, $instance->group);
settings()->deleteGroup(self::SETTINGS_CUSTOMIZER_VARS_KEY);
Cache::forget($instance->generateCacheKey(self::SETTINGS_CUSTOMIZER_VARS_KEY));
Cache::forget(cache_manager($instance->component, $instance->group)->key(self::SETTINGS_CUSTOMIZER_VARS_KEY));
}
/**
* Limpia las caches del admin y del customizador Vuexy.
*/
public static function clearCache(): void
{
//Cache::forget(self::SETTINGS_ADMIN_VARS_KEY);
//Cache::forget(self::SETTINGS_CUSTOMIZER_VARS_KEY);
}
/**
* Construye las variables del admin.
*/
private function buildAdminVarsArray(array $settings): array
{
return [
'title' => $settings['title'] ?? config("{$this->namespace}.title", 'Default Title'),
'author' => $settings['author'] ?? config("{$this->namespace}.author", 'Default Author'),
'description' => $settings['description'] ?? config("{$this->namespace}.description", 'Default Description'),
'favicon' => $this->buildFaviconPaths($settings),
'app_name' => $settings['app_name'] ?? config("{$this->namespace}.app_name", 'Default App Name'),
'image_logo' => $this->buildImageLogoPaths($settings),
];
}
/**
* Construye las variables de Vuexy customizer.
*/
private function buildVuexyCustomizerVars(array $settings): array
{
$defaults = config("{$this->namespace}.admin.vuexy");
return collect($defaults)
->mapWithKeys(function ($defaultValue, $key) use ($settings) {
$vuexyKey = $key;
$value = $settings[$vuexyKey] ?? $defaultValue;
if (in_array($key, [
'hasCustomizer', 'displayCustomizer', 'footerFixed',
'menuFixed', 'menuCollapsed', 'showDropdownOnHover'
], true)) {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
return [$key => $value];
})
->toArray();
}
/**
* Construye las rutas de favicon.
*/
private function buildFaviconPaths(array $settings): array
{
$namespace = $settings['favicon_ns'] ?? null;
$defaultFavicon = config("{$this->namespace}.favicon", 'favicon.ico');
return [
'namespace' => $namespace,
'16x16' => $namespace ? "{$namespace}_16x16.png" : $defaultFavicon,
'76x76' => $namespace ? "{$namespace}_76x76.png" : $defaultFavicon,
'120x120' => $namespace ? "{$namespace}_120x120.png" : $defaultFavicon,
'152x152' => $namespace ? "{$namespace}_152x152.png" : $defaultFavicon,
'180x180' => $namespace ? "{$namespace}_180x180.png" : $defaultFavicon,
'192x192' => $namespace ? "{$namespace}_192x192.png" : $defaultFavicon,
];
}
/**
* Construye las rutas de logos.
*/
private function buildImageLogoPaths(array $settings): array
{
$defaultLogo = config("{$this->namespace}.app_logo", 'logo-default.png');
return [
'small' => $settings['image_logo_small'] ?? $defaultLogo,
'medium' => $settings['image_logo_medium'] ?? $defaultLogo,
'large' => $settings['image_logo'] ?? $defaultLogo,
'small_dark' => $settings['image_logo_small_dark'] ?? $defaultLogo,
'medium_dark' => $settings['image_logo_medium_dark'] ?? $defaultLogo,
'large_dark' => $settings['image_logo_dark'] ?? $defaultLogo,
];
}
/**
* Valores de fallback si no hay base de datos.
*/
private function getDefaultAdminVars(?string $key = null): array
{
return $key
? ($this->buildAdminVarsArray([])[$key] ?? null)
: $this->buildAdminVarsArray([]);
}
/**
* Valores de fallback para customizer Vuexy.
*/
private function getDefaultVuexyVars(): array
{
return Config::get("{$this->namespace}.admin.vuexy", []);
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Config\Cast;
class VuexyLayoutCast
{
public function cast(mixed $value, string $key): mixed
{
return match (true) {
in_array($key, ['hasCustomizer', 'displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover']) => (bool) $value,
$key === 'maxQuickLinks' => (int) $value,
default => $value,
};
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Koneko\VuexyAdmin\Application\Config\Contracts;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
interface ConfigRepositoryInterface
{
// ==================== Factory ====================
public static function make(): static;
public static function fromArray(array $context): static;
public static function fromRequest(?Request $request = null): static;
// ==================== Context ====================
public function setNamespace(string $namespace): static;
public function setEnvironment(?string $environment = null): static;
public function setComponent(string $component): static;
public function context(string $group, ?string $section = null, ?string $subGroup = null): static;
public function setContextArray(array $context): static;
public function setScope(Model|string|false $scope, int|null|false $scopeId = false): static;
public function setScopeId(?int $scopeId): static;
public function setUser(Authenticatable|int|null|false $user): static;
public function withScopeFromModel(Model $model): static;
public function setGroup(string $group): static;
public function setSection(string $section): static;
public function setSubGroup(string $subGroup): static;
public function setKeyName(string $keyName): static;
// ==================== Getters ====================
public function get(string $qualifiedKey, mixed $default = null): mixed;
public function fromDb(bool $fromDb = true): static;
public function sourceOf(string $qualifiedKeySufix): ?string;
public function qualifiedKey(?string $key = null): string;
public function getScopeModel(): ?Model;
// ==================== Advanced ====================
public function syncFromRegistry(string $configKey, bool $forceReload = false): static;
/*
public function loadAll(): array;
public function loadByContext(): array;
public function rememberConfig(Closure $callback): mixed;
*/
// ==================== Utils ====================
public function has(string $qualifiedKey): bool;
//public function hasContext(): bool;
public function reset(): static;
// ==================== Diagnostics ====================
public function info(): array;
}

View File

@ -0,0 +1,62 @@
<?php
namespace Koneko\VuexyAdmin\Application\Config\Manager\Concerns;
use Koneko\VuexyAdmin\Application\Traits\System\Context\HasBaseContext;
use Koneko\VuexyAdmin\Application\Traits\System\Context\HasConfigContextValidation;
trait __HasConfigContext
{
use HasBaseContext;
use HasConfigContextValidation;
protected function validateKeyName(string $keyName): string
{
if (!preg_match('/^[a-zA-Z0-9]+$/', $keyName)) {
throw new \InvalidArgumentException("El valor '{$keyName}' de 'keyName' debe ser un slug válido.");
}
if (strlen($keyName) > 64) {
throw new \InvalidArgumentException("El valor de 'keyName' excede 64 caracteres.");
}
return $keyName;
}
// ======================= GETTERS =========================
public function qualifiedKey(?string $key = null): string
{
$parts = [
$this->context['namespace'],
$this->context['component'],
$this->context['group'],
$this->context['section'],
$this->context['sub_group'],
$key ?? $this->context['key_name'],
];
return collect($parts)
->filter()
->implode('.');
}
public function qualifiedKeyPrefix(): string
{
$parts = [
$this->context['namespace'],
$this->context['component'],
];
return collect($parts)->filter()->implode('.');
}
// ======================= HELPERS =========================
public function ensureQualifiedKey(): void
{
if (!$this->hasBaseContext()) {
throw new \InvalidArgumentException("Falta definir el contexto base y 'key_name' en config().");
}
}
}

View File

@ -0,0 +1,257 @@
<?php
namespace Koneko\VuexyAdmin\Application\Config\Manager;
use Illuminate\Support\Facades\{Auth, Config};
use Koneko\VuexyAdmin\Application\Config\Registry\ConfigBlockRegistry;
use Koneko\VuexyAdmin\Application\Config\Contracts\ConfigRepositoryInterface;
use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\Traits\System\Context\{HasBaseContext, HasConfigContextValidation};
final class KonekoConfigManager implements ConfigRepositoryInterface
{
use HasBaseContext;
use HasConfigContextValidation;
public bool $fromDb = false;
public function __construct()
{
$this->setNamespace(CoreModule::NAMESPACE)
->setEnvironment()
->setComponent(CoreModule::COMPONENT);
}
// ==================== Factory ====================
public static function make(): static
{
return new static();
}
// ======================= 🔍 LECTURA =========================
public function get(?string $keyName = null, mixed $default = null): mixed
{
$this->setKeyName($keyName ?? $this->context['key_name']);
// Resolvemos el qualified key
$qualifiedKey = $this->qualifiedKey();
// Si directo, obtenemos el valor directamente de config
if (!$this->fromDb) {
return config($qualifiedKey, $default);
}
// Prioridad 1: override desde settings
$value = settings()
->setContextArray($this->context)
->get($this->context['key_name']);
// Prioridad 2: valor directo de config
return $value ?? config($qualifiedKey, $default);
}
public function fromDb(bool $fromDb = true): static
{
$this->fromDb = $fromDb;
return $this;
}
public function has(string $key): bool
{
$this->setKeyName($key);
return settings()->setContextArray($this->context)->exists($key)
|| config()->has($this->qualifiedKey());
}
public function sourceOf(?string $key = null): string
{
$this->setKeyName($key ?? $this->context['key_name']);
$qualifiedKey = $this->qualifiedKey();
// Prioridad 1: override desde settings
if (settings()->setContextArray($this->context)->exists($this->context['key_name'])) {
return 'database';
}
// Prioridad 2: valor directo de config
if (config()->has($qualifiedKey)) {
return 'config';
}
return 'default';
}
public function info(): array
{
$qualified = $this->qualifiedKey();
return [
'qualified_key' => $qualified,
'context' => $this->context,
'value' => $this->get(),
'source' => $this->sourceOf(),
'has_config' => config()->has($qualified),
'has_db' => settings()->setContextArray($this->context)->exists($this->context['key_name']),
];
}
// ======================= HELPERS =========================
protected function validateSlug(string $field, string $value, int $maxLength): string
{
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $value)) {
throw new \InvalidArgumentException("El valor '{$value}' de '{$field}' debe ser un slug válido.");
}
if (strlen($value) > $maxLength) {
throw new \InvalidArgumentException("El valor de '{$field}' excede {$maxLength} caracteres.");
}
return $value;
}
protected function validateKeyName(string $keyName): string
{
if (!preg_match('/^[a-zA-Z0-9-._]+$/', $keyName)) {
throw new \InvalidArgumentException("El valor '{$keyName}' de 'keyName' debe ser un slug válido.");
}
if (strlen($keyName) > 64) {
throw new \InvalidArgumentException("El valor de 'keyName' excede 64 caracteres.");
}
return $keyName;
}
public function ensureQualifiedKey(): void
{
if (!$this->hasBaseContext()) {
throw new \InvalidArgumentException("Falta definir el contexto base y 'key_name' en config().");
}
}
// ======================= GETTERS =========================
public function qualifiedKey(?string $key = null): string
{
$parts = [
$this->context['namespace'],
$this->context['component'],
$this->context['group'],
$this->context['section'],
$this->context['sub_group'],
$key ?? $this->context['key_name'],
];
return collect($parts)
->filter()
->implode('.');
}
public function qualifiedKeyPrefix(): string
{
$parts = [
$this->context['namespace'],
$this->context['component'],
];
return collect($parts)->filter()->implode('.');
}
// ======================= HELPERS =========================
public function reset(): static
{
return $this;
}
// ======================= Config Blocks =========================
public function syncFromRegistry(string $configKey, bool $forceReload = false): static
{
$config = ConfigBlockRegistry::get($configKey);
$manager = cache_m()
->setComponent($config['component'])
->context($config['group'], $config['section'], $config['sub_group'])
->setUser(Auth::user())
->setKeyName($config['key_name']);
if ($forceReload) {
$manager->forget();
}
$castFn = isset($config['cast']) && class_exists($config['cast'])
? [app($config['cast']), 'cast']
: fn ($v, $k) => $v;
if (!$manager->isEnabled()) {
// Bypass de cache: usamos el callback sin guardar en Redis
$castFn = isset($config['cast']) && class_exists($config['cast'])
? [app($config['cast']), 'cast']
: fn ($v, $k) => $v;
$base = config($configKey, []);
$settings = settings()
->setComponent($config['component'])
->context($config['group'], $config['section'], $config['sub_group'])
->setUser(Auth::user())
->getSubGroup(true);
$merged = array_replace_recursive($base, array_map($castFn, $settings, array_keys($settings)));
} else {
// Cache activada, usamos remember
$merged = $manager->rememberWithTTLResolution(function () use ($configKey, $config, $castFn) {
$base = config($configKey, []);
$settings = settings()
->setComponent($config['component'])
->context($config['group'], $config['section'], $config['sub_group'])
->setUser(Auth::user())
->getSubGroup(true);
return array_replace_recursive($base, array_map($castFn, $settings, array_keys($settings)));
});
}
Config::set($configKey, $merged);
return $this;
}
// ======================= ESCRITURA =========================
public function set(mixed $value, ?string $keyName = null): void
{
$this->setKeyName($keyName ?? $this->context['key_name']);
$qualified = $this->qualifiedKey();
// Seguridad: solo sobrescribir valores existentes en config
if (!config()->has($qualified)) {
throw new \LogicException("❌ No se puede sobrescribir '{$qualified}' porque no existe en archivo de configuración.");
}
// Seguridad: si ya hay un setting y no es de tipo config
$existing = settings()
->setContextArray($this->context)
->get($this->context['key_name']);
if ($existing && !($existing->is_config ?? false)) {
throw new \LogicException("⚠️ El setting '{$qualified}' ya existe en DB pero no está marcado como 'is_config'.");
}
// Escritura segura con flag `is_config = true`
settings()
->setContextArray($this->context)
->markAsSystem(true)
->markAsActive(true)
->setDescription("Override del archivo de configuración '{$qualified}'")
->setHint("Este valor reemplaza el valor original definido en config/")
->setInternalConfigFlag()
->set($value);
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace Koneko\VuexyAdmin\Application\Config\Manager;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Application\Settings\Manager\KonekoSettingManager;
final class ___KonekoConfigManager
{
protected string $namespace;
protected string $component;
protected string $group;
protected string $section = 'config';
protected string $subGroup = 'default';
protected ?string $scope = null;
protected ?string $scope_id = null;
protected ?string $key = null;
public function __construct(
protected KonekoSettingManager $settings
) {}
public function from(string|object $module, string $qualifiedGroup): static
{
[$this->namespace, $this->component] = $this->resolveModuleParts($module);
$parts = explode('.', $qualifiedGroup);
$this->group = $parts[0] ?? 'general';
$this->subGroup = $parts[1] ?? 'default';
return $this;
}
public function scopedToUser(int|string $userId): static
{
$this->scope = 'user';
$this->scope_id = (string) $userId;
return $this;
}
public function setSection(string $section): static
{
$this->section = $section;
return $this;
}
public function get(string $key, mixed $default = null): mixed
{
$this->key = $key;
// Paso 1: SETTINGS DB
$value = $this->settings
->setNamespace($this->namespace)
->setComponent($this->component)
->setGroup($this->group)
->setSection($this->section)
->setSubGroup($this->subGroup)
->setKeyName($this->key);
if ($this->scope && $this->scope_id) {
$value->setScope($this->scope, $this->scope_id);
}
$result = $value->get();
if (!is_null($result)) return $result;
// Paso 2: ENV
$envKey = strtoupper(str_replace('.', '_', $this->qualifiedKey()));
if (env($envKey) !== null) return env($envKey);
// Paso 3: CONFIG LOCAL (config/ publicado)
$configValue = config($this->qualifiedKey());
if (!is_null($configValue)) return $configValue;
// Paso 4: CONFIG DEL MÓDULO (registrado en tiempo de boot)
if (KonekoModuleRegistry::has($this->component)) {
$moduleConfig = config("{$this->namespace}.{$this->component}");
return Arr::get($moduleConfig, $this->nestedKey(), $default);
}
return $default;
}
public function sourceOf(string $key): ?string
{
$this->key = $key;
$checker = $this->settings
->setNamespace($this->namespace)
->setComponent($this->component)
->setGroup($this->group)
->setSection($this->section)
->setSubGroup($this->subGroup);
if ($this->scope && $this->scope_id) {
$checker->setScope($this->scope, $this->scope_id);
}
if ($checker->exists($key)) return 'settings';
$envKey = strtoupper(str_replace('.', '_', $this->qualifiedKey()));
if (env($envKey) !== null) return 'env';
if (!is_null(config($this->qualifiedKey()))) return 'config';
if (KonekoModuleRegistry::has($this->component)) return 'module';
return null;
}
protected function resolveModuleParts(string|object $module): array
{
if (is_object($module)) {
$module = get_class($module);
}
if (class_exists($module) && defined("$module::NAMESPACE") && defined("$module::COMPONENT")) {
return [constant("$module::NAMESPACE"), constant("$module::COMPONENT")];
}
if (Str::contains($module, '.')) {
return explode('.', $module, 2);
}
throw new \InvalidArgumentException("No se pudo resolver el módulo desde: {$module}");
}
protected function qualifiedKey(): string
{
return implode('.', [
$this->namespace,
$this->component,
$this->group,
$this->subGroup,
$this->key
]);
}
protected function nestedKey(): string
{
return implode('.', [
$this->group,
$this->subGroup,
$this->key
]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Koneko\VuexyAdmin\Application\Config\Registry;
final class ConfigBlockRegistry
{
protected static array $blocks = [];
public static function register(string $key, array $config): void
{
static::$blocks[$key] = $config;
}
public static function get(string $key): array
{
if (!static::exists($key)) {
throw new \InvalidArgumentException("Bloque '$key' no registrado.");
}
return static::$blocks[$key];
}
public static function exists(string $key): bool
{
return isset(static::$blocks[$key]);
}
public static function all(): array
{
return static::$blocks;
}
public static function clear(): void
{
static::$blocks = [];
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace Koneko\VuexyAdmin\Application\Config\Builder;
use Illuminate\Support\Arr;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Koneko\VuexyAdmin\Application\Settings\KonekoSettingManager;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
class KonekoConfigResolverService
{
protected KonekoSettingManager $settings;
public function __construct(KonekoSettingManager $settings)
{
$this->settings = $settings;
$this
->setNamespace(CoreModule::NAMESPACE)
->setEnvironment();
}
/**
* Obtiene una clave con jerarquía: settings > .env > config local > config de módulo.
*/
public function get(string $qualifiedKey, mixed $default = null): mixed
{
// Extraer segmentos para contexto (namespace.component.group.section.subgroup.key)
$parts = explode('.', $qualifiedKey);
if (count($parts) < 3) {
return config($qualifiedKey, $default); // fallback si es un config no calificado
}
$namespace = $parts[0];
$component = $parts[1];
$group = $parts[2] ?? 'general';
$section = 'config';
$subGroup = $parts[3] ?? 'default';
$key = $parts[4] ?? end($parts);
// 1. SETTINGS (BD, sección config)
$value = $this->settings
->setNamespace($namespace)
->setComponent($component)
->setGroup($group)
->setSection($section)
->setSubGroup($subGroup)
->setKeyName($key)
->get();
if (!is_null($value)) return $value;
// 2. ENV (si existe como override)
$envKey = strtoupper(str_replace('.', '_', $qualifiedKey));
if (env($envKey) !== null) {
return env($envKey);
}
// 3. CONFIG LOCAL (config/koneko/{component}.php)
$value = config($qualifiedKey);
if (!is_null($value)) return $value;
// 4. CONFIG DEL MÓDULO (registrado por orquestador)
$moduleKey = "$namespace.$component";
if ($module = KonekoModuleRegistry::get($component)) {
$moduleConfig = config($moduleKey);
$nestedKey = implode('.', array_slice($parts, 2));
return Arr::get($moduleConfig, $nestedKey, $default);
}
return $default;
}
/**
* Resuelve una clave usando una clase declarativa del módulo como contexto.
*
* @param string $moduleClass Ej: CoreModule::class
* @param string $qualifiedKey Ej: 'core.menu.cache.ttl'
* @param mixed $default
*/
public function fromModuleClass(string $moduleClass, string $qualifiedKey, mixed $default = null): mixed
{
if (!class_exists($moduleClass)) {
throw new \InvalidArgumentException("Clase de módulo no encontrada: {$moduleClass}");
}
if (!defined("$moduleClass::NAMESPACE") || !defined("$moduleClass::COMPONENT")) {
throw new \InvalidArgumentException("La clase de módulo debe definir las constantes NAMESPACE y COMPONENT.");
}
$namespace = constant("$moduleClass::NAMESPACE");
$component = constant("$moduleClass::COMPONENT");
// Prefijar si aún no lo está (ej: 'core.menu.cache.ttl' -> 'koneko.core.menu.cache.ttl')
if (!str_starts_with($qualifiedKey, "$namespace.")) {
$qualifiedKey = "$namespace.$qualifiedKey";
}
return $this->get($qualifiedKey, $default);
}
/**
* Devuelve la fuente de la clave (para inspección o debugging).
*/
public function sourceOf(string $qualifiedKey): ?string
{
$parts = explode('.', $qualifiedKey);
if (count($parts) < 3) return null;
$namespace = $parts[0];
$component = $parts[1];
$group = $parts[2] ?? 'general';
$section = 'config';
$subGroup = $parts[3] ?? 'default';
$key = $parts[4] ?? end($parts);
$has = $this->settings
->setNamespace($namespace)
->setComponent($component)
->setGroup($group)
->setSection($section)
->setSubGroup($subGroup)
->exists($key);
if ($has) return 'settings';
$envKey = strtoupper(str_replace('.', '_', $qualifiedKey));
if (env($envKey) !== null) {
return 'env';
}
if (!is_null(config($qualifiedKey))) {
return 'config';
}
if (KonekoModuleRegistry::has($component)) {
return 'module';
}
return null;
}
}

View File

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Contracts\ApiRegistry;
use Illuminate\Support\Collection;
use Koneko\VuexyAdmin\Models\ExternalApi;
/**
* Contrato para servicios de registro de APIs externas.
*/
interface ExternalApiRegistryInterface
{
public function all(): Collection;
public function active(): Collection;
public function groupByProvider(): Collection;
public function groupByModule(): Collection;
public function forModule(string $module): Collection;
public function forProvider(string $provider): Collection;
public function summary(): array;
public function slugifyName(string $name): string;
public function find(string $slug): ?ExternalApi;
}

View File

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Contracts\Catalogs;
interface CatalogServiceInterface
{
public function catalogs(): array;
public function exists(string $catalog): bool;
public function getCatalog(string $catalog, string $searchTerm = '', array $options = []): array;
public function getCatalogMeta(string $catalog): array;
}

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Contracts\Factories;
interface ProbabilisticAttributesFactoryInterface
{
public function maybe(int $percentage, mixed $value): mixed;
public function maybeDefault(mixed $value): mixed;
}

View File

@ -1,89 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Contracts\Modules;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
interface KonekoModuleServiceInterface
{
/**
* Lista los módulos instalados en el sistema.
*
* @return KonekoModule[]
*/
public function listInstalled(): array;
/**
* Lista los módulos disponibles en el marketplace oficial.
*
* @return array Módulos con metadatos
*/
public function listAvailable(): array;
/**
* Obtiene un módulo instalado por su slug.
*/
public function get(string $slug): ?KonekoModule;
/**
* Instala un módulo oficial desde el marketplace.
*/
public function installFromMarketplace(string $slug): bool;
/**
* Instala un módulo desde una URL externa (ej. GitHub, repositorio privado).
*/
public function installFromUrl(string $url): bool;
/**
* Activa un módulo previamente instalado.
*/
public function enable(string $slug): bool;
/**
* Desactiva un módulo instalado (sin eliminarlo físicamente).
*/
public function disable(string $slug): bool;
/**
* Desinstala un módulo: elimina archivos y entrada en base de datos.
*/
public function uninstall(string $slug): bool;
/**
* Ejecuta migraciones de un módulo.
*/
public function migrate(string $slug): bool;
/**
* Reversión de migraciones del módulo (rollback).
*/
public function rollback(string $slug): bool;
/**
* Publica los assets del módulo.
*/
public function publishAssets(string $slug): bool;
/**
* Elimina los assets previamente publicados.
*/
public function removeAssets(string $slug): bool;
/**
* Sincroniza los módulos detectados en el filesystem con los registrados en la DB.
*/
public function syncFromModules(): bool;
/**
* Lista todos los paquetes composer (instalados) que podrían ser módulos.
*/
public function detectAllComposerModules(): array;
/**
* Obtiene metadatos extendidos de un módulo (instalado o no).
*/
public function getExtendedDetails(string $slug): array;
}

View File

@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Contracts\Settings;
use Koneko\VuexyAdmin\Models\Setting;
/**
* Contrato para los servicios de gestión de Settings modulares.
* Proporciona una API fluida para acceder y modificar configuraciones de manera modular.
*/
interface SettingsRepositoryInterface
{
public function get(string $key, ...$args): mixed;
public function set(string $key, mixed $value, ?int $userId = null, ...$args): ?Setting;
public function delete(string $key, ?int $userId = null): bool;
public function exists(string $key): bool;
public function getGroup(string $group, ?int $userId = null): array;
public function getComponent(string $component, ?int $userId = null): array;
public function deleteGroup(string $group, ?int $userId = null): int;
public function deleteComponent(string $component, ?int $userId = null): int;
public function listGroups(): array;
public function listComponents(): array;
public function currentNamespace(): string;
public function currentGroup(): string;
public function setContext(string $component, string $group): static;
public function setComponent(string $component): static;
public function setGroup(string $group): static;
}

View File

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Contracts\Settings;
use Koneko\VuexyAdmin\Models\Setting;
/**
* Contrato para los servicios de gestión de Settings modulares.
* Proporciona una API fluida para acceder y modificar configuraciones de manera modular.
*/
interface SettingsRepositoryInterface
{
public function set(string $key, mixed $value): ?Setting;
public function get(string $key, mixed $default = null): mixed;
public function delete(string $key): bool;
public function exists(string $key): bool;
public function markAsSystem(bool $state = true): static;
public function markAsEncrypted(bool $state = true): static;
public function markAsSensitive(bool $state = true): static;
public function markAsEditable(bool $state = true): static;
public function markAsActive(bool $state = true): static;
public function withoutUsageTracking(): static;
}

View File

@ -0,0 +1,9 @@
<?php
namespace Koneko\VuexyAdmin\Application;
final class CoreModule
{
public const NAMESPACE = 'koneko';
public const COMPONENT = 'core';
}

View File

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Enums\ExternalApi;
enum ApiProvider: string
{
case Google = 'google';
case Banxico = 'banxico';
case SAT = 'sat';
case Custom = 'custom';
case Esys = 'esys';
case Facebook = 'facebook';
case Twitter = 'twitter';
case TawkTo = 'tawk_to';
public function label(): string
{
return match ($this) {
self::Google => 'Google',
self::Banxico => 'Banxico',
self::SAT => 'SAT (México)',
self::Custom => 'Personalizado',
self::Esys => 'ESYS',
self::Facebook => 'Facebook',
self::Twitter => 'Twitter',
self::TawkTo => 'Tawk.to',
};
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\Enums\Settings;
use Koneko\VuexyAdmin\Support\Traits\Enums\HasEnumHelpers;
enum SettingEnvironment: string
{
use HasEnumHelpers;
case PROD = 'prod';
case DEV = 'dev';
case STAGING = 'staging';
case TEST = 'test';
}

View File

@ -1,16 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\Enums\Settings;
use Koneko\VuexyAdmin\Support\Traits\Enums\HasEnumHelpers;
enum SettingScope: string
{
use HasEnumHelpers;
case GLOBAL = 'global';
case TENANT = 'tenant';
case BRANCH = 'branch';
case USER = 'user';
case GUEST = 'guest';
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Events\Notifications;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class NotificationEmitted implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public readonly object $notification
) {
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('notifications'),
];
}
}

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Events\Settings;
use Illuminate\Foundation\Events\Dispatchable;
class SettingChanged
{
use Dispatchable;
/**
* @param string $key Clave completa del setting (ej. 'koneko.admin.site.logo')
* @param string $namespace Namespace base (ej. 'koneko.admin.site.')
* @param int|null $userId Usuario relacionado si es setting scoped, null si es global
*/
public function __construct(public string $key) {}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Support\Helpers;
namespace Koneko\VuexyAdmin\Application\Helpers;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;

View File

@ -6,14 +6,13 @@ namespace Koneko\VuexyAdmin\Application\Helpers;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use Koneko\VuexyAdmin\Application\CoreModule;
class VuexyHelper
{
public const NAMESPACE = 'koneko';
public static function appClasses()
{
$data = config('koneko.admin.vuexy');
$data = config_m()->get('layout.vuexy', []);
// default data array
$DefaultData = [
@ -203,8 +202,10 @@ class VuexyHelper
{
if (isset($pageConfigs)) {
if (count($pageConfigs) > 0) {
$config_path = CoreModule::NAMESPACE . '.' . CoreModule::COMPONENT . '.layout.vuexy.';
foreach ($pageConfigs as $config => $val) {
Config::set('koneko.admin.vuexy.' . $config, $val);
Config::set($config_path . $config, $val);
}
}
}

View File

@ -6,8 +6,8 @@ namespace Koneko\VuexyAdmin\Application\Http\Controllers;
use Illuminate\Routing\Controller;
use Illuminate\Http\{JsonResponse, Request};
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Application\UX\Navbar\{VuexyQuicklinksBuilderService,VuexySearchBarBuilderService};
use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\UX\Navbar\VuexySearchBarBuilder;
class VuexyNavbarController extends Controller
{
@ -21,7 +21,7 @@ class VuexyNavbarController extends Controller
{
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
return response()->json(app(VuexySearchBarBuilderService::class)->getSearchData());
return response()->json(app(VuexySearchBarBuilder::class)->getSearchData());
}
/**
@ -36,15 +36,26 @@ class VuexyNavbarController extends Controller
{
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
/** @var string Settings Context */
$group = 'website-admin';
$section = 'layout';
$sub_group = 'navbar';
/** @var string Cache keyName */
$key_name = 'quicklinks';
$validated = $request->validate([
'action' => 'required|in:update,remove',
'route' => 'required|string',
]);
$key = 'vuexy-quicklinks.user';
$userId = Auth::user()->id;
//$userId = Auth::user()->id;
$quickLinks = settings()->setContext('core', 'navbar')->get($key, $userId)?? [];
$quickLinks = settings(CoreModule::COMPONENT)
->context($group, $section, $sub_group)
->setScope($request->user())
->get($key_name)?? [];
if ($validated['action'] === 'update') {
if (!in_array($validated['route'], $quickLinks)) {
@ -58,8 +69,11 @@ class VuexyNavbarController extends Controller
));
}
settings()->setContext('core', 'navbar')->set($key, json_encode($quickLinks), $userId);
settings(CoreModule::COMPONENT)
->context($group, $section, $sub_group)
->setScope($request->user())
->set($key_name, json_encode($quickLinks));
VuexyQuicklinksBuilderService::forgetCacheForUser();
//VuexyQuicklinksBuilder::forgetCacheForUser();
}
}

View File

@ -1,146 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
use Illuminate\Routing\Controller;
use Illuminate\Http\Request;
use Laravel\Fortify\Features;
class UsersAuthController extends Controller
{
/*
public function loginView()
{
dd($viewMode);
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.login-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function registerView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function confirmPasswordView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.confirm-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function resetPasswordView()
{
if (!Features::enabled(Features::resetPasswords()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function requestPasswordResetLinkView(Request $request)
{
if (!Features::enabled(Features::resetPasswords()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.reset-password-{$viewMode}", ['pageConfigs' => $pageConfigs, 'request' => $request]);
}
public function twoFactorChallengeView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.two-factor-challenge-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function twoFactorRecoveryCodesView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function twoFactorAuthenticationView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function verifyEmailView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.verify-email-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function showEmailVerificationForm()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
public function userProfileView()
{
if (!Features::enabled(Features::registration()))
abort(403, 'El registro está deshabilitado.');
$viewMode = config('vuexy.custom.authViewMode');
$pageConfigs = ['myLayout' => 'blank'];
return view("vuexy-admin::auth.register-{$viewMode}", ['pageConfigs' => $pageConfigs]);
}
*/
}

View File

@ -1,190 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Http\Controllers;
use Illuminate\Routing\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Koneko\VuexyAdmin\Application\UX\Navbar\{VuexyQuicklinksBuilderService,VuexySearchBuilderService};
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarInitialsService;
use Koneko\VuexyAdmin\Application\UX\ConfigBuilders\System\EnvironmentVarsTableConfigBuilder;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* Controlador para la gestión de funcionalidades administrativas de Vuexy
*/
class VuexyController extends Controller
{
public function __construct(
private readonly AvatarInitialsService $avatarService
) {}
/**
* Realiza búsqueda en la barra de navegación
*
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
public function searchNavbar(): JsonResponse
{
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
return response()->json(app(VuexySearchBuilderService::class)->getForUser());
}
/**
* Actualiza los enlaces rápidos del usuario
*
* @param Request $request Datos de la solicitud
* @return void
* @throws \Illuminate\Http\Exceptions\HttpResponseException
* @throws \Illuminate\Validation\ValidationException
*/
public function quickLinksUpdate(Request $request): void
{
abort_if(!request()->expectsJson(), 403, __('errors.ajax_only'));
$validated = $request->validate([
'action' => 'required|in:update,remove',
'route' => 'required|string',
]);
$quickLinks = settings()->get('quicklinks', Auth::user()->id, []);
if ($validated['action'] === 'update') {
if (!in_array($validated['route'], $quickLinks)) {
$quickLinks[] = $validated['route'];
}
} elseif ($validated['action'] === 'remove') {
$quickLinks = array_values(array_filter(
$quickLinks,
fn($route) => $route !== $validated['route']
));
}
settings()->set('quicklinks', json_encode($quickLinks), Auth::user()->id, 'vuexy-admin');
app(VuexyQuicklinksBuilderService::class)->clearCache(Auth::user());
}
/**
* Muestra la vista de configuraciones generales
*
* @return \Illuminate\View\View
*/
public function generalSettings(): View
{
return view('vuexy-admin::general-settings.index');
}
/**
* Muestra la vista de configuraciones SMTP
*
* @return \Illuminate\View\View
*/
public function smtpSettings(): View
{
return view('vuexy-admin::smtp-settings.index');
}
/**
* Muestra la vista de configuraciones de interfaz
*
* @return \Illuminate\View\View
*/
public function InterfaceSettings(): View
{
return view('vuexy-admin::interface-settings.index');
}
/**
* Muestra el listado de accesos al sistema (Bootstrap Table AJAX or View).
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
*/
public function userLogs(Request $request): JsonResponse|View
{
if ($request->ajax()) {
$builder = app(UserLoginTableConfigBuilder::class)->getQueryBuilder($request);
return $builder->getJson();
}
return view('vuexy-admin::user-logs.index');
}
/**
* Muestra el listado de eventos de auditoría (Bootstrap Table AJAX or View).
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse|\Illuminate\View\View
*/
public function securityEvents(Request $request): JsonResponse|View
{
if ($request->ajax()) {
$builder = app(SecurityEventsTableConfigBuilder::class)->getQueryBuilder($request);
return $builder->getJson();
}
return view('vuexy-admin::security-events.index');
}
/**
* Display a listing of the resource (Bootstrap Table AJAX or View).
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\View\View
*/
public function environmentVars(Request $request): JsonResponse|View
{
if ($request->ajax()) {
$builder = app(EnvironmentVarsTableConfigBuilder::class)->getQueryBuilder($request);
return $builder->getJson();
}
return view('vuexy-admin::environment-vars.index');
}
public function generateAvatar(Request $request): BinaryFileResponse
{
try {
$validated = $request->validate([
'name' => 'required|string|max:255',
'color' => 'nullable|string|regex:/^[0-9a-fA-F]{6}$/',
'background' => 'nullable|string|regex:/^[0-9a-fA-F]{6}$/',
'size' => 'nullable|integer|min:20|max:1024',
'max_length' => 'nullable|integer|min:1|max:3'
]);
$response = $this->avatarService->getAvatarImage(
name: $validated['name'],
forcedColor: $validated['color'] ?? null,
forcedBackground: $validated['background'] ?? null,
size: $validated['size'] ?? null,
maxLength: $validated['max_length'] ?? null
);
$response->headers->set('Cache-Control', 'public, max-age=86400');
return $response;
} catch (\Throwable $e) {
return $this->avatarService->getAvatarImage(
name: 'E R R',
forcedColor: '#FF0000',
maxLength: 3
);
}
}
}

View File

@ -6,30 +6,24 @@ namespace Koneko\VuexyAdmin\Application\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\View;
use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
use Koneko\VuexyAdmin\Application\UX\Content\VuexyBreadcrumbsBuilderService;
use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
use Koneko\VuexyAdmin\Application\UX\Breadcrumbs\VuexyBreadcrumbsBuilder;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
use Koneko\VuexyAdmin\Application\UX\Notifications\VuexyNotificationsBuilderService;
use Koneko\VuexyAdmin\Application\UX\Template\{VuexyConfigSynchronizer};
use Koneko\VuexyAdmin\Application\UX\Notifications\Builder\VuexyNotificationsBuilder;
class AdminTemplateMiddleware
{
public function __construct()
{
//
}
public function handle($request, Closure $next)
{
// Aplicar configuración de layout antes de que la vista se cargue
if (str_contains($request->header('Accept'), 'text/html')) {
app(VuexyConfigSynchronizer::class)->sync();
config_m()->syncFromRegistry('koneko.core.layout.vuexy');
View::share([
'_admin' => app(VuexyVarsBuilderService::class)->getAdminVars(),
'_admin' => app(KonekoAdminVarsBuilder::class)->get(),
'vuexyMenu' => app(VuexyMenuFormatter::class)->getMenu(),
'vuexyNotifications' => app(VuexyNotificationsBuilderService::class)->getForUser(),
'vuexyBreadcrumbs' => app(VuexyBreadcrumbsBuilderService::class)->getBreadcrumbs(),
'vuexyNotifications' => app(VuexyNotificationsBuilder::class)->getNotifications(),
'vuexyBreadcrumbs' => app(VuexyBreadcrumbsBuilder::class)->getBreadcrumbs(),
]);
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Listeners\Authentication;
use Illuminate\Auth\Events\Failed;
use Koneko\VuexyAdmin\Application\Logger\KonekoSecurityAuditLogger;
use Koneko\VuexyAdmin\Application\Loggers\KonekoSecurityAuditLogger;
class HandleFailedLogin
{

View File

@ -5,9 +5,10 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Listeners\Authentication;
use Illuminate\Auth\Events\Logout;
use Koneko\VuexyAdmin\Application\UX\Navbar\{VuexySearchBarBuilderService,VuexyQuicklinksBuilderService};
use Koneko\VuexyAdmin\Application\UX\Navbar\VuexySearchBarBuilder;
use Koneko\VuexyAdmin\Application\UX\Navbar\VuexyQuicklinksBuilder;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
use Koneko\VuexyAdmin\Application\UX\Notifications\VuexyNotificationsBuilderService;
use Koneko\VuexyAdmin\Application\UX\Notifications\VuexyNotificationsBuilder;
use Koneko\VuexyAdmin\Models\UserLogin;
class HandleUserLogout
@ -28,8 +29,8 @@ class HandleUserLogout
private function clearUserCaches(int $userId): void
{
VuexyMenuFormatter::forgetCacheForUser($userId);
VuexySearchBarBuilderService::forgetCacheForUser($userId);
VuexyQuicklinksBuilderService::clearCacheForUser($userId);
VuexyNotificationsBuilderService::clearCacheForUser($userId);
VuexySearchBarBuilder::forgetCacheForUser($userId);
VuexyQuicklinksBuilder::clearCacheForUser($userId);
VuexyNotificationsBuilder::clearCacheForUser($userId);
}
}

View File

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Listeners\Settings;
use Koneko\VuexyAdmin\Application\Events\Settings\VuexyCustomizerSettingsUpdated;
use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
class ApplyVuexyCustomizerSettings
{
public function handle(VuexyCustomizerSettingsUpdated $event): void
{
foreach ($event->settings as $key => $value) {
settings()->self('vuexy')->set($key, $value);
}
VuexyVarsBuilderService::clearCache();
}
}

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Listeners\Settings;
use Illuminate\Support\Facades\Cache;
use Koneko\VuexyAdmin\Application\Events\Settings\SettingChanged;
class SettingCacheListener
{
public function handle(SettingChanged $event): void
{
$keyHash = md5($event->key);
/*
$groupHash = md5($event->namespace);
$suffix = $event->userId !== null ? "u:{$event->userId}" : 'global';
Cache::forget("koneko.admin.settings.setting:{$keyHash}:{$suffix}");
Cache::forget("koneko.admin.settings.group:{$groupHash}:{$suffix}");
*/
}
}

View File

@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Services;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Models\SystemLog;
use Koneko\VuexyAdmin\Application\Enums\{LogLevel, LogTriggerType};
class ____SystemLoggerService
{
/**
* Registra un log en la tabla system_logs.
*/
public function log(
string|LogLevel $level,
string $module,
string $message,
array $context = [],
LogTriggerType $triggerType = LogTriggerType::System,
?int $triggerId = null,
?Model $relatedModel = null
): SystemLog {
return SystemLog::create([
'module' => $module,
'level' => $level instanceof LogLevel ? $level : LogLevel::from($level),
'message' => $message,
'context' => $context,
'trigger_type' => $triggerType,
'trigger_id' => $triggerId,
'user_id' => Auth::id(),
'related_model_type' => $relatedModel?->getMorphClass(),
'related_model_id' => $relatedModel?->getKey(),
]);
}
public function info(
string $module,
string $message,
array $context = [],
LogTriggerType $triggerType = LogTriggerType::System,
?int $triggerId = null,
?Model $relatedModel = null
): SystemLog {
return $this->log(LogLevel::Info, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
}
public function warning(
string $module,
string $message,
array $context = [],
LogTriggerType $triggerType = LogTriggerType::System,
?int $triggerId = null,
?Model $relatedModel = null
): SystemLog {
return $this->log(LogLevel::Warning, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
}
public function error(
string $module,
string $message,
array $context = [],
LogTriggerType $triggerType = LogTriggerType::System,
?int $triggerId = null,
?Model $relatedModel = null
): SystemLog {
return $this->log(LogLevel::Error, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
}
public function debug(
string $module,
string $message,
array $context = [],
LogTriggerType $triggerType = LogTriggerType::System,
?int $triggerId = null,
?Model $relatedModel = null
): SystemLog {
return $this->log(LogLevel::Debug, $module, $message, $context, $triggerType, $triggerId, $relatedModel);
}
}

View File

@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Logger;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use Koneko\VuexyAdmin\Models\UserInteraction;
use Koneko\VuexyAdmin\Application\Enums\UserInteractions\InteractionSecurityLevel;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
class ____UserInteractionLoggerService
{
public function record(
string $action,
array $context = [],
InteractionSecurityLevel|string $security = InteractionSecurityLevel::Normal,
?string $livewireComponent = null
): ?UserInteraction {
$user = Auth::user();
if (!$user) {
return null;
}
$component = $this->resolveCurrentComponent();
return UserInteraction::create([
'user_id' => $user->id,
'actor_type' => $user->actor_type ?? 'unknown',
'component' => $component,
'livewire_component' => $livewireComponent,
'action' => $action,
'security_level' => is_string($security) ? $security : $security->value,
'ip_address' => Request::ip(),
'user_agent' => Request::userAgent(),
'context' => $context,
'user_flags' => $user->flags ?? [],
'user_roles' => $user->roles ?? [],
]);
}
/**
* Infiera el componente actual (el módulo) para la auditoría.
*/
private function resolveCurrentComponent(): string
{
// Opcional: Puedes mejorarlo si quieres tracking de "módulo activo"
$modules = KonekoModuleRegistry::enabled();
if (empty($modules)) {
return 'unknown-module';
}
// Si hay muchos módulos activos, podrías basarlo en la ruta actual
$currentRoute = request()->route()?->getName();
foreach ($modules as $module) {
if (str_contains($currentRoute, $module->slug)) {
return $module->getId(); // Este es slugificado
}
}
// Fallback: solo devuelve el primero activo
return reset($modules)?->getId() ?? 'unknown-module';
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Logger;
namespace Koneko\VuexyAdmin\Application\Loggers;
use GeoIp2\Database\Reader;
use Illuminate\Http\Request;

View File

@ -1,13 +1,13 @@
<?php
namespace Koneko\VuexyAdmin\Support\Logger;
namespace Koneko\VuexyAdmin\Application\Loggers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Jenssegers\Agent\Agent;
use Koneko\VuexyAdmin\Application\Enums\SecurityEvents\SecurityEventStatus;
use Koneko\VuexyAdmin\Application\Enums\SecurityEvents\SecurityEventType;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
use Koneko\VuexyAdmin\Support\Enums\SecurityEvents\SecurityEventStatus;
use Koneko\VuexyAdmin\Support\Enums\SecurityEvents\SecurityEventType;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Models\SecurityEvent;
use Koneko\VuexyAdmin\Support\Geo\GeoLocationResolver;
@ -26,7 +26,7 @@ class KonekoSecurityLogger
$geo = GeoLocationResolver::resolve($request->ip());
return SecurityEvent::create([
'module' => KonekoComponentContextRegistrar::currentComponent() ?? 'unknown',
'module' => KonekoModuleRegistry::current()->componentNamespace ?? 'unknown',
'user_id' => $userId ?? Auth::id(),
'event_type' => (string) $type,
'status' => SecurityEventStatus::NEW,

View File

@ -1,14 +1,13 @@
<?php
namespace Koneko\VuexyAdmin\Alication\Logger;
namespace Koneko\VuexyAdmin\Application\Loggers;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Koneko\VuexyAdmin\Application\Enums\SystemLog\{LogTriggerType, LogLevel};
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Models\SystemLog;
use Koneko\VuexyAdmin\Support\Enums\SystemLog\LogLevel;
use Koneko\VuexyAdmin\Support\Enums\SystemLog\LogTriggerType;
use Illuminate\Support\Facades\Auth;
/**
* Logger de sistema contextual para el ecosistema Koneko
@ -19,7 +18,7 @@ class KonekoSystemLogger
public function __construct(?string $component = null)
{
$this->component = $component ?? KonekoComponentContextRegistrar::currentComponent() ?? 'core';
$this->component = $component ?? KonekoModuleRegistry::current()->componentNamespace ?? 'core';
}
public function log(
@ -32,7 +31,7 @@ class KonekoSystemLogger
): SystemLog {
return SystemLog::create([
'module' => $this->component,
'user_id' => auth()->id(),
'user_id' => Auth::id(),
'level' => $level instanceof LogLevel ? $level->value : $level,
'message' => $message,
'context' => $context,

View File

@ -2,14 +2,14 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Logger;
namespace Koneko\VuexyAdmin\Application\Loggers;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Auth;
use Koneko\VuexyAdmin\Application\Enums\UserInteractions\InteractionSecurityLevel;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Models\UserInteraction;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Support\Enums\UserInteractions\InteractionSecurityLevel;
/**
* 📋 Logger de interacciones de usuario con nivel de seguridad.
@ -32,7 +32,7 @@ class KonekoUserInteractionLogger
$user = Auth::user();
return UserInteraction::create([
'module' => KonekoComponentContextRegistrar::currentComponent(),
'module' => KonekoModuleRegistry::current()->componentNamespace ?? 'core',
'user_id' => $userId,
'action' => $action,
'livewire_component'=> $livewireComponent,

View File

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Logger;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Log;
use Koneko\VuexyAdmin\Application\Services\SystemLoggerService;
class LogMacrosServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Macro principal: log central
Log::macro('system', function (): SystemLoggerService {
return app(SystemLoggerService::class);
});
// 🔧 Ejemplo de uso inmediato (puedes remover o modificar):
/*
Log::system()->info(
module: 'vuexy-admin',
message: 'Sistema de macros de log inicializado correctamente',
context: ['environment' => app()->environment()]
);
*/
}
}

View File

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Macros;
use Illuminate\Support\Facades\App;
App::macro('settings', function () {
$settingsService = app(\Koneko\VuexyAdmin\Application\System\SettingsService::class);
return new class($settingsService) {
public function __construct(private $settingsService) {}
public function get(string $key, ...$args): mixed
{
return $this->settingsService->get($key, ...$args);
}
public function set(string $key, mixed $value, ...$args): mixed
{
return $this->settingsService->set($key, $value, ...$args);
}
public function admin(): object
{
return new class($this->settingsService) {
public function __construct(private $settingsService) {}
public function get(string $key, ...$args): mixed
{
return $this->settingsService->get('koneko.admin.' . $key, ...$args);
}
public function set(string $key, mixed $value, ...$args): mixed
{
return $this->settingsService->set('koneko.admin.' . $key, $value, ...$args);
}
};
}
};
});

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Macros;
use Illuminate\Support\Facades\Log;
Log::macro('vuexyAdminLogger', function () {
return new class {
public function info(string $message, array $context = []) {
return Log::system()->info('vuexy-admin', $message, $context);
}
public function warning(string $message, array $context = []) {
return Log::system()->warning('vuexy-admin', $message, $context);
}
public function error(string $message, array $context = []) {
return Log::system()->error('vuexy-admin', $message, $context);
}
public function debug(string $message, array $context = []) {
return Log::system()->debug('vuexy-admin', $message, $context);
}
};
});

View File

@ -1,167 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Macros;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Collection;
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
App::macro('vuexySettings', function () {
$settingsService = app(SettingsRepositoryInterface::class);
return new class($settingsService) {
/**
* @var string Default namespace assigned during module boot
*/
private string $defaultNamespace = '';
/**
* @var string|null Current namespace used for operations
*/
private ?string $currentNamespace = null;
/**
* Constructor.
*
* @param SettingsRepositoryInterface $settingsService
*/
public function __construct(private SettingsRepositoryInterface $settingsService) {}
/**
* Sets the default namespace (typically from the module at boot time).
*
* @param string $namespace
* @return self
*/
public function setDefaultNamespace(string $namespace): self
{
$this->defaultNamespace = rtrim($namespace, '.') . '.';
$this->currentNamespace = $this->defaultNamespace;
return $this;
}
/**
* Sets the namespace to the module's own namespace.
*
* @return self
*/
public function self(): self
{
$this->currentNamespace = $this->defaultNamespace;
return $this;
}
/**
* Switches the namespace to a custom one.
*
* @param string $namespace
* @return self
*/
public function in(string $namespace): self
{
$this->currentNamespace = rtrim($namespace, '.') . '.';
return $this;
}
/**
* Gets a setting key with the current namespace applied.
*
* @param string $key
* @param mixed ...$args
* @return mixed
*/
public function get(string $key, ...$args): mixed
{
return $this->settingsService->get($this->qualifyKey($key), ...$args);
}
/**
* Sets a setting key with the current namespace applied.
*
* @param string $key
* @param mixed $value
* @param mixed ...$args
* @return mixed
*/
public function set(string $key, mixed $value, ...$args): mixed
{
return $this->settingsService->set($this->qualifyKey($key), $value, ...$args);
}
/**
* Lists all groups (namespaces) available.
*
* @return SettingsGroupCollection
*/
public function listGroups(): SettingsGroupCollection
{
// Extraemos todos los keys agrupados
$allSettings = $this->settingsService->getGroup('');
// Mapeamos
$groups = collect($allSettings)
->keys()
->map(function ($key) {
$parts = explode('.', $key);
return $parts[0] ?? 'unknown';
})
->unique()
->values();
// Aplicamos filtro si es namespace específico
if ($this->currentNamespace !== null) {
$namespaceRoot = rtrim($this->currentNamespace, '.');
$groups = $groups->filter(fn($group) => $group === $namespaceRoot);
}
return new SettingsGroupCollection($groups);
}
/**
* Returns the current namespace.
*
* @return string
*/
public function currentNamespace(): string
{
return $this->currentNamespace ?? $this->defaultNamespace;
}
/**
* Internal method to prepend namespace to key.
*
* @param string $key
* @return string
*/
protected function qualifyKey(string $key): string
{
return $this->currentNamespace . $key;
}
};
});
/**
* Small helper class for group collection.
*/
class SettingsGroupCollection extends Collection
{
/**
* Adds details (number of keys per group, etc.) to each group.
*
* @return Collection
*/
public function details(): Collection
{
return $this->map(function ($group) {
// Aquí puedes agregar más metadata en el futuro
return [
'name' => $group,
'key_prefix' => $group . '.',
'total_keys' => settings()->in($group)->countKeys(),
];
});
}
}

View File

@ -2,11 +2,12 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\RBAC;
namespace Koneko\VuexyAdmin\Application\RBAC\Sync;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Koneko\VuexyAdmin\Application\Bootstrap\{KonekoModuleBootManager, KonekoModuleRegistry};
use Koneko\VuexyAdmin\Application\Bootstrap\Manager\KonekoModuleBootManager;
use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Models\{PermissionMeta, RoleMeta, PermissionGroup};
use Spatie\Permission\Exceptions\PermissionDoesNotExist;

View File

@ -1,64 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\Security;
use Illuminate\Support\Facades\Crypt;
use Koneko\VuexyAdmin\Models\VaultKey;
class VaultKeyService
{
public function generateKey(
string $alias,
string $ownerProject = 'default_project',
string $algorithm = 'AES-256-CBC',
bool $isSensitive = true
): VaultKey {
if (!in_array($algorithm, openssl_get_cipher_methods())) {
throw new \InvalidArgumentException("Algoritmo no soportado: $algorithm");
}
$keyMaterial = random_bytes(openssl_cipher_iv_length($algorithm) * 2);
$encryptedKey = Crypt::encrypt($keyMaterial);
return VaultKey::create([
'environment' => app()->environment(),
'namespace' => config('app.name'),
'scope' => 'global',
'alias' => $alias,
'owner_project' => $ownerProject,
'algorithm' => $algorithm,
'key_material' => $encryptedKey,
'is_active' => true,
'is_sensitive' => $isSensitive,
'rotated_at' => now(),
'rotation_count' => 0,
]);
}
public function retrieveKey(string $alias): string
{
$keyEntry = VaultKey::where('alias', $alias)->where('is_active', true)->firstOrFail();
return Crypt::decrypt($keyEntry->key_material);
}
public function rotateKey(string $alias): VaultKey
{
$keyEntry = VaultKey::where('alias', $alias)->firstOrFail();
$newKeyMaterial = random_bytes(openssl_cipher_iv_length($keyEntry->algorithm) * 2);
$keyEntry->update([
'key_material' => Crypt::encrypt($newKeyMaterial),
'rotated_at' => now(),
'rotation_count' => $keyEntry->rotation_count + 1,
]);
return $keyEntry;
}
public function deactivateKey(string $alias): bool
{
return VaultKey::where('alias', $alias)->update(['is_active' => false]);
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
/**
* Trait para soporte de procesamiento por lotes (chunks) en seeders.

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
/**
* Trait para generación de registros faker vía Factory con fallback.

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
/**
* Trait para emitir logs desde seeders compatibles con Artisan o CLI.

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
/**
* Trait para manejar barras de progreso CLI desde seeders.

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
trait SanitizeRowWithFillableAndCasts
{

View File

@ -7,10 +7,9 @@ namespace Koneko\VuexyAdmin\Application\Seeding;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Koneko\VuexyAdmin\Application\Seeding\SeederReportBuilder;
use Koneko\VuexyAdmin\Application\Traits\Seeders\Main\HasSeederLogger;
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarImageService;
use Koneko\VuexyAdmin\Application\UI\Avatar\AvatarInitialsService;
use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
use Koneko\VuexyAdmin\Application\Seeding\Concerns\Main\HasSeederLogger;
use Koneko\VuexyAdmin\Application\UI\Avatar\{AvatarImageService, AvatarInitialsService};
use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder;
class SeederOrchestrator
{

View File

@ -0,0 +1,54 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Concerns;
use Carbon\Carbon;
use Koneko\VuexyAdmin\Application\Settings\SettingDefaults;
trait HasSettingAttributes
{
protected array $attributes = [
'is_system' => false,
'is_sensitive' => false,
'is_file' => false,
'is_encrypted' => false,
'is_config' => false,
'is_editable' => true,
'is_track_usage' => SettingDefaults::DEFAULT_TRACK_USAGE,
'is_should_cache' => SettingDefaults::DEFAULT_SHOULD_CACHE,
'is_active' => true,
'expires_at' => null,
];
public function markAsSystem(bool $state = true): static { return $this->setFlag('is_system', $state); }
public function markAsSensitive(bool $state = true): static { return $this->setFlag('is_sensitive', $state); }
public function markAsEditable(bool $state = true): static { return $this->setFlag('is_editable', $state); }
public function markAsActive(bool $state = true): static { return $this->setFlag('is_active', $state); }
public function expiresAt(Carbon|string|false|null $date): static
{
$this->attributes['expires_at'] = $date instanceof Carbon
? $date
: ($date ? Carbon::parse($date) : null);
return $this;
}
public function trackUsage(bool $state = true): static
{
$this->attributes['track_usage'] = $state;
return $this;
}
// ==================== Helpers ====================
protected function setFlag(string $flag, bool $value = true): static
{
$this->attributes[$flag] = $value;
return $this;
}
// ======================= 🔐 FLAG PRIVADO =========================
public function setInternalConfigFlag(bool $state = true): static { return $this->setFlag('is_config', $state); }
}

View File

@ -0,0 +1,137 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Concerns;
use Closure;
use Carbon\Carbon;
use Koneko\VuexyAdmin\Application\Cache\Driver\KonekoCacheDriver;
use Koneko\VuexyAdmin\Application\Cache\Manager\KonekoCacheManager;
use Koneko\VuexyAdmin\Models\Setting;
trait HasSettingCache
{
protected array $cache = [
'cache_ttl' => null,
'cache_expires_at' => null,
];
public function enableCache(bool $state = true): static
{
$this->attributes['is_should_cache'] = $state;
return $this;
}
public function setCacheTTL(int $seconds): static
{
$this->attributes['is_should_cache'] = true;
$this->cache['cache_ttl'] = $seconds;
return $this;
}
public function setCacheExpiresAt(Carbon|string|false|null $date): static
{
$this->attributes['is_should_cache'] = true;
$this->cache['cache_expires_at'] = $date instanceof Carbon
? $date
: ($date ? Carbon::parse($date) : null);
return $this;
}
public function forgetCache(?string $keyName = null): static
{
$this->getCacheManager()
->setKeyName($keyName ?? $this->context['key_name'])
->forget();
return $this;
}
public function remember(Closure $callback): mixed
{
$manager = $this->getCacheManager();
// Desactivar cache o forzar bypass
if (!$manager->isEnabled() || $this->bypassCache) {
return $callback();
}
$key = $manager->qualifiedKey();
$cached = KonekoCacheDriver::get($key);
if (!is_null($cached)) {
return $cached;
}
// Ejecutar callback y guardar en caché
$value = $callback();
// Si el valor es null, no lo cacheamos
if (!is_null($value)) {
$model = $this->queryForModel(); // <- Asegúrate de definirlo en el manager
$ttl = $model ? $this->resolveModelTTL($model, $manager) : $manager->resolveTTL();
if ($ttl > 0) {
$manager->put($value, $ttl);
}
}
return $value;
}
public function cacheModel(?Setting $model = null): void
{
$model ??= $this->queryForModel();
$manager = $this->getCacheManager();
// validación
if (!$manager->isEnabled()) {
$manager->forget();
return;
}
if ($model && $this->shouldCacheModel($model)) {
$ttl = $this->resolveModelTTL($model, $manager);
if ($ttl > 0) {
$manager->put($model->value, $ttl);
}
} else {
$manager->forget();
}
}
public function getCacheManager(): KonekoCacheManager
{
return cache_m(
$this->context['component'],
$this->context['group'],
$this->context['sub_group'],
)
->setScope($this->context['scope'])
->setScopeId($this->context['scope_id'])
->setKeyName($this->context['key_name']);
}
// ==================== Helpers ====================
protected function shouldCacheModel(Setting $model): bool
{
return $model->is_active
&& $model->is_should_cache
&& !$model->is_encrypted
&& !$model->is_sensitive;
}
protected function resolveModelTTL(Setting $model, KonekoCacheManager $manager): int
{
if ($model->cache_expires_at instanceof Carbon) {
$ttl = now()->diffInMinutes($model->cache_expires_at, false);
return $ttl > 0 ? $ttl : 0;
}
return $model->cache_ttl ?? $manager->resolveTTL();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Concerns;
use Carbon\Carbon;
use Koneko\VuexyAdmin\Application\Settings\SettingDefaults;
trait HasSettingEncryption
{
protected array $encryption = [
'encryption_algorithm' => null,
'encryption_key' => null,
'encryption_rotated_at' => null,
];
public function enableEncryption(bool $state = true): static
{
$this->attributes['is_encrypted'] = $state;
if ($state) {
$this->setEncryption(
$this->encryption['encryption_algorithm'],
$this->encryption['encryption_key']
);
}
return $this;
}
public function setEncryption(string $algorithm = SettingDefaults::DEFAULT_ALGORITHM, ?string $key = null): static
{
$this->attributes['is_encrypted'] = true;
$this->encryption['encryption_algorithm'] = $algorithm;
$this->encryption['encryption_key'] = $key ?? config('app.key');
return $this;
}
public function setEncryptionAlgorithm(string $algorithm): static
{
$this->attributes['is_encrypted'] = true;
$this->encryption['encryption_algorithm'] = $algorithm;
return $this;
}
public function setEncryptionKey(string $key): static
{
$this->attributes['is_encrypted'] = true;
$this->encryption['encryption_key'] = $key;
if ($this->encryption['encryption_algorithm'] === null) {
$this->encryption['encryption_algorithm'] = SettingDefaults::DEFAULT_ALGORITHM;
}
return $this;
}
public function setEncryptionRotatedAt(Carbon|string|false|null $date): static
{
if (!$this->attributes['is_encrypted']) {
throw new \InvalidArgumentException('Debe activar la encriptación antes de establecer la fecha de rotación');
}
$this->encryption['encryption_rotated_at'] = $date instanceof Carbon
? $date
: ($date ? Carbon::parse($date) : null);
return $this;
}
// ==================== Validaciones ====================
protected function validateEncryption(): void
{
if ($this->attributes['is_encrypted'] && empty($this->encryption['encryption_key'])) {
throw new \InvalidArgumentException("Se requiere 'encryption_key' para valores cifrados.");
}
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Concerns;
use Illuminate\Http\UploadedFile;
trait HasSettingFileSupport
{
protected array $file = [
'mime_type' => null,
'file_name' => null,
];
public function enableFile(bool $state = true): static
{
$this->attributes['is_file'] = $state;
return $this;
}
public function setFile(string $mime_type, string $file_name): static
{
$this->attributes['is_file'] = true;
$this->file['mime_type'] = $mime_type;
$this->file['file_name'] = $file_name;
return $this;
}
public function setMimeType(string $mime_type): static
{
$this->attributes['is_file'] = true;
$this->file['mime_type'] = $mime_type;
return $this;
}
public function setFileName(string $file_name): static
{
$this->attributes['is_file'] = true;
$this->file['file_name'] = $file_name;
return $this;
}
public function handleFileUpload(UploadedFile $file, string $storageDisk = 'public'): static
{
$path = $file->store('settings_files', $storageDisk);
$this->setFile(
mime_type: $file->getMimeType() ?? 'application/octet-stream',
file_name: basename($path)
);
return $this;
}
// ==================== Validaciones ====================
protected function validateFile(): void
{
if ($this->attributes['is_file']) {
if (empty($this->file['mime_type']) || empty($this->file['file_name'])) {
throw new \InvalidArgumentException("Se requiere 'mime_type' y 'file_name' para archivos.");
}
}
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Concerns;
trait HasSettingMetadata
{
protected array $metadata = [
'description' => null,
'hint' => null,
];
public function setDescription(string $description): static
{
$this->metadata['description'] = $description;
return $this;
}
public function setHint(string $hint): static
{
$this->metadata['hint'] = $hint;
return $this;
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Contracts;
use Carbon\Carbon;
use Closure;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\{Request, UploadedFile};
use Illuminate\Support\Collection;
use Koneko\VuexyAdmin\Application\Settings\SettingDefaults;
use Koneko\VuexyAdmin\Models\Setting;
interface SettingsRepositoryInterface
{
// ==================== Factory ====================
public static function make(): static;
public static function fromArray(array $context): static;
public static function fromRequest(?Request $request = null): static;
// ==================== Context ====================
public function setNamespace(string $namespace): static;
public function setEnvironment(string $environment): static;
public function setComponent(string $component): static;
public function context(string $group, ?string $section = null, ?string $subGroup = null): static;
public function setContextArray(array $context): static;
public function setScope(Model|string|false $scope, int|null|false $scopeId = false): static;
public function setScopeId(?int $scopeId): static;
public function setUser(Authenticatable|int|null|false $user): static;
public function withScopeFromModel(Model $model): static;
public function setGroup(string $group): static;
public function setSection(string $section): static;
public function setSubGroup(string $subGroup): static;
public function setKeyName(string $keyName): static;
public function includeDisabled(bool $state = true): static;
public function includeExpired(bool $state = true): static;
public function bypassCache(bool $state = true): static;
public function asArray(bool $state = true): static;
// ==================== Encryption ====================
public function enableEncryption(bool $state = true): static;
public function setEncryption(string $algorithm = SettingDefaults::DEFAULT_ALGORITHM, ?string $key = null): static;
public function setEncryptionAlgorithm(string $algorithm): static;
public function setEncryptionKey(string $key): static;
public function setEncryptionRotatedAt(Carbon|string|false|null $date): static;
// ==================== Files ====================
public function enableFile(bool $state = true): static;
public function setFile(string $mime_type, string $file_name): static;
public function setMimeType(string $mime_type): static;
public function setFileName(string $file_name): static;
public function handleFileUpload(UploadedFile $file, string $storageDisk = 'public'): static;
// ==================== Markers ====================
public function markAsSystem(bool $state = true): static;
public function markAsSensitive(bool $state = true): static;
public function markAsEditable(bool $state = true): static;
public function markAsActive(bool $state = true): static;
public function expiresAt(Carbon|string|false|null $date): static;
public function trackUsage(bool $state = true): static;
public function setInternalConfigFlag(bool $state = true): static;
// ==================== Metadata ====================
public function setDescription(string $description): static;
public function setHint(string $hint): static;
// ==================== CRUD ====================
public function set(mixed $value, ?string $keyName = null): void;
public function get(?string $keyName = null, mixed $default = null): mixed;
public function delete(string $qualifiedKey): void;
public function deleteByContext(): int;
public function deleteGroup(): int;
public function deleteSubGroup(): int;
// ==================== Fetchers ====================
public function getGroup(bool $asArray = false): Collection|array;
public function getSubGroup(bool $asArray = false): Collection|array;
public function getComponents(bool $asArray = false): Collection|array;
public function getGroups(bool $asArray = false): Collection|array;
public function getSubGroups(bool $asArray = false): Collection|array;
// ==================== Cache ====================
public function enableCache(bool $state = true): static;
public function setCacheTTL(int $seconds): static;
public function setCacheExpiresAt(Carbon|string|false|null $date): static;
public function cacheModel(?Setting $model = null): void;
public function forgetCache(?string $keyName = null): static;
public function remember(Closure $callback): mixed;
// ==================== Getters ====================
public function qualifiedKey(?string $key = null): string;
public function exists(string $qualifiedKey): bool;
public function existsByContext(): bool;
public function isUsable(): bool;
public function getScopeModel(): ?Model;
// ==================== Utils ====================
public function has(string $qualifiedKey): bool;
public function hasContext(): bool;
public function setInactiveByContext(): int;
public function reset(): void;
// ==================== Diagnostics ====================
public function info(): array;
}

View File

@ -0,0 +1,383 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Manager;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
use Koneko\VuexyAdmin\Application\Cache\Builders\SettingCacheKeyBuilder;
use Koneko\VuexyAdmin\Application\Cache\Driver\KonekoCacheDriver;
use Koneko\VuexyAdmin\Application\Cache\Contracts\CacheRepositoryInterface;
use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\Settings\Concerns\{HasSettingAttributes, HasSettingCache, HasSettingEncryption, HasSettingFileSupport, HasSettingMetadata};
use Koneko\VuexyAdmin\Application\Settings\SettingDefaults;
use Koneko\VuexyAdmin\Application\Traits\System\Context\{HasBaseContext, HasContextQueryBuilder, HasSettingsContextValidation};
use Koneko\VuexyAdmin\Models\Setting;
final class KonekoSettingManager implements SettingsRepositoryInterface
{
use HasBaseContext;
use HasSettingAttributes;
use HasSettingEncryption;
use HasSettingFileSupport;
use HasSettingMetadata;
use HasSettingCache;
use HasContextQueryBuilder;
use HasSettingsContextValidation;
private bool $includeDisabled = false;
private bool $includeExpired = false;
private bool $asArray = false;
protected bool $bypassCache = false;
protected string $settingModel = Setting::class;
public function __construct()
{
$this->setNamespace(CoreModule::NAMESPACE)
->setEnvironment()
->setComponent(CoreModule::COMPONENT)
->setGroup(SettingDefaults::DEFAULT_GROUP)
->setSection(SettingDefaults::DEFAULT_SECTION)
->setSubGroup(SettingDefaults::DEFAULT_SUB_GROUP);
}
// ==================== Factory ====================
public static function make(): static
{
return new static();
}
// ==================== Context ====================
public function includeDisabled(bool $state = true): static
{
$this->includeDisabled = $state;
return $this;
}
public function includeExpired(bool $state = true): static
{
$this->includeExpired = $state;
return $this;
}
public function bypassCache(bool $state = true): static
{
$this->bypassCache = $state;
return $this;
}
public function asArray(bool $state = true): static
{
$this->asArray = $state;
return $this;
}
// ==================== CRUD ====================
public function set(mixed $value, ?string $keyName = null): void
{
$this->validateContextWithScope();
if ($keyName) {
$this->setKeyName($keyName);
}
$qualifiedKey = $this->qualifiedKey();
/** @var Setting $setting */
$setting = $this->settingModel::updateOrCreate(
['key' => $qualifiedKey],
array_merge(
$this->context,
$this->attributes,
$this->file,
$this->encryption,
$this->cache,
$this->metadata
)
);
$setting->value = $value;
$setting->save();
$this->cacheModel($setting);
$this->reset();
}
public function setGroupSettings(array $data): void
{
$this->validateContextWithScope();
foreach ($data as $key => $value) {
$this->setKeyName($key);
$qualifiedKey = $this->qualifiedKey();
/** @var Setting $setting */
$setting = $this->settingModel::updateOrCreate(
['key' => $qualifiedKey],
array_merge(
$this->context,
$this->attributes,
$this->file,
$this->encryption,
$this->cache,
$this->metadata
)
);
$setting->value = $value;
$setting->save();
$this->cacheModel($setting);
}
$this->reset();
}
public function get(?string $keyName = null, mixed $default = null): mixed
{
if ($this->isTableNotExists()) return $default;
if ($keyName) {
$this->setKeyName($keyName);
}
$this->validateContextWithScope();
$manager = $this->getCacheManager();
if (!$manager->isEnabled() || $this->bypassCache) {
return $this->queryByKey()->first()?->value ?? $default;
}
$key = $manager->qualifiedKey();
$cached = KonekoCacheDriver::get($key);
if (!is_null($cached)) {
return $cached;
}
$model = $this->queryByKey()->first();
if (!$model) {
$manager->forget();
return $default;
}
$this->cacheModel($model);
return $model->value;
}
public function exists(string $key): bool
{
return SettingCacheKeyBuilder::isQualified($key)
? $this->settingModel::query()->where('key', $key)->exists()
: $this->queryByKey()->exists();
}
public function existsByContext(): bool
{
$this->validateContextWithScope();
return $this->query()->exists();
}
public function delete(string $qualifiedKey): void
{
$this->settingModel::where('key', $qualifiedKey)->delete();
$this->getCacheManager()->setKeyName($qualifiedKey)->forget();
}
public function deleteByContext(): int
{
$this->validateContextWithScope();
return $this->query()
->tap(fn($q) => $q->each(fn($setting) => $this->getCacheManager()->setKeyName($setting->key_name)->forget()))
->delete()
->count();
}
public function deleteGroup(): int
{
return $this->queryByGroup($this->newQuery(), $this->context)
->tap(fn($q) => $q->each(fn($setting) => $this->getCacheManager()->setKeyName($setting->key_name)->forget()))
->delete()
->count();
}
public function deleteSubGroup(): int
{
return $this->queryBySubGroup($this->newQuery(), $this->context)
->tap(fn($q) => $q->each(fn($setting) => $this->getCacheManager()->setKeyName($setting->key_name)->forget()))
->delete()
->count();
}
// ================= Fetchers =================
public function getGroup(bool $asArray = false): Collection|array
{
$query = $this->queryByGroup($this->newQuery(), $this->context)->get();
return $asArray || $this->asArray ? $query->pluck('group')->toArray() : $query;
}
public function getSubGroup(bool $asArray = false): Collection|array
{
if ($this->isTableNotExists()) return [];
$query = $this->queryBySubGroup($this->newQuery(), $this->context)->get();
return $asArray || $this->asArray ? $query->pluck('sub_group')->toArray() : $query;
}
public function getComponents(bool $asArray = false): Collection|array
{
$query = $this->newQuery()
->select('component')
->distinct()
->get();
return $asArray || $this->asArray ? $query->pluck('component')->toArray() : $query;
}
public function getGroups(bool $asArray = false): Collection|array
{
$query = $this->newQuery()
->select('group')
->distinct()
->get();
return $asArray || $this->asArray ? $query->pluck('group')->toArray() : $query;
}
public function getSubGroups(bool $asArray = false): Collection|array
{
$query = $this->newQuery()
->select('sub_group')
->distinct()
->get();
return $asArray || $this->asArray ? $query->pluck('sub_group')->toArray() : $query;
}
// ======================= HELPERS =========================
public function queryForModel(): ?Model
{
return $this->queryByKey()->first();
}
public function getCacheManager(): CacheRepositoryInterface
{
return cache_m()->setContextArray($this->context);
}
public function isUsable(): bool
{
return $this->queryByKey()
->where('is_active', true)
->where(function ($q) {
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
})
->exists();
}
public function setInactiveByContext(): int
{
return $this->query()->update([
'is_active' => false,
'expires_at' => now(),
]);
}
public function has(string $qualifiedKey): bool
{
return $this->queryByKey()->where('key', $qualifiedKey)->exists();
}
public function hasContext(): bool
{
return $this->hasBaseContext()
&& $this->hasGroupContext();
}
public function defaults(): static
{
$this->reset();
return $this;
}
public function reset(): void
{
$this->includeDisabled = false;
$this->includeExpired = false;
$this->asArray = false;
$this->bypassCache = false;
$this->context['group'] = SettingDefaults::DEFAULT_GROUP;
$this->context['section'] = SettingDefaults::DEFAULT_SECTION;
$this->context['sub_group'] = SettingDefaults::DEFAULT_SUB_GROUP;
$this->attributes = [
'is_system' => false,
'is_sensitive' => false,
'is_file' => false,
'is_encrypted' => false,
'is_editable' => true,
'is_track_usage' => SettingDefaults::DEFAULT_TRACK_USAGE,
'is_should_cache' => SettingDefaults::DEFAULT_SHOULD_CACHE,
'is_active' => true,
'expires_at' => null,
];
$this->file = [
'mime_type' => null,
'file_name' => null,
];
$this->encryption = [
'encryption_algorithm' => SettingDefaults::DEFAULT_ALGORITHM,
'encryption_key' => null,
'encryption_rotated_at' => null,
];
$this->cache = [
'cache_ttl' => null,
'cache_expires_at' => null,
];
$this->metadata = [
'description' => null,
'hint' => null,
];
}
public function info(): array
{
return [
'context' => $this->context,
'attributes' => $this->attributes,
'file' => $this->file,
'encryption' => $this->encryption,
'cache' => $this->cache,
'metadata' => $this->metadata,
];
}
// ======================= PROTECTED =========================
protected function isTableNotExists(): bool
{
return !Schema::hasTable((new $this->settingModel)->getTable());
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings\Registry;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Koneko\VuexyAdmin\Support\Traits\Auth\HasResolvableUser;
final class ScopeRegistry
{
use HasResolvableUser;
protected static array $registeredScopes = [];
/**
* Registra un nuevo tipo de scope.
*/
public static function register(string $scope, string $modelClass): void
{
if (!is_subclass_of($modelClass, Model::class)) {
throw new \InvalidArgumentException("El modelo '{$modelClass}' debe extender Illuminate\\Database\\Eloquent\\Model.");
}
static::$registeredScopes[$scope] = $modelClass;
}
/**
* Verifica si un scope está registrado.
*/
public static function isRegistered(string $scope): bool
{
return isset(static::$registeredScopes[$scope]) || in_array($scope, static::$registeredScopes);
}
/**
* Devuelve la clase del modelo asociado a un scope.
*/
public static function modelFor(string $scope): ?string
{
return static::$registeredScopes[$scope] ?? null;
}
/**
* Devuelve una instancia de modelo para el scope e ID dados.
*/
public static function getModelInstance(string $scope, int|string $id): ?Model
{
$modelClass = static::modelFor($scope);
if (!$modelClass || !class_exists($modelClass)) {
return null;
}
return $modelClass::find($id);
}
/**
* Retorna todos los scopes registrados.
*/
public static function all(): array
{
return static::$registeredScopes;
}
/**
* Descubre el contexto de scope de un modelo.
*/
public static function resolveScopeFromModel(Model $model): ?array
{
foreach (static::$registeredScopes as $scope => $modelClass) {
if ($model instanceof $modelClass) {
return [
'scope' => $scope,
'scope_id' => $model->getKey(),
];
}
}
return null;
}
/**
* Descubre el contexto scope del usuario actual.
*/
public static function guessUserScopeContext(Authenticatable|int|null|false $user = null): ?array
{
$userId = static::resolveUserId($user);
if (!$userId || !static::isRegistered('user')) {
return null;
}
return [
'scope' => 'user',
'scope_id'=> $userId,
];
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Koneko\VuexyAdmin\Application\Settings;
final class SettingDefaults
{
public const DEFAULT_GROUP = 'settings';
public const DEFAULT_SECTION = 'component';
public const DEFAULT_SUB_GROUP = 'default';
public const DEFAULT_ALGORITHM = 'AES-256-CBC';
public const DEFAULT_SHOULD_CACHE = true;
public const DEFAULT_TRACK_USAGE = false;
}

View File

@ -1,333 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\System;
use Illuminate\Support\Facades\{Event, Schema};
use Illuminate\Support\Str;
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Events\Settings\SettingChanged;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
class KonekoSettingManager extends AbstractKeyValueCacheBuilder implements SettingsRepositoryInterface
{
protected bool $is_system = false;
protected bool $is_encrypted = false;
protected bool $is_sensitive = false;
protected bool $is_editable = true;
protected bool $is_active = true;
// Module name
protected string $settingModel = Setting::class;
public function __construct($component, $group, $subGroup, $scope)
{
$this->setContext($component, $group, $subGroup, $scope);
}
/**
* Establece un setting.
*/
public function set(string $key, mixed $value, ?int $userId = null, ...$args): ?Setting
{
if (!preg_match('/^[a-z0-9\.\-_]+$/', $key)) {
throw new \InvalidArgumentException("La clave '{$key}' no es válida. Solo se permiten caracteres alfanuméricos, '.', '-', y '_'.");
}
$fullKey = $this->qualifyKey($key);
$columns = [
'namespace' => $this->namespace,
'module' => $this->module,
'user_id' => $userId,
'value_string' => null,
'value_integer' => null,
'value_boolean' => null,
'value_float' => null,
'value_text' => null,
'value_binary' => null,
];
if (is_string($value)) {
$columns[strlen($value) > 250 ? 'value_text' : 'value_string'] = $value;
} elseif (is_int($value)) {
$columns['value_integer'] = $value;
} elseif (is_bool($value)) {
$columns['value_boolean'] = $value;
} elseif (is_float($value)) {
$columns['value_float'] = $value;
} elseif (is_array($value) || is_object($value)) {
$columns['value_text'] = is_string($value)
? $value
: json_encode($value, JSON_UNESCAPED_UNICODE);
}
$setting = Setting::updateOrCreate(
['key' => $fullKey, 'user_id' => $userId],
$columns
);
Event::dispatch(new SettingChanged(
key: $fullKey,
namespace: $this->namespace,
userId: $userId
));
return $setting;
}
/**
* Obtiene un setting por su clave calificada.
*/
public function get(string $key, ...$args): mixed
{
$fullKey = $this->qualifyKey($key);
return $this->rememberCache($key, fn () => $this->query($fullKey));
}
/**
* Obtiene todos los settings de un componente.
*/
public function getScoped(string $component, ?string $group = null, ?int $userId = null): array {
$query = $this->settingModel::query()
->where('namespace', $this->namespace)
->where('component', $component);
if (!is_null($group)) {
$query->where('group', $group);
}
if (!is_null($userId)) {
$query->where('user_id', $userId);
}
return $query->get()->toArray();
}
/**
* Obtiene todos los settings de un componente.
*/
public function getComponent(string $component, ?int $userId = null): array
{
return $this->getScoped($component, null, $userId);
}
/**
* Obtiene todos los settings de un grupo.
*/
public function getGroup(string $group, ?int $userId = null): array
{
return $this->getScoped($this->component, $group, $userId);
}
/**
* Elimina un setting por su clave calificada.
*/
public function delete(string $key, ?int $userId = null): bool
{
$fullKey = $this->qualifyKey($key);
$deleted = $this->settingModel::where('key', $fullKey)->delete();
if ($deleted) {
Event::dispatch(new SettingChanged(
key: $fullKey,
namespace: $this->namespace,
userId: $userId
));
}
return $deleted > 0;
}
/**
* Elimina todos los settings de un componente.
*/
public function deleteScoped(string $component, ?string $group = null, ?int $userId = null): int {
$query = $this->settingModel::query()
->where('namespace', $this->namespace)
->where('component', $component);
if (!is_null($group)) {
$query->where('group', $group);
}
if (!is_null($userId)) {
$query->where('user_id', $userId);
}
return $query->delete();
}
/**
* Elimina todos los settings de un componente.
*/
public function deleteComponent(string $component, ?int $userId = null): int
{
return $this->deleteScoped(
component: $component,
group: null,
userId: $userId
);
}
/**
* Elimina todos los settings de un grupo.
*/
public function deleteGroup(string $group, ?int $userId = null): int
{
if (empty($this->component)) {
throw new \InvalidArgumentException("El componente es obligatorio.");
}
return $this->deleteScoped(
component: $this->component,
group: $group,
userId: $userId
);
}
/**
* Obtiene todos los componentes.
*/
public function listComponents(): array
{
return Setting::select('component')->distinct()->get()->pluck('component')->toArray();
}
/**
* Obtiene todos los grupos.
*/
public function listGroups(): array
{
if (empty($this->component)) {
throw new \InvalidArgumentException("El componente es obligatorio.");
}
return Setting::select('group')->where('component', $this->component)->distinct()->get()->pluck('group')->toArray();
}
public function markAsSystem(bool $state = true): static
{
$this->is_system = $state;
return $this;
}
public function markAsEncrypted(bool $state = true): static
{
$this->is_encrypted = $state;
return $this;
}
public function markAsSensitive(bool $state = true): static
{
$this->is_sensitive = $state;
return $this;
}
public function markAsEditable(bool $state = true): static
{
$this->is_editable = $state;
return $this;
}
public function markAsActive(bool $state = true): static
{
$this->is_active = $state;
return $this;
}
/**
* Verifica si un setting existe.
*/
public function exists(string $key): bool
{
return $this->settingModel::where('key', $this->qualifyKey($key))->exists();
}
/**
* Genera una clave calificada para un setting.
*/
protected function qualifyKey(string $key): string
{
$this->validateContext();
return "{$this->namespace}.{$this->component}.{$this->group}.{$key}";
}
/**
* Obtiene un setting por su clave calificada.
*/
protected function query(string $key): mixed
{
if (!Schema::hasTable('settings')) return null;
$setting = $this->settingModel::where('key', $key)->first();
return $setting ? $this->decode($setting) : null;
}
/**
* Decodifica el valor de un setting.
*/
protected function decode(Setting $setting, bool $asArray = true): mixed
{
// Orden de prioridad: JSON largo, texto simple, luego tipos básicos
$value = $setting->value_text
?? $setting->value_string
?? $setting->value_integer
?? $setting->value_boolean
?? $setting->value_float
?? $setting->value_binary
?? null;
if (is_string($value)) {
$value = trim($value);
// Limpieza de caracteres invisibles si es string
$value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
// Intentar decodificar si parece JSON
if (Str::startsWith($value, ['[', '{'])) {
$decoded = json_decode($value, $asArray);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
logger()->warning('⚠️ JSON decode failed', [
'key' => $setting->key,
'value_preview' => Str::limit($value, 200),
'error' => json_last_error_msg(),
]);
}
}
return $value;
}
}

View File

@ -1,234 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\Settings;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
use Koneko\VuexyAdmin\Application\Enums\Settings\SettingValueType;
use Koneko\VuexyAdmin\Application\Events\Settings\SettingChanged;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
class KonekoSettingManager extends AbstractKeyValueCacheBuilder// implements SettingsRepositoryInterface
{
// Flags de control del setting
protected bool $is_system = false;
protected bool $is_encrypted = false;
protected bool $is_sensitive = false;
protected bool $is_editable = true;
protected bool $is_active = true;
protected string $settingModel = Setting::class;
// ========== Fluent Flag Setters ==========
public function markAsSystem(bool $state = true): static { $this->is_system = $state; return $this; }
public function markAsEncrypted(bool $state = true): static { $this->is_encrypted = $state; return $this; }
public function markAsSensitive(bool $state = true): static { $this->is_sensitive = $state; return $this; }
public function markAsEditable(bool $state = true): static { $this->is_editable = $state; return $this; }
public function markAsActive(bool $state = true): static { $this->is_active = $state; return $this; }
// ========== Settings Operations ==========
public function set(string $key, mixed $value): ?Setting
{
$this->validateKey($key);
//$this->user =
$fullKey = $this->generateCacheKey($key);
$columns = [
'namespace' => $this->namespace,
'environment' => app()->environment(),
'scope' => $this->scope,
'component' => $this->component,
'module' => $this->module,
'group' => $this->group,
'sub_group' => $this->subGroup,
'key_name' => $key,
'user_id' => $userId,
'is_system' => $this->is_system,
'is_encrypted' => $this->is_encrypted,
'is_sensitive' => $this->is_sensitive,
'is_editable' => $this->is_editable,
'is_active' => $this->is_active,
];
$this->assignValueColumns($columns, $value);
$setting = Setting::updateOrCreate(
['key' => $fullKey],
$columns
);
Event::dispatch(new SettingChanged($fullKey, $this->namespace, $userId));
return $setting;
}
public function get(string $key): mixed
{
$fullKey = $this->generateCacheKey($key);
return $this->rememberCache($key, fn() => $this->query($fullKey));
}
public function delete(string $key): bool
{
$fullKey = $this->generateCacheKey($key);
$deleted = Setting::where('key', $fullKey)->delete();
if ($deleted) {
Event::dispatch(new SettingChanged($fullKey));
}
return $deleted > 0;
}
public function exists(string $key): bool
{
return Setting::where('key', $this->generateCacheKey($key))->exists();
}
// ===================== BÚSQUEDA AVANZADA =====================
public function getScoped(string $scope, ?int $userId = null): array
{
return Setting::where('scope', $scope)
->when($userId, fn($q) => $q->where('user_id', $userId))
->get()->toArray();
}
public function getComponent(string $component, ?int $userId = null): array
{
return Setting::where('component', $component)
->when($userId, fn($q) => $q->where('user_id', $userId))
->get()->toArray();
}
public function getGroup(string $group, ?int $userId = null): array
{
return Setting::where('group', $group)
->when($userId, fn($q) => $q->where('user_id', $userId))
->get()->toArray();
}
public function getSubGroup(string $subGroup, ?int $userId = null): array
{
return Setting::where('sub_group', $subGroup)
->when($userId, fn($q) => $q->where('user_id', $userId))
->get()->toArray();
}
// ===================== ELIMINACIÓN =====================
public function deleteScoped(string $scope): int
{
return Setting::where('scope', $scope)->delete();
}
public function deleteComponent(string $component): int
{
return Setting::where('component', $component)->delete();
}
public function deleteGroup(string $group): int
{
return Setting::where('group', $group)->delete();
}
public function deleteSubGroup(string $subGroup): int
{
return Setting::where('sub_group', $subGroup)->delete();
}
// ===================== LISTADOS =====================
public function listComponents(): array
{
return Setting::select('component')->distinct()->pluck('component')->toArray();
}
public function listGroups(): array
{
return Setting::select('group')->distinct()->pluck('group')->toArray();
}
public function listSubGroups(): array
{
return Setting::select('sub_group')->distinct()->pluck('sub_group')->toArray();
}
// ===================== Internal Helpers =====================
/**
* Obtiene un setting por su clave calificada.
*/
protected function query(string $key): mixed
{
if (!Schema::hasTable('settings')) return null;
$setting = $this->settingModel::where('key', $key)->first();
return $setting ? $this->decode($setting) : null;
}
/**
* Decodifica el valor de un setting.
*/
protected function decode(Setting $setting, bool $asArray = true): mixed
{
// Orden de prioridad: JSON largo, texto simple, luego tipos básicos
$value = $setting->value_text
?? $setting->value_string
?? $setting->value_integer
?? $setting->value_boolean
?? $setting->value_float
?? $setting->value_binary
?? null;
if (is_string($value)) {
$value = trim($value);
// Limpieza de caracteres invisibles si es string
$value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
// Intentar decodificar si parece JSON
if (Str::startsWith($value, ['[', '{'])) {
$decoded = json_decode($value, $asArray);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
logger()->warning('⚠️ JSON decode failed', [
'key' => $setting->key,
'value_preview' => Str::limit($value, 200),
'error' => json_last_error_msg(),
]);
}
}
return $value;
}
protected function assignValueColumns(array &$columns, mixed $value): void
{
foreach (SettingValueType::cases() as $type) {
$columns["value_{$type->value}"] = null;
}
match (true) {
is_string($value) => $columns[strlen($value) > 250 ? 'value_text' : 'value_string'] = $value,
is_int($value) => $columns['value_integer'] = $value,
is_bool($value) => $columns['value_boolean'] = $value,
is_float($value) => $columns['value_float'] = $value,
is_array($value), is_object($value) => $columns['value_text'] = json_encode($value, JSON_UNESCAPED_UNICODE),
default => null
};
}
}

View File

@ -1,137 +0,0 @@
<?php
namespace Koneko\VuexyAdmin\Application\System;
use Illuminate\Support\Facades\Cache;
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoComponentContextRegistrar;
use Koneko\VuexyAdmin\Application\Cache\KonekoCacheManager;
use Koneko\VuexyAdmin\Application\Settings\SettingsGroupCollection;
use Koneko\VuexyAdmin\Models\Setting;
use Illuminate\Support\Str;
/**
* 📊 Gestor extendido de Settings del ecosistema Koneko.
* Soporte para componentes, namespaces, TTL inteligente y helper fluido.
*/
class ____KonekoSettingManager implements SettingsRepositoryInterface
{
protected string $component;
protected string $namespace;
protected ?string $moduleSlug = null;
public function __construct(?string $component = null)
{
$this->component = $component ?? KonekoComponentContextRegistrar::currentComponent() ?? 'admin';
$this->namespace = $this->component . '.'; // Default to root namespace
}
public function setDefaultNamespace(string $namespace, ?string $slug = null): self
{
$this->namespace = rtrim($namespace, '.') . '.';
$this->moduleSlug = $slug;
return $this;
}
public function self(?string $subNamespace = null): self
{
return $this->in($this->namespace . ltrim((string) $subNamespace, '.') . '.');
}
public function in(string $namespace): self
{
$this->namespace = rtrim($namespace, '.') . '.';
return $this;
}
public function get(string $key, ...$args): mixed
{
$fullKey = $this->qualifyKey($key);
$cache = new KonekoCacheManager($this->component, 'settings');
if (! $cache->enabled()) {
return $this->query($fullKey);
}
return Cache::remember($cache->key(md5($fullKey)), $cache->ttl(), fn () => $this->query($fullKey));
}
public function getGroup(?string $prefix = ''): array
{
$query = Setting::where('key', 'like', $this->namespace . '%');
if ($prefix) {
$query->where('key', 'like', $this->namespace . trim($prefix, '.') . '.%');
}
return $query->pluck('value', 'key')->mapWithKeys(function ($val, $key) {
$shortKey = Str::after($key, $this->namespace);
return [$shortKey => $this->decode($val)];
})->toArray();
}
public function set(string $key, mixed $value, ?int $userId = null, ...$args): ?Setting
{
$fullKey = $this->qualifyKey($key);
$columns = [
'user_id' => $userId,
'module' => $this->moduleSlug,
'value' => is_array($value) || is_object($value)
? json_encode($value)
: $value,
];
Cache::forget((new KonekoCacheManager($this->component, 'settings'))->key(md5($fullKey)));
return Setting::updateOrCreate(['key' => $fullKey, 'user_id' => $userId], $columns);
}
public function delete(string $key): bool
{
$fullKey = $this->qualifyKey($key);
Cache::forget((new KonekoCacheManager($this->component, 'settings'))->key(md5($fullKey)));
return Setting::where('key', $fullKey)->delete() > 0;
}
public function deleteNamespace(string $namespace, bool $fireEvents = true): int
{
return Setting::where('key', 'like', rtrim($namespace, '.') . '.%')->delete();
}
public function listGroups(): SettingsGroupCollection
{
$keys = Setting::pluck('key')->toArray();
return new SettingsGroupCollection(
collect($keys)->map(fn($key) => explode('.', $key)[0])->unique()->values()
);
}
public function currentNamespace(): string
{
return $this->namespace;
}
protected function qualifyKey(string $key): string
{
return $this->namespace . ltrim($key, '.');
}
protected function query(string $key): mixed
{
$row = Setting::where('key', $key)->first();
return $this->decode($row?->value);
}
protected function decode(mixed $value): mixed
{
if (is_string($value) && Str::startsWith($value, ['[', '{'])) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
}
return $value;
}
}

View File

@ -1,120 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\System;
use Illuminate\Support\Facades\{Event, Schema};
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Events\Settings\SettingChanged;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
class KonekoSettingManager extends AbstractKeyValueCacheBuilder implements SettingsRepositoryInterface
{
// Flags de control del setting
protected bool $is_system = false;
protected bool $is_encrypted = false;
protected bool $is_sensitive = false;
protected bool $is_editable = true;
protected bool $is_active = true;
protected bool $trackUsage = true;
protected string $settingModel = Setting::class;
// ========== Fluent Flag Setters ==========
public function markAsSystem(bool $state = true): static { $this->is_system = $state; return $this; }
public function markAsEncrypted(bool $state = true): static { $this->is_encrypted = $state; return $this; }
public function markAsSensitive(bool $state = true): static { $this->is_sensitive = $state; return $this; }
public function markAsEditable(bool $state = true): static { $this->is_editable = $state; return $this; }
public function markAsActive(bool $state = true): static { $this->is_active = $state; return $this; }
// ========== Usage Tracking Control ==========
public function withoutUsageTracking(): static
{
$this->trackUsage = false;
return $this;
}
// ========== Settings Operations ==========
public function set(string $key, mixed $value): ?Setting
{
$this->validateKey($key);
$fullKey = $this->generateCacheKey($key);
$columns = [
'namespace' => $this->namespace,
'environment' => app()->environment(),
'scope' => $this->scope,
'component' => $this->component,
'module' => $this->module,
'group' => $this->group,
'sub_group' => $this->subGroup,
'key_name' => $key,
'user_id' => $this->currentUser()?->getAuthIdentifier() ?? null,
'is_system' => $this->is_system,
'is_encrypted' => $this->is_encrypted,
'is_sensitive' => $this->is_sensitive,
'is_editable' => $this->is_editable,
'is_active' => $this->is_active,
];
$setting = $this->settingModel::updateOrCreate(
['key' => $fullKey],
[...$columns, 'value' => $value]
);
Event::dispatch(new SettingChanged($fullKey));
return $setting;
}
public function get(string $key, mixed $default = null): mixed
{
$value = $this->rememberCache($key, fn() => $this->query($this->generateCacheKey($key)));
return $value !== null ? $value : $default;
}
public function delete(string $key): bool
{
$fullKey = $this->generateCacheKey($key);
$deleted = $this->settingModel::where('key', $fullKey)->delete();
if ($deleted) {
Event::dispatch(new SettingChanged($fullKey));
}
return $deleted > 0;
}
public function exists(string $key): bool
{
return $this->settingModel::where('key', $this->generateCacheKey($key))->exists();
}
/**
* Obtiene un setting por su clave calificada.
*/
protected function query(string $key): mixed
{
if (!Schema::hasTable((new $this->settingModel)->getTable())) return null;
$setting = $this->settingModel::where('key', $key)->first();
if ($setting && $this->trackUsage) {
$setting->incrementUsage();
}
// Reiniciar flag para evitar que quede en false en la siguiente consulta
$this->trackUsage = true;
return $setting?->value;
}
}

View File

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyApisAndIntegrations\Application\Services;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Koneko\VuexyApisAndIntegrations\Models\ExternalApi;
/**
* Servicio de consulta y agrupación de APIs externas del sistema.
*
* Este servicio centraliza el acceso a `ExternalApi`, útil para
* dashboards, Livewire, comandos Artisan o UI administrativas.
*/
/**
* Servicio principal para consulta y agrupación de APIs.
*/
class ExternalApiRegistryService implements ExternalApiRegistryInterface
{
public function all(): Collection
{
return ExternalApi::all();
}
public function active(): Collection
{
return ExternalApi::where('is_active', true)->get();
}
public function groupByProvider(): Collection
{
return $this->all()->groupBy('provider');
}
public function groupByModule(): Collection
{
return $this->all()->groupBy('module');
}
public function forModule(string $module): Collection
{
return ExternalApi::where('module', $module)->get();
}
public function forProvider(string $provider): Collection
{
return ExternalApi::where('provider', $provider)->get();
}
public function summary(): array
{
return [
'total' => ExternalApi::count(),
'active' => ExternalApi::where('is_active', true)->count(),
'inactive' => ExternalApi::where('is_active', false)->count(),
'by_provider' => ExternalApi::select('provider')->distinct()->pluck('provider')->toArray(),
'by_environment' => ExternalApi::select('environment')->distinct()->pluck('environment')->toArray(),
];
}
public function slugifyName(string $name): string
{
return Str::slug($name);
}
public function find(string $slug): ?ExternalApi
{
return ExternalApi::where('slug', $slug)->first();
}
}

View File

@ -1,109 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\System;
use Illuminate\Support\Facades\{File,Config};
use Spatie\Permission\Models\{Permission, Role};
use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
class ___RbacManagerService
{
public static function importAll(): void
{
foreach (KonekoModuleRegistry::enabled() as $module) {
static::importPermissions($module);
$moduleKey = $module->slug;
$moduleConfig = Config::get("vuexy-admin.rbac.modules.{$moduleKey}", []);
if (($moduleConfig['publish_roles'] ?? false) || ($moduleConfig['auto_overwrite_roles'] ?? false)) {
static::importRoles($module, (bool) ($moduleConfig['auto_overwrite_roles'] ?? false));
}
}
}
public static function importPermissions($module): void
{
$path = $module->rbac['permissions_path'] ?? null;
if (!$path || !file_exists($module->basePath . '/' . $path)) {
return;
}
$json = json_decode(file_get_contents($module->basePath . '/' . $path), true);
foreach ($json as $permission) {
Permission::updateOrCreate(['name' => $permission]);
}
}
public static function importRoles($module, bool $overwrite = false): void
{
$path = $module->rbac['roles_path'] ?? null;
if (!$path || !file_exists($module->basePath . '/' . $path)) {
return;
}
$json = json_decode(file_get_contents($module->basePath . '/' . $path), true);
foreach ($json as $name => $data) {
$role = Role::firstOrCreate(['name' => $name], ['style' => $data['style'] ?? 'primary']);
if ($overwrite) {
$role->syncPermissions($data['permissions'] ?? []);
} else {
$role->givePermissionTo($data['permissions'] ?? []);
}
}
}
public static function exportAll(): void
{
foreach (KonekoModuleRegistry::enabled() as $module) {
static::exportPermissions($module);
static::exportRoles($module);
}
}
public static function exportPermissions($module): void
{
$path = $module->rbac['permissions_path'] ?? null;
if (!$path) return;
$permissions = Permission::query()
->where('name', 'LIKE', "%{$module->name}%")
->pluck('name')
->unique()
->values()
->toArray();
static::writeJson($module->basePath . '/' . $path, $permissions);
}
public static function exportRoles($module): void
{
$path = $module->rbac['roles_path'] ?? null;
if (!$path) return;
$roles = Role::all()->mapWithKeys(function ($role) {
return [$role->name => [
'style' => $role->style ?? 'primary',
'permissions' => $role->permissions->pluck('name')->sort()->values()->toArray(),
]];
})->toArray();
static::writeJson($module->basePath . '/' . $path, $roles);
}
protected static function writeJson(string $fullPath, array $data): void
{
File::ensureDirectoryExists(dirname($fullPath));
File::put($fullPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
}

View File

@ -1,328 +0,0 @@
<?php
declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\System;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\Events\Settings\SettingChanged;
use Koneko\VuexyAdmin\Application\Settings\SettingsGroupCollection;
use Koneko\VuexyAdmin\Models\Setting;
use Koneko\VuexyAdmin\Support\Traits\Cache\InteractsWithKonekoVarsCache;
/**
* Servicio principal para gestionar configuraciones del sistema y módulos.
*/
class ___SettingsService implements SettingsRepositoryInterface
{
use InteractsWithKonekoVarsCache;
private const CACHE_PREFIX = 'settings_user_id:';
/**
* Namespace base del módulo (ej. koneko.admin).
*/
private string $defaultNamespace = '';
/**
* Namespace activo para operaciones dinámicas.
*/
private ?string $currentNamespace = null;
/**
* Slug del módulo actual para auto-llenar campo 'module' en tabla settings.
*/
private ?string $currentModuleSlug = null;
/**
* Constructor.
*/
public function __construct()
{
$this->initCacheConfig();
$this->defaultNamespace = '';
$this->currentNamespace = null;
$this->currentModuleSlug = null;
}
/**
* Define el namespace base y slug del módulo actual.
*
* @param string $namespace
* @param string|null $slug
* @return self
*/
public function setDefaultNamespace(string $namespace, ?string $slug = null): self
{
$this->defaultNamespace = rtrim($namespace, '.') . '.';
$this->currentNamespace = $this->defaultNamespace;
$this->currentModuleSlug = $slug;
return $this;
}
/**
* Usa el namespace propio del módulo actual, con posibilidad de extenderlo.
*
* @param string|null $subNamespace
* @return self
*/
public function self(?string $subNamespace = null): self
{
$namespace = $this->defaultNamespace;
if ($subNamespace !== null) {
$namespace .= rtrim($subNamespace, '.') . '.';
}
$this->currentNamespace = $namespace;
return $this;
}
/**
* Cambia temporalmente a otro namespace.
*
* @param string $namespace
* @return self
*/
public function in(string $namespace): self
{
$this->currentNamespace = rtrim($namespace, '.') . '.';
return $this;
}
/**
* Obtiene un valor de configuración.
*
* @param string $key
* @param mixed ...$args
* @return mixed
*/
public function get(string $key, ...$args): mixed
{
return $this->retrieveSetting($this->qualifyKey($key));
}
/**
* Obtiene un conjunto de settings basado en el namespace actual.
*
* @param string|null $prefix Prefijo adicional dentro del namespace (puede ser vacío '')
* @return array
*/
public function getGroup(?string $prefix = ''): array
{
$namespace = rtrim($this->currentNamespace ?? $this->defaultNamespace, '.');
$query = Setting::where('key', 'like', "{$namespace}.%");
if ($prefix !== '') {
$prefix = trim($prefix, '.');
$query->where('key', 'like', "{$namespace}.{$prefix}.%");
}
$settings = $query->pluck('value', 'key')->toArray();
// Limpiar claves: quitar el namespace
$cleaned = [];
foreach ($settings as $fullKey => $value) {
$shortKey = (string) str($fullKey)->after("{$namespace}.");
$cleaned[$shortKey] = $value;
}
return $cleaned;
}
/**
* Guarda o actualiza un valor de configuración.
*
* @param string $key
* @param mixed $value
* @param int|null $userId
* @param mixed ...$args
* @return Setting|null
*/
public function set(string $key, mixed $value, ?int $userId = null, ...$args): ?Setting
{
$qualifiedKey = $this->qualifyKey($key);
$data = [
'user_id' => $userId,
'module' => $this->currentModuleSlug,
'value_string' => null,
'value_integer' => null,
'value_boolean' => null,
'value_float' => null,
'value_text' => null,
'value_binary' => null,
];
if (is_string($value)) {
$data[strlen($value) > 250 ? 'value_text' : 'value_string'] = $value;
} elseif (is_int($value)) {
$data['value_integer'] = $value;
} elseif (is_bool($value)) {
$data['value_boolean'] = $value;
} elseif (is_float($value)) {
$data['value_float'] = $value;
} elseif (is_resource($value) || $value instanceof \SplFileInfo) {
$data['value_binary'] = is_resource($value)
? stream_get_contents($value)
: file_get_contents($value->getRealPath());
} elseif (is_array($value) || is_object($value)) {
$data['value_text'] = json_encode($value);
}
$setting = Setting::updateOrCreate(
['key' => $qualifiedKey, 'user_id' => $userId],
$data
);
Event::dispatch(new SettingChanged($qualifiedKey));
return $setting;
}
/**
* Lista todos los grupos disponibles.
*
* @return SettingsGroupCollection
*/
public function listGroups(): SettingsGroupCollection
{
$allSettings = Setting::pluck('key')->toArray();
$groups = collect($allSettings)
->map(fn($key) => explode('.', $key)[0] ?? 'unknown')
->unique()
->values();
if ($this->currentNamespace !== null) {
$namespaceRoot = rtrim($this->currentNamespace, '.');
$groups = $groups->filter(fn($group) => $group === $namespaceRoot);
}
return new SettingsGroupCollection($groups);
}
/**
* Devuelve el namespace actualmente activo.
*
* @return string
*/
public function currentNamespace(): string
{
return $this->currentNamespace ?? $this->defaultNamespace;
}
/**
* Internamente califica la key con el namespace activo.
*
* @param string $key
* @return string
*/
private function qualifyKey(string $key): string
{
return $this->currentNamespace . $key;
}
/**
* Recupera un setting directamente de la base de datos.
*
* @param string $key
* @return mixed
*/
private function retrieveSetting(string $key): mixed
{
$cacheKey = $this->generateCacheKey($key);
return $this->cacheOrCompute($cacheKey, fn () => $this->querySettingFromDatabase($key));
}
/**
* Consulta un setting directamente desde la base de datos.
*
* @param string $key
* @return mixed
*/
private function querySettingFromDatabase(string $key): mixed
{
// Evita error si la tabla aún no existe (por ejemplo durante migrate:fresh)
if (!Schema::hasTable('settings')) {
return null;
}
$setting = Setting::where('key', $key)->first();
$value = $setting?->value;
if (is_string($value) && $this->isJson($value)) {
$decoded = json_decode($value, true);
if (is_array($decoded)) {
return $decoded;
}
}
return $value;
}
/**
* Verifica si un valor string es JSON.
*
* @param string $value
* @return bool
*/
private function isJson(string $value): bool
{
json_decode($value);
return json_last_error() === JSON_ERROR_NONE;
}
public function delete(string $key): bool
{
$qualifiedKey = $this->qualifyKey($key);
$deleted = Setting::where('key', $qualifiedKey)->delete();
if ($deleted) {
Event::dispatch(new SettingChanged($qualifiedKey));
}
return $deleted > 0;
}
public function deleteNamespace(string $namespace, bool $fireEvents = true): int
{
$qualifiedNamespace = rtrim($namespace, '.') . '.';
$settings = Setting::where('key', 'like', "{$qualifiedNamespace}%")->get();
$deletedCount = 0;
foreach ($settings as $setting) {
$setting->delete();
$deletedCount++;
if ($fireEvents) {
Event::dispatch(new SettingChanged($setting->key));
}
}
return $deletedCount;
}
/**
* Genera una cache key para un setting.
*
* @param string $key
* @return string
*/
private function generateCacheKey(string $key): string
{
return self::CACHE_PREFIX . md5($key);
}
}

Some files were not shown because too many files have changed in this diff Show More