@@ -59,17 +64,17 @@
{{ $item['title'] }}
- @if(config('koneko.admin.menu.debug.show_broken_routers') && $item['url'] == "javascript:;")
+ @if($show_broken_routes && $item['url'] == "javascript:;")
❌ Sin URL válida
@endif
- @if(config('koneko.admin.menu.debug.show_disallowed_links') && $item['disallowed_link'])
+ @if($show_disallowed_links && $item['disallowed_link'])
🔒 Sin permisos
@endif
- @if(config('koneko.admin.menu.debug.show_hidden_items') && $item['hidden_item'])
+ @if($show_hidden_items && $item['hidden_item'])
🚧 Vista forzada
diff --git a/routes/koneko.php b/routes/koneko.php
index ef5faae..5c2d6fb 100644
--- a/routes/koneko.php
+++ b/routes/koneko.php
@@ -5,7 +5,6 @@ use Koneko\VuexyAdmin\Application\Http\Controllers\HomeController;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
use Koneko\VuexyAdmin\Support\Routing\RouteScope;
-
RouteScope::auto(__FILE__, function (RouteScope $r) {
$r->route('', 'pages.', HomeController::class, function () {
Route::get('acerca-de', 'about')->name('about.index');
@@ -35,5 +34,4 @@ RouteScope::auto(__FILE__, function (RouteScope $r) {
]);
})->name('folder.view');
});
-
});
diff --git a/src/Application/Bootstrap/Extenders/Api/ApiModuleRegistry.php b/src/Application/Bootstrap/Extenders/Api/ApiModuleRegistry.php
index 861405d..4ec6c98 100644
--- a/src/Application/Bootstrap/Extenders/Api/ApiModuleRegistry.php
+++ b/src/Application/Bootstrap/Extenders/Api/ApiModuleRegistry.php
@@ -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
{
diff --git a/src/Application/Bootstrap/KonekoModule.php b/src/Application/Bootstrap/KonekoModule.php
index 4b3acdc..93e9f60 100644
--- a/src/Application/Bootstrap/KonekoModule.php
+++ b/src/Application/Bootstrap/KonekoModule.php
@@ -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;
}
}
diff --git a/src/Application/Bootstrap/KonekoModuleBootManager.php b/src/Application/Bootstrap/Manager/KonekoModuleBootManager.php
similarity index 72%
rename from src/Application/Bootstrap/KonekoModuleBootManager.php
rename to src/Application/Bootstrap/Manager/KonekoModuleBootManager.php
index d20a333..52545dd 100644
--- a/src/Application/Bootstrap/KonekoModuleBootManager.php
+++ b/src/Application/Bootstrap/Manager/KonekoModuleBootManager.php
@@ -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}");
}
diff --git a/src/Application/Bootstrap/KonekoModuleRegistry.php b/src/Application/Bootstrap/Registry/KonekoModuleRegistry.php
similarity index 90%
rename from src/Application/Bootstrap/KonekoModuleRegistry.php
rename to src/Application/Bootstrap/Registry/KonekoModuleRegistry.php
index 865d710..1fd4226 100644
--- a/src/Application/Bootstrap/KonekoModuleRegistry.php
+++ b/src/Application/Bootstrap/Registry/KonekoModuleRegistry.php
@@ -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.
*/
diff --git a/src/Application/Bootstrap/KonekoComponentContextRegistrar.php b/src/Application/Bootstrap/Registry/___KonekoComponentContextRegistrar.php
similarity index 92%
rename from src/Application/Bootstrap/KonekoComponentContextRegistrar.php
rename to src/Application/Bootstrap/Registry/___KonekoComponentContextRegistrar.php
index bef9550..1c17f39 100644
--- a/src/Application/Bootstrap/KonekoComponentContextRegistrar.php
+++ b/src/Application/Bootstrap/Registry/___KonekoComponentContextRegistrar.php
@@ -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);
diff --git a/src/Application/Cache/Builders/KonekoAdminVarsBuilder copy.php b/src/Application/Cache/Builders/KonekoAdminVarsBuilder copy.php
new file mode 100644
index 0000000..2c0f614
--- /dev/null
+++ b/src/Application/Cache/Builders/KonekoAdminVarsBuilder copy.php
@@ -0,0 +1,104 @@
+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,
+ ];
+ }
+}
diff --git a/src/Application/Cache/Builders/KonekoAdminVarsBuilder.php b/src/Application/Cache/Builders/KonekoAdminVarsBuilder.php
new file mode 100644
index 0000000..872b498
--- /dev/null
+++ b/src/Application/Cache/Builders/KonekoAdminVarsBuilder.php
@@ -0,0 +1,114 @@
+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,
+ ];
+ }
+}
diff --git a/src/Application/Cache/Builders/KonekoVuexyCustomizerVarsBuilder.php b/src/Application/Cache/Builders/KonekoVuexyCustomizerVarsBuilder.php
new file mode 100644
index 0000000..3a72740
--- /dev/null
+++ b/src/Application/Cache/Builders/KonekoVuexyCustomizerVarsBuilder.php
@@ -0,0 +1,28 @@
+setContext([
+ 'component' => CoreModule::COMPONENT,
+ 'group' => 'layout',
+ 'sub_group' => 'vuexy',
+ ]);
+
+ $this->setScope('customizer');
+
+ return parent::build();
+ }
+}
diff --git a/src/Application/Cache/Builders/SettingCacheKeyBuilder.php b/src/Application/Cache/Builders/SettingCacheKeyBuilder.php
new file mode 100644
index 0000000..163f148
--- /dev/null
+++ b/src/Application/Cache/Builders/SettingCacheKeyBuilder.php
@@ -0,0 +1,100 @@
+ 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));
+ }
+}
diff --git a/src/Application/Cache/CacheConfigService.php b/src/Application/Cache/CacheConfigService.php
deleted file mode 100644
index 93f1963..0000000
--- a/src/Application/Cache/CacheConfigService.php
+++ /dev/null
@@ -1,301 +0,0 @@
- $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'),
- ]);
- }
-}
diff --git a/src/Application/Cache/Contracts/CacheRepositoryInterface.php b/src/Application/Cache/Contracts/CacheRepositoryInterface.php
new file mode 100644
index 0000000..f3879ee
--- /dev/null
+++ b/src/Application/Cache/Contracts/CacheRepositoryInterface.php
@@ -0,0 +1,67 @@
+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);
+ }
+}
+
diff --git a/src/Application/Cache/KonekoCacheManager copy 2.php b/src/Application/Cache/KonekoCacheManager copy 2.php
deleted file mode 100644
index 90822c4..0000000
--- a/src/Application/Cache/KonekoCacheManager copy 2.php
+++ /dev/null
@@ -1,186 +0,0 @@
-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é.");
- }
- }
-}
diff --git a/src/Application/Cache/KonekoCacheManager copy.php b/src/Application/Cache/KonekoCacheManager copy.php
deleted file mode 100644
index b5be98a..0000000
--- a/src/Application/Cache/KonekoCacheManager copy.php
+++ /dev/null
@@ -1,185 +0,0 @@
-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(),
- ];
- }
-}
diff --git a/src/Application/Cache/KonekoCacheManager.php b/src/Application/Cache/KonekoCacheManager.php
deleted file mode 100644
index 6f0fc7d..0000000
--- a/src/Application/Cache/KonekoCacheManager.php
+++ /dev/null
@@ -1,232 +0,0 @@
-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, '');
- }
-}
diff --git a/src/Application/Cache/KonekoSessionManager.php b/src/Application/Cache/KonekoSessionManager.php
deleted file mode 100644
index 8df2397..0000000
--- a/src/Application/Cache/KonekoSessionManager.php
+++ /dev/null
@@ -1,155 +0,0 @@
-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);
- }
-}
diff --git a/src/Application/Cache/Manager/Concerns/___HasCacheContext.php b/src/Application/Cache/Manager/Concerns/___HasCacheContext.php
new file mode 100644
index 0000000..be2a4eb
--- /dev/null
+++ b/src/Application/Cache/Manager/Concerns/___HasCacheContext.php
@@ -0,0 +1,21 @@
+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()),
+ ];
+ }
+}
diff --git a/src/Application/Cache/LaravelCacheManager.php b/src/Application/Cache/Manager/LaravelCacheManager.php
similarity index 99%
rename from src/Application/Cache/LaravelCacheManager.php
rename to src/Application/Cache/Manager/LaravelCacheManager.php
index 41721c5..ff51e7f 100644
--- a/src/Application/Cache/LaravelCacheManager.php
+++ b/src/Application/Cache/Manager/LaravelCacheManager.php
@@ -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;
/**
diff --git a/src/Application/Cache/Services/KonekoVarsService.php b/src/Application/Cache/Services/KonekoVarsService.php
new file mode 100644
index 0000000..7e384c5
--- /dev/null
+++ b/src/Application/Cache/Services/KonekoVarsService.php
@@ -0,0 +1,131 @@
+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,
+ ]);
+ }
+}
diff --git a/src/Application/Cache/VuexyVarsBuilderService.php b/src/Application/Cache/VuexyVarsBuilderService.php
deleted file mode 100644
index 65731e6..0000000
--- a/src/Application/Cache/VuexyVarsBuilderService.php
+++ /dev/null
@@ -1,191 +0,0 @@
-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", []);
- }
-
-
-}
\ No newline at end of file
diff --git a/src/Application/Config/Cast/VuexyLayoutCast.php b/src/Application/Config/Cast/VuexyLayoutCast.php
new file mode 100644
index 0000000..a5da943
--- /dev/null
+++ b/src/Application/Config/Cast/VuexyLayoutCast.php
@@ -0,0 +1,17 @@
+ (bool) $value,
+ $key === 'maxQuickLinks' => (int) $value,
+ default => $value,
+ };
+ }
+}
diff --git a/src/Application/Config/Contracts/ConfigRepositoryInterface.php b/src/Application/Config/Contracts/ConfigRepositoryInterface.php
new file mode 100644
index 0000000..9a60742
--- /dev/null
+++ b/src/Application/Config/Contracts/ConfigRepositoryInterface.php
@@ -0,0 +1,64 @@
+ 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().");
+ }
+ }
+}
diff --git a/src/Application/Config/Manager/KonekoConfigManager.php b/src/Application/Config/Manager/KonekoConfigManager.php
new file mode 100644
index 0000000..c78ba15
--- /dev/null
+++ b/src/Application/Config/Manager/KonekoConfigManager.php
@@ -0,0 +1,257 @@
+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);
+ }
+}
diff --git a/src/Application/Config/Manager/___KonekoConfigManager copy.php b/src/Application/Config/Manager/___KonekoConfigManager copy.php
new file mode 100644
index 0000000..b4b91fa
--- /dev/null
+++ b/src/Application/Config/Manager/___KonekoConfigManager copy.php
@@ -0,0 +1,149 @@
+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
+ ]);
+ }
+}
diff --git a/src/Application/Config/Registry/ConfigBlockRegistry.php b/src/Application/Config/Registry/ConfigBlockRegistry.php
new file mode 100644
index 0000000..4878db1
--- /dev/null
+++ b/src/Application/Config/Registry/ConfigBlockRegistry.php
@@ -0,0 +1,37 @@
+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;
+ }
+}
diff --git a/src/Application/Contracts/ApiRegistry/ExternalApiRegistryInterface.php b/src/Application/Contracts/ApiRegistry/ExternalApiRegistryInterface.php
deleted file mode 100644
index 1607c3a..0000000
--- a/src/Application/Contracts/ApiRegistry/ExternalApiRegistryInterface.php
+++ /dev/null
@@ -1,32 +0,0 @@
- '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',
- };
- }
-}
\ No newline at end of file
diff --git a/src/Application/Enums/Settings/SettingEnvironment.php b/src/Application/Enums/Settings/SettingEnvironment.php
deleted file mode 100644
index 6f0f8e8..0000000
--- a/src/Application/Enums/Settings/SettingEnvironment.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
+ */
+ public function broadcastOn(): array
+ {
+ return [
+ new PrivateChannel('notifications'),
+ ];
+ }
+}
diff --git a/src/Application/Events/Settings/SettingChanged.php b/src/Application/Events/Settings/SettingChanged.php
deleted file mode 100644
index 5b8af55..0000000
--- a/src/Application/Events/Settings/SettingChanged.php
+++ /dev/null
@@ -1,19 +0,0 @@
-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);
}
}
}
diff --git a/src/Application/Http/Controllers/VuexyNavbarController.php b/src/Application/Http/Controllers/VuexyNavbarController.php
index 25306b2..7bf0706 100644
--- a/src/Application/Http/Controllers/VuexyNavbarController.php
+++ b/src/Application/Http/Controllers/VuexyNavbarController.php
@@ -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();
}
}
diff --git a/src/Application/Http/Controllers/___AuthUserController.php b/src/Application/Http/Controllers/___AuthUserController.php
deleted file mode 100644
index e130184..0000000
--- a/src/Application/Http/Controllers/___AuthUserController.php
+++ /dev/null
@@ -1,146 +0,0 @@
- '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]);
- }
- */
-}
diff --git a/src/Application/Http/Controllers/___VuexyController.php b/src/Application/Http/Controllers/___VuexyController.php
deleted file mode 100644
index 5e6188a..0000000
--- a/src/Application/Http/Controllers/___VuexyController.php
+++ /dev/null
@@ -1,190 +0,0 @@
-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
- );
- }
- }
-
-}
diff --git a/src/Application/Http/Middleware/AdminTemplateMiddleware.php b/src/Application/Http/Middleware/AdminTemplateMiddleware.php
index 16fab77..7608d76 100644
--- a/src/Application/Http/Middleware/AdminTemplateMiddleware.php
+++ b/src/Application/Http/Middleware/AdminTemplateMiddleware.php
@@ -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(),
]);
}
diff --git a/src/Application/Listeners/Authentication/HandleFailedLogin.php b/src/Application/Listeners/Authentication/HandleFailedLogin.php
index 9074952..4edafdd 100644
--- a/src/Application/Listeners/Authentication/HandleFailedLogin.php
+++ b/src/Application/Listeners/Authentication/HandleFailedLogin.php
@@ -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
{
diff --git a/src/Application/Listeners/Authentication/HandleUserLogout.php b/src/Application/Listeners/Authentication/HandleUserLogout.php
index 5fe6e4f..b6e2480 100644
--- a/src/Application/Listeners/Authentication/HandleUserLogout.php
+++ b/src/Application/Listeners/Authentication/HandleUserLogout.php
@@ -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);
}
}
diff --git a/src/Application/Listeners/Settings/ApplyVuexyCustomizerSettings.php b/src/Application/Listeners/Settings/ApplyVuexyCustomizerSettings.php
deleted file mode 100644
index 023afa7..0000000
--- a/src/Application/Listeners/Settings/ApplyVuexyCustomizerSettings.php
+++ /dev/null
@@ -1,20 +0,0 @@
-settings as $key => $value) {
- settings()->self('vuexy')->set($key, $value);
- }
-
- VuexyVarsBuilderService::clearCache();
- }
-}
diff --git a/src/Application/Listeners/Settings/SettingCacheListener.php b/src/Application/Listeners/Settings/SettingCacheListener.php
deleted file mode 100644
index 8398d10..0000000
--- a/src/Application/Listeners/Settings/SettingCacheListener.php
+++ /dev/null
@@ -1,23 +0,0 @@
-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}");
- */
- }
-}
diff --git a/src/Application/Logger/____SystemLoggerService.php b/src/Application/Logger/____SystemLoggerService.php
deleted file mode 100644
index 08961f0..0000000
--- a/src/Application/Logger/____SystemLoggerService.php
+++ /dev/null
@@ -1,82 +0,0 @@
- $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);
- }
-}
diff --git a/src/Application/Logger/____UserInteractionLoggerService.php b/src/Application/Logger/____UserInteractionLoggerService.php
deleted file mode 100644
index 54d9aa9..0000000
--- a/src/Application/Logger/____UserInteractionLoggerService.php
+++ /dev/null
@@ -1,68 +0,0 @@
-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';
- }
-}
diff --git a/src/Application/Contracts/Loggers/SecurityLoggerInterface.php b/src/Application/Loggers/Contracts/SecurityLoggerInterface.php
similarity index 100%
rename from src/Application/Contracts/Loggers/SecurityLoggerInterface.php
rename to src/Application/Loggers/Contracts/SecurityLoggerInterface.php
diff --git a/src/Application/Contracts/Loggers/SystemLoggerInterface.php b/src/Application/Loggers/Contracts/SystemLoggerInterface.php
similarity index 100%
rename from src/Application/Contracts/Loggers/SystemLoggerInterface.php
rename to src/Application/Loggers/Contracts/SystemLoggerInterface.php
diff --git a/src/Application/Contracts/Loggers/UserInteractionLoggerInterface.php b/src/Application/Loggers/Contracts/UserInteractionLoggerInterface.php
similarity index 100%
rename from src/Application/Contracts/Loggers/UserInteractionLoggerInterface.php
rename to src/Application/Loggers/Contracts/UserInteractionLoggerInterface.php
diff --git a/src/Application/Logger/KonekoSecurityAuditLogger.php b/src/Application/Loggers/KonekoSecurityAuditLogger.php
similarity index 97%
rename from src/Application/Logger/KonekoSecurityAuditLogger.php
rename to src/Application/Loggers/KonekoSecurityAuditLogger.php
index edfddd9..df73840 100644
--- a/src/Application/Logger/KonekoSecurityAuditLogger.php
+++ b/src/Application/Loggers/KonekoSecurityAuditLogger.php
@@ -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;
diff --git a/src/Application/Logger/KonekoSecurityLogger.php b/src/Application/Loggers/KonekoSecurityLogger.php
similarity index 83%
rename from src/Application/Logger/KonekoSecurityLogger.php
rename to src/Application/Loggers/KonekoSecurityLogger.php
index f0eb4c8..aa91dbc 100644
--- a/src/Application/Logger/KonekoSecurityLogger.php
+++ b/src/Application/Loggers/KonekoSecurityLogger.php
@@ -1,13 +1,13 @@
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,
diff --git a/src/Application/Logger/KonekoSystemLogger.php b/src/Application/Loggers/KonekoSystemLogger.php
similarity index 81%
rename from src/Application/Logger/KonekoSystemLogger.php
rename to src/Application/Loggers/KonekoSystemLogger.php
index 9c06884..2d3734a 100644
--- a/src/Application/Logger/KonekoSystemLogger.php
+++ b/src/Application/Loggers/KonekoSystemLogger.php
@@ -1,14 +1,13 @@
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,
diff --git a/src/Application/Logger/KonekoUserInteractionLogger.php b/src/Application/Loggers/KonekoUserInteractionLogger.php
similarity index 81%
rename from src/Application/Logger/KonekoUserInteractionLogger.php
rename to src/Application/Loggers/KonekoUserInteractionLogger.php
index 879b2b6..9279b7d 100644
--- a/src/Application/Logger/KonekoUserInteractionLogger.php
+++ b/src/Application/Loggers/KonekoUserInteractionLogger.php
@@ -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,
diff --git a/src/Application/Macros/___LogMacrosServiceProvider.php b/src/Application/Macros/___LogMacrosServiceProvider.php
deleted file mode 100644
index 07240ae..0000000
--- a/src/Application/Macros/___LogMacrosServiceProvider.php
+++ /dev/null
@@ -1,29 +0,0 @@
-info(
- module: 'vuexy-admin',
- message: 'Sistema de macros de log inicializado correctamente',
- context: ['environment' => app()->environment()]
- );
- */
- }
-}
diff --git a/src/Application/Macros/___SettingsScopesMacro.php b/src/Application/Macros/___SettingsScopesMacro.php
deleted file mode 100644
index 034b2c5..0000000
--- a/src/Application/Macros/___SettingsScopesMacro.php
+++ /dev/null
@@ -1,42 +0,0 @@
-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);
- }
- };
- }
- };
-});
diff --git a/src/Application/Macros/___VuexyAdminLoggerMacro.php b/src/Application/Macros/___VuexyAdminLoggerMacro.php
deleted file mode 100644
index 1059a2d..0000000
--- a/src/Application/Macros/___VuexyAdminLoggerMacro.php
+++ /dev/null
@@ -1,27 +0,0 @@
-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);
- }
- };
-});
diff --git a/src/Application/Macros/___VuexyAdminSettingsMacro.php b/src/Application/Macros/___VuexyAdminSettingsMacro.php
deleted file mode 100644
index 3864dee..0000000
--- a/src/Application/Macros/___VuexyAdminSettingsMacro.php
+++ /dev/null
@@ -1,167 +0,0 @@
-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(),
- ];
- });
- }
-}
diff --git a/src/Application/UX/ModulePackages/ModulePackageAnalyzerService.php b/src/Application/ModulePackages/ModulePackageAnalyzerService.php
similarity index 100%
rename from src/Application/UX/ModulePackages/ModulePackageAnalyzerService.php
rename to src/Application/ModulePackages/ModulePackageAnalyzerService.php
diff --git a/src/Application/UX/ModulePackages/ModulePackageGitFetcher.php b/src/Application/ModulePackages/ModulePackageGitFetcher.php
similarity index 100%
rename from src/Application/UX/ModulePackages/ModulePackageGitFetcher.php
rename to src/Application/ModulePackages/ModulePackageGitFetcher.php
diff --git a/src/Application/UX/ModulePackages/ModulePackageInstallPreviewService.php b/src/Application/ModulePackages/ModulePackageInstallPreviewService.php
similarity index 100%
rename from src/Application/UX/ModulePackages/ModulePackageInstallPreviewService.php
rename to src/Application/ModulePackages/ModulePackageInstallPreviewService.php
diff --git a/src/Application/UX/ModulePackages/ModulePackageInstallerService.php b/src/Application/ModulePackages/ModulePackageInstallerService.php
similarity index 100%
rename from src/Application/UX/ModulePackages/ModulePackageInstallerService.php
rename to src/Application/ModulePackages/ModulePackageInstallerService.php
diff --git a/src/Application/UX/ModulePackages/ModulePackageRegistrarService.php b/src/Application/ModulePackages/ModulePackageRegistrarService.php
similarity index 100%
rename from src/Application/UX/ModulePackages/ModulePackageRegistrarService.php
rename to src/Application/ModulePackages/ModulePackageRegistrarService.php
diff --git a/src/Application/UX/ModulePackages/ModulePackageZipDownloader.php b/src/Application/ModulePackages/ModulePackageZipDownloader.php
similarity index 100%
rename from src/Application/UX/ModulePackages/ModulePackageZipDownloader.php
rename to src/Application/ModulePackages/ModulePackageZipDownloader.php
diff --git a/src/Application/RBAC/KonekoRbacSyncManager.php b/src/Application/RBAC/Sync/KonekoRbacSyncManager.php
similarity index 98%
rename from src/Application/RBAC/KonekoRbacSyncManager.php
rename to src/Application/RBAC/Sync/KonekoRbacSyncManager.php
index 043e1bc..a44950a 100644
--- a/src/Application/RBAC/KonekoRbacSyncManager.php
+++ b/src/Application/RBAC/Sync/KonekoRbacSyncManager.php
@@ -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;
diff --git a/src/Application/Security/VaultKeyService.php b/src/Application/Security/VaultKeyService.php
deleted file mode 100644
index 82e0ca2..0000000
--- a/src/Application/Security/VaultKeyService.php
+++ /dev/null
@@ -1,64 +0,0 @@
- 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]);
- }
-}
diff --git a/src/Application/Traits/Seeders/Main/HandlesSeederLoggingAndReporting.php b/src/Application/Seeding/Concerns/Main/HandlesSeederLoggingAndReporting.php
similarity index 98%
rename from src/Application/Traits/Seeders/Main/HandlesSeederLoggingAndReporting.php
rename to src/Application/Seeding/Concerns/Main/HandlesSeederLoggingAndReporting.php
index f526d22..078a8e1 100644
--- a/src/Application/Traits/Seeders/Main/HandlesSeederLoggingAndReporting.php
+++ b/src/Application/Seeding/Concerns/Main/HandlesSeederLoggingAndReporting.php
@@ -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;
diff --git a/src/Application/Traits/Seeders/Main/HasSeederChunkSupport.php b/src/Application/Seeding/Concerns/Main/HasSeederChunkSupport.php
similarity index 96%
rename from src/Application/Traits/Seeders/Main/HasSeederChunkSupport.php
rename to src/Application/Seeding/Concerns/Main/HasSeederChunkSupport.php
index 163ce0e..a66b66b 100644
--- a/src/Application/Traits/Seeders/Main/HasSeederChunkSupport.php
+++ b/src/Application/Seeding/Concerns/Main/HasSeederChunkSupport.php
@@ -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.
diff --git a/src/Application/Traits/Seeders/Main/HasSeederFactorySupport.php b/src/Application/Seeding/Concerns/Main/HasSeederFactorySupport.php
similarity index 86%
rename from src/Application/Traits/Seeders/Main/HasSeederFactorySupport.php
rename to src/Application/Seeding/Concerns/Main/HasSeederFactorySupport.php
index 8ceb35a..2f609b4 100644
--- a/src/Application/Traits/Seeders/Main/HasSeederFactorySupport.php
+++ b/src/Application/Seeding/Concerns/Main/HasSeederFactorySupport.php
@@ -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.
diff --git a/src/Application/Traits/Seeders/Main/HasSeederLogger.php b/src/Application/Seeding/Concerns/Main/HasSeederLogger.php
similarity index 89%
rename from src/Application/Traits/Seeders/Main/HasSeederLogger.php
rename to src/Application/Seeding/Concerns/Main/HasSeederLogger.php
index 2210344..c244f57 100644
--- a/src/Application/Traits/Seeders/Main/HasSeederLogger.php
+++ b/src/Application/Seeding/Concerns/Main/HasSeederLogger.php
@@ -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.
diff --git a/src/Application/Traits/Seeders/Main/HasSeederProgressBar.php b/src/Application/Seeding/Concerns/Main/HasSeederProgressBar.php
similarity index 93%
rename from src/Application/Traits/Seeders/Main/HasSeederProgressBar.php
rename to src/Application/Seeding/Concerns/Main/HasSeederProgressBar.php
index c862dd1..140da37 100644
--- a/src/Application/Traits/Seeders/Main/HasSeederProgressBar.php
+++ b/src/Application/Seeding/Concerns/Main/HasSeederProgressBar.php
@@ -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.
diff --git a/src/Application/Traits/Seeders/Main/SanitizeRowWithFillableAndCasts.php b/src/Application/Seeding/Concerns/Main/SanitizeRowWithFillableAndCasts.php
similarity index 95%
rename from src/Application/Traits/Seeders/Main/SanitizeRowWithFillableAndCasts.php
rename to src/Application/Seeding/Concerns/Main/SanitizeRowWithFillableAndCasts.php
index b1374ea..d3b0151 100644
--- a/src/Application/Traits/Seeders/Main/SanitizeRowWithFillableAndCasts.php
+++ b/src/Application/Seeding/Concerns/Main/SanitizeRowWithFillableAndCasts.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Traits\Seeders\Main;
+namespace Koneko\VuexyAdmin\Application\Seeding\Concerns\Main;
trait SanitizeRowWithFillableAndCasts
{
diff --git a/src/Application/Seeding/SeederOrchestrator.php b/src/Application/Seeding/SeederOrchestrator.php
index 045a767..308989f 100644
--- a/src/Application/Seeding/SeederOrchestrator.php
+++ b/src/Application/Seeding/SeederOrchestrator.php
@@ -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
{
diff --git a/src/Application/Settings/Concerns/HasSettingAttributes.php b/src/Application/Settings/Concerns/HasSettingAttributes.php
new file mode 100644
index 0000000..1f56b62
--- /dev/null
+++ b/src/Application/Settings/Concerns/HasSettingAttributes.php
@@ -0,0 +1,54 @@
+ 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); }
+}
diff --git a/src/Application/Settings/Concerns/HasSettingCache.php b/src/Application/Settings/Concerns/HasSettingCache.php
new file mode 100644
index 0000000..34febd2
--- /dev/null
+++ b/src/Application/Settings/Concerns/HasSettingCache.php
@@ -0,0 +1,137 @@
+ 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();
+ }
+}
diff --git a/src/Application/Settings/Concerns/HasSettingEncryption.php b/src/Application/Settings/Concerns/HasSettingEncryption.php
new file mode 100644
index 0000000..ef1d5d1
--- /dev/null
+++ b/src/Application/Settings/Concerns/HasSettingEncryption.php
@@ -0,0 +1,81 @@
+ 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.");
+ }
+ }
+}
diff --git a/src/Application/Settings/Concerns/HasSettingFileSupport.php b/src/Application/Settings/Concerns/HasSettingFileSupport.php
new file mode 100644
index 0000000..a10c201
--- /dev/null
+++ b/src/Application/Settings/Concerns/HasSettingFileSupport.php
@@ -0,0 +1,68 @@
+ 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.");
+ }
+ }
+ }
+}
diff --git a/src/Application/Settings/Concerns/HasSettingMetadata.php b/src/Application/Settings/Concerns/HasSettingMetadata.php
new file mode 100644
index 0000000..80bca71
--- /dev/null
+++ b/src/Application/Settings/Concerns/HasSettingMetadata.php
@@ -0,0 +1,23 @@
+ 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;
+ }
+}
diff --git a/src/Application/Settings/Contracts/SettingsRepositoryInterface.php b/src/Application/Settings/Contracts/SettingsRepositoryInterface.php
new file mode 100644
index 0000000..f756344
--- /dev/null
+++ b/src/Application/Settings/Contracts/SettingsRepositoryInterface.php
@@ -0,0 +1,122 @@
+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());
+ }
+}
diff --git a/src/Application/Settings/Registry/ScopeRegistry.php b/src/Application/Settings/Registry/ScopeRegistry.php
new file mode 100644
index 0000000..c06c148
--- /dev/null
+++ b/src/Application/Settings/Registry/ScopeRegistry.php
@@ -0,0 +1,98 @@
+ $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,
+ ];
+ }
+}
diff --git a/src/Application/Settings/SettingDefaults.php b/src/Application/Settings/SettingDefaults.php
new file mode 100644
index 0000000..8f4c7b3
--- /dev/null
+++ b/src/Application/Settings/SettingDefaults.php
@@ -0,0 +1,14 @@
+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;
- }
-}
diff --git a/src/Application/System/KonekoSettingManager copy 3.php b/src/Application/System/KonekoSettingManager copy 3.php
deleted file mode 100644
index ed0fce7..0000000
--- a/src/Application/System/KonekoSettingManager copy 3.php
+++ /dev/null
@@ -1,234 +0,0 @@
-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
- };
- }
-
-
-
-
-
-
-}
\ No newline at end of file
diff --git a/src/Application/System/KonekoSettingManager copy.php b/src/Application/System/KonekoSettingManager copy.php
deleted file mode 100644
index 149eb1f..0000000
--- a/src/Application/System/KonekoSettingManager copy.php
+++ /dev/null
@@ -1,137 +0,0 @@
-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;
- }
-}
diff --git a/src/Application/System/KonekoSettingManager.php b/src/Application/System/KonekoSettingManager.php
deleted file mode 100644
index fbf0e9a..0000000
--- a/src/Application/System/KonekoSettingManager.php
+++ /dev/null
@@ -1,120 +0,0 @@
-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;
- }
-}
diff --git a/src/Application/System/___ExternalApiRegistryService.php b/src/Application/System/___ExternalApiRegistryService.php
deleted file mode 100644
index eddcb0f..0000000
--- a/src/Application/System/___ExternalApiRegistryService.php
+++ /dev/null
@@ -1,72 +0,0 @@
-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();
- }
-}
\ No newline at end of file
diff --git a/src/Application/System/___RbacManagerService.php b/src/Application/System/___RbacManagerService.php
deleted file mode 100644
index 3a26e07..0000000
--- a/src/Application/System/___RbacManagerService.php
+++ /dev/null
@@ -1,109 +0,0 @@
-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));
- }
-}
diff --git a/src/Application/System/___SettingsService.php b/src/Application/System/___SettingsService.php
deleted file mode 100644
index cffdb64..0000000
--- a/src/Application/System/___SettingsService.php
+++ /dev/null
@@ -1,328 +0,0 @@
-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);
- }
-}
diff --git a/src/Application/Traits/Indexing/HandlesIndexColumns.php b/src/Application/Traits/ConfigBuilder/HandlesIndexColumns.php
similarity index 100%
rename from src/Application/Traits/Indexing/HandlesIndexColumns.php
rename to src/Application/Traits/ConfigBuilder/HandlesIndexColumns.php
diff --git a/src/Application/Traits/Indexing/HandlesIndexLabels.php b/src/Application/Traits/ConfigBuilder/HandlesIndexLabels.php
similarity index 100%
rename from src/Application/Traits/Indexing/HandlesIndexLabels.php
rename to src/Application/Traits/ConfigBuilder/HandlesIndexLabels.php
diff --git a/src/Application/Traits/Indexing/HandlesModelMetadata.php b/src/Application/Traits/ConfigBuilder/HandlesModelMetadata.php
similarity index 100%
rename from src/Application/Traits/Indexing/HandlesModelMetadata.php
rename to src/Application/Traits/ConfigBuilder/HandlesModelMetadata.php
diff --git a/src/Application/Traits/Indexing/HandlesQueryBuilder.php b/src/Application/Traits/ConfigBuilder/HandlesQueryBuilder.php
similarity index 100%
rename from src/Application/Traits/Indexing/HandlesQueryBuilder.php
rename to src/Application/Traits/ConfigBuilder/HandlesQueryBuilder.php
diff --git a/src/Application/Traits/Indexing/HandlesTableConfig.php b/src/Application/Traits/ConfigBuilder/HandlesTableConfig.php
similarity index 100%
rename from src/Application/Traits/Indexing/HandlesTableConfig.php
rename to src/Application/Traits/ConfigBuilder/HandlesTableConfig.php
diff --git a/src/Application/Traits/Indexing/HandlesFactory.php b/src/Application/Traits/Indexing/HandlesFactory.php
deleted file mode 100644
index 5166fed..0000000
--- a/src/Application/Traits/Indexing/HandlesFactory.php
+++ /dev/null
@@ -1,16 +0,0 @@
- null,
+ 'environment' => null,
+ 'component' => null,
+ 'scope' => null,
+ 'scope_id' => null,
+ 'group' => null,
+ 'section' => null,
+ 'sub_group' => null,
+ 'key_name' => null,
+ ];
+
+ // ==================== Factory ====================
+
+ public static function fromArray(array $context): static
+ {
+ return static::make()->setContextArray($context);
+ }
+
+ public static function fromRequest(?Request $request = null): static
+ {
+ return static::make()->resolveFromRequest($request);
+ }
+
+ public function resolveFromRequest(?Request $request = null): static
+ {
+ $request ??= request();
+
+ if ($model = $request->route()?->parameter('model')) {
+ $this->withScopeFromModel($model);
+
+ } elseif (Auth::check()) {
+ $this->setUser(Auth::user());
+ }
+
+ $this->setEnvironment();
+
+ return $this;
+ }
+
+ public function context(string $group, ?string $section = null, ?string $subGroup = null): static
+ {
+ $this->context['group'] = $this->validateSlug('group', $group, 16);
+
+ if ($section) {
+ $this->context['section'] = $this->validateSlug('section', $section, 16);
+ }
+
+ if ($subGroup) {
+ $this->context['sub_group'] = $this->validateSlug('sub_group', $subGroup, 16);
+ }
+
+ return $this;
+ }
+
+ public function setContextArray(array $context): static
+ {
+ $this->reset();
+
+ if (isset($context['namespace'])) $this->setNamespace($context['namespace']);
+ if (isset($context['environment'])) $this->setEnvironment($context['environment']);
+ if (isset($context['component'])) $this->setComponent($context['component']);
+ if (isset($context['group'])) $this->setGroup($context['group']);
+ if (isset($context['section'])) $this->setSection($context['section']);
+ if (isset($context['sub_group'])) $this->setSubGroup($context['sub_group']);
+ if (isset($context['key_name'])) $this->setKeyName($context['key_name']);
+
+ if (isset($context['scope'], $context['scope_id'])) {
+ $this->setScope($context['scope'], $context['scope_id']);
+ }
+
+ return $this;
+ }
+
+ // ======================= Context Base =========================
+
+ public function setNamespace(string $namespace): static
+ {
+ $this->context['namespace'] = $this->validateSlug('namespace', $namespace, 8);
+ return $this;
+ }
+
+ public function setEnvironment(?string $environment = null): static
+ {
+ $this->context['environment'] = $environment
+ ? $this->validateSlug('environment', $environment, 7)
+ : app()->environment();
+ return $this;
+ }
+
+ public function setComponent(string $component): static
+ {
+ if (class_exists($component)) {
+ return $this->loadModuleClass($component);
+ }
+
+ $this->context['component'] = $this->validateSlug('component', $component, 16);
+ return $this;
+ }
+
+ /**
+ * Carga el contexto de un módulo usando una clase declarativa.
+ */
+ protected function loadModuleClass(string $moduleClass): static
+ {
+ 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");
+
+ return $this
+ ->setNamespace($namespace)
+ ->setComponent($component);
+ }
+
+ // ======================= Scope =========================
+
+ public function setScope(Model|string|false $scope, int|null|false $scopeId = false): static
+ {
+ if ($scope === false) {
+ $this->context['scope'] = null;
+ $this->context['scope_id'] = null;
+ return $this;
+ }
+
+ // Limpiamos el scopeId si es false
+ if ($scopeId === false) {
+ $scopeId = null;
+ }
+
+ // Obtenemos el scope y el scope_id de un modelo
+ if ($scope instanceof Model) {
+ $this->withScopeFromModel($scope);
+
+ // Si el scope es una cadena, validamos el slug
+ } else {
+ $this->context['scope'] = $this->validateScope($scope);
+ }
+
+ // Si el scopeId no es false, lo asignamos
+ if ($scopeId !== false) {
+ $this->context['scope_id'] = $scopeId;
+ }
+
+ return $this;
+ }
+
+ public function setScopeId(?int $scopeId): static
+ {
+ $this->context['scope_id'] = $scopeId;
+ return $this;
+ }
+
+ public function setUser(Authenticatable|int|null|false $user): static
+ {
+ $this->context['scope'] = 'user';
+ $this->context['scope_id'] = $this->resolveUserId($user);
+ return $this;
+ }
+
+ public function withScopeFromModel(Model $model): static
+ {
+ $context = ScopeRegistry::resolveScopeFromModel($model);
+
+ if (!$context) {
+ throw new \InvalidArgumentException('El modelo proporcionado no está asociado a ningún scope registrado.');
+ }
+
+ return $this->setScope($context['scope'], $context['scope_id']);
+ }
+
+ // ======================= Context =========================
+
+ public function setGroup(string $group): static
+ {
+ $this->context['group'] = $this->validateSlug('group', $group, 16);
+ return $this;
+ }
+
+ public function setSection(string $section): static
+ {
+ $this->context['section'] = $this->validateSlug('section', $section, 16);
+ return $this;
+ }
+
+ public function setSubGroup(string $subGroup): static
+ {
+ $this->context['sub_group'] = $this->validateSlug('sub_group', $subGroup, 16);
+ return $this;
+ }
+
+ public function setKeyName(string $keyName): static
+ {
+ $this->context['key_name'] = $this->validateKeyName($keyName);
+ return $this;
+ }
+
+ // ======================= Context =========================
+
+ public function asArray(bool $state = true): static
+ {
+ $this->asArray = $state;
+ return $this;
+ }
+
+ // ======================= GETTERS =========================
+
+ public function qualifiedKey(?string $key = null): string
+ {
+ $this->validateContextWithScope();
+
+ return SettingCacheKeyBuilder::build(
+ $this->context['namespace'],
+ $this->context['environment'],
+ $this->context['scope'],
+ $this->context['scope_id'],
+ $this->context['component'],
+ $this->context['group'],
+ $this->context['section'],
+ $this->context['sub_group'],
+ $key ?? $this->context['key_name']
+ );
+ }
+
+ public function getScopeModel(): ?Model
+ {
+ return $this->context['scope'] && $this->context['scope_id']
+ ? ScopeRegistry::getModelInstance($this->context['scope'], $this->context['scope_id'])
+ : null;
+ }
+
+ // ======================= BOOLEAN ATTRIBUTES =========================
+
+ public function hasComponentContext(): bool
+ {
+ return $this->context['namespace']
+ && $this->context['environment']
+ && $this->context['component'];
+ }
+
+ public function hasBaseContext(): bool
+ {
+ return $this->hasComponentContext()
+ && $this->context['key_name'];
+ }
+
+ public function hasScopeContext(): bool
+ {
+ return $this->context['scope']
+ && $this->context['scope_id'];
+ }
+
+ public function hasGroupContext(): bool
+ {
+ return $this->context['group']
+ && $this->context['section']
+ && $this->context['sub_group'];
+ }
+
+ public function hasFullContext(): bool
+ {
+ return $this->hasBaseContext()
+ && $this->hasScopeContext()
+ && $this->hasGroupContext();
+ }
+
+ // ======================= HELPERS =========================
+
+ public function ensureQualifiedKey(): void
+ {
+ if (!$this->hasBaseContext() || !$this->hasGroupContext()) {
+ throw new \InvalidArgumentException("Falta definir el contexto base y 'key_name' en settings().");
+ }
+ }
+
+ public function resetComponentContext(): void
+ {
+ $this->context['namespace'] = CoreModule::NAMESPACE;
+ $this->context['environment'] = app()->environment();
+ $this->context['component'] = CoreModule::COMPONENT;
+ }
+
+ public function resetScopeContext(): void
+ {
+ $this->context['scope'] = null;
+ $this->context['scope_id'] = null;
+ }
+
+ public function resetGroupContext(): void
+ {
+ $this->context['group'] = null;
+ $this->context['section'] = null;
+ $this->context['sub_group'] = null;
+ }
+}
diff --git a/src/Application/Traits/System/Context/HasBaseContextValidator.php b/src/Application/Traits/System/Context/HasBaseContextValidator.php
new file mode 100644
index 0000000..307fef6
--- /dev/null
+++ b/src/Application/Traits/System/Context/HasBaseContextValidator.php
@@ -0,0 +1,69 @@
+ $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) > 24) {
+ throw new \InvalidArgumentException("El valor de 'keyName' excede 24 caracteres.");
+ }
+
+ return $keyName;
+ }
+
+ protected function validateScopeContext(?string $scope, ?int $scopeId): void
+ {
+ if ($scope === null && $scopeId === null) {
+ return;
+ }
+
+ if ($scopeId && $scope === null) {
+ throw new \LogicException("Se definió scopeId sin indicar el tipo de scope.");
+ }
+
+ $this->validateScope($scope);
+
+ if ($scope && $scopeId === null) {
+ throw new \InvalidArgumentException("Scope '{$scope}' requiere un scopeId.");
+ }
+ }
+
+ protected function validateScope(?string $scope): ?string
+ {
+ if ($scope && !ScopeRegistry::isRegistered($scope)) {
+ throw new \InvalidArgumentException("Scope '{$scope}' no registrado en ScopeRegistry.");
+ }
+
+ return $scope;
+ }
+
+ protected function requireKeyName(): void
+ {
+ if (empty($this->context['key_name'] ?? null)) {
+ throw new \InvalidArgumentException("No se ha definido 'key_name' en contexto.");
+ }
+ }
+}
diff --git a/src/Application/Traits/System/Context/HasCacheContextValidation.php b/src/Application/Traits/System/Context/HasCacheContextValidation.php
new file mode 100644
index 0000000..07739dd
--- /dev/null
+++ b/src/Application/Traits/System/Context/HasCacheContextValidation.php
@@ -0,0 +1,24 @@
+context[$field] ?? null)) {
+ throw new \InvalidArgumentException("Falta definir '{$field}' en contexto cache_m().");
+ }
+ }
+ }
+
+ public function validateContextWithScope(): void
+ {
+ $this->validateBaseContext();
+ $this->requireKeyName();
+ $this->validateScopeContext($this->context['scope'], $this->context['scope_id']);
+ }
+}
diff --git a/src/Application/Traits/System/Context/HasConfigContextValidation.php b/src/Application/Traits/System/Context/HasConfigContextValidation.php
new file mode 100644
index 0000000..6c6abbe
--- /dev/null
+++ b/src/Application/Traits/System/Context/HasConfigContextValidation.php
@@ -0,0 +1,26 @@
+context[$field] ?? null)) {
+ throw new \InvalidArgumentException("Falta definir '{$field}' en contexto config_m().");
+ }
+ }
+ }
+
+ public function validateContextWithScope(): void
+ {
+ $this->validateBaseContext();
+ $this->requireKeyName();
+ $this->validateScopeContext($this->context['scope'], $this->context['scope_id']);
+ }
+}
diff --git a/src/Application/Traits/System/Context/HasContextQueryBuilder.php b/src/Application/Traits/System/Context/HasContextQueryBuilder.php
new file mode 100644
index 0000000..67728a2
--- /dev/null
+++ b/src/Application/Traits/System/Context/HasContextQueryBuilder.php
@@ -0,0 +1,122 @@
+settingModel::query();
+ }
+
+ /**
+ * Aplica filtros de contexto dinámico a un query builder.
+ *
+ * @param Builder $query
+ * @param array $filters (claves: namespace, environment, scope, scope_id, component, group, section, sub_group, key_name)
+ *
+ * @return Builder
+ */
+ protected function applyContextFilters(Builder $query, array $filters = []): Builder
+ {
+ $ctx = $this->context;
+
+ $query = $query
+ ->when($filters['namespace'] ?? false, fn($q) => $q->where('namespace', $ctx['namespace'] ?? null))
+ ->when($filters['environment'] ?? false, fn($q) => $q->where('environment', $ctx['environment'] ?? null))
+ ->when($filters['scope'] ?? false, fn($q) => $q->where('scope', $ctx['scope'] ?? null))
+ ->when($filters['scope_id'] ?? false, fn($q) => $q->where('scope_id', $ctx['scope_id'] ?? null))
+ ->when($filters['component'] ?? false, fn($q) => $q->where('component', $ctx['component'] ?? null))
+ ->when($filters['group'] ?? false, fn($q) => $q->where('group', $ctx['group'] ?? null))
+ ->when($filters['section'] ?? false, fn($q) => $q->where('section', $ctx['section'] ?? null))
+ ->when($filters['sub_group'] ?? false, fn($q) => $q->where('sub_group', $ctx['sub_group'] ?? null))
+ ->when($filters['key_name'] ?? false, fn($q) => $q->where('key_name', $ctx['key_name'] ?? null));
+
+ // Solo aplicar estado si no se anula
+ if (!($this->includeDisabled ?? false)) {
+ $query->where('is_active', true);
+ }
+
+ if (!($this->includeExpired ?? false)) {
+ $query->where(function ($q) {
+ $q->whereNull('expires_at')->orWhere('expires_at', '>', now());
+ });
+ }
+
+ return $query;
+ }
+
+ protected function query(): Builder
+ {
+ return $this->applyContextFilters($this->newQuery(), [
+ 'namespace' => true,
+ 'environment' => true,
+ 'scope' => true,
+ 'scope_id' => true,
+ 'component' => true,
+ 'group' => true,
+ 'section' => true,
+ 'sub_group' => true,
+ ]);
+ }
+
+ protected function queryByKey(): Builder
+ {
+ return $this->applyContextFilters($this->newQuery(), [
+ 'namespace' => true,
+ 'environment' => true,
+ 'scope' => true,
+ 'scope_id' => true,
+ 'component' => true,
+ 'group' => true,
+ 'section' => true,
+ 'sub_group' => true,
+ 'key_name' => true,
+ ]);
+ }
+
+ protected function queryByGroup(): Builder
+ {
+ return $this->applyContextFilters($this->newQuery(), [
+ 'scope' => true,
+ 'scope_id' => true,
+ 'component' => true,
+ 'group' => true,
+ ]);
+ }
+
+ protected function queryBySection(): Builder
+ {
+ return $this->applyContextFilters($this->newQuery(), [
+ 'scope' => true,
+ 'scope_id' => true,
+ 'component' => true,
+ 'group' => true,
+ 'section' => true,
+ ]);
+ }
+
+ protected function queryBySubGroup(): Builder
+ {
+ return $this->applyContextFilters($this->newQuery(), [
+ 'scope' => true,
+ 'scope_id' => true,
+ 'component' => true,
+ 'group' => true,
+ 'sub_group' => true,
+ ]);
+ }
+
+ protected function queryForValue(mixed $default = null): mixed
+ {
+ return $this->queryByKey()->first()?->value ?? $default;
+ }
+}
diff --git a/src/Application/Traits/System/Context/HasSettingsContextValidation.php b/src/Application/Traits/System/Context/HasSettingsContextValidation.php
new file mode 100644
index 0000000..6619c52
--- /dev/null
+++ b/src/Application/Traits/System/Context/HasSettingsContextValidation.php
@@ -0,0 +1,24 @@
+context[$field] ?? null)) {
+ throw new \InvalidArgumentException("Falta definir '{$field}' en contexto settings().");
+ }
+ }
+ }
+
+ public function validateContextWithScope(): void
+ {
+ $this->validateBaseContext();
+ $this->requireKeyName();
+ $this->validateScopeContext($this->context['scope'], $this->context['scope_id']);
+ }
+}
diff --git a/src/Application/UI/Avatar/AvatarImageService.php b/src/Application/UI/Avatar/AvatarImageService.php
index 791cba4..7387334 100644
--- a/src/Application/UI/Avatar/AvatarImageService.php
+++ b/src/Application/UI/Avatar/AvatarImageService.php
@@ -10,6 +10,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Intervention\Image\Interfaces\ImageInterface;
+use Koneko\VuexyAdmin\Application\CoreModule;
use Symfony\Component\Mime\MimeTypes;
class AvatarImageService
@@ -27,7 +28,7 @@ class AvatarImageService
protected function configureFromSettings(): void
{
- $config = config('koneko.admin.avatar.image', []);
+ $config = config_m()->get('ui.avatar.image', []);
$this->avatarDisk = $config['disk'] ?? $this->avatarDisk;
$this->profilePhotoDir = $config['directory'] ?? $this->profilePhotoDir;
@@ -172,4 +173,4 @@ class AvatarImageService
Storage::disk($this->avatarDisk)->deleteDirectory($this->profilePhotoDir);
$this->ensureProfilePhotoDirectoryExists();
}
-}
\ No newline at end of file
+}
diff --git a/src/Application/UI/Livewire/KonekoVuexy/Plugins/VuexyQuicklinks.php b/src/Application/UI/Livewire/KonekoVuexy/Plugins/VuexyQuicklinks.php
index e201c73..2e7ff6c 100644
--- a/src/Application/UI/Livewire/KonekoVuexy/Plugins/VuexyQuicklinks.php
+++ b/src/Application/UI/Livewire/KonekoVuexy/Plugins/VuexyQuicklinks.php
@@ -5,13 +5,12 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UI\Livewire\KonekoVuexy\Plugins;
use Illuminate\Support\Facades\Route;
-use Koneko\VuexyAdmin\Application\UX\Navbar\VuexyQuicklinksBuilderService;
-use Koneko\VuexyAdmin\Support\Traits\Livewire\Notifications\HandlesAsyncNotifications;
+use Koneko\VuexyAdmin\Application\UX\Navbar\VuexyQuicklinksBuilder;
use Livewire\Component;
class VuexyQuicklinks extends Component
{
- use HandlesAsyncNotifications;
+ //use HandlesAsyncNotifications;
public $vuexyQuickLinks;
@@ -20,11 +19,18 @@ class VuexyQuicklinks extends Component
public string $currentRouteId = '';
public string $slug = '';
+ private VuexyQuicklinksBuilder $quickLinksBuilder;
+
+ public function __construct()
+ {
+ $this->quickLinksBuilder = app(VuexyQuicklinksBuilder::class);
+ }
+
public function mount()
{
$this->slug = Route::current()->parameter('slug') ?? '';
$this->currentRouteId = $this->getCurrentRouteId();
- $this->isRouteAllowed = app(VuexyQuicklinksBuilderService::class)->isRouteAllowed($this->currentRouteId);
+ $this->isRouteAllowed = $this->quickLinksBuilder->isRouteAllowed($this->currentRouteId);
$this->loadQuickLinks();
}
@@ -33,7 +39,7 @@ class VuexyQuicklinks extends Component
{
if (!$this->isRouteAllowed) return;
- app(VuexyQuicklinksBuilderService::class)->addRoute($this->currentRouteId);
+ $this->quickLinksBuilder->addRoute($this->currentRouteId);
$this->loadQuickLinks();
$this->notify('Atajo agregado correctamente', 'success');
@@ -42,7 +48,7 @@ class VuexyQuicklinks extends Component
public function remove(): void
{
- app(VuexyQuicklinksBuilderService::class)->removeRoute($this->currentRouteId);
+ $this->quickLinksBuilder->removeRoute($this->currentRouteId);
$this->loadQuickLinks();
$this->notify('Atajo removido correctamente', 'warning');
@@ -54,12 +60,11 @@ class VuexyQuicklinks extends Component
return $this->slug ? "slug:{$this->slug}" : Route::currentRouteName();
}
-
public function loadQuickLinks(?string $routeId = null): void
{
$routeId ??= $this->currentRouteId;
- $quickLinks = app(VuexyQuicklinksBuilderService::class)->getUserQuicklinks();
+ $quickLinks = $this->quickLinksBuilder->getUserQuicklinks();
$quickLinks['current_page_in_list'] = collect($quickLinks['rows'] ?? [])
->flatten(1)
diff --git a/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards copy.php b/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards copy.php
deleted file mode 100644
index e4691c1..0000000
--- a/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards copy.php
+++ /dev/null
@@ -1,281 +0,0 @@
-slug) {
- $entry = app(VuexyMenuFormatter::class)->getMenuBySlug($this->slug);
-
-echo '
';
- print_r($entry);
-echo '
';
-
-
- if ($entry) {
- $key = array_key_first($entry);
- $node = $entry[$key];
- $trail = $node['_trail'] ?? [];
-
- $this->title = $trail[array_key_last($trail)]['label'] ?? $key;
- $this->description = $node['description'] ?? ($node['_meta']['description'] ?? '');
- $this->icon = $node['icon'] ?? ($node['_meta']['icon'] ?? 'ti ti-folder');
-
- $submenu = $node['submenu'] ?? [];
- $layout = $node['_meta']['home_layout'] ?? 'flat';
-
- $this->menuCards = match ($layout) {
- 'grouped' => $this->buildGroupedCards($submenu),
- default => $this->buildFlatCards($submenu),
- };
-
- $this->components = $this->extractComponentsFromRegistry($this->menuCards);
- return;
- }
- }
-
- $menu = app(VuexyMenuFormatter::class)->getMenu();
-
- $this->menuCards = $this->buildGroupedCards($menu);
- $this->components = $this->extractComponentsFromRegistry($this->menuCards);
- }
-
- protected function extractComponentsFromRegistry(): array
- {
- $usedComponents = collect($this->menuCards)
- ->flatMap(fn($group) => $group['cards'] ?? [])
- ->pluck('component')
- ->filter()
- ->unique()
- ->values();
-
- return collect(KonekoModuleRegistry::enabled())
- ->filter(fn($module) => $usedComponents->contains($module->componentNamespace))
- ->map(fn($module) => [
- 'component' => $module->componentNamespace,
- 'label' => $module->name,
- ])
- ->sortBy(function ($item) {
- return $item['component'] === 'core' ? -1 : $item['label'];
- })
- ->values()
- ->toArray();
- }
-
- /*
- protected function hasHomeAtRoot(array $submenu): bool
- {
- return collect($submenu)
- ->filter(fn($item) => !empty($item['_meta']['home_at_root']))
- ->count() > 1;
- }
- */
-
- protected function buildGroupedCards(array $menu): array
- {
- $roots = $this->extractRootElements($menu);
- $cards = $this->collectCards($menu);
-
- $excludedAutoIds = $this->getAutoIdsFromRootGroups($roots, $cards);
-
- $mainGroup = [
- 'type' => 'root',
- 'title' => $this->title,
- 'icon' => $this->icon,
- 'description' => $this->description,
- 'cards' => collect($cards)
- ->reject(fn($c) => in_array($c['auto_id'], $excludedAutoIds))
- ->unique('auto_id')
- ->values()
- ->map(fn($c) => Arr::only($c, ['title', 'icon', 'url', '_target', 'disallowed_link', 'component', 'hidden_item']))
- ->toArray(),
- ];
-
-
- // 2. Agregamos los grupos secundarios como "Configuraciones de contactos"
- $subGroups = collect($roots)->map(function ($root) use ($cards) {
- $grouped = collect($cards)
- ->filter(fn($c) => $c['category'] === $root['title'])
- ->unique('auto_id')
- ->values()
- ->map(fn($c) => Arr::only($c, ['title', 'icon', 'url', '_target', 'disallowed_link', 'component', 'hidden_item']))
- ->toArray();
-
- return array_merge($root, ['cards' => $grouped]);
- })->toArray();
-
- return array_merge([$mainGroup], $subGroups);
- }
-
-
- protected function buildFlatCards(array $submenu): array
- {
- $cards = $this->collectCards($submenu);
-
- return [[
- 'type' => 'root',
- 'title' => $this->title,
- 'icon' => $this->icon,
- 'description' => $this->description,
- 'cards' => collect($cards)
- ->unique('auto_id')
- ->values()
- ->map(fn($c) => Arr::only($c, ['title', 'icon', 'url', '_target', 'disallowed_link', 'component', 'hidden_item']))
- ->toArray(),
- ]];
- }
-
- protected function extractRootElements(array $menu): array
- {
- return $this->collectPromotedRootsRecursive($menu);
- }
-
- protected function collectPromotedRootsRecursive(array $submenu, array &$roots = []): array
- {
- foreach ($submenu as $key => $item) {
- if (!empty($item['_meta']['home_at_root'])) {
- $roots[] = $this->formatAsRootItem($key, $item);
- }
-
- if (!empty($item['submenu']) && is_array($item['submenu'])) {
- $this->collectPromotedRootsRecursive($item['submenu'], $roots);
- }
- }
-
- return $roots;
- }
-
-
- protected function collectPromotedRoots(array $submenu, array &$roots): void
- {
- foreach ($submenu as $key => $item) {
- if (!empty($item['_meta']['home_at_root'])) {
- $roots[] = $this->formatAsRootItem($key, $item);
- }
-
- if (!empty($item['submenu'])) {
- $this->collectPromotedRoots($item['submenu'], $roots);
- }
- }
- }
-
- protected function collectCards(array $menu, array &$collector = [], ?string $category = null): array
- {
- foreach ($menu as $key => $item) {
- $label = $item['_meta']['widget_label'] ?? $item['_meta']['original_key'] ?? $key;
-
- // Este es el nombre que define el grupo real
- $groupName = !empty($item['_meta']['home_at_root']) ? $label : ($category ?? $label);
-
-
- if (!empty($item['route']) || !empty($item['url'])) {
- $colection = [
- 'auto_id' => $item['_meta']['auto_id'] ?? crc32($key . ($item['route'] ?? $item['url'] ?? '')),
- 'title' => $label,
- 'icon' => $item['icon'] ?? 'ti ti-circle',
- 'url' => isset($item['route']) && Route::has($item['route'])
- ? route($item['route'])
- : ($item['url'] ?? 'javascript:;'),
- '_target' => (isset($item['url']) && str_starts_with($item['url'], 'http')) ? '_blank' : null,
- 'component' => isset($item['_meta']['component'])? $item['_meta']['component']: null,
- 'category' => $groupName,
- ];
-
- if(config('koneko.admin.menu.debug.show_disallowed_links')){
- $colection['disallowed_link'] = $item['_meta']['disallowed_link'];
- }
-
- if(config('koneko.admin.menu.debug.show_hidden_items')){
- $colection['hidden_item'] = $item['_meta']['hidden_item'];
- }
-
- $collector[] = $colection;
- }
-
- if (!empty($item['submenu'])) {
- $this->collectCards($item['submenu'], $collector, $groupName);
- }
- }
-
- return $collector;
- }
-
- protected function formatAsRootItem(string $key, array $item): array
- {
- return [
- 'type' => 'root',
- 'title' => $item['_meta']['widget_label'] ?? $item['_meta']['original_key'] ?? $key,
- 'icon' => $item['icon'] ?? 'ti ti-folder',
- 'description' => $item['description'] ?? '',
- ];
- }
-
- /*
- protected function shouldGroupSubmenu(array $submenu): bool
- {
- $homeAtRoot = collect($submenu)
- ->filter(fn($item) => !empty($item['_meta']['home_at_root']));
-
- // Si hay más de uno, agrupamos
- if ($homeAtRoot->count() > 1) return true;
-
- // Si solo hay uno con submenu, agrupamos
- if ($homeAtRoot->count() === 1 && !empty($homeAtRoot->first()['submenu'])) {
- return true;
- }
-
- // Si no hay ninguno o es plano, NO agrupamos
- return false;
- }
- */
-
- protected function getAutoIdsFromRootGroups(array $roots, array $cards): array
- {
- return collect($roots)
- ->flatMap(function ($root) use ($cards) {
- return collect($cards)
- ->filter(fn($c) => $c['category'] === $root['title'])
- ->pluck('auto_id');
- })
- ->unique()
- ->values()
- ->toArray();
- }
-
-
- public function render()
- {
- return view('vuexy-admin::livewire.pages.dashboard.menu-access-cards', [
- 'menuCards' => $this->menuCards,
- ]);
- }
-}
diff --git a/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards.php b/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards.php
index 5ad1b1b..f2b5e74 100644
--- a/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards.php
+++ b/src/Application/UI/Livewire/Pages/Dashboards/MenuAccessCards.php
@@ -4,7 +4,7 @@ namespace Koneko\VuexyAdmin\Application\UI\Livewire\Pages\Dashboards;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
-use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
+use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
use Livewire\Component;
@@ -83,7 +83,7 @@ class MenuAccessCards extends Component
])
->push([
'tag' => 'project',
- 'label' => config('koneko.admin.project_label', 'Proyecto'),
+ 'label' => config('project.label', 'Proyecto'),
])
->sortBy(fn($item) => $item['tag'] === 'core' ? -1 : $item['label']);
}
diff --git a/src/Application/UI/Livewire/Settings/EnvironmentVars/EnvironmentVarsOffCanvasForm.php b/src/Application/UI/Livewire/Settings/EnvironmentVars/EnvironmentVarsOffCanvasForm.php
index aa20c81..e21a622 100644
--- a/src/Application/UI/Livewire/Settings/EnvironmentVars/EnvironmentVarsOffCanvasForm.php
+++ b/src/Application/UI/Livewire/Settings/EnvironmentVars/EnvironmentVarsOffCanvasForm.php
@@ -78,14 +78,14 @@ class EnvironmentVarsOffCanvasForm extends AbstractFormOffCanvasComponent
}
return [
- 'key' => ['required', 'string', $uniqueRule],
- 'module' => ['nullable', 'string', 'max:96'],
- 'user_id' => ['nullable', 'integer', 'exists:users,id'],
- 'value_string' => ['nullable', 'string', 'max:255'],
- 'value_integer' => ['nullable', 'integer'],
- 'value_boolean' => ['nullable', 'boolean'],
- 'value_float' => ['nullable', 'numeric'],
- 'value_text' => ['nullable', 'string'],
+ 'key' => ['required', 'string', $uniqueRule],
+ 'module' => ['nullable', 'string', 'max:96'],
+ 'user_id' => ['nullable', 'integer', 'exists:users,id'],
+ 'value_string' => ['nullable', 'string', 'max:255'],
+ 'value_integer' => ['nullable', 'integer'],
+ 'value_boolean' => ['nullable', 'boolean'],
+ 'value_float' => ['nullable', 'numeric'],
+ 'value_text' => ['nullable', 'string'],
];
}
diff --git a/src/Application/UI/Livewire/Settings/VuexyInterface/VuexyInterfaceIndex.php b/src/Application/UI/Livewire/Settings/VuexyInterface/VuexyInterfaceIndex.php
index 7757a57..727dccf 100644
--- a/src/Application/UI/Livewire/Settings/VuexyInterface/VuexyInterfaceIndex.php
+++ b/src/Application/UI/Livewire/Settings/VuexyInterface/VuexyInterfaceIndex.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UI\Livewire\Settings\VuexyInterface;
use Koneko\VuexyAdmin\Application\Events\Settings\VuexyCustomizerSettingsUpdated;
-use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
+use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
use Livewire\Component;
class VuexyInterfaceIndex extends Component
@@ -64,7 +64,7 @@ class VuexyInterfaceIndex extends Component
public function clearCustomConfig()
{
// Elimina las claves koneko.admin.vuexy.* para cargar los valores por defecto
- VuexyVarsBuilderService::deleteVuexyCustomizerVars();
+ KonekoAdminVarsBuilder::deleteVuexyCustomizerVars();
// Refrescar el componente actual
$this->dispatch('refreshAndNotify');
@@ -73,7 +73,7 @@ class VuexyInterfaceIndex extends Component
public function loadForm()
{
// Obtener los valores de las configuraciones de la base de datos
- $settings = app(VuexyVarsBuilderService::class)->getVuexyCustomizerVars();
+ $settings = app(KonekoAdminVarsBuilder::class)->getVuexyCustomizerVars();
$this->vuexy_myLayout = $settings['myLayout'];
$this->vuexy_myTheme = $settings['myTheme'];
diff --git a/src/Application/UI/Livewire/Settings/WebInterface/AppFaviconCard.php b/src/Application/UI/Livewire/Settings/WebInterface/AppFaviconCard.php
index 6c1bf40..42d2afc 100644
--- a/src/Application/UI/Livewire/Settings/WebInterface/AppFaviconCard.php
+++ b/src/Application/UI/Livewire/Settings/WebInterface/AppFaviconCard.php
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UI\Livewire\Settings\WebInterface;
-use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
-use Koneko\VuexyAdmin\Application\System\VuexyAdminImageHandlerService;
+use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
+use Koneko\VuexyAdmin\Application\Template\ImageHandler\WebAdminImageHandler;
use Livewire\Component;
use Livewire\WithFileUploads;
@@ -36,10 +36,10 @@ class AppFaviconCard extends Component
]);
// Procesar favicon
- app(VuexyAdminImageHandlerService::class)->processAndSaveFavicon($this->upload_image_favicon);
+ app(WebAdminImageHandler::class)->processAndSaveFavicon($this->upload_image_favicon);
// Limpiamos la cache
- app(VuexyVarsBuilderService::class)->clearCache();
+ app(KonekoAdminVarsBuilder::class)->clearCache();
// Recargamos el formulario
$this->loadForm();
@@ -56,7 +56,7 @@ class AppFaviconCard extends Component
public function loadForm()
{
// Obtener los valores de las configuraciones de la base de datos
- $settings = app(VuexyVarsBuilderService::class)->getAdminVars();
+ $settings = app(KonekoAdminVarsBuilder::class)->getAdminVars();
$this->upload_image_favicon = null;
$this->admin_favicon_16x16 = $settings['favicon']['16x16'];
diff --git a/src/Application/UI/Livewire/Settings/WebInterface/LogoOnDarkBgCard.php b/src/Application/UI/Livewire/Settings/WebInterface/LogoOnDarkBgCard.php
index 96c37d7..610ea95 100644
--- a/src/Application/UI/Livewire/Settings/WebInterface/LogoOnDarkBgCard.php
+++ b/src/Application/UI/Livewire/Settings/WebInterface/LogoOnDarkBgCard.php
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UI\Livewire\Settings\WebInterface;
-use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
-use Koneko\VuexyAdmin\Application\System\VuexyAdminImageHandlerService;
+use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
+use Koneko\VuexyAdmin\Application\Template\ImageHandler\WebAdminImageHandler;
use Livewire\Component;
use Livewire\WithFileUploads;
@@ -30,10 +30,10 @@ class LogoOnDarkBgCard extends Component
]);
// Procesar favicon si se ha cargado una imagen
- app(VuexyAdminImageHandlerService::class)->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark');
+ app(WebAdminImageHandler::class)->processAndSaveImageLogo($this->upload_image_logo_dark, 'dark');
// Limpiamos la cache
- app(VuexyVarsBuilderService::class)->clearCache();
+ app(KonekoAdminVarsBuilder::class)->clearCache();
// Recargamos el formulario
$this->loadForm();
@@ -50,7 +50,7 @@ class LogoOnDarkBgCard extends Component
public function loadForm()
{
// Obtener los valores de las configuraciones de la base de datos
- $settings = app(VuexyVarsBuilderService::class)->getAdminVars();
+ $settings = app(KonekoAdminVarsBuilder::class)->getAdminVars();
$this->upload_image_logo_dark = null;
$this->admin_image_logo_dark = $settings['image_logo']['large_dark'];
diff --git a/src/Application/UI/Livewire/Settings/WebInterface/LogoOnLightBgCard.php b/src/Application/UI/Livewire/Settings/WebInterface/LogoOnLightBgCard.php
index fb14014..825750f 100644
--- a/src/Application/UI/Livewire/Settings/WebInterface/LogoOnLightBgCard.php
+++ b/src/Application/UI/Livewire/Settings/WebInterface/LogoOnLightBgCard.php
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UI\Livewire\Settings\WebInterface;
-use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
-use Koneko\VuexyAdmin\Application\System\VuexyAdminImageHandlerService;
+use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
+use Koneko\VuexyAdmin\Application\Template\ImageHandler\WebAdminImageHandler;
use Livewire\Component;
use Livewire\WithFileUploads;
@@ -30,10 +30,10 @@ class LogoOnLightBgCard extends Component
]);
// Procesar favicon si se ha cargado una imagen
- app(VuexyAdminImageHandlerService::class)->processAndSaveImageLogo($this->upload_image_logo);
+ app(WebAdminImageHandler::class)->processAndSaveImageLogo($this->upload_image_logo);
// Limpiamos la cache
- app(VuexyVarsBuilderService::class)->clearCache();
+ app(KonekoAdminVarsBuilder::class)->clearCache();
// Recargamos el formulario
$this->loadForm();
@@ -50,7 +50,7 @@ class LogoOnLightBgCard extends Component
public function loadForm()
{
// Obtener los valores de las configuraciones de la base de datos
- $settings = app(VuexyVarsBuilderService::class)->getAdminVars();
+ $settings = app(KonekoAdminVarsBuilder::class)->getAdminVars();
$this->upload_image_logo = null;
$this->admin_image_logo = $settings['image_logo']['large'];
diff --git a/src/Application/UX/Content/VuexyBreadcrumbsBuilderService.php b/src/Application/UX/Breadcrumbs/VuexyBreadcrumbsBuilder.php
similarity index 97%
rename from src/Application/UX/Content/VuexyBreadcrumbsBuilderService.php
rename to src/Application/UX/Breadcrumbs/VuexyBreadcrumbsBuilder.php
index 7258bd8..b7ddb90 100644
--- a/src/Application/UX/Content/VuexyBreadcrumbsBuilderService.php
+++ b/src/Application/UX/Breadcrumbs/VuexyBreadcrumbsBuilder.php
@@ -2,12 +2,12 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\UX\Content;
+namespace Koneko\VuexyAdmin\Application\UX\Breadcrumbs;
use Illuminate\Support\Facades\Route;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
-class VuexyBreadcrumbsBuilderService
+class VuexyBreadcrumbsBuilder
{
private array $menu;
diff --git a/src/Application/UX/Template/VuexyConfigSynchronizer.php b/src/Application/UX/Customize/___VuexyConfigSynchronizer.php
similarity index 69%
rename from src/Application/UX/Template/VuexyConfigSynchronizer.php
rename to src/Application/UX/Customize/___VuexyConfigSynchronizer.php
index dd1bc0d..8a998ea 100644
--- a/src/Application/UX/Template/VuexyConfigSynchronizer.php
+++ b/src/Application/UX/Customize/___VuexyConfigSynchronizer.php
@@ -2,29 +2,43 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\UX\Template;
+namespace Koneko\VuexyAdmin\Application\Template\Customize;
use Illuminate\Support\Facades\{Cache, Config};
-use Koneko\VuexyAdmin\Application\Cache\{KonekoCacheManager, VuexyVarsBuilderService};
+use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
+use Koneko\VuexyAdmin\Application\Cache\KonekoCacheManager;
-class VuexyConfigSynchronizer
+class ___VuexyConfigSynchronizer
{
+ /** @var string Settings Context */
+ private const GROUP = 'website-admin';
+ private const SECTION = 'layout';
+ private const SUB_GROUP = 'vuexy';
+
+ /** @var string Cache keyName */
+ private const KEY_NAME = 'customizer';
+
+
+
+
+
+
private static function manager(): KonekoCacheManager
{
- $component = VuexyVarsBuilderService::$component;
- $group = VuexyVarsBuilderService::$group;
+ $component = KonekoAdminVarsBuilder::$component;
+ $group = KonekoAdminVarsBuilder::$group;
- return cache_manager()->setContext($component, $group);
+ return cache_m()->setContext($component, $group);
}
private static function cacheKey(): string
{
- return VuexyVarsBuilderService::SETTINGS_CUSTOMIZER_VARS_KEY;;
+ return KonekoAdminVarsBuilder::SETTINGS_CUSTOMIZER_VARS_KEY;;
}
private static function configKey(): string
{
- $namespace = VuexyVarsBuilderService::$namespace;
+ $namespace = KonekoAdminVarsBuilder::$namespace;
return "{$namespace}.admin.vuexy";
}
diff --git a/src/Application/UX/Customize/___VuexyLayoutConfigSynchronizer.php b/src/Application/UX/Customize/___VuexyLayoutConfigSynchronizer.php
new file mode 100644
index 0000000..e42b53e
--- /dev/null
+++ b/src/Application/UX/Customize/___VuexyLayoutConfigSynchronizer.php
@@ -0,0 +1,68 @@
+setComponent(self::COMPONENT)
+ ->context(self::GROUP, self::SECTION, self::SUBGROUP)
+ ->setUser(Auth::user()) // Aplica scope=user si está logueado
+ ->setKeyName(self::CACHE_KEY);
+
+ $overrides = $manager->rememberWithTTLResolution(
+ fn () => static::loadMerged()
+ );
+
+ Config::set(self::CONFIG_KEY, $overrides);
+ }
+
+ public static function clear(): void
+ {
+ cache_m()
+ ->setComponent(self::COMPONENT)
+ ->context(self::GROUP, self::SECTION, self::SUBGROUP)
+ ->setUser(Auth::user())
+ ->setKeyName(self::CACHE_KEY)
+ ->forget();
+ }
+
+ private static function loadMerged(): array
+ {
+ $base = config(self::CONFIG_KEY, []);
+
+ $settings = settings()
+ ->setComponent(self::COMPONENT)
+ ->context(self::GROUP, self::SECTION, self::SUBGROUP)
+ ->setUser(Auth::user())
+ ->getSubGroup(true);
+
+ return array_replace_recursive($base, array_map(
+ fn ($val, $key) => static::castValue($key, $val),
+ $settings,
+ array_keys($settings)
+ ));
+ }
+
+ private static function castValue(string $key, mixed $value): mixed
+ {
+ return match (true) {
+ in_array($key, ['hasCustomizer', 'displayCustomizer', 'footerFixed', 'menuFixed', 'menuCollapsed', 'showDropdownOnHover'], true) => filter_var($value, FILTER_VALIDATE_BOOLEAN),
+ $key === 'maxQuickLinks' => (int) $value,
+ default => $value,
+ };
+ }
+}
diff --git a/src/Application/UX/Template/VuexyAdminImageHandlerService.php b/src/Application/UX/ImageHandler/WebAdminImageHandler.php
similarity index 97%
rename from src/Application/UX/Template/VuexyAdminImageHandlerService.php
rename to src/Application/UX/ImageHandler/WebAdminImageHandler.php
index f896b00..e089c03 100644
--- a/src/Application/UX/Template/VuexyAdminImageHandlerService.php
+++ b/src/Application/UX/ImageHandler/WebAdminImageHandler.php
@@ -2,16 +2,16 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\System;
+namespace Koneko\VuexyAdmin\Application\UX\ImageHandler;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
-use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
+use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
/**
* Servicio para gestionar favicon y logos administrativos.
*/
-class VuexyAdminImageHandlerService
+class WebAdminImageHandler
{
private string $driver;
private string $imageDisk = 'public';
diff --git a/src/Application/UX/Menu/VuexyMenuBuilderService.php b/src/Application/UX/Menu/VuexyMenuBuilder.php
similarity index 68%
rename from src/Application/UX/Menu/VuexyMenuBuilderService.php
rename to src/Application/UX/Menu/VuexyMenuBuilder.php
index 5479eb0..a549acf 100644
--- a/src/Application/UX/Menu/VuexyMenuBuilderService.php
+++ b/src/Application/UX/Menu/VuexyMenuBuilder.php
@@ -4,18 +4,11 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UX\Menu;
-use Illuminate\Contracts\Auth\Authenticatable;
-use Illuminate\Support\Facades\{Auth,Route};
-use Illuminate\Support\Str;
-use Koneko\VuexyAdmin\Models\User;
-use Koneko\VuexyAdmin\Support\Traits\Cache\InteractsWithKonekoVarsCache;
-use Spatie\Permission\Exceptions\PermissionDoesNotExist;
-
/**
* Clase encargada de construir el menú dinámico Vuexy
* basado en permisos, configuración, y visibilidad.
*/
-class VuexyMenuBuilderService
+class VuexyMenuBuilder
{
/**
* Obtiene el menú procesado para un usuario específico, visitante o el autenticado.
diff --git a/src/Application/UX/Menu/VuexyMenuFormatter.php b/src/Application/UX/Menu/VuexyMenuFormatter.php
index a65d741..46b0692 100644
--- a/src/Application/UX/Menu/VuexyMenuFormatter.php
+++ b/src/Application/UX/Menu/VuexyMenuFormatter.php
@@ -5,75 +5,84 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UX\Menu;
use Illuminate\Contracts\Auth\Authenticatable;
-use Illuminate\Support\Facades\{Auth , Route};
+use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
-use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
+use Koneko\VuexyAdmin\Application\Config\Contracts\ConfigRepositoryInterface;
+use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
+use Koneko\VuexyAdmin\Support\Traits\Auth\HasResolvableUser;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
-class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
+class VuexyMenuFormatter
{
- /** @var string Componente base */
- private const COMPONENT = 'core';
- private const GROUP = 'layout';
+ use HasResolvableUser;
- /** @var string Cache key */
- private const CACHE_KEY = 'menu';
+ private const GROUP = 'website-admin';
+ private const SECTION = 'layout';
+ private const SUB_GROUP = 'menu';
- /** @var bool Cache scope */
- protected bool $isUserScoped = true;
+ private const MENU_KEY_NAME = 'menu';
+ private const EXTRA_QUICKLINKS_KEY_NAME = 'extra-quicklinks';
- /** @var array Opciones de formato */
- private array $options = [];
-
- public function __construct()
+ public function getMenu(Authenticatable|int|null|false $user = null): array
{
- parent::__construct(self::COMPONENT, self::GROUP);
- }
-
- public function getMenu(false|null|Authenticatable $user = null, array $options = []): array
- {
- $this->user = $user === false ? null : ($user ?? Auth::user());
-
- $this->options = $options;
- $this->isUserScoped = $user !== false;
-
- $menu = $this->rememberCache(self::CACHE_KEY, fn () => $this->format());
+ $menu = self::settings($user)
+ ->setKeyName(self::MENU_KEY_NAME)
+ ->remember(fn () => $this->format($user));
$this->markActiveTrail($menu);
return $menu;
}
- public function getMenuBySlug(string $slug, false|null|Authenticatable $user = null, array $options = []): ?array
+ public function getMenuBySlug(string $slug, Authenticatable|int|null|false $user = null): ?array
{
- $menu = $this->getMenu($user, $options);
+ $menu = $this->getMenu($user);
return $this->findInMenuWithKey($menu, fn($item) => ($item['_slug'] ?? null) === $slug);
}
- public function getMenuByAutoId(int $autoId, false|null|Authenticatable $user = null, array $options = []): ?array
+ public function getMenuByAutoId(int $autoId, Authenticatable|int|null|false $user = null): ?array
{
- $menu = $this->getMenu($user, $options);
+ $menu = $this->getMenu($user);
return $this->findInMenuWithKey($menu, fn($item) => ($item['_meta']['auto_id'] ?? null) === $autoId);
}
- /**
- * Retorna los ítems definidos en _extra_quicklinks del menú, procesados como atajos rápidos.
- */
- public function getExtraQuicklinks(false|null|Authenticatable $user = null): array
+ public function getExtraQuicklinks(Authenticatable|int|null|false $user = null): array
{
- $this->user = $user === false ? null : ($user ?? Auth::user());
- $this->isUserScoped = !is_null($this->user);
+ return self::settings($user)
+ ->setKeyName(self::EXTRA_QUICKLINKS_KEY_NAME)
+ ->remember(fn () => $this->buildExtraQuicklinks($user));
+ }
- $rawMenu = app(VuexyMenuRegistry::class)->getMerged();
+ protected function format(Authenticatable|int|null|false $user = null): array
+ {
+ $rawMenu = $this->getRawMenu();
+ $menu = $this->processRecursive($rawMenu);
- $extra = $rawMenu['_extra_quicklinks'] ?? [];
+ $this->assignAutoIds($menu);
+ $this->assignSlugs($menu);
+ $this->assignCounts($menu);
+ if (self::config()->get('debug.show_disallowed_links', false)
+ || self::config()->get('debug.show_hidden_items', false)
+ || self::config()->get('debug.show_broken_routes', false)
+ ) {
+ $this->markDebugFlags($menu, $user);
+ }
+
+ unset($menu['_extra']);
+
+ return $menu;
+ }
+
+ protected function buildExtraQuicklinks(Authenticatable|int|null|false $user = null): array
+ {
+ $extra = $this->getRawMenu()['_extra']['_quicklinks'] ?? [];
$links = [];
foreach ($extra as $key => $item) {
- if (!$this->isVisible($item)) continue;
+ if (!$this->isVisible($item, $user)) continue;
$this->convertRouteToUrl($item);
@@ -89,6 +98,11 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
return $links;
}
+ protected function getRawMenu(): array
+ {
+ return app(VuexyMenuRegistry::class)->getMerged();
+ }
+
/**
* Recorre recursivamente el menú para encontrar el ítem que cumpla con la condición.
*/
@@ -114,29 +128,6 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
return null;
}
- protected function format(): array
- {
- $rawMenu = app(VuexyMenuRegistry::class)->getMerged();
-
- $menu = $this->processRecursive($rawMenu);
-
- $this->assignAutoIds($menu);
- $this->assignSlugs($menu);
- $this->assignCounts($menu);
-
- if (
- config('koneko.admin.menu.debug.show_disallowed_links', false)
- || config('koneko.admin.menu.debug.show_hidden_items', false)
- || config('koneko.admin.menu.debug.show_broken_routers', false)
- ) {
- $this->markDebugFlags($menu);
- }
-
- unset($menu['_extra_quicklinks']);
-
- return $menu;
- }
-
protected function processRecursive(array $menu): array
{
$result = [];
@@ -253,7 +244,7 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
return $sorted;
}
- protected function isVisible(array $item): bool
+ protected function isVisible(array $item, Authenticatable|int|null|false $user = null): bool
{
if (isset($item['_meta']['visible']) && $item['_meta']['visible'] === false)
return false;
@@ -261,31 +252,31 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
if (isset($item['visible']) && $item['visible'] === false)
return false;
- if (isset($item['can']) && !$this->userCan($item['can']))
- return config('koneko.admin.menu.debug.show_disallowed_links', false);
+ if (isset($item['can']) && !$this->userCan($user, $item['can']))
+ return self::config()->get('debug.show_disallowed_links', false);
if (isset($item['route']) && !Route::has($item['route']))
- return config('koneko.admin.menu.debug.show_broken_routers', false);
+ return self::config()->get('debug.show_broken_routes', false);
return true;
}
- protected function userCan(string|array $permissions): bool
+ protected function userCan(Authenticatable|int|null|false $user, string|array $permissions): bool
{
- if (!$this->user || !method_exists($this->user, 'hasPermissionTo')) {
- return false;
- }
+ $user = $this->resolveUser($user);
+
+ if (!$user || !method_exists($user, 'hasPermissionTo')) { return false; }
try {
if (is_array($permissions)) {
foreach ($permissions as $perm) {
- if ($this->user->hasPermissionTo($perm)) return true;
+ if ($user->hasPermissionTo($perm)) return true;
}
return false;
}
- return $this->user->hasPermissionTo($permissions);
+ return $user->hasPermissionTo($permissions);
} catch (PermissionDoesNotExist) {
return false;
@@ -347,7 +338,7 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
protected function markActiveTrail(array &$menu): void
{
$currentRoute = Route::currentRouteName();
- $currentUrl = request()->url();
+ $currentUrl = request()->url();
$markTrail = function (&$items, $trail = []) use (&$markTrail, $currentRoute, $currentUrl): bool {
foreach ($items as &$item) {
@@ -355,6 +346,7 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
if (isset($item['route']) && $item['route'] === $currentRoute) {
$match = true;
+
} elseif (isset($item['url']) && Str::is($item['url'], $currentUrl)) {
$match = true;
}
@@ -380,49 +372,50 @@ class VuexyMenuFormatter extends AbstractKeyValueCacheBuilder
$markTrail($menu);
}
- protected function markDebugFlags(array &$menu): void
+ protected function markDebugFlags(array &$menu, Authenticatable|int|null|false $user): void
{
foreach ($menu as &$item) {
// Flag: el usuario no tiene permiso
- if (config('koneko.admin.menu.debug.show_disallowed_links', false)) {
- $item['_meta']['disallowed_link'] = isset($item['can']) && !$this->userCan($item['can']);
+ if (self::config()->get('debug.show_disallowed_links', false)) {
+ $item['_meta']['disallowed_link'] = isset($item['can']) && !$this->userCan($user, $item['can']);
}
// Flag: está marcado como oculto
- if (config('koneko.admin.menu.debug.show_hidden_items', false)) {
+ if (self::config()->get('debug.show_hidden_items', false)) {
$item['_meta']['hidden_item'] = isset($item['_meta']['visible']) && $item['_meta']['visible'] === false;
}
// Flag: la ruta no existe
- if (config('koneko.admin.menu.debug.show_broken_routers', false)) {
+ if (self::config()->get('debug.show_broken_routes', false)) {
$item['_meta']['broken_route'] = isset($item['route']) && !Route::has($item['route']);
}
if (!empty($item['submenu'])) {
- $this->markDebugFlags($item['submenu']);
+ $this->markDebugFlags($item['submenu'], $user);
}
}
}
- public static function forgetCacheForUser(?int $userId = null): void
+ public static function forgetCacheForUser(Authenticatable|int|null|false $user = null): void
{
- $instance = new static();
+ self::settings($user)
+ ->setKeyName(self::MENU_KEY_NAME)
+ ->forgetCache();
- $instance->user = $userId
- ? app('auth')->getProvider()->retrieveById($userId)
- : Auth::user();
- $instance->isUserScoped = true;
-
- $instance->forgetCache(self::CACHE_KEY);
+ self::settings($user)
+ ->setKeyName(self::EXTRA_QUICKLINKS_KEY_NAME)
+ ->forgetCache();
}
- public static function forgetVisitorCache(): void
+ private static function settings(Authenticatable|int|null|false $user = null): SettingsRepositoryInterface
{
- $instance = new static();
+ return settings()
+ ->context(self::GROUP, self::SECTION, self::SUB_GROUP)
+ ->setUser($user);
+ }
- $instance->user = null;
- $instance->isUserScoped = true;
-
- $instance->forgetCache(self::CACHE_KEY);
+ private static function config(): ConfigRepositoryInterface
+ {
+ return config_m()->context(self::SECTION, self::SUB_GROUP);
}
}
diff --git a/src/Application/UX/Menu/VuexyMenuMergeService.php b/src/Application/UX/Menu/VuexyMenuMergeService.php
deleted file mode 100644
index 6df6f7a..0000000
--- a/src/Application/UX/Menu/VuexyMenuMergeService.php
+++ /dev/null
@@ -1,81 +0,0 @@
- $overrideItem) {
- // Si la clave ya existe en el menú base
- if (isset($base[$key])) {
- // Si ambos son arrays con submenu, hacer merge profundo
- if (is_array($overrideItem) && is_array($base[$key])) {
- $merged = $base[$key];
-
- // Merge especial de _meta si ambos lo tienen
- if (isset($merged['_meta']) && isset($overrideItem['_meta'])) {
- $merged['_meta'] = array_merge($merged['_meta'], $overrideItem['_meta']);
- unset($overrideItem['_meta']);
- }
-
- // Si hay submenu en ambos, hacer merge recursivo
- if (isset($merged['submenu']) && isset($overrideItem['submenu'])) {
- $merged['submenu'] = static::mergeMenus($merged['submenu'], $overrideItem['submenu']);
- unset($overrideItem['submenu']);
- }
-
- // Resto de propiedades se sobrescriben
- $base[$key] = array_merge($merged, $overrideItem);
-
- } else {
- // Si no son arrays compatibles, sobrescribe completo
- $base[$key] = $overrideItem;
- }
-
- } else {
- // Clave nueva, simplemente agregar
- $base[$key] = $overrideItem;
- }
- }
-
- return $base;
- }
-
- /**
- * Valida la estructura general del menú para evitar errores comunes
- */
- public static function validate(array $menu, string $path = ''): array
- {
- $errors = [];
-
- foreach ($menu as $key => $item) {
- $label = $path . $key;
-
- if (!is_array($item)) {
- $errors[] = "[{$label}] no es un array válido";
- continue;
- }
-
- if (!isset($item['_meta'])) {
- $errors[] = "[{$label}] no contiene clave _meta";
- }
-
- if (isset($item['submenu'])) {
- if (!is_array($item['submenu'])) {
- $errors[] = "[{$label}] submenu debe ser un array";
- } else {
- $errors = array_merge($errors, static::validate($item['submenu'], $label . '/'));
- }
- }
- }
-
- return $errors;
- }
-}
diff --git a/src/Application/UX/Menu/VuexyMenuRegistry.php b/src/Application/UX/Menu/VuexyMenuRegistry.php
index 8cc4428..498b778 100644
--- a/src/Application/UX/Menu/VuexyMenuRegistry.php
+++ b/src/Application/UX/Menu/VuexyMenuRegistry.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UX\Menu;
use Illuminate\Support\Facades\File;
-use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
+use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
class VuexyMenuRegistry
{
@@ -63,7 +63,8 @@ class VuexyMenuRegistry
if (File::exists($fullPath)) {
$menu = require $fullPath;
- //dd('core menu', $menu); // verifica si la clave Inicio aparece aquí
+
+ // verifica si la clave Inicio aparece aquí
return $this->injectComponentMeta($menu, 'core');
}
@@ -125,8 +126,15 @@ class VuexyMenuRegistry
foreach ($menu as &$item) {
if (!is_array($item)) continue;
+ // Verificaos Extra Quicklinks
+ if (isset($item['_quicklinks'])) {
+ $item['_quicklinks'] = $this->injectComponentMeta($item['_quicklinks'], $component);
+ continue;
+ }
+
// Inyectar en _meta solo si 'component' no está definido
$item['_meta'] ??= [];
+
if (!isset($item['_meta']['component'])) {
$item['_meta']['component'] = $component;
}
diff --git a/src/Application/UX/Navbar/VuexyQuicklinksBuilderService.php b/src/Application/UX/Navbar/VuexyQuicklinksBuilder.php
similarity index 52%
rename from src/Application/UX/Navbar/VuexyQuicklinksBuilderService.php
rename to src/Application/UX/Navbar/VuexyQuicklinksBuilder.php
index 0b51a0a..bc1f819 100644
--- a/src/Application/UX/Navbar/VuexyQuicklinksBuilderService.php
+++ b/src/Application/UX/Navbar/VuexyQuicklinksBuilder.php
@@ -4,66 +4,60 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UX\Navbar;
-use Illuminate\Support\Facades\Auth;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
-use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
-class VuexyQuicklinksBuilderService extends AbstractKeyValueCacheBuilder
+class VuexyQuicklinksBuilder
{
- /** @var string Componente base */
- private const COMPONENT = 'core';
- private const GROUP = 'layout';
+ private const GROUP = 'website-admin';
+ private const SECTION = 'layout';
+ private const SUB_GROUP = 'navbar';
+ private const KEY_NAME = 'quicklinks';
- /** @var string Settings & Cache key */
- private const SETTINGS_KEY = 'navbar.quicklinks.user';
+ protected array $quicklinkRoutes = [];
- /** @var bool Cache scope */
- protected bool $isUserScoped = true;
-
- // Quicklink routes
- private array $quicklinkRoutes = [];
-
- public function __construct()
+ public function getUserQuicklinks(Authenticatable|int|null $user = null): array
{
- parent::__construct(self::COMPONENT, self::GROUP);
+ $quickLinks = self::settings($user)->get() ?? [];
+ $quickLinks = $this->buildQuicklinks($quickLinks, $user);
+
+ // dump($quickLinks); die;
+ return $quickLinks;
}
- public function getUserQuicklinks(): array
+ /*
+ protected function getQuicklinks(Authenticatable|int|null $user = null): array
{
- return $this->rememberCache(static::SETTINGS_KEY, fn () => $this->buildQuicklinks());
- }
+ $menu = self::getMenu($user);
+ $extra = self::getExtraQuicklinks($user);
- public function addRoute(string $route): void
- {
- $routes = settings()->setContext($this->component, $this->group)->get(static::SETTINGS_KEY, $this->user->id) ?? [];
+ $this->quicklinkRoutes = self::settings($user)
+ ->asArray()
+ ->getSubGroup() ?? [];
- //if (count($routes) >= 20) return;
+ $links = [];
+ $this->collectFromMenu($menu, $links);
- if (!in_array($route, $routes)) {
- $routes[] = $route;
-
- settings()->setContext($this->component, $this->group)->set(static::SETTINGS_KEY, json_encode($routes), $this->user->id);
- $this->forgetCache(static::SETTINGS_KEY);
+ foreach ($extra as $item) {
+ if (in_array($item['route'], $this->quicklinkRoutes)) {
+ $links[] = $item;
+ }
}
+ dd($links);
+ return [
+ 'totalLinks' => count($links),
+ 'rows' => array_chunk($links, 2),
+ ];
}
+ */
- public function removeRoute(string $route): void
+ protected function buildQuicklinks(array $routes, Authenticatable|int|null $user = null): array
{
- $routes = settings()->setContext($this->component, $this->group)->get(static::SETTINGS_KEY, $this->user->id) ?? [];
+ $menu = self::getMenu($user);
+ $extra = self::getExtraQuicklinks($user);
- // Filtra el arreglo para eliminar el valor igual al $route
- $routes = array_values(array_filter($routes, fn($r) => $r !== $route));
-
- settings()->setContext($this->component, $this->group)->set(static::SETTINGS_KEY, json_encode($routes), $this->user->id);
- $this->forgetCache(static::SETTINGS_KEY);
- }
-
- private function buildQuicklinks(): array
- {
- $menu = app(VuexyMenuFormatter::class)->getMenu();
- $extra = app(VuexyMenuFormatter::class)->getExtraQuicklinks();
-
- $this->quicklinkRoutes = settings()->setContext($this->component, $this->group)->get(static::SETTINGS_KEY, $this->user->id) ?? [];
+ $this->quicklinkRoutes = $routes;
$links = [];
$this->collectFromMenu($menu, $links);
@@ -76,10 +70,65 @@ class VuexyQuicklinksBuilderService extends AbstractKeyValueCacheBuilder
return [
'totalLinks' => count($links),
- 'rows' => array_chunk($links, 2),
+ 'rows' => array_chunk($links, 2),
];
}
+ public function addRoute(string $route, Authenticatable|int|null $user = null): void
+ {
+ $routes = self::settings($user)->get() ?? [];
+
+ if (count($routes) >= config_m()->get('layout.vuexy.maxQuickLinks', 12)) return;
+
+ if (!in_array($route, $routes)) {
+ $routes[] = $route;
+
+ self::settings($user)->set($routes);
+ }
+ }
+
+ public function removeRoute(string $route, Authenticatable|int|null $user = null): void
+ {
+ $routes = self::settings($user)->get() ?? [];
+
+ // Filtra el arreglo para eliminar el valor igual al $route
+ $routes = array_values(array_filter($routes, fn($r) => $r !== $route));
+
+ self::settings($user)->set($routes);
+ }
+
+ public function isRouteAllowed(string $routeId): bool
+ {
+ $menu = self::getMenu();
+ $extra = self::getExtraQuicklinks();
+
+ $allAllowed = [];
+
+ // Recolectar rutas estándar
+ $this->collectAllRoutesFromMenu($menu, $allAllowed);
+
+ foreach ($extra as $item) {
+ if (isset($item['route'])) {
+ $allAllowed[] = $item['route'];
+ }
+ }
+
+ // Recolectar slugs de carpetas
+ $slugs = [];
+ $this->collectAllSlugsFromMenu($menu, $slugs);
+
+ foreach ($slugs as $slug) {
+ $allAllowed[] = "slug:$slug";
+ }
+
+ return in_array($routeId, $allAllowed, true);
+ }
+
+ public static function forgetCacheForUser(Authenticatable|int|null $user = null): void
+ {
+ self::settings($user)->forgetCache();
+ }
+
private function collectFromMenu(array $menu, array &$links, ?string $parent = null): void
{
foreach ($menu as $title => $item) {
@@ -110,33 +159,6 @@ class VuexyQuicklinksBuilderService extends AbstractKeyValueCacheBuilder
}
}
- public function isRouteAllowed(string $routeId): bool
- {
- $menu = app(VuexyMenuFormatter::class)->getMenu();
- $extra = app(VuexyMenuFormatter::class)->getExtraQuicklinks();
-
- $allAllowed = [];
-
- // Recolectar rutas estándar
- $this->collectAllRoutesFromMenu($menu, $allAllowed);
-
- foreach ($extra as $item) {
- if (isset($item['route'])) {
- $allAllowed[] = $item['route'];
- }
- }
-
- // Recolectar slugs de carpetas
- $slugs = [];
- $this->collectAllSlugsFromMenu($menu, $slugs);
-
- foreach ($slugs as $slug) {
- $allAllowed[] = "slug:$slug";
- }
-
- return in_array($routeId, $allAllowed, true);
- }
-
private function collectAllSlugsFromMenu(array $menu, array &$slugs): void
{
foreach ($menu as $item) {
@@ -163,14 +185,21 @@ class VuexyQuicklinksBuilderService extends AbstractKeyValueCacheBuilder
}
}
- public static function forgetCacheForUser(?int $userId = null): void
+ private static function getMenu(Authenticatable|int|null|false $user = null): array
{
- $instance = new static();
+ return app(VuexyMenuFormatter::class)->getMenu($user);
+ }
- $instance->user = $userId
- ? app('auth')->getProvider()->retrieveById($userId)
- : Auth::user();
+ private static function getExtraQuicklinks(Authenticatable|int|null|false $user = null): array
+ {
+ return app(VuexyMenuFormatter::class)->getExtraQuicklinks($user);
+ }
- $instance->forgetCache(static::SETTINGS_KEY);
+ private static function settings(Authenticatable|int|null|false $user = null): SettingsRepositoryInterface
+ {
+ return settings()
+ ->context(self::GROUP, self::SECTION, self::SUB_GROUP)
+ ->setKeyName(self::KEY_NAME)
+ ->setUser($user);
}
}
diff --git a/src/Application/UX/Navbar/VuexySearchBarBuilderService.php b/src/Application/UX/Navbar/VuexySearchBarBuilder.php
similarity index 76%
rename from src/Application/UX/Navbar/VuexySearchBarBuilderService.php
rename to src/Application/UX/Navbar/VuexySearchBarBuilder.php
index 1ba8f21..04c5449 100644
--- a/src/Application/UX/Navbar/VuexySearchBarBuilderService.php
+++ b/src/Application/UX/Navbar/VuexySearchBarBuilder.php
@@ -4,34 +4,39 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Application\UX\Navbar;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Koneko\VuexyAdmin\Application\CoreModule;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuFormatter;
-use Koneko\VuexyAdmin\Support\Cache\AbstractKeyValueCacheBuilder;
+use Koneko\VuexyAdmin\Models\User;
/**
* 🔍 Generador del índice de búsqueda basado en el menú visible por el usuario.
*/
-class VuexySearchBarBuilderService extends AbstractKeyValueCacheBuilder
+class VuexySearchBarBuilder
{
- private const COMPONENT = 'core';
- private const GROUP = 'layout';
+ private const GROUP = 'website-admin';
+ private const SECTION = 'layout';
+ private const SUB_GROUP = 'navbar';
- // Cache scope
- protected bool $isUserScoped = true;
+ /** @var Model Scope */
+ private const SCOPE = User::class;
- /** @var string Cache key */
- private const CACHE_KEY = 'navbar.search-bar';
-
- public function __construct()
- {
- parent::__construct(self::COMPONENT, self::GROUP, self::CACHE_KEY);
- }
+ /** @var string Cache keyName */
+ private const KEY_NAME = 'search-bar';
/**
* Obtiene el índice de búsqueda para el usuario autenticado.
*/
- public function getSearchData(): array
+ public function getSearchData(Authenticatable|int|null $user): array
{
- return $this->rememberCache(self::CACHE_KEY, fn () => $this->buildIndex());
+ return $this->rememberKeyCache(
+ self::COMPONENT,
+ self::GROUP,
+ self::SUB_GROUP,
+ self::CACHE_KEY,
+ fn () => $this->buildIndex(),
+ $user,
+ );
}
/**
@@ -130,23 +135,17 @@ class VuexySearchBarBuilderService extends AbstractKeyValueCacheBuilder
return $categories;
}
+ public static function forgetCacheForUser(Authenticatable|int|null $user): void
+ {
+ cache_m(self::COMPONENT, self::GROUP, self::SUB_GROUP)
+ ->setUser($user)
+ ->forget(self::CACHE_KEY);
+ }
+
public static function forgetVisitorCache(): void
{
- $instance = new static();
-
- $instance->user = null;
- $instance->isUserScoped = true;
-
- $instance->forgetCache(self::CACHE_KEY);
- }
-
- public static function forgetCacheForUser(int $userId): void
- {
- $instance = new static();
-
- $instance->user = app('auth')->getProvider()->retrieveById($userId);
- $instance->isUserScoped = true;
-
- $instance->forgetCache(self::CACHE_KEY);
+ cache_m(self::COMPONENT, self::GROUP, self::SUB_GROUP)
+ ->setUser(false)
+ ->forget(self::CACHE_KEY);
}
}
diff --git a/src/Application/UX/Notifications/Builder/NotifyBuilder.php b/src/Application/UX/Notifications/Builder/NotifyBuilder.php
new file mode 100644
index 0000000..55a46fe
--- /dev/null
+++ b/src/Application/UX/Notifications/Builder/NotifyBuilder.php
@@ -0,0 +1,86 @@
+data['scope'] = $scope;
+ return $this;
+ }
+
+ public function to(mixed $user): static
+ {
+ $this->data['data']['to'] = $user;
+ return $this;
+ }
+
+ public function channel(string $channel): static
+ {
+ $this->data['channel'] = $channel;
+ return $this;
+ }
+
+ public function type(string $type): static
+ {
+ $this->data['type'] = $type;
+ return $this;
+ }
+
+ public function title(string $title): static
+ {
+ $this->data['title'] = $title;
+ return $this;
+ }
+
+ public function body(string $body): static
+ {
+ $this->data['body'] = $body;
+ return $this;
+ }
+
+ public function target(string $target): static
+ {
+ $this->data['target'] = $target;
+ return $this;
+ }
+
+ public function timeout(int $ms): static
+ {
+ $this->data['timeout'] = $ms;
+ return $this;
+ }
+
+ public function persist(bool $value = true): static
+ {
+ $this->data['persist'] = $value;
+ return $this;
+ }
+
+ public function requiresConfirmation(bool $value = true): static
+ {
+ $this->data['requiresConfirmation'] = $value;
+ return $this;
+ }
+
+ public function withData(array $extra): static
+ {
+ $this->data['data'] = array_merge($this->data['data'] ?? [], $extra);
+ return $this;
+ }
+
+ public function send(): void
+ {
+ $manager = app(KonekoNotifyManager::class);
+
+ $payload = NotifyPayload::make($this->data);
+
+ $manager->send($payload);
+ }
+}
diff --git a/src/Application/UX/Notifications/Builder/VuexyNotificationsBuilder.php b/src/Application/UX/Notifications/Builder/VuexyNotificationsBuilder.php
new file mode 100644
index 0000000..3772e5b
--- /dev/null
+++ b/src/Application/UX/Notifications/Builder/VuexyNotificationsBuilder.php
@@ -0,0 +1,37 @@
+user = $user ?? Auth::user();
+ //$this->initCacheConfig();
+ }
+
+ public function getNotifications(): array
+ {
+ return [];
+ }
+}
diff --git a/src/Application/UX/Notifications/Concerns/ResolvesNotifySettings.php b/src/Application/UX/Notifications/Concerns/ResolvesNotifySettings.php
new file mode 100644
index 0000000..e97ada2
--- /dev/null
+++ b/src/Application/UX/Notifications/Concerns/ResolvesNotifySettings.php
@@ -0,0 +1,42 @@
+setComponent('vuexy-admin')
+ ->setGroup('notifications')
+ ->setKeyName("channel_driver.{$channel}")
+ ->get() ?? "default.{$channel}";
+ }
+
+ /**
+ * Aplica valores por defecto según configuración de canal.
+ */
+ protected function applyChannelDefaults(string $channel, NotifyPayload $payload): NotifyPayload
+ {
+ $configPrefix = "channel_config.{$channel}";
+
+ $payload->type = $payload->type ?: settings()
+ ->setComponent('vuexy-admin')
+ ->setGroup('notifications')
+ ->setKeyName("{$configPrefix}.type")
+ ->get() ?? 'info';
+
+ $payload->timeout = $payload->timeout ?? settings()
+ ->setComponent('vuexy-admin')
+ ->setGroup('notifications')
+ ->setKeyName("{$configPrefix}.timeout")
+ ->get() ?? 3000;
+
+ return $payload;
+ }
+}
diff --git a/src/Application/UX/Notifications/Contracts/NotificationChannelDriverInterface.php b/src/Application/UX/Notifications/Contracts/NotificationChannelDriverInterface.php
new file mode 100644
index 0000000..68a293d
--- /dev/null
+++ b/src/Application/UX/Notifications/Contracts/NotificationChannelDriverInterface.php
@@ -0,0 +1,23 @@
+ $payload->type,
+ 'title' => $payload->title,
+ 'message' => $payload->body,
+ 'timeout' => $payload->timeout,
+ 'target' => $payload->target,
+ 'scope' => $payload->scope,
+ ];
+
+ if (method_exists($this, 'dispatchBrowserEvent')) {
+ $this->dispatch('notify', $event);
+ } else {
+ // fallback para contexto no Livewire
+ echo Blade::render("");
+ }
+ }
+
+ public function supports(): array
+ {
+ return ['web', 'livewire'];
+ }
+}
diff --git a/src/Application/UX/Notifications/Manager/KonekoNotifyManager.php b/src/Application/UX/Notifications/Manager/KonekoNotifyManager.php
new file mode 100644
index 0000000..12d94e7
--- /dev/null
+++ b/src/Application/UX/Notifications/Manager/KonekoNotifyManager.php
@@ -0,0 +1,36 @@
+channel ?? $this->defaultChannel;
+
+ // Resuelve driver activo vía settings
+ $driverName = $this->resolveDriverForChannel($channel);
+ $driver = NotificationChannelRegistry::get($driverName);
+
+ // Aplica valores por defecto si están ausentes
+ $payload = $this->applyChannelDefaults($channel, $payload);
+
+ // Ejecuta el envío
+ $driver->send($payload);
+ }
+}
diff --git a/src/Application/UX/Notifications/Registry/NotificationChannelRegistry.php b/src/Application/UX/Notifications/Registry/NotificationChannelRegistry.php
new file mode 100644
index 0000000..3c038ed
--- /dev/null
+++ b/src/Application/UX/Notifications/Registry/NotificationChannelRegistry.php
@@ -0,0 +1,49 @@
+
+ */
+ protected static array $drivers = [];
+
+ /**
+ * Registra un driver.
+ */
+ public static function register(NotificationChannelDriverInterface $driver): void
+ {
+ static::$drivers[$driver->name()] = $driver;
+ }
+
+ /**
+ * Recupera un driver registrado.
+ */
+ public static function get(string $name): NotificationChannelDriverInterface
+ {
+ if (! isset(static::$drivers[$name])) {
+ throw new \RuntimeException("El driver de notificación '{$name}' no está registrado.");
+ }
+
+ return static::$drivers[$name];
+ }
+
+ /**
+ * Devuelve todos los drivers registrados.
+ */
+ public static function all(): array
+ {
+ return static::$drivers;
+ }
+
+ /**
+ * Limpia el registro (útil en tests).
+ */
+ public static function flush(): void
+ {
+ static::$drivers = [];
+ }
+}
diff --git a/src/Application/UX/Notifications/Support/NotifyPayload.php b/src/Application/UX/Notifications/Support/NotifyPayload.php
new file mode 100644
index 0000000..3d36cd6
--- /dev/null
+++ b/src/Application/UX/Notifications/Support/NotifyPayload.php
@@ -0,0 +1,33 @@
+ $value) {
+ if (property_exists($instance, $key)) {
+ $instance->{$key} = $value;
+ }
+ }
+
+ return $instance;
+ }
+}
diff --git a/src/Application/UX/Notifications/VuexyNotificationsBuilderService.php b/src/Application/UX/Notifications/VuexyNotificationsBuilderService.php
deleted file mode 100644
index 6e0184f..0000000
--- a/src/Application/UX/Notifications/VuexyNotificationsBuilderService.php
+++ /dev/null
@@ -1,29 +0,0 @@
-user = $user ?? Auth::user();
- $this->initCacheConfig();
- }
-
- public function getForUser(): array
- {
- return [];
- }
-}
\ No newline at end of file
diff --git a/src/Application/UX/Notifications/___VuexyNotificationDispatcher.php b/src/Application/UX/Notifications/___VuexyNotificationDispatcher.php
deleted file mode 100644
index a813ad1..0000000
--- a/src/Application/UX/Notifications/___VuexyNotificationDispatcher.php
+++ /dev/null
@@ -1,88 +0,0 @@
-id : $user;
-
- return Cache::remember("vuexy_notifications_user_{$userId}", now()->addMinutes(10), function () use ($userId) {
- return VuexyNotification::query()
- ->where('user_id', $userId)
- ->where('is_read', false)
- ->where('is_dismissed', false)
- ->where('is_deleted', false)
- ->latest('created_at')
- ->get();
- });
- }
-
- /**
- * Elimina la caché de notificaciones de un usuario.
- */
- public function clearCacheForUser(int|User $user): void
- {
- $userId = $user instanceof User ? $user->id : $user;
- Cache::forget("vuexy_notifications_user_{$userId}");
- }
-
- /**
- * Elimina la caché de todos los usuarios (para mantenimiento).
- */
- public function clearAllCaches(): void
- {
- foreach (User::pluck('id') as $id) {
- Cache::forget("vuexy_notifications_user_{$id}");
- }
- }
-
- /**
- * Crea una nueva notificación y actualiza la caché.
- */
- public function createNotification(array $data): VuexyNotification
- {
- $notification = VuexyNotification::create($data);
- $this->clearCacheForUser($notification->user_id);
- return $notification;
- }
-
- /**
- * Marca una notificación como leída.
- */
- public function markAsRead(VuexyNotification $notification): void
- {
- $notification->update(['is_read' => true]);
- $this->clearCacheForUser($notification->user_id);
- }
-
- /**
- * Descarta una notificación visualmente sin eliminarla.
- */
- public function dismissNotification(VuexyNotification $notification): void
- {
- $notification->update(['is_dismissed' => true]);
- $this->clearCacheForUser($notification->user_id);
- }
-
- /**
- * Elimina lógicamente una notificación.
- */
- public function deleteNotification(VuexyNotification $notification): void
- {
- $notification->update(['is_deleted' => true]);
- $this->clearCacheForUser($notification->user_id);
- }
-}
diff --git a/src/Application/UI/Notifications/CustomResetPasswordNotification.php b/src/Application/UX/Notifications_1/Email/CustomResetPasswordNotification.php
similarity index 100%
rename from src/Application/UI/Notifications/CustomResetPasswordNotification.php
rename to src/Application/UX/Notifications_1/Email/CustomResetPasswordNotification.php
diff --git a/src/Application/Helpers/VuexyNotifyHelper.php b/src/Application/UX/Notifications_1/Helpers/VuexyNotifyHelper.php
similarity index 100%
rename from src/Application/Helpers/VuexyNotifyHelper.php
rename to src/Application/UX/Notifications_1/Helpers/VuexyNotifyHelper.php
diff --git a/src/Application/Helpers/VuexyToastrHelper.php b/src/Application/UX/Notifications_1/Helpers/VuexyToastrHelper.php
similarity index 100%
rename from src/Application/Helpers/VuexyToastrHelper.php
rename to src/Application/UX/Notifications_1/Helpers/VuexyToastrHelper.php
diff --git a/src/Support/Notifications/NotifyChannelManager.php b/src/Application/UX/Notifications_1/Manager/NotifyChannelManager.php
similarity index 97%
rename from src/Support/Notifications/NotifyChannelManager.php
rename to src/Application/UX/Notifications_1/Manager/NotifyChannelManager.php
index 1292043..9e5d2ce 100644
--- a/src/Support/Notifications/NotifyChannelManager.php
+++ b/src/Application/UX/Notifications_1/Manager/NotifyChannelManager.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Support\Notifications;
+namespace Koneko\VuexyAdmin\Application\UX\Notifications\Manager;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Event;
diff --git a/src/Application/Vault/Drivers/VaultKeyApiDriver.php b/src/Application/Vault/Drivers/VaultKeyApiDriver.php
new file mode 100644
index 0000000..27ca904
--- /dev/null
+++ b/src/Application/Vault/Drivers/VaultKeyApiDriver.php
@@ -0,0 +1,39 @@
+get('security.key_vault.drivers.koneko_api', []);
+
+ $this->baseUrl = rtrim($config['base_url'] ?? '', '/');
+ $this->token = $config['api_token'] ?? '';
+ $this->timeout = $config['timeout'] ?? 3;
+ }
+
+ public function get(string $project, string $namespace, string $alias): ?string
+ {
+ $url = "{$this->baseUrl}/{$project}/{$namespace}/{$alias}";
+
+ $response = Http::withToken($this->token)
+ ->timeout($this->timeout)
+ ->get($url);
+
+ if (!$response->ok()) {
+ report("Vault API error: {$response->status()} - {$response->body()}");
+ return null;
+ }
+
+ $data = $response->json();
+ return $data['key_material'] ?? null;
+ }
+}
diff --git a/src/Application/Vault/VaultKeyClient.php b/src/Application/Vault/VaultKeyClient.php
new file mode 100644
index 0000000..378f7af
--- /dev/null
+++ b/src/Application/Vault/VaultKeyClient.php
@@ -0,0 +1,97 @@
+connection = config('settings.security.key_vault.drivers.database.connection', 'vault');
+ $this->project = config('settings.project.code'); // ej. 'erp'
+ $this->namespace = config('settings.security.key_vault.default_namespace', 'default');
+ $this->clientId = config('settings.client.id'); // puede venir de session, JWT, etc.
+ }
+
+ public function usingConnection(string $connection): static
+ {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ public function fromProject(string $code): static
+ {
+ $this->project = $code;
+ return $this;
+ }
+
+ public function withNamespace(string $namespace): static
+ {
+ $this->namespace = $namespace;
+ return $this;
+ }
+
+ public function forClient(string|int|null $clientId): static
+ {
+ $this->clientId = $clientId;
+ return $this;
+ }
+
+ /**
+ * Obtiene una clave desencriptada.
+ */
+ public function get(string $alias): ?string
+ {
+ $driver = config('settings.security.key_vault.driver', 'laravel');
+
+ return match ($driver) {
+ 'database' => $this->getFromDatabase($alias),
+ 'koneko_api' => $this->getFromApi($alias),
+ 'laravel' => config('app.key'), // fallback interno
+ default => null,
+ };
+ }
+
+ protected function getFromDatabase(string $alias): ?string
+ {
+ return VaultKeyService::make()
+ ->connection($this->connection)
+ ->project($this->project)
+ ->namespace($this->namespace)
+ ->useClient($this->clientId)
+ ->get($alias);
+ }
+
+ protected function getFromApi(string $alias): ?string
+ {
+ return (new VaultKeyApiDriver)
+ ->get($this->project, $this->namespace, $alias);
+ }
+
+
+ /**
+ * Obtiene el modelo sin desencriptar, por si se requiere metadata.
+ */
+ public function raw(string $alias)
+ {
+ return VaultKeyService::make()
+ ->connection($this->connection)
+ ->project($this->project)
+ ->namespace($this->namespace)
+ ->useClient($this->clientId)
+ ->raw($alias);
+ }
+}
diff --git a/src/Application/Vault/VaultKeyService.php b/src/Application/Vault/VaultKeyService.php
new file mode 100644
index 0000000..c90f045
--- /dev/null
+++ b/src/Application/Vault/VaultKeyService.php
@@ -0,0 +1,98 @@
+connection(config('settings.security.key_vault.drivers.database.connection', 'vault'))
+ ->project(config('settings.security.key_vault.default_project'))
+ ->namespace(config('settings.security.key_vault.default_namespace'))
+ ->useClient(config('settings.security.key_vault.default_client_id'));
+ }
+
+
+ public function connection(string $name): static
+ {
+ $this->connection = $name;
+ return $this;
+ }
+
+ public function project(string $code): static
+ {
+ $this->projectCode = $code;
+ return $this;
+ }
+
+ public function namespace(string $namespace): static
+ {
+ $this->namespace = $namespace;
+ return $this;
+ }
+
+ public function useClient(string|int|null $clientId): static
+ {
+ $this->clientId = $clientId;
+ return $this;
+ }
+
+ public function get(string $alias): ?string
+ {
+ $model = $this->query()->byAlias($alias)->first();
+
+ if (!$model) {
+ return null;
+ }
+
+ return $model->key_material
+ ? Crypt::decryptString($model->key_material)
+ : null;
+ }
+
+ public function raw(string $alias): ?VaultClientKey
+ {
+ return $this->query()->byAlias($alias)->first();
+ }
+
+ public function rotate(string $alias, string $newMaterial): bool
+ {
+ $model = $this->raw($alias);
+
+ if (!$model) {
+ return false;
+ }
+
+ $model->key_material = Crypt::encryptString($newMaterial);
+ $model->rotated_at = now();
+ $model->rotation_count++;
+ $model->save();
+
+ return true;
+ }
+
+ public function query()
+ {
+ return VaultClientKey::on($this->connection)
+ ->newQuery()
+ ->active()
+ ->when($this->projectCode, fn($q) => $q->byProject($this->projectCode))
+ ->when($this->namespace, fn($q) => $q->withNamespace($this->namespace))
+ ->when($this->clientId, fn($q) => $q->where('client_id', $this->clientId));
+ }
+}
diff --git a/src/Console/Commands/KonekoCacheHelperCommand.php b/src/Console/Commands/Cache/KonekoCacheHelperCommand.php
similarity index 76%
rename from src/Console/Commands/KonekoCacheHelperCommand.php
rename to src/Console/Commands/Cache/KonekoCacheHelperCommand.php
index cb28377..649a21c 100644
--- a/src/Console/Commands/KonekoCacheHelperCommand.php
+++ b/src/Console/Commands/Cache/KonekoCacheHelperCommand.php
@@ -1,9 +1,9 @@
option('enabled')) {
- $this->line("✅ Habilitado:
" . ($manager->enabled() ? 'true' : 'false') . "");
+ $this->line("✅ Habilitado:
" . ($manager->isEnabled() ? 'true' : 'false') . "");
}
if ($this->option('ttl')) {
- $this->line("🕒 TTL efectivo:
{$manager->ttl()} seg");
+ $this->line("🕒 TTL efectivo:
{$manager->resolveTTL()} seg");
}
if ($this->option('driver')) {
$this->line("⚙️ Driver actual:
{$manager->driver()}");
}
- if ($this->option('tags')) {
- $tags = $manager->tags();
- $this->line("🏷 Etiquetas:
" . implode(', ', $tags) . "");
- }
-
if (! $this->option('show') &&
! $this->option('ttl') &&
! $this->option('enabled') &&
! $this->option('driver') &&
- ! $this->option('tags') &&
! $this->option('flush')) {
$this->warn("⚠️ No se especificó ninguna acción. Usa --help para ver las opciones disponibles.");
diff --git a/src/Console/Commands/DownloadGeoIpDatabase.php b/src/Console/Commands/Geolocationg/DownloadGeoIpDatabase.php
similarity index 97%
rename from src/Console/Commands/DownloadGeoIpDatabase.php
rename to src/Console/Commands/Geolocationg/DownloadGeoIpDatabase.php
index f9d5e57..f177988 100644
--- a/src/Console/Commands/DownloadGeoIpDatabase.php
+++ b/src/Console/Commands/Geolocationg/DownloadGeoIpDatabase.php
@@ -1,6 +1,6 @@
info('Menú para visitante:');
- $menu = app(VuexyMenuBuilderService::class)->getForUser(null);
+ $menu = app(VuexyMenuBuilder::class)->getForUser(null);
$this->renderMenu($menu, $showJson, $showDump, $summary, $targetId);
return self::SUCCESS;
}
@@ -75,8 +75,8 @@ class VuexyMenuBuildCommand extends Command
if ($fresh) {
$user
- ? VuexyMenuBuilderService::clearCacheForUser($user)
- : VuexyMenuBuilderService::clearAllCache();
+ ? VuexyMenuBuilder::clearCacheForUser($user)
+ : VuexyMenuBuilder::clearAllCache();
$this->info('Caché de menú limpiado.');
}
@@ -102,7 +102,7 @@ class VuexyMenuBuildCommand extends Command
}
}
- $menu = app(VuexyMenuBuilderService::class)->getForUser($user);
+ $menu = app(VuexyMenuBuilder::class)->getForUser($user);
$this->renderMenu($menu, $showJson, $showDump, $summary, $targetId);
return self::SUCCESS;
}
@@ -206,4 +206,4 @@ class VuexyMenuBuildCommand extends Command
$user->syncRoles($role);
return $user;
}
-}
\ No newline at end of file
+}
diff --git a/src/Console/Commands/VuexyMenuListModulesCommand.php b/src/Console/Commands/Layout/VuexyMenuListModulesCommand.php
similarity index 95%
rename from src/Console/Commands/VuexyMenuListModulesCommand.php
rename to src/Console/Commands/Layout/VuexyMenuListModulesCommand.php
index 20dd808..21bbec3 100644
--- a/src/Console/Commands/VuexyMenuListModulesCommand.php
+++ b/src/Console/Commands/Layout/VuexyMenuListModulesCommand.php
@@ -2,11 +2,11 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Console\Commands;
+namespace Koneko\VuexyAdmin\Console\Commands\Layout;
use Illuminate\Console\Command;
+use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
use Koneko\VuexyAdmin\Application\UX\Menu\VuexyMenuRegistry;
-use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleRegistry;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'vuexy:menu:list-modules')]
diff --git a/src/Console/Commands/VuexyDeviceTokenPruneCommand.php b/src/Console/Commands/Notifications/VuexyDeviceTokenPruneCommand.php
similarity index 91%
rename from src/Console/Commands/VuexyDeviceTokenPruneCommand.php
rename to src/Console/Commands/Notifications/VuexyDeviceTokenPruneCommand.php
index 34bc2da..5e2486a 100644
--- a/src/Console/Commands/VuexyDeviceTokenPruneCommand.php
+++ b/src/Console/Commands/Notifications/VuexyDeviceTokenPruneCommand.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Console\Commands;
+namespace Koneko\VuexyAdmin\Console\Commands\Notifications;
use Illuminate\Console\Command;
use Koneko\VuexyAdmin\Models\DeviceToken;
diff --git a/src/Console/Commands/ApisReportCommand.php b/src/Console/Commands/Orquestator/ApisReportCommand.php
similarity index 98%
rename from src/Console/Commands/ApisReportCommand.php
rename to src/Console/Commands/Orquestator/ApisReportCommand.php
index 06fd0ec..674d349 100644
--- a/src/Console/Commands/ApisReportCommand.php
+++ b/src/Console/Commands/Orquestator/ApisReportCommand.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyApisAndIntegrations\Console\Commands;
+namespace Koneko\VuexyAdmin\Console\Commands\Orquestator;
use Illuminate\Console\Command;
use Symfony\Component\Console\Helper\Table;
diff --git a/src/Console/Commands/VuexyListCatalogsCommand.php b/src/Console/Commands/Orquestator/VuexyListCatalogsCommand.php
similarity index 97%
rename from src/Console/Commands/VuexyListCatalogsCommand.php
rename to src/Console/Commands/Orquestator/VuexyListCatalogsCommand.php
index 48ff5a7..ebdbb63 100644
--- a/src/Console/Commands/VuexyListCatalogsCommand.php
+++ b/src/Console/Commands/Orquestator/VuexyListCatalogsCommand.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Console\Commands;
+namespace Koneko\VuexyAdmin\Console\Commands\Orquestator;
use Illuminate\Console\Command;
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Catalog\CatalogModuleRegistry;
diff --git a/src/Console/Commands/VuexyModuleInstallCommand.php b/src/Console/Commands/Orquestator/VuexyModuleInstallCommand.php
similarity index 93%
rename from src/Console/Commands/VuexyModuleInstallCommand.php
rename to src/Console/Commands/Orquestator/VuexyModuleInstallCommand.php
index 883054d..8ceeff0 100644
--- a/src/Console/Commands/VuexyModuleInstallCommand.php
+++ b/src/Console/Commands/Orquestator/VuexyModuleInstallCommand.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Console\Commands;
+namespace Koneko\VuexyAdmin\Console\Commands\Orquestator;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
diff --git a/src/Console/Commands/VuexySeedCommand.php b/src/Console/Commands/Orquestator/VuexySeedCommand.php
similarity index 99%
rename from src/Console/Commands/VuexySeedCommand.php
rename to src/Console/Commands/Orquestator/VuexySeedCommand.php
index 256f603..38647fc 100644
--- a/src/Console/Commands/VuexySeedCommand.php
+++ b/src/Console/Commands/Orquestator/VuexySeedCommand.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Console\Commands;
+namespace Koneko\VuexyAdmin\Console\Commands\Orquestator;
use Illuminate\Console\Command;
use Koneko\VuexyAdmin\Application\Seeding\SeederOrchestrator;
diff --git a/src/Console/Commands/VuexyRbacCommand.php b/src/Console/Commands/RBAC/VuexyRbacCommand.php
similarity index 90%
rename from src/Console/Commands/VuexyRbacCommand.php
rename to src/Console/Commands/RBAC/VuexyRbacCommand.php
index 0bff6b5..23e3e4b 100644
--- a/src/Console/Commands/VuexyRbacCommand.php
+++ b/src/Console/Commands/RBAC/VuexyRbacCommand.php
@@ -1,12 +1,12 @@
isEmpty()) {
$this->info('📭 No hay claves registradas.');
diff --git a/src/Console/Commands/Vault/VaultKeyInfoCommand.php b/src/Console/Commands/Vault/VaultKeyInfoCommand.php
new file mode 100644
index 0000000..a17ee62
--- /dev/null
+++ b/src/Console/Commands/Vault/VaultKeyInfoCommand.php
@@ -0,0 +1,48 @@
+argument('alias');
+ $project = $this->option('project') ?? config('settings.project.code');
+ $namespace = $this->option('namespace') ?? 'default';
+ $clientId = $this->option('client') ?? null;
+
+ $vault = VaultKeyService::make()
+ ->project($project)
+ ->namespace($namespace)
+ ->useClient($clientId);
+
+ $key = $vault->raw($alias);
+
+ if (!$key) {
+ $this->error("❌ Clave '{$alias}' no encontrada.");
+ return;
+ }
+
+ $this->info("🔐 Información de clave:");
+ $this->line("Alias: {$key->alias}");
+ $this->line("Proyecto: {$key->project_code}");
+ $this->line("Cliente ID: {$key->client_id}");
+ $this->line("Namespace: {$key->namespace}");
+ $this->line("Activo: " . ($key->is_active ? 'Sí' : 'No'));
+ $this->line("Algoritmo: {$key->algorithm}");
+ $this->line("Sensitiva: " . ($key->is_sensitive ? 'Sí' : 'No'));
+ $this->line("Rotaciones: {$key->rotation_count}");
+ $this->line("Última rotación: " . ($key->rotated_at ?? 'Nunca'));
+ $this->line("Creada en: {$key->created_at}");
+ }
+}
diff --git a/src/Console/Commands/Vault/VaultKeyRotateCommand.php b/src/Console/Commands/Vault/VaultKeyRotateCommand.php
new file mode 100644
index 0000000..de6feca
--- /dev/null
+++ b/src/Console/Commands/Vault/VaultKeyRotateCommand.php
@@ -0,0 +1,40 @@
+argument('alias');
+ $project = $this->option('project') ?? config('settings.project.code');
+ $namespace = $this->option('namespace') ?? 'default';
+ $clientId = $this->option('client') ?? null;
+ $length = (int) $this->option('length');
+
+ $newKey = VaultKeyService::generateRandomKey($length);
+
+ $success = VaultKeyService::make()
+ ->project($project)
+ ->namespace($namespace)
+ ->useClient($clientId)
+ ->rotate($alias, $newKey);
+
+ if ($success) {
+ $this->info("✅ Clave '{$alias}' rotada exitosamente.");
+ } else {
+ $this->error("❌ No se encontró la clave '{$alias}'.");
+ }
+ }
+}
diff --git a/src/Console/Commands/Vault/VaultResetCommand.php b/src/Console/Commands/Vault/VaultResetCommand.php
new file mode 100644
index 0000000..f9ba4e0
--- /dev/null
+++ b/src/Console/Commands/Vault/VaultResetCommand.php
@@ -0,0 +1,43 @@
+warn('❌ Vault reset está deshabilitado por configuración. Revisa KONEKO_KEY_VAULT_RESET_ENABLED.');
+ return;
+ }
+
+ // Confirmación interactiva
+ if (!$this->option('force') && !$this->confirm('¿Seguro que deseas eliminar todas las claves del vault?')) {
+ $this->info('⛔ Operación cancelada.');
+ return;
+ }
+
+ $connection = Config::get('settings.security.key_vault.drivers.database.connection', 'vault');
+ $table = Config::get('settings.security.key_vault.drivers.database.table', 'vault_keys');
+
+ if (!Schema::connection($connection)->hasTable($table)) {
+ $this->warn("⚠️ La tabla `{$table}` no existe en la conexión [{$connection}].");
+ return;
+ }
+
+ Schema::connection($connection)->dropIfExists($table);
+ $this->info("✅ Tabla `{$table}` eliminada correctamente de la conexión [{$connection}].");
+ }
+}
diff --git a/src/Console/Commands/VuexyRbacCommand copy.php b/src/Console/Commands/VuexyRbacCommand copy.php
deleted file mode 100644
index 68a5a39..0000000
--- a/src/Console/Commands/VuexyRbacCommand copy.php
+++ /dev/null
@@ -1,89 +0,0 @@
-option('module');
- $sync = $this->option('sync');
- $export = $this->option('export');
- $publish = $this->option('publish');
- $rolesOnly = $this->option('roles-only');
- $permissionsOnly = $this->option('permissions-only');
- $overwrite = $this->option('overwrite');
- $force = $this->option('force');
-
- if (!($sync || $export || $publish)) {
- $this->error('Debes especificar al menos una opción: --sync, --export o --publish');
- return 1;
- }
-
- $modules = $moduleName
- ? [KonekoModuleRegistry::get($moduleName)]
- : KonekoModuleRegistry::enabled();
-
- foreach ($modules as $module) {
- if (!$module) {
- $this->warn("⚠️ Módulo no encontrado: {$moduleName}");
- continue;
- }
-
- $this->info("🎯 Procesando módulo: {$module->name}");
-
- if ($sync) {
- if (!$rolesOnly) {
- $this->line(" 📥 Importando permisos...");
- RbacManagerService::importPermissions($module);
- }
-
- if (!$permissionsOnly) {
- $this->line(" 🔐 Importando roles...");
- RbacManagerService::importRoles($module, $overwrite);
- }
- }
-
- if ($export) {
- if (!$rolesOnly) {
- $this->line(" 📤 Exportando permisos...");
- RbacManagerService::exportPermissions($module);
- }
-
- if (!$permissionsOnly) {
- $this->line(" 🔐 Exportando roles...");
- RbacManagerService::exportRoles($module);
- }
- }
-
- if ($publish) {
- // Puedes definir aquí lógica futura si decides generar archivos
- // en `base_path('database/data/vuexy-x/rbac.json')` en lugar de sobrescribir los originales
- $this->comment("🚧 Publish: aún no implementado. Usa export + config para lograrlo.");
- }
- }
-
- $this->info('✅ Comando RBAC finalizado.');
- return 0;
- }
-}
diff --git a/src/Database/Factories/DeviceTokenFactory.php b/src/Database/Factories/DeviceTokenFactory.php
index 357ebb5..c0c8d07 100644
--- a/src/Database/Factories/DeviceTokenFactory.php
+++ b/src/Database/Factories/DeviceTokenFactory.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Factories;
use Koneko\VuexyAdmin\Models\DeviceToken;
-use Koneko\VuexyAdmin\Support\Factories\AbstractModelFactory;
+use Koneko\VuexyAdmin\Support\Factories\Base\AbstractModelFactory;
use Koneko\VuexyAdmin\Support\Traits\Factories\HasFactorySupport;
/**
diff --git a/src/Database/Factories/NotificationFactory.php b/src/Database/Factories/NotificationFactory.php
index 7d4c337..0573a0e 100644
--- a/src/Database/Factories/NotificationFactory.php
+++ b/src/Database/Factories/NotificationFactory.php
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Factories;
-use Illuminate\Support\Carbon;
+use Carbon\Carbon;
use Koneko\VuexyAdmin\Models\Notification;
use Koneko\VuexyAdmin\Models\User;
-use Koneko\VuexyAdmin\Support\Factories\AbstractModelFactory;
+use Koneko\VuexyAdmin\Support\Factories\Base\AbstractModelFactory;
use Koneko\VuexyAdmin\Support\Traits\Factories\HasFactorySupport;
/**
diff --git a/src/Database/Factories/SystemNotificationFactory.php b/src/Database/Factories/SystemNotificationFactory.php
index 67ef879..3ff746e 100644
--- a/src/Database/Factories/SystemNotificationFactory.php
+++ b/src/Database/Factories/SystemNotificationFactory.php
@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Factories;
use Illuminate\Support\{Carbon, Str};
-use Koneko\VuexyAdmin\Application\Enums\SystemNotifications\{
+use Koneko\VuexyAdmin\Support\Enums\SystemNotifications\{
SystemNotificationScope,
SystemNotificationType,
SystemNotificationStyle,
SystemNotificationPriority
};
use Koneko\VuexyAdmin\Models\{SystemNotification, SystemNotificationUser};
-use Koneko\VuexyAdmin\Support\Factories\AbstractModelFactory;
+use Koneko\VuexyAdmin\Support\Factories\Base\AbstractModelFactory;
use Koneko\VuexyAdmin\Models\User;
class SystemNotificationFactory extends AbstractModelFactory
diff --git a/src/Database/Factories/UserFactory.php b/src/Database/Factories/UserFactory.php
index b901d28..89ad832 100644
--- a/src/Database/Factories/UserFactory.php
+++ b/src/Database/Factories/UserFactory.php
@@ -7,8 +7,9 @@ namespace Koneko\VuexyAdmin\Database\Factories;
use Koneko\VuexyAdmin\Application\Enums\User\UserBaseFlags;
use Koneko\VuexyAdmin\Application\Traits\Factories\{HasUserFactoryRoleExtension,HasUserFactoryAvatarExtension, HasUserFactoryNotificationExtension};
use Koneko\VuexyAdmin\Models\User;
-use Koneko\VuexyAdmin\Support\Factories\AbstractModelFactory;
-use Koneko\VuexyAdmin\Support\Traits\Factories\{HasDynamicFactoryExtenders,HasUserFactoryFlagsExtension};
+use Koneko\VuexyAdmin\Support\Factories\Base\AbstractModelFactory;
+use Koneko\VuexyAdmin\Support\Traits\Flags\Factories\HasUserFactoryFlagsExtension;
+use Koneko\VuexyAdmin\Support\Traits\Factories\HasDynamicFactoryExtenders;
/**
* 🧲 UserFactory
diff --git a/src/Database/Seeders/DeviceTokenSeeder.php b/src/Database/Seeders/DeviceTokenSeeder.php
index 91f507d..ca13305 100644
--- a/src/Database/Seeders/DeviceTokenSeeder.php
+++ b/src/Database/Seeders/DeviceTokenSeeder.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Seeders;
use Koneko\VuexyAdmin\Models\DeviceToken;
-use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
+use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder;
/**
* 🌱 DeviceTokenSeeder
diff --git a/src/Database/Seeders/NotificationSeeder.php b/src/Database/Seeders/NotificationSeeder.php
index 3de0c28..d0c69ff 100644
--- a/src/Database/Seeders/NotificationSeeder.php
+++ b/src/Database/Seeders/NotificationSeeder.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Seeders;
use Koneko\VuexyAdmin\Models\Notification;
-use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
+use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder;
/**
* 🌱 NotificationSeeder
diff --git a/src/Database/Seeders/RbacSeeder.php b/src/Database/Seeders/RbacSeeder.php
index c3730b5..ddd6e22 100644
--- a/src/Database/Seeders/RbacSeeder.php
+++ b/src/Database/Seeders/RbacSeeder.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Seeders;
use Illuminate\Database\Seeder;
-use Koneko\VuexyAdmin\Application\RBAC\KonekoRbacSyncManager;
+use Koneko\VuexyAdmin\Application\RBAC\Sync\KonekoRbacSyncManager;
class RbacSeeder extends Seeder
{
diff --git a/src/Database/Seeders/SettingSeeder.php b/src/Database/Seeders/SettingSeeder.php
index 9c5c092..b3f44b7 100644
--- a/src/Database/Seeders/SettingSeeder.php
+++ b/src/Database/Seeders/SettingSeeder.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Seeders;
use Koneko\VuexyAdmin\Models\Setting;
-use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
+use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder;
use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders;
class SettingSeeder extends AbstractDataSeeder
diff --git a/src/Database/Seeders/SystemNotificationSeeder.php b/src/Database/Seeders/SystemNotificationSeeder.php
index 7283aa1..1d3cb0a 100644
--- a/src/Database/Seeders/SystemNotificationSeeder.php
+++ b/src/Database/Seeders/SystemNotificationSeeder.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Database\Seeders;
use Koneko\VuexyAdmin\Models\SystemNotification;
-use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
+use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder;
/**
* 🌱 SystemNotificationSeeder
diff --git a/src/Database/Seeders/UserSeeder.php b/src/Database/Seeders/UserSeeder.php
index dfa2876..550a3c1 100644
--- a/src/Database/Seeders/UserSeeder.php
+++ b/src/Database/Seeders/UserSeeder.php
@@ -6,9 +6,9 @@ namespace Koneko\VuexyAdmin\Database\Seeders;
use Koneko\VuexyAdmin\Models\User;
use Koneko\VuexyAdmin\Application\Enums\User\UserBaseFlags;
-use Koneko\VuexyAdmin\Application\Traits\Seeders\Main\HasSeederFactorySupport;
+use Koneko\VuexyAdmin\Application\Seeding\Concerns\Main\HasSeederFactorySupport;
use Koneko\VuexyAdmin\Application\Traits\Seeders\User\{HandlesSeederAvatars,HandlesSeederRoles};
-use Koneko\VuexyAdmin\Support\Seeders\AbstractDataSeeder;
+use Koneko\VuexyAdmin\Support\Seeders\Base\AbstractDataSeeder;
use Koneko\VuexyAdmin\Support\Traits\Seeders\HandlesFileSeeders;
/**
diff --git a/src/Models/Notification.php b/src/Models/Notification.php
index 533ba5e..0fa185a 100644
--- a/src/Models/Notification.php
+++ b/src/Models/Notification.php
@@ -6,7 +6,7 @@ namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
-use Illuminate\Support\Carbon;
+use Carbon\Carbon;
use Koneko\VuexyAdmin\Database\Factories\NotificationFactory;
use Koneko\VuexyAdmin\Support\Traits\Audit\{HasEmitter,HasUpdater,HasUser};
diff --git a/src/Models/PermissionMeta.php b/src/Models/PermissionMeta.php
index b0e9fb3..0f7eda2 100644
--- a/src/Models/PermissionMeta.php
+++ b/src/Models/PermissionMeta.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
-use Koneko\VuexyAdmin\Application\Enums\Permissions\PermissionAction;
+use Koneko\VuexyAdmin\Support\Enums\Permissions\PermissionAction;
use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata;
use Spatie\Permission\Models\Permission;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
diff --git a/src/Models/SecurityEvent.php b/src/Models/SecurityEvent.php
index 25de776..8b69de5 100644
--- a/src/Models/SecurityEvent.php
+++ b/src/Models/SecurityEvent.php
@@ -5,10 +5,10 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model;
-use Koneko\VuexyAdmin\Application\Enums\SecurityEvents\{SecurityEventStatus,SecurityEventType};
+use Koneko\VuexyAdmin\Support\Enums\SecurityEvents\{SecurityEventStatus,SecurityEventType};
use Koneko\VuexyAdmin\Support\Traits\Audit\{HasDeleter,HasUser};
use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata;
-use Koneko\VuexyAdmin\Support\Traits\Helpers\HasGeolocation;
+use Koneko\VuexyAdmin\Support\Traits\Geolocation\HasGeolocation;
class SecurityEvent extends Model
{
diff --git a/src/Models/Setting.php b/src/Models/Setting.php
index f995349..e71b2dc 100644
--- a/src/Models/Setting.php
+++ b/src/Models/Setting.php
@@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
-use Koneko\VuexyAdmin\Application\Enums\Settings\{SettingEnvironment, SettingScope, SettingValueType};
+use Koneko\VuexyAdmin\Application\Enums\Settings\SettingValueType;
use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator,HasDeleter,HasUpdater,HasUser};
use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
@@ -35,30 +36,40 @@ class Setting extends Model implements AuditableContract
protected $fillable = [
'key',
'namespace',
- 'environment', // Entorno de aplicación (prod, dev, test, staging), permite sobrescribir valores según ambiente.
- 'scope', // Define el alcance: global, tenant, branch, user, etc. Útil en arquitecturas multicliente.
-
+ 'environment',
'component', // Nombre de Componente o proyecto
'module', // composerName de módulo Autocalculado
+ 'scope',
+ 'scope_id',
'group', // Grupo de configuraciones
+ 'section',
'sub_group', // Sub grupo de configuraciones
'key_name', // Nombre de la clave de configuraciones
- 'user_id', // Usuario (null para globales)
'is_system', // Indica si es un setting de sistema
- 'is_encrypted', // Si el valor está cifrado (para secretos, tokens, passwords).
'is_sensitive', // Marca datos sensibles (ej. datos personales, claves API). Puede ocultarse en UI o logs.
+ 'is_file', // Indica si el setting es un archivo
+ 'is_encrypted', // Si el valor está cifrado (para secretos, tokens, passwords).
+ 'is_config', // Indica si el setting es un archivo de configuración
+ 'is_track_usage', // Indica si el contador de uso está habilitado.
+ 'is_should_cache', // Indica si el setting debe ser cacheado.
'is_editable', // Permite o bloquea edición desde la UI (útil para settings de solo lectura).
'is_active', // Permite activar/desactivar la aplicación de un setting sin eliminarlo.
- 'encryption_key',
+ 'mime_type',
+ 'file_name',
'encryption_algorithm',
+ 'encryption_key',
'encryption_rotated_at',
- 'description', // Descripción legible para el setting (ayuda en la UI).
- 'hint', // Breve consejo o ayuda contextual (tooltip en la UI).
- 'last_used_at', // Última vez que este setting fue consultado/aplicado.
- 'usage_count', // Contador de veces usado (útil para limpieza de settings obsoletos).
+ 'expires_at',
+ 'usage_count',
+ 'last_used_at',
+ 'cache_ttl',
+ 'cache_expires_at',
+
+ 'description', // Descripción legible para el setting (ayuda en la UI).
+ 'hint', // Breve consejo o ayuda contextual (tooltip en la UI).
'value_string',
'value_integer',
@@ -66,55 +77,89 @@ class Setting extends Model implements AuditableContract
'value_float',
'value_text',
'value_binary',
- 'mime_type',
- 'file_name',
+
'created_by',
'updated_by',
'deleted_by',
];
- protected $appends = ['value', 'decrypted_value'];
-
protected $auditInclude = [
'is_system',
- 'is_encrypted',
'is_sensitive',
+ 'is_file',
+ 'is_encrypted',
+ 'is_config',
'is_editable',
+ 'is_track_usage',
+ 'is_should_cache',
'is_active',
- 'scope',
+ 'mime_type',
+ 'file_name',
+ 'encryption_algorithm',
+ 'encryption_key',
+ 'encryption_rotated_at',
+ 'expires_at',
+ 'cache_ttl',
+ 'cache_expires_at',
'description',
'hint',
- 'last_used_at',
- 'usage_count',
'value_string',
'value_integer',
'value_boolean',
'value_float',
'value_text',
- 'mime_type',
- 'file_name',
];
protected $casts = [
- 'environment' => SettingEnvironment::class,
- 'scope' => SettingScope::class,
- 'user_id' => 'integer',
- 'is_system' => 'boolean',
- 'is_encrypted' => 'boolean',
- 'is_sensitive' => 'boolean',
- 'is_editable' => 'boolean',
- 'is_active' => 'boolean',
- 'last_used_at' => 'datetime',
- 'usage_count' => 'integer',
- 'value_integer' => 'integer',
- 'value_boolean' => 'boolean',
- 'value_float' => 'float',
- 'created_by' => 'integer',
- 'deleted_by' => 'integer',
- 'updated_by' => 'integer',
+ 'scope_id' => 'integer',
+ 'is_system' => 'boolean',
+ 'is_encrypted' => 'boolean',
+ 'is_config' => 'boolean',
+ 'is_sensitive' => 'boolean',
+ 'is_editable' => 'boolean',
+ 'is_active' => 'boolean',
+ 'expires_at' => 'datetime',
'encryption_rotated_at' => 'datetime',
+ 'should_cache' => 'boolean',
+ 'cache_ttl' => 'integer',
+ 'cache_expires_at' => 'datetime',
+ 'value_integer' => 'integer',
+ 'value_boolean' => 'boolean',
+ 'value_float' => 'float',
+ 'created_by' => 'integer',
+ 'deleted_by' => 'integer',
+ 'updated_by' => 'integer',
];
+
+ // ===================== BOOT =====================
+
+ protected static function booted(): void
+ {
+ static::saving(function (self $model) {
+ if (strlen($model->key) > 255) {
+ throw new \RuntimeException("La clave '{$model->key}' excede el límite permitido.");
+ }
+ });
+
+ static::updating(function (self $model) {
+ $original = $model->getOriginal();
+
+ foreach ([
+ 'key', 'namespace',
+ 'environment',
+ 'scope', 'scope_id',
+ 'component', 'module', 'group', 'sub_group',
+ 'key_name',
+ ] as $locked) {
+ if ($model->$locked !== $original[$locked]) {
+ throw new \RuntimeException("El campo '{$locked}' no puede ser modificado una vez creado.");
+ }
+ }
+ });
+ }
+
+
// ===================== GETTERS =====================
public function getDisplayName(): string
@@ -122,21 +167,17 @@ class Setting extends Model implements AuditableContract
return collect([
$this->key,
$this->module ? "Module: {$this->module}" : null,
- $this->user_id ? "User: {$this->user_id}" : null,
+ $this->scope_id ? "Scope: {$this->scope_id}" : null,
])->filter()->implode(' | ');
}
public function getValueAttribute(): mixed
{
- foreach (SettingValueType::cases() as $type) {
- $field = "value_{$type->value}";
-
- if (!is_null($this->$field)) {
- return $this->decode($this->$field);
- }
+ if (!app()->runningInConsole() && !Schema::hasTable($this->getTable())) {
+ return null;
}
- return null;
+ return $this->resolveValueAndTrackUsage();
}
public function getDecryptedValueAttribute(): mixed
@@ -150,87 +191,35 @@ class Setting extends Model implements AuditableContract
return $this->decode($this->value, $asArray);
}
- $encoded = $this->value_text ?? $this->value_string;
- $key = $this->getEncryptionKey();
- $algorithm = $this->encryption_algorithm ?? 'AES-256-CBC';
-
- try {
- $raw = base64_decode($encoded, true);
- $ivLength = openssl_cipher_iv_length($algorithm);
- $iv = substr($raw, 0, $ivLength);
- $cipher = substr($raw, $ivLength);
-
- $decrypted = openssl_decrypt($cipher, $algorithm, $key, 0, $iv);
-
- if ($decrypted === false) {
- throw new \RuntimeException("Error al descifrar el valor para '{$this->key}'.");
- }
- } catch (\Throwable $e) {
- logger()->error('❌ Error al descifrar valor de setting.', [
- 'key' => $this->key,
- 'error' => $e->getMessage(),
- ]);
- return null;
- }
-
- return $this->decode($decrypted, $asArray);
+ return $this->decryptValue($asArray);
}
+
// ===================== SETTERS =====================
public function setValueAttribute($value): void
{
- foreach (SettingValueType::cases() as $type) {
- $field = "value_{$type->value}";
- $this->$field = null;
+ foreach ($this->encodeValue($value) as $key => $val) {
+ $this->$key = $val;
}
-
- if ($this->is_encrypted) {
- $key = $this->getEncryptionKey();
- $algorithm = $this->encryption_algorithm ?? 'AES-256-CBC';
-
- $ivLength = openssl_cipher_iv_length($algorithm);
- $iv = random_bytes($ivLength);
- $cipher = openssl_encrypt($value, $algorithm, $key, 0, $iv);
-
- if ($cipher === false) {
- throw new \RuntimeException("Error al cifrar el valor para '{$this->key}'.");
- }
-
- // Guardamos el IV junto con el valor en base64
- $this->value_text = base64_encode($iv . $cipher);
- return;
- }
-
- match (true) {
- is_string($value) => $this->{strlen($value) > 250 ? 'value_text' : 'value_string'} = $value,
- is_int($value) => $this->value_integer = $value,
- is_bool($value) => $this->value_boolean = $value,
- is_float($value) => $this->value_float = $value,
- is_array($value), is_object($value) => $this->value_text = json_encode($value, JSON_UNESCAPED_UNICODE),
- default => null
- };
}
+
protected function isJson(mixed $value): bool
{
if (!is_string($value)) return false;
+
$value = trim($value);
+
return Str::startsWith($value, ['{', '[']) && json_validate($value);
}
+
// ===================== SCOPES =====================
- public function scopeForUser($query, int $userId)
- {
- return $query->where('user_id', $userId);
- }
- public function scopeGlobal($query)
- {
- return $query->whereNull('user_id');
- }
+
// ===================== HELPERS =====================
@@ -239,38 +228,105 @@ class Setting extends Model implements AuditableContract
*/
protected function decode(mixed $value, bool $asArray = true): mixed
{
- return $this->isJson($value) ? json_decode(trim($value), $asArray) : $value;
+ return $this->isJson($value)
+ ? json_decode(trim($value), $asArray)
+ : $value;
+ }
+
+ protected function encodeValue(mixed $value): array
+ {
+ foreach (SettingValueType::cases() as $type) {
+ $field = "value_{$type->value}";
+ $this->$field = null;
+ }
+
+ if ($this->is_encrypted) {
+ $key = $this->getEncryptionKey();
+ $algorithm = $this->encryption_algorithm ?? 'AES-256-CBC';
+ $ivLength = openssl_cipher_iv_length($algorithm);
+ $iv = random_bytes($ivLength);
+ $cipher = openssl_encrypt($value, $algorithm, $key, 0, $iv);
+
+ if ($cipher === false) {
+ throw new \RuntimeException("Error al cifrar el valor para '{$this->key}'.");
+ }
+
+ return ['value_text' => base64_encode($iv . $cipher)];
+ }
+
+ return match (true) {
+ is_string($value) => [strlen($value) > 250 ? 'value_text' : 'value_string' => $value],
+ is_int($value) => ['value_integer' => $value],
+ is_bool($value) => ['value_boolean' => $value],
+ is_float($value) => ['value_float' => $value],
+ is_array($value), is_object($value) => ['value_text' => json_encode($value, JSON_UNESCAPED_UNICODE)],
+ default => []
+ };
}
/**
- * Obtiene la clave de encriptación.
+ * Obtiene y valida la clave de encriptación para este setting.
+ *
+ * @throws \LogicException Si no hay clave definida.
+ * @throws \RuntimeException Si la clave es inválida o insegura.
*/
protected function getEncryptionKey(): string
{
+ // Obtener la clave del modelo o fallback de configuración
$key = $this->encryption_key ?? config('app.key');
if (empty($key)) {
- throw new \LogicException("No se ha definido una clave de encriptación para '{$this->key}'.");
+ throw new \LogicException("No se ha definido una clave de encriptación para el setting '{$this->key}'.");
}
- $key = Str::startsWith($key, 'base64:') ? base64_decode(substr($key, 7)) : $key;
+ // Decodificar si está en formato base64
+ $decoded = Str::startsWith($key, 'base64:') ? base64_decode(substr($key, 7), true) : $key;
- // Validación adicional por seguridad
- if (strlen($key) < 16) {
- throw new \RuntimeException("La clave de encriptación es demasiado corta.");
+ if (!$decoded || !is_string($decoded)) {
+ throw new \RuntimeException("La clave de encriptación es inválida o no puede ser decodificada.");
}
- return $key;
+ // Validar longitud mínima por seguridad
+ if (strlen($decoded) < 16) {
+ throw new \RuntimeException("La clave de encriptación es demasiado corta (mínimo 16 bytes).");
+ }
+
+ // Validar que no contenga caracteres no imprimibles si no es base64
+ if (!Str::startsWith($key, 'base64:') && !ctype_print($key)) {
+ throw new \RuntimeException("La clave de encriptación contiene caracteres no válidos.");
+ }
+
+ return $decoded;
}
-
- /**
- * Incrementa el contador de uso y actualiza la fecha de última utilización.
- */
- public function incrementUsage(): void
+ protected function decryptValue(bool $asArray = true): mixed
{
- $this->increment('usage_count');
- $this->update(['last_used_at' => now()]);
+ $encoded = $this->value_text ?? $this->value_string;
+ $key = $this->getEncryptionKey();
+ $algorithm = $this->encryption_algorithm ?? 'AES-256-CBC';
+
+ try {
+ $raw = base64_decode($encoded, true);
+ $ivLength = openssl_cipher_iv_length($algorithm);
+ $iv = substr($raw, 0, $ivLength);
+ $cipher = substr($raw, $ivLength);
+
+ $decrypted = openssl_decrypt($cipher, $algorithm, $key, 0, $iv);
+
+ if ($decrypted === false) {
+ throw new \RuntimeException("Error al descifrar el valor para '{$this->key}'.");
+ }
+
+ return $this->decode($decrypted, $asArray);
+
+ } catch (\Throwable $e) {
+ logger()->error('❌ Error al descifrar valor de setting.', [
+ 'key' => $this->key,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return null;
+ }
}
/**
@@ -281,4 +337,42 @@ class Setting extends Model implements AuditableContract
$this->encryption_rotated_at = now();
$this->save();
}
+
+ /**
+ * Obtiene el valor resuelto del setting.
+ */
+ public function getResolvedValue(bool $decrypt = false, bool $asArray = true): mixed
+ {
+ return $decrypt ? $this->getDecryptedValue($asArray) : $this->resolveValueAndTrackUsage();
+ }
+
+ /**
+ * Resuelve el valor del setting y actualiza el uso.
+ */
+ protected function resolveValueAndTrackUsage(): mixed
+ {
+ foreach (SettingValueType::cases() as $type) {
+ $field = "value_{$type->value}";
+ $raw = $this->$field;
+
+ if (!is_null($raw)) {
+ if ($this->track_usage) {
+ $this->incrementUsage();
+ }
+
+ return $this->decode($raw);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Incrementa el contador de uso y actualiza la fecha de última utilización.
+ */
+ public function incrementUsage(): void
+ {
+ $this->increment('usage_count');
+ $this->update(['last_used_at' => now()]);
+ }
}
diff --git a/src/Models/SystemNotification.php b/src/Models/SystemNotification.php
index 6cd8b75..7ca763d 100644
--- a/src/Models/SystemNotification.php
+++ b/src/Models/SystemNotification.php
@@ -7,8 +7,8 @@ namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\{Model,Builder};
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
-use Illuminate\Support\Carbon;
-use Koneko\VuexyAdmin\Application\Enums\SystemNotifications\{SystemNotificationPriority, SystemNotificationType,SystemNotificationScope, SystemNotificationStyle};
+use Carbon\Carbon;
+use Koneko\VuexyAdmin\Support\Enums\SystemNotifications\{SystemNotificationPriority, SystemNotificationType,SystemNotificationScope, SystemNotificationStyle};
use Koneko\VuexyAdmin\Database\Factories\SystemNotificationFactory;
use Koneko\VuexyAdmin\Support\Traits\Audit\{HasCreator,HasDeleter,HasUpdater};
use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata;
diff --git a/src/Models/UserLogin.php b/src/Models/UserLogin.php
index 0934f96..7d1784d 100644
--- a/src/Models/UserLogin.php
+++ b/src/Models/UserLogin.php
@@ -7,7 +7,7 @@ namespace Koneko\VuexyAdmin\Models;
use Illuminate\Database\Eloquent\Model;
use Koneko\VuexyAdmin\Support\Traits\Audit\HasUser;
use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata;
-use Koneko\VuexyAdmin\Support\Traits\Helpers\HasGeolocation;
+use Koneko\VuexyAdmin\Support\Traits\Geolocation\HasGeolocation;
class UserLogin extends Model
{
diff --git a/src/Models/VaultClientKey.php b/src/Models/VaultClientKey.php
new file mode 100644
index 0000000..1a30e28
--- /dev/null
+++ b/src/Models/VaultClientKey.php
@@ -0,0 +1,87 @@
+ 'boolean',
+ 'is_sensitive' => 'boolean',
+ 'rotated_at' => 'datetime',
+ 'rotation_count' => 'integer',
+ ];
+
+ // ===================== SCOPES =====================
+
+ public function scopeActive($query)
+ {
+ return $query->where('is_active', true);
+ }
+
+ public function scopeByProject($query, string $project)
+ {
+ return $query->where('project_code', $project);
+ }
+
+ public function scopeWithNamespace($query, string $namespace)
+ {
+ return $query->where('namespace', $namespace);
+ }
+
+ public function scopeByAlias($query, string $alias)
+ {
+ return $query->where('alias', $alias);
+ }
+
+ // ===================== MÉTODOS =====================
+
+ public function getKeyMaterialDecryptedAttribute(): ?string
+ {
+ try {
+ return decrypt($this->attributes['key_material']);
+ } catch (\Throwable $e) {
+ logger()->error('[Vault] Falló decrypt key_material.', [
+ 'alias' => $this->alias,
+ 'error' => $e->getMessage(),
+ ]);
+ return null;
+ }
+ }
+
+ public function rotateKey(string $newMaterial): void
+ {
+ $this->key_material = encrypt($newMaterial);
+ $this->rotated_at = now();
+ $this->rotation_count++;
+ $this->save();
+ }
+
+ public static function generateRandomKey(int $length = 32): string
+ {
+ return base64_encode(random_bytes($length));
+ }
+}
diff --git a/src/Models/VaultKey.php b/src/Models/VaultKey.php
deleted file mode 100644
index ddf944a..0000000
--- a/src/Models/VaultKey.php
+++ /dev/null
@@ -1,118 +0,0 @@
- 'boolean',
- 'is_sensitive' => 'boolean',
- 'rotated_at' => 'datetime',
- 'rotation_count' => 'integer',
- ];
-
- // ==================== ACCESSORS ====================
-
- protected function getEncryptionKey(): string
- {
- return vault_value_key();
- }
-
- public function getKeyMaterialDecryptedAttribute(): ?string
- {
- try {
- return Crypt::decryptString($this->attributes['key_material']);
-
- } catch (\Throwable $e) {
- logger()->error('❌ Error al desencriptar key_material.', [
- 'alias' => $this->alias,
- 'error' => $e->getMessage(),
- ]);
- return null;
- }
- }
-
- public function getDecodedKeyMaterial(): ?string
- {
- return $this->getKeyMaterialDecryptedAttribute();
- }
-
- // ==================== MUTATORS ====================
-
- /*
- public function setKeyMaterialAttribute(string $value): void
- {
- if (empty($value)) {
- throw new \InvalidArgumentException("El valor de la clave no puede ser vacío.");
- }
-
- $this->attributes['key_material'] = Crypt::encryptString($value);
- }
- */
-
- // ===================== SCOPES =====================
-
- public function scopeActive($query)
- {
- return $query->where('is_active', true);
- }
-
- public function scopeByProject($query, string $project)
- {
- return $query->where('owner_project', $project);
- }
-
- public function scopeForEnvironment($query, string $env)
- {
- return $query->where('environment', $env);
- }
-
- public function scopeWithNamespace($query, string $namespace)
- {
- return $query->where('namespace', $namespace);
- }
-
- public function scopeWithScope($query, string $scope)
- {
- return $query->where('scope', $scope);
- }
-
- // ==================== HELPERS ====================
-
- public function rotateKey(string $newMaterial): void
- {
- $this->key_material = $newMaterial;
- $this->rotated_at = now();
- $this->increment('rotation_count');
- }
-
- public static function generateRandomKey(int $length = 32): string
- {
- return base64_encode(random_bytes($length));
- }
-}
diff --git a/src/Providers/Concerns/EnforcesHttps.php b/src/Providers/Concerns/EnforcesHttps.php
new file mode 100644
index 0000000..179ad6a
--- /dev/null
+++ b/src/Providers/Concerns/EnforcesHttps.php
@@ -0,0 +1,19 @@
+get('security.https.force', false) ||
+ request()->header('X-Forwarded-Proto') === 'https') {
+ URL::forceScheme('https');
+ app('request')->server->set('HTTPS', 'on');
+ }
+ }
+}
diff --git a/src/Providers/Concerns/RegistersTrustedProxies.php b/src/Providers/Concerns/RegistersTrustedProxies.php
index 132bdc9..098ecba 100644
--- a/src/Providers/Concerns/RegistersTrustedProxies.php
+++ b/src/Providers/Concerns/RegistersTrustedProxies.php
@@ -5,19 +5,16 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Providers\Concerns;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\URL;
trait RegistersTrustedProxies
{
- protected function registersTrustedProxies(): void
+ protected function registerTrustedProxies(): void
{
- $trust_proxy = config('koneko.admin.security.trust_proxy', false);
- $trust_proxy_ips = config('koneko.admin.security.trust_proxy_ips', '*');
- $force_https = config('koneko.admin.security.force_https', false);
+ $proxies = config_m()->get('security.proxies', []);
- if ($trust_proxy) {
+ if ($proxies['enabled'] ?? false) {
Request::setTrustedProxies(
- explode(',', $trust_proxy_ips), // admite múltiples IPs separadas por coma
+ explode(',', $proxies['ips'] ?? '*'),
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
@@ -25,10 +22,5 @@ trait RegistersTrustedProxies
Request::HEADER_X_FORWARDED_PREFIX
);
}
-
- if ($force_https || request()->header('X-Forwarded-Proto') === 'https') {
- URL::forceScheme('https');
- app('request')->server->set('HTTPS', 'on');
- }
}
}
diff --git a/src/Providers/FortifyServiceProvider.php b/src/Providers/FortifyServiceProvider.php
index 3a91d92..82208c5 100644
--- a/src/Providers/FortifyServiceProvider.php
+++ b/src/Providers/FortifyServiceProvider.php
@@ -7,9 +7,9 @@ namespace Koneko\VuexyAdmin\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\{ServiceProvider,Str};
-use Illuminate\Support\Facades\{Config,Hash,RateLimiter, Schema};
-use Koneko\VuexyAdmin\Application\Auth\Actions\Fortify\{CreateNewUser,ResetUserPassword,UpdateUserPassword,UpdateUserProfileInformation};
-use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
+use Illuminate\Support\Facades\{Hash, RateLimiter};
+use Koneko\VuexyAdmin\Application\Auth\Actions\Fortify\{CreateNewUser, ResetUserPassword, UpdateUserPassword, UpdateUserProfileInformation};
+use Koneko\VuexyAdmin\Application\Cache\Builders\KonekoAdminVarsBuilder;
use Koneko\VuexyAdmin\Models\User;
use Laravel\Fortify\Fortify;
@@ -53,17 +53,11 @@ class FortifyServiceProvider extends ServiceProvider
}
});
-
// Obtiene el modo de vista de autenticación
- /*
- $viewMode = Schema::hasTable('settings')
- ? settings()->setContext('core', 'auth')->get('vuexy.authViewMode')?? Config::get('koneko.admin.vuexy.authViewMode')
- : Config::get('koneko.admin.vuexy.authViewMode');
- */
- $viewMode = settings()->setContext('core', 'auth')->get('vuexy.authViewMode')?? Config::get('koneko.admin.vuexy.authViewMode');
+ $viewMode = config_m()->get('layout.vuexy.authViewMode', 'cover');
// Share defaults for Fortify views (important!)
- view()->share(['_admin' => app(VuexyVarsBuilderService::class)->getAdminVars()]);
+ view()->share(['_admin' => app(KonekoAdminVarsBuilder::class)->get()]);
// Configurar la vista del login
Fortify::loginView(function () use ($viewMode) {
diff --git a/src/Providers/VuexyAdminServiceProvider.php b/src/Providers/VuexyAdminServiceProvider.php
index 75fd76e..0bc3002 100644
--- a/src/Providers/VuexyAdminServiceProvider.php
+++ b/src/Providers/VuexyAdminServiceProvider.php
@@ -5,14 +5,15 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Providers;
use Illuminate\Support\ServiceProvider;
-use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModuleBootManager;
-use Koneko\VuexyAdmin\Providers\Concerns\RegistersTrustedProxies;
+use Koneko\VuexyAdmin\Application\Bootstrap\Manager\KonekoModuleBootManager;
+use Koneko\VuexyAdmin\Providers\Concerns\{EnforcesHttps, RegistersTrustedProxies};
use Koneko\VuexyAdmin\Support\Traits\Modules\KonekoModuleBoots;
class VuexyAdminServiceProvider extends ServiceProvider
{
use KonekoModuleBoots;
- use RegistersTrustedProxies;
+ use EnforcesHttps,
+ RegistersTrustedProxies;
public function register(): void
{
@@ -21,7 +22,8 @@ class VuexyAdminServiceProvider extends ServiceProvider
public function boot(): void
{
- $this->registersTrustedProxies();
+ $this->enforceHttps();
+ $this->registerTrustedProxies();
$this->registerAllKonekoModules();
diff --git a/src/Support/Builders/AbstractTableConfigBuilder.php b/src/Support/Builders/Table/AbstractTableConfigBuilder.php
similarity index 95%
rename from src/Support/Builders/AbstractTableConfigBuilder.php
rename to src/Support/Builders/Table/AbstractTableConfigBuilder.php
index e87bbb3..5a11b09 100644
--- a/src/Support/Builders/AbstractTableConfigBuilder.php
+++ b/src/Support/Builders/Table/AbstractTableConfigBuilder.php
@@ -6,7 +6,6 @@ namespace Koneko\VuexyAdmin\Support\Builders;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Database\Eloquent\Model;
-use Koneko\VuexyAdmin\Application\Traits\Indexing\HandlesFactory;
use Koneko\VuexyAdmin\Application\Traits\Indexing\HandlesModelMetadata;
use Koneko\VuexyAdmin\Application\Traits\Indexing\{HandlesIndexColumns,HandlesIndexLabels,HandlesTableConfig};
use Koneko\VuexyAdmin\Application\Traits\Indexing\HandlesQueryBuilder;
@@ -18,7 +17,6 @@ use Koneko\VuexyAdmin\Support\Traits\Model\HasVuexyModelMetadata;
*/
abstract class AbstractTableConfigBuilder
{
- use HandlesFactory;
use HandlesModelMetadata;
use HandlesIndexColumns,
HandlesIndexLabels,
diff --git a/src/Support/Cache/AbstractCachedBuilderService.php b/src/Support/Cache/AbstractCachedBuilderService.php
deleted file mode 100644
index 0aa40b3..0000000
--- a/src/Support/Cache/AbstractCachedBuilderService.php
+++ /dev/null
@@ -1,99 +0,0 @@
-user = $user ?? Auth::user();
- $this->manager = new KonekoCacheManager($this->component, $this->group);
-
- if ($this->isUserScoped && !$this->user) {
- throw new RuntimeException("Se requiere un usuario para builders con scope de usuario.");
- }
-
- $this->cacheKey = $this->buildCacheKey();
- }
-
- /**
- * Método que debe construir y retornar los datos sin cache.
- */
- abstract protected function build(): array;
-
- /**
- * Retorna los datos, desde caché si está habilitado.
- */
- public function get(): array
- {
- if (!$this->manager->enabled()) {
- return $this->build();
- }
-
- return Cache::remember($this->cacheKey, $this->ttl(), fn () => $this->build());
- }
-
- /**
- * Fuerza la invalidación del caché.
- */
- public function forget(): void
- {
- Cache::forget($this->cacheKey);
- }
-
- /**
- * TTL como DateTimeInterface.
- */
- public function ttl(): DateTimeInterface
- {
- return now()->addMinutes($this->manager->ttl());
- }
-
- /**
- * Construye una clave única para el componente.
- */
- protected function buildCacheKey(): string
- {
- $key = $this->manager->path();
-
- if ($this->isUserScoped) {
- return "$key.user:{$this->user->getAuthIdentifier()}";
- }
-
- return "$key.global";
- }
-
- /**
- * Información útil de debug.
- */
- public function info(): array
- {
- return [
- 'cache_key' => $this->cacheKey,
- 'user_id' => $this->user?->getAuthIdentifier(),
- ...$this->manager->info(),
- ];
- }
-}
diff --git a/src/Support/Cache/AbstractKeyValueCacheBuilder.php b/src/Support/Cache/AbstractKeyValueCacheBuilder.php
deleted file mode 100644
index 4e567da..0000000
--- a/src/Support/Cache/AbstractKeyValueCacheBuilder.php
+++ /dev/null
@@ -1,250 +0,0 @@
-value;
- private const USER_GUEST_ALIAS = SettingScope::GUEST->value;
-
- protected string $namespace;
- protected string $scope;
-
- protected string $component;
- protected ?string $module = null;
- protected string $group;
- protected string $subGroup;
-
- protected bool $isUserScoped = false;
-
- protected ?Authenticatable $user = null;
- protected KonekoCacheManager $manager;
-
- /**
- * Establece el contexto completo de la caché.
- */
- public function setContext(
- string $component,
- string $group,
- string $subGroup,
- string $scope = self::DEFAULT_SCOPE
- ): static {
- $this->manager = cache_manager($component, $group, $subGroup, $scope);
-
- return $this
- ->setComponent($this->manager->currentComponent())
- ->setGroup($this->manager->currentGroup())
- ->setSubGroup($this->manager->currentSubGroup())
- ->setScope($scope);
- }
-
- public function setComponent(string $component): static
- {
- return $this->setContext(
- component: $component,
- group: $this->group,
- subGroup: $this->subGroup,
- scope: $this->scope ?? self::DEFAULT_SCOPE
- );
- }
-
- public function setGroup(string $group): static
- {
- return $this->setContext(
- component: $this->component,
- group: $group,
- subGroup: $this->subGroup,
- scope: $this->scope ?? self::DEFAULT_SCOPE
- );
- }
-
- public function setSubGroup(string $subGroup): static
- {
- return $this->setContext(
- component: $this->component,
- group: $this->group,
- subGroup: $subGroup,
- scope: $this->scope ?? self::DEFAULT_SCOPE
- );
- }
-
- public function setScope(SettingScope|string $scope): static
- {
- return $this->setContext(
- component: $this->component,
- group: $this->group,
- subGroup: $this->subGroup,
- scope: is_string($scope) ? SettingScope::fromOrFail(strtolower($scope))->value : $scope->value
- );
- }
-
- public function setUser(int|Authenticatable|null|false $user = null): static
- {
- match (true) {
- $user === false => $this->resetUserScope(),
- is_int($user) => $this->assignUser(User::findOrFail($user)),
- $user instanceof Authenticatable => $this->assignUser($user),
- default => $this->assignUser(Auth::user())
- };
-
- 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;
- }
-
- protected function refreshContext(): static
- {
- return $this->setContext(
- component: $this->component,
- group: $this->group,
- subGroup: $this->subGroup,
- scope: $this->scope ?? self::DEFAULT_SCOPE
- );
- }
-
- 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;
- }
-
- public function currentUser(): ?Authenticatable
- {
- return $this->user;
- }
-
- public function currentUserId(): string
- {
- return $this->user?->getAuthIdentifier() ?? self::USER_GUEST_ALIAS;
- }
-
- public function isUserScoped(): bool
- {
- return $this->isUserScoped;
- }
-
- protected function rememberCache(string $cacheKey, callable $callback): mixed
- {
- $this->validateContext();
-
- $key = $this->manager->key($cacheKey);
-
- if (!$this->manager->enabled()) {
- return $callback();
- }
-
- return Cache::remember($key, $this->manager->ttl(), $callback);
- }
-
- protected function forgetCache(string $cacheKey): void
- {
- $this->validateContext();
- Cache::forget($this->manager->key($cacheKey));
- }
-
- protected function generateCacheKey(string $cacheKey): string
- {
- $this->validateContext();
-
- $userSegment = $this->isUserScoped ?
- 'u.' . $this->currentUserId() :
- self::DEFAULT_SCOPE;
-
- $scopeSegment = $this->scope === self::DEFAULT_SCOPE
- ? null
- : "scope." . SettingScope::from($this->scope)->value;
-
- $base = implode('.', array_filter([
- $this->namespace,
- app()->environment(),
- $this->component,
- $this->group,
- $this->subGroup,
- $scopeSegment,
- $userSegment,
- $cacheKey
- ]));
-
- return strlen($base) > KonekoCacheManager::MAX_KEY_LENGTH
- ? 'h:' . crc32($base)
- : $base;
- }
-
- protected function validateContext(): void
- {
- if ($this->component !== 'project') {
- $module = KonekoModuleRegistry::get($this->component);
-
- if (!$module) {
- throw new \InvalidArgumentException("El componente '{$this->component}' no está registrado en KonekoModuleRegistry.");
- }
-
- $this->module = $module->composerName;
-
- } else {
- $this->module = null;
- }
- }
-
- protected function validateKey(string $key): void
- {
- if (!preg_match('/^[a-z0-9\._\-]+$/', $key)) {
- throw new \InvalidArgumentException("La clave '{$key}' no es válida.");
- }
- }
-
- public function cacheInfo(string $cacheKey): array
- {
- return [
- 'key' => $this->generateCacheKey($cacheKey),
- 'enabled' => $this->manager->enabled(),
- 'ttl' => $this->manager->ttl(),
- 'user' => $this->user?->getAuthIdentifier(),
- 'driver' => config('cache.default'),
- 'component' => $this->component,
- 'group' => $this->group,
- 'scoped' => $this->isUserScoped,
- ];
- }
-}
diff --git a/src/Support/Catalogs/AbstractCatalogService.php b/src/Support/Catalogs/AbstractCatalogService.php
index 6b6ad75..6804c21 100644
--- a/src/Support/Catalogs/AbstractCatalogService.php
+++ b/src/Support/Catalogs/AbstractCatalogService.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Catalogs;
+namespace Koneko\VuexyAdmin\Support\Catalogs;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
diff --git a/src/Application/Contracts/Enums/LabeledEnumInterface.php b/src/Support/Contracts/Enums/LabeledEnumInterface.php
similarity index 100%
rename from src/Application/Contracts/Enums/LabeledEnumInterface.php
rename to src/Support/Contracts/Enums/LabeledEnumInterface.php
diff --git a/src/Application/Contracts/Files/ParsableFileInterface.php b/src/Support/Contracts/Files/ParsableFileInterface.php
similarity index 87%
rename from src/Application/Contracts/Files/ParsableFileInterface.php
rename to src/Support/Contracts/Files/ParsableFileInterface.php
index abbb659..bd53a76 100644
--- a/src/Application/Contracts/Files/ParsableFileInterface.php
+++ b/src/Support/Contracts/Files/ParsableFileInterface.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Contracts\Files;
+namespace Koneko\VuexyAdmin\Support\Contracts\Files;
interface ParsableFileInterface
{
diff --git a/src/Application/Contracts/Flags/FlagEnumInterface.php b/src/Support/Contracts/Flags/FlagEnumInterface.php
similarity index 100%
rename from src/Application/Contracts/Flags/FlagEnumInterface.php
rename to src/Support/Contracts/Flags/FlagEnumInterface.php
diff --git a/src/Application/Contracts/Seeders/DataSeederInterface.php b/src/Support/Contracts/Seeders/DataSeederInterface.php
similarity index 100%
rename from src/Application/Contracts/Seeders/DataSeederInterface.php
rename to src/Support/Contracts/Seeders/DataSeederInterface.php
diff --git a/src/Application/Enums/ExternalApi/ApiAuthType.php b/src/Support/Enums/ExternalApi/ApiAuthType.php
similarity index 89%
rename from src/Application/Enums/ExternalApi/ApiAuthType.php
rename to src/Support/Enums/ExternalApi/ApiAuthType.php
index 5aeef76..d0670be 100644
--- a/src/Application/Enums/ExternalApi/ApiAuthType.php
+++ b/src/Support/Enums/ExternalApi/ApiAuthType.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\ExternalApi;
+namespace Koneko\VuexyAdmin\Support\Enums\ExternalApi;
enum ApiAuthType: string
{
diff --git a/src/Application/Enums/ExternalApi/ApiEnvironment.php b/src/Support/Enums/ExternalApi/ApiEnvironment.php
similarity index 87%
rename from src/Application/Enums/ExternalApi/ApiEnvironment.php
rename to src/Support/Enums/ExternalApi/ApiEnvironment.php
index 5c539fe..42f5956 100644
--- a/src/Application/Enums/ExternalApi/ApiEnvironment.php
+++ b/src/Support/Enums/ExternalApi/ApiEnvironment.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\ExternalApi;
+namespace Koneko\VuexyAdmin\Support\Enums\ExternalApi;
enum ApiEnvironment: string
{
diff --git a/src/Application/Enums/Notifications/NotificationCahennel.php b/src/Support/Enums/Notifications/NotificationChannel.php
similarity index 65%
rename from src/Application/Enums/Notifications/NotificationCahennel.php
rename to src/Support/Enums/Notifications/NotificationChannel.php
index 49bbbd8..4a36346 100644
--- a/src/Application/Enums/Notifications/NotificationCahennel.php
+++ b/src/Support/Enums/Notifications/NotificationChannel.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\Notifications;
+namespace Koneko\VuexyAdmin\Support\Enums\Notifications;
-enum NotificationCahennel: string
+enum NotificationChannel: string
{
case Toast = 'toast';
case Push = 'push';
diff --git a/src/Application/Enums/Permissions/PermissionAction.php b/src/Support/Enums/Permissions/PermissionAction.php
similarity index 98%
rename from src/Application/Enums/Permissions/PermissionAction.php
rename to src/Support/Enums/Permissions/PermissionAction.php
index b984b11..f97ab98 100644
--- a/src/Application/Enums/Permissions/PermissionAction.php
+++ b/src/Support/Enums/Permissions/PermissionAction.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\Permissions;
+namespace Koneko\VuexyAdmin\Support\Enums\Permissions;
use Illuminate\Support\Facades\App;
diff --git a/src/Application/Enums/SecurityEvents/SecurityEventStatus.php b/src/Support/Enums/SecurityEvents/SecurityEventStatus.php
similarity index 87%
rename from src/Application/Enums/SecurityEvents/SecurityEventStatus.php
rename to src/Support/Enums/SecurityEvents/SecurityEventStatus.php
index fb5f13f..6b48def 100644
--- a/src/Application/Enums/SecurityEvents/SecurityEventStatus.php
+++ b/src/Support/Enums/SecurityEvents/SecurityEventStatus.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\SecurityEvents;
+namespace Koneko\VuexyAdmin\Support\Enums\SecurityEvents;
enum SecurityEventStatus: string
{
diff --git a/src/Application/Enums/SecurityEvents/SecurityEventType.php b/src/Support/Enums/SecurityEvents/SecurityEventType.php
similarity index 88%
rename from src/Application/Enums/SecurityEvents/SecurityEventType.php
rename to src/Support/Enums/SecurityEvents/SecurityEventType.php
index 5a151b8..0bae1b9 100644
--- a/src/Application/Enums/SecurityEvents/SecurityEventType.php
+++ b/src/Support/Enums/SecurityEvents/SecurityEventType.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\SecurityEvents;
+namespace Koneko\VuexyAdmin\Support\Enums\SecurityEvents;
enum SecurityEventType: string
{
diff --git a/src/Application/Enums/SystemLog/LogLevel.php b/src/Support/Enums/SystemLog/LogLevel.php
similarity index 75%
rename from src/Application/Enums/SystemLog/LogLevel.php
rename to src/Support/Enums/SystemLog/LogLevel.php
index bd7b4fe..c011464 100644
--- a/src/Application/Enums/SystemLog/LogLevel.php
+++ b/src/Support/Enums/SystemLog/LogLevel.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\SystemLog;
+namespace Koneko\VuexyAdmin\Support\Enums\SystemLog;
enum LogLevel: string
{
diff --git a/src/Application/Enums/SystemLog/LogTriggerType.php b/src/Support/Enums/SystemLog/LogTriggerType.php
similarity index 84%
rename from src/Application/Enums/SystemLog/LogTriggerType.php
rename to src/Support/Enums/SystemLog/LogTriggerType.php
index ed77cf7..11cb6e4 100644
--- a/src/Application/Enums/SystemLog/LogTriggerType.php
+++ b/src/Support/Enums/SystemLog/LogTriggerType.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\SystemLog;
+namespace Koneko\VuexyAdmin\Support\Enums\SystemLog;
enum LogTriggerType: string
{
diff --git a/src/Application/Enums/SystemNotifications/SystemNotificationPriority.php b/src/Support/Enums/SystemNotifications/SystemNotificationPriority.php
similarity index 74%
rename from src/Application/Enums/SystemNotifications/SystemNotificationPriority.php
rename to src/Support/Enums/SystemNotifications/SystemNotificationPriority.php
index 79438d4..5b4ba68 100644
--- a/src/Application/Enums/SystemNotifications/SystemNotificationPriority.php
+++ b/src/Support/Enums/SystemNotifications/SystemNotificationPriority.php
@@ -3,7 +3,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Application\Enums\SystemNotifications;
+namespace Koneko\VuexyAdmin\Support\Enums\SystemNotifications;
enum SystemNotificationPriority: string
{
diff --git a/src/Application/Enums/SystemNotifications/SystemNotificationScope.php b/src/Support/Enums/SystemNotifications/SystemNotificationScope.php
similarity index 67%
rename from src/Application/Enums/SystemNotifications/SystemNotificationScope.php
rename to src/Support/Enums/SystemNotifications/SystemNotificationScope.php
index 1ccb224..62dd027 100644
--- a/src/Application/Enums/SystemNotifications/SystemNotificationScope.php
+++ b/src/Support/Enums/SystemNotifications/SystemNotificationScope.php
@@ -1,6 +1,6 @@
null, // Forzar sin usuario
+ $user instanceof Authenticatable => $user->getAuthIdentifier(),
+ $user === null => Auth::check() ? Auth::id() : null,
+ default => $user,
+ };
+ }
+
+ protected function resolveUser(Authenticatable|int|null|false $user): ?Authenticatable
+ {
+ return match (true) {
+ $user === false => null, // Forzar sin usuario
+ $user instanceof Authenticatable => $user,
+ $user === null => Auth::check() ? Auth::user() : null,
+ default => Auth::findUserById($user),
+ };
+ }
+}
diff --git a/src/Support/Traits/Cache/HasCacheManagerHelpers.php b/src/Support/Traits/Cache/HasCacheManagerHelpers.php
new file mode 100644
index 0000000..a911d76
--- /dev/null
+++ b/src/Support/Traits/Cache/HasCacheManagerHelpers.php
@@ -0,0 +1,40 @@
+setKeyName($key)
+ ->forget();
+ }
+
+ /**
+ * Obtiene o genera una caché por clave y usuario
+ */
+ protected function rememberKeyCache(
+ string $component,
+ string $group,
+ string $subGroup,
+ string $key,
+ callable $callback,
+ Authenticatable|int|null|false $user = false,
+ ?int $ttl = null
+ ): mixed {
+ return cache_m($component, $group, $subGroup, $user)
+ ->setKeyName($key)
+ ->rememberWithTTLResolution($callback, $ttl);
+ }
+}
diff --git a/src/Support/Traits/Cache/InteractsWithKonekoVarsCache.php b/src/Support/Traits/Cache/InteractsWithKonekoVarsCache.php
deleted file mode 100644
index c6da2cb..0000000
--- a/src/Support/Traits/Cache/InteractsWithKonekoVarsCache.php
+++ /dev/null
@@ -1,118 +0,0 @@
-cacheIsMenuRelated = $isMenuRelated;
- $this->cacheEnabled = $this->resolveCacheEnabled();
- $this->cacheTTL = (int) Config::get(
- $isMenuRelated ? 'koneko.admin.menu.cache.ttl' : 'koneko.admin.cache.ttl',
- 1440
- );
- }
-
- /**
- * Verifica si la cache está activada globalmente (y opcionalmente por tipo de menú).
- */
- protected function resolveCacheEnabled(): bool
- {
- return Config::get('koneko.admin.cache.enabled', true)
- && (!$this->cacheIsMenuRelated || Config::get('koneko.admin.menu.cache.enabled', true));
- }
-
- /**
- * Devuelve la instancia DateTimeInterface para TTL.
- */
- protected function getCacheTtl(): DateTimeInterface
- {
- return now()->addMinutes($this->cacheTTL);
- }
-
- /**
- * Cachea o computa un valor si el caché está activado.
- */
- protected function cacheOrCompute(string $key, callable $callback): mixed
- {
- if (!$this->cacheEnabled) {
- return $callback();
- }
-
- return Cache::remember($key, $this->getCacheTtl(), $callback);
- }
-
- protected function cacheOrComputeForUser(callable $callback): mixed
- {
- if (!defined('static::CACHE_PREFIX')) {
- throw new \RuntimeException('Debe definir CACHE_PREFIX en la clase que usa este trait.');
- }
-
- if (!property_exists($this, 'user') || !($this->user instanceof Authenticatable)) {
- throw new \RuntimeException('La clase debe tener una propiedad $user de tipo Authenticatable');
- }
-
- $key = static::makeCacheKeyForUser($this->user->getAuthIdentifier());
-
- return $this->cacheOrCompute($key, $callback);
- }
-
- protected function cacheOrComputeTagged(string $key, callable $callback): mixed
- {
- if (!$this->cacheEnabled) {
- return $callback();
- }
-
- if (defined('static::CACHE_TAG')) {
- return Cache::tags([static::CACHE_TAG])->remember($key, $this->getCacheTtl(), $callback);
- }
-
- return Cache::remember($key, $this->getCacheTtl(), $callback);
- }
-
- /**
- * Elimina el valor de caché especificado.
- */
- protected function forgetCache(string $key): void
- {
- Cache::forget($key);
- }
-
- /**
- * Genera una clave de caché para un usuario específico.
- * Requiere que la clase que usa este trait defina una constante CACHE_PREFIX.
- */
- protected static function makeCacheKeyForUser(int|string $userId): string
- {
- return static::CACHE_PREFIX . $userId;
- }
-
- /**
- * Limpia el caché para un usuario específico.
- */
- public static function clearCacheForUser(int|string $userId): void
- {
- Cache::forget(static::makeCacheKeyForUser($userId));
- }
-
- protected static function flushCacheTags(string|array $tags): void
- {
- Cache::tags((array) $tags)->flush();
- }
-}
diff --git a/src/Support/Traits/Cache/___KonekoCacheSupport.php b/src/Support/Traits/Cache/___KonekoCacheSupport.php
deleted file mode 100644
index ae9cd84..0000000
--- a/src/Support/Traits/Cache/___KonekoCacheSupport.php
+++ /dev/null
@@ -1,161 +0,0 @@
-konekoCacheEnabled = Config::get("{$configKey}.enabled", true);
- $this->konekoCacheTTL = (int) Config::get("{$configKey}.ttl", $defaultTTL);
- }
-
- /**
- * Genera TTL compatible con Cache::remember
- */
- protected function getKonekoCacheTTL(): DateTimeInterface
- {
- return now()->addMinutes($this->konekoCacheTTL);
- }
-
- /**
- * Recuerda el valor cacheado o ejecuta el callback
- */
- protected function remember(string $key, Closure $callback): mixed
- {
- if (!$this->konekoCacheEnabled) {
- return $callback();
- }
-
- return Cache::remember($key, $this->getKonekoCacheTTL(), $callback);
- }
-
- /**
- * Elimina el valor de caché
- */
- protected function forget(string $key): void
- {
- Cache::forget($key);
- }
-
- /**
- * Forzar almacenamiento de un valor por TTL
- */
- protected function put(string $key, mixed $value): void
- {
- Cache::put($key, $value, $this->getKonekoCacheTTL());
- }
-
- /**
- * Verifica si existe una clave
- */
- protected function has(string $key): bool
- {
- return Cache::has($key);
- }
-
-
-
-
-
-
-
-
-
- /**
- * TTL como DateTimeInterface.
- */
- protected function konekoCacheTtl(): DateTimeInterface
- {
- return now()->addMinutes($this->cacheTTL);
- }
-
- /**
- * Genera clave de caché consistente.
- */
- protected function makeKonekoCacheKey(string $key): string
- {
- return "koneko:{$this->cacheNamespace}:{$this->cacheGroup}:{$key}";
- }
-
- /**
- * Cachea o computa un valor.
- */
- protected function cacheKoneko(string $key, Closure $callback): mixed
- {
- if (!$this->cacheEnabled) {
- return $callback();
- }
-
- return Cache::remember($this->makeKonekoCacheKey($key), $this->konekoCacheTtl(), $callback);
- }
-
- /**
- * Cachea por usuario autenticado (requiere propiedad $user).
- */
- protected function cacheKonekoPerUser(Closure $callback): mixed
- {
- if (!property_exists($this, 'user') || !($this->user instanceof Authenticatable)) {
- throw new RuntimeException('Falta propiedad $user para cache per-user');
- }
-
- $key = 'user:' . $this->user->getAuthIdentifier();
-
- return $this->cacheKoneko($key, $callback);
- }
-
- /**
- * Borra un valor específico de caché.
- */
- protected function forgetKonekoCache(string $key): void
- {
- Cache::forget($this->makeKonekoCacheKey($key));
- }
-
- /**
- * Borra el caché asociado a un usuario.
- */
- protected function forgetKonekoCacheForUser(int|string $userId): void
- {
- $key = $this->makeKonekoCacheKey('user:' . $userId);
- Cache::forget($key);
- }
-
- /**
- * Construye clave de forma estática.
- */
- public static function buildKonekoCacheKey(string $namespace, string $group, string $key): string
- {
- return "koneko:{$namespace}:{$group}:{$key}";
- }
-}
diff --git a/src/Application/Traits/Indexing/HandlesStaticRegistryMerge.php b/src/Support/Traits/ConfigBuilder/HandlesStaticRegistryMerge.php
similarity index 100%
rename from src/Application/Traits/Indexing/HandlesStaticRegistryMerge.php
rename to src/Support/Traits/ConfigBuilder/HandlesStaticRegistryMerge.php
diff --git a/src/Support/Traits/Helpers/HasLabeledEnumHelpers.php b/src/Support/Traits/Enums/HasLabeledEnumHelpers.php
similarity index 100%
rename from src/Support/Traits/Helpers/HasLabeledEnumHelpers.php
rename to src/Support/Traits/Enums/HasLabeledEnumHelpers.php
diff --git a/src/Support/Traits/Factories/HasUserFactoryFlagsExtension.php b/src/Support/Traits/Flags/Factories/HasUserFactoryFlagsExtension.php
similarity index 88%
rename from src/Support/Traits/Factories/HasUserFactoryFlagsExtension.php
rename to src/Support/Traits/Flags/Factories/HasUserFactoryFlagsExtension.php
index 7e91d12..a62797c 100644
--- a/src/Support/Traits/Factories/HasUserFactoryFlagsExtension.php
+++ b/src/Support/Traits/Flags/Factories/HasUserFactoryFlagsExtension.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Support\Traits\Factories;
+namespace Koneko\VuexyAdmin\Support\Traits\Flags\Factories;
trait HasUserFactoryFlagsExtension
{
diff --git a/src/Support/Traits/Helpers/HasGeolocation.php b/src/Support/Traits/Geolocation/HasGeolocation.php
similarity index 77%
rename from src/Support/Traits/Helpers/HasGeolocation.php
rename to src/Support/Traits/Geolocation/HasGeolocation.php
index fa5b371..27ddc3e 100644
--- a/src/Support/Traits/Helpers/HasGeolocation.php
+++ b/src/Support/Traits/Geolocation/HasGeolocation.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Koneko\VuexyAdmin\Support\Traits\Helpers;
+namespace Koneko\VuexyAdmin\Support\Traits\Geolocation;
trait HasGeolocation
{
diff --git a/src/Support/Traits/Livewire/Cache/HasKonekoCacheSupport.php b/src/Support/Traits/Livewire/Cache/HasKonekoCacheSupport.php
deleted file mode 100644
index bb56cc9..0000000
--- a/src/Support/Traits/Livewire/Cache/HasKonekoCacheSupport.php
+++ /dev/null
@@ -1,66 +0,0 @@
-cacheComponentKey = $component ?? $this->cacheComponentKey;
- $this->cacheGroupKey = $group ?? $this->cacheGroupKey;
- $this->cacheUserContext = $user ?? Auth::user();
- $this->cacheManager = new KonekoCacheManager($this->cacheComponentKey, $this->cacheGroupKey);
- }
-
- protected function cacheKey(string $suffix): string
- {
- return $this->cacheManager->key($suffix);
- }
-
- protected function cacheRemember(string $keySuffix, \Closure $callback): mixed
- {
- if (! $this->cacheManager->enabled()) {
- return $callback();
- }
-
- return cache()->remember(
- $this->cacheKey($keySuffix),
- now()->addMinutes($this->cacheManager->ttl()),
- $callback
- );
- }
-
- protected function cacheRememberPerUser(string $keySuffix, \Closure $callback): mixed
- {
- $id = $this->cacheUserContext?->getAuthIdentifier() ?? 'guest';
- return $this->cacheRemember("{$id}:{$keySuffix}", $callback);
- }
-
- protected function cacheForget(string $keySuffix): void
- {
- cache()->forget($this->cacheKey($keySuffix));
- }
-
- protected function cacheTtl(): int
- {
- return $this->cacheManager->ttl();
- }
-
- protected function cacheInfo(): array
- {
- return $this->cacheManager->info();
- }
-}
diff --git a/src/Support/Traits/Livewire/Notifications/HandlesAsyncNotifications.php b/src/Support/Traits/Livewire/Notifications/HandlesAsyncNotifications.php
deleted file mode 100644
index f803ccd..0000000
--- a/src/Support/Traits/Livewire/Notifications/HandlesAsyncNotifications.php
+++ /dev/null
@@ -1,19 +0,0 @@
-dispatch('notify', [
- 'message' => $message,
- 'type' => $type, // info, success, warning, error
- 'target' => isset($this->targetNotify) ? $this->targetNotify : 'body',
- 'delay' => $delay,
- 'channel' => 'toastify'
- ]);
- }
-}
diff --git a/src/Support/Traits/Helpers/HasDynamicModelExtenders.php b/src/Support/Traits/Model/HasDynamicModelExtenders.php
similarity index 100%
rename from src/Support/Traits/Helpers/HasDynamicModelExtenders.php
rename to src/Support/Traits/Model/HasDynamicModelExtenders.php
diff --git a/src/Support/Traits/Model/HasVuexyModelMetadata.php b/src/Support/Traits/Model/HasVuexyModelMetadata.php
index ee2a894..033bb6c 100644
--- a/src/Support/Traits/Model/HasVuexyModelMetadata.php
+++ b/src/Support/Traits/Model/HasVuexyModelMetadata.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Koneko\VuexyAdmin\Support\Traits\Model;
use Illuminate\Support\Str;
-use Koneko\VuexyAdmin\Application\Macros\StrMacros;
+use Koneko\VuexyAdmin\Support\Macros\StrMacros;
StrMacros::register();
diff --git a/src/Support/Traits/Modules/KonekoModuleBoots.php b/src/Support/Traits/Modules/KonekoModuleBoots.php
index 706b81e..86767c8 100644
--- a/src/Support/Traits/Modules/KonekoModuleBoots.php
+++ b/src/Support/Traits/Modules/KonekoModuleBoots.php
@@ -7,7 +7,9 @@ namespace Koneko\VuexyAdmin\Support\Traits\Modules;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
-use Koneko\VuexyAdmin\Application\Bootstrap\{KonekoModule ,KonekoModuleBootManager, KonekoModuleRegistry};
+use Koneko\VuexyAdmin\Application\Bootstrap\KonekoModule;
+use Koneko\VuexyAdmin\Application\Bootstrap\Manager\KonekoModuleBootManager;
+use Koneko\VuexyAdmin\Application\Bootstrap\Registry\KonekoModuleRegistry;
/**
* Trait universal para bootstrapping de módulos Vuexy,
diff --git a/src/Support/Traits/Modules/____BootsVuexyModule copy.php b/src/Support/Traits/Modules/____BootsVuexyModule copy.php
deleted file mode 100644
index ea930ea..0000000
--- a/src/Support/Traits/Modules/____BootsVuexyModule copy.php
+++ /dev/null
@@ -1,58 +0,0 @@
-publishedFiles)) {
- $this->registerPublishedFiles($module);
- }
- }
-
- /**
- * Registra los archivos publicables del módulo usando publishes()
- */
- protected function registerPublishedFiles(KonekoModule $module): void
- {
- foreach ($module->publishedFiles as $tag => $files) {
- $resolvedFiles = [];
-
- foreach ($files as $from => $to) {
- $fromFull = $this->resolvePath($module->basePath, $from);
-
- if (file_exists($fromFull)) {
- $resolvedFiles[$fromFull] = $to;
- } else {
- Log::warning("📁 Archivo a publicar no encontrado: {$fromFull}");
- }
- }
-
- if (!empty($resolvedFiles) && $this instanceof ServiceProvider) {
- $this->publishes($resolvedFiles, "{$module->slug}-{$tag}");
- }
- }
- }
-
- protected function resolvePath(string $base, ?string $relative): string
- {
- return $base . '/' . ltrim($relative, '/');
- }
-}
diff --git a/src/Support/Traits/Notifications/____DispatchesVuexyNotifications.php b/src/Support/Traits/Notifications/____DispatchesVuexyNotifications.php
deleted file mode 100644
index 1966773..0000000
--- a/src/Support/Traits/Notifications/____DispatchesVuexyNotifications.php
+++ /dev/null
@@ -1,74 +0,0 @@
-dispatch('vuexy:notify', [
- 'type' => $type,
- 'message' => $message,
- 'target' => $this->targetNotify,
- 'delay' => $delay,
- ]);
- }
-
- /**
- * Notificación tipo success.
- *
- * @param string $message
- * @param int $delay
- */
- public function vuexyNotifySuccess(string $message, int $delay = 3000): void
- {
- $this->vuexyNotify($message, 'success', $delay);
- }
-
- /**
- * Notificación tipo error (danger).
- *
- * @param string $message
- * @param int $delay
- */
- public function vuexyNotifyError(string $message, int $delay = 3000): void
- {
- $this->vuexyNotify($message, 'danger', $delay);
- }
-
- /**
- * Notificación tipo warning.
- *
- * @param string $message
- * @param int $delay
- */
- public function vuexyNotifyWarning(string $message, int $delay = 3000): void
- {
- $this->vuexyNotify($message, 'warning', $delay);
- }
-
- /**
- * Notificación tipo info.
- *
- * @param string $message
- * @param int $delay
- */
- public function vuexyNotifyInfo(string $message, int $delay = 3000): void
- {
- $this->vuexyNotify($message, 'info', $delay);
- }
-}
diff --git a/src/Support/Traits/Seeders/HandlesFileSeeders.php b/src/Support/Traits/Seeders/HandlesFileSeeders.php
index 31e0c8c..1c35552 100644
--- a/src/Support/Traits/Seeders/HandlesFileSeeders.php
+++ b/src/Support/Traits/Seeders/HandlesFileSeeders.php
@@ -96,6 +96,7 @@ trait HandlesFileSeeders
foreach ($argv as $index => $arg) {
if (str_starts_with($arg, '--file=')) {
$this->targetFile = substr($arg, 7);
+
} elseif ($arg === '--file' && isset($argv[$index + 1]) && !str_starts_with($argv[$index + 1], '--')) {
$this->targetFile = $argv[$index + 1];
}
@@ -160,4 +161,3 @@ trait HandlesFileSeeders
}
}
}
-
diff --git a/src/helpers.php b/src/helpers.php
index b96ca76..c8d0592 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -2,16 +2,21 @@
declare(strict_types=1);
-use Illuminate\Support\Str;
+use Illuminate\Database\Eloquent\Model;
use Koneko\VuexyAdmin\Application\Bootstrap\Extenders\Catalog\CatalogModuleRegistry;
-use Koneko\VuexyAdmin\Application\Cache\KonekoCacheManager;
-use Koneko\VuexyAdmin\Application\Contracts\ApiRegistry\ExternalApiRegistryInterface;
-use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
-use Koneko\VuexyAdmin\Application\Enums\Settings\SettingScope;
-use Koneko\VuexyAdmin\Application\Helpers\{VuexyHelper, VuexyNotifyHelper,VuexyToastrHelper};
-use Koneko\VuexyAdmin\Application\System\KonekoSettingManager;
-use Koneko\VuexyAdmin\Models\ExternalApi;
+use Koneko\VuexyAdmin\Application\Cache\Contracts\CacheRepositoryInterface;
+use Koneko\VuexyAdmin\Application\Cache\Manager\KonekoCacheManager;
+use Koneko\VuexyAdmin\Application\Config\Contracts\ConfigRepositoryInterface;
+use Koneko\VuexyAdmin\Application\Config\Manager\KonekoConfigManager;
+use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
+use Koneko\VuexyAdmin\Application\Settings\Manager\KonekoSettingManager;
+use Koneko\VuexyAdmin\Application\Helpers\VuexyHelper;
+use Koneko\VuexyAdmin\Application\Loggers\{KonekoSecurityLogger, KonekoSystemLogger, KonekoUserInteractionLogger};
+use Koneko\VuexyAdmin\Application\UX\Notifications\Manager\KonekoNotifyManager;
+use Koneko\VuexyAdmin\Models\SystemLog;
use Koneko\VuexyAdmin\Models\UserInteraction;
+use Koneko\VuexyAdmin\Support\Enums\SystemLog\LogLevel;
+use Koneko\VuexyAdmin\Support\Enums\UserInteractions\InteractionSecurityLevel;
// =================== HELPERS ===================
@@ -22,79 +27,83 @@ if (!function_exists('Helper')) {
}
}
-// =================== SETTINGS ===================
-if (!function_exists('settings')) {
- function settings(
- ?string $component = null,
- ?string $group = null,
- ?string $subGroup = null,
- ?string $scope = null
- ): SettingsRepositoryInterface {
- return app(KonekoSettingManager::class)
- ->setContext(
- $component,
- $group,
- $subGroup,
- $scope
- );
+// =================== CONFIG ===================
+
+if (!function_exists('config_m')) {
+ function config_m(?string $moduleComponent = null): ConfigRepositoryInterface
+ {
+ $manager = KonekoConfigManager::make();
+
+ // Componente o Clase de Modulo
+ if ($moduleComponent) {
+ $manager->setComponent($moduleComponent);
+ }
+
+ return $manager;
}
}
+
+// =================== SETTINGS ===================
+
+if (!function_exists('settings')) {
+ /**
+ * Devuelve una instancia de SettingsManager con contexto aplicado automáticamente.
+ *
+ * @param string|array|Model|null $context
+ * - string: asume solo componente.
+ * - array: se mapea a component, group, sub_group, section, key_name, etc.
+ * - Model: se intenta extraer scope con `withScopeFromModel()`.
+ *
+ * @return SettingsRepositoryInterface
+ */
+ function settings(?string $moduleComponent = null): SettingsRepositoryInterface
+ {
+ $manager = KonekoSettingManager::make();
+
+ // Componente o Clase de Modulo
+ if ($moduleComponent) {
+ $manager->setComponent($moduleComponent);
+ }
+
+ return $manager;
+ }
+}
+
+
// =================== CACHE ===================
-if (!function_exists('cache_manager')) {
- function cache_manager(
- ?string $component = null,
- ?string $group = null,
- ?string $subGroup = null,
- ?string $scope = null
- ): KonekoCacheManager {
- return (new KonekoCacheManager(VuexyHelper::NAMESPACE))
- ->setContext(
- $component,
- $group,
- $subGroup,
- $scope
- );
- }
-}
-// =================== KEY VAULT ===================
-if (!function_exists('vault_value_key')) {
- function vault_value_key(): string
+if (!function_exists('cache_m')) {
+ /**
+ * Crea un gestor de caché con contexto aplicado.
+ *
+ * Ejemplos:
+ * - `cache_m('site')`
+ * - `cache_m(['component' => 'site', 'group' => 'seo', 'key_name' => 'enabled'])`
+ * - `cache_m($empresaModel)`
+ *
+ * @param string|array|Model|null $context
+ * @return CacheRepositoryInterface
+ */
+ function cache_m(?string $moduleComponent = null): CacheRepositoryInterface
{
- static $cachedKey = null;
+ $manager = KonekoCacheManager::make();
- if ($cachedKey) {
- return $cachedKey;
+ // Componente o Clase de Modulo
+ if ($moduleComponent) {
+ $manager->setComponent($moduleComponent);
}
- $path = env('VAULT_VALUE_KEY_PATH');
-
- if (!$path || !file_exists($path)) {
- throw new \RuntimeException("Vault Value Key file not found at {$path}");
- }
-
- $key = trim(file_get_contents($path));
-
- if (Str::startsWith($key, 'base64:')) {
- $key = base64_decode(substr($key, 7));
- }
-
- if (empty($key)) {
- throw new \RuntimeException("Vault Value Key is invalid or empty.");
- }
-
- return $cachedKey = $key;
+ return $manager;
}
}
-
// =================== LOGGERS ===================
if (!function_exists('log_system')) {
function log_system(
- string|\Koneko\VuexyAdmin\Application\Enums\SystemLog\LogLevel $level,
+ string|LogLevel $level,
string $message,
array $context = [],
?\Illuminate\Database\Eloquent\Model $related = null
@@ -121,7 +130,7 @@ if (!function_exists('log_interaction')) {
function log_interaction(
string $action,
array $context = [],
- \Koneko\VuexyAdmin\Application\Enums\UserInteractions\InteractionSecurityLevel|string $security = 'normal',
+ InteractionSecurityLevel|string $security = 'normal',
?string $livewireComponent = null
): ?UserInteraction {
return app(KonekoUserInteractionLogger::class)
@@ -130,7 +139,7 @@ if (!function_exists('log_interaction')) {
}
// =================== GEOIP ===================
-
+/*
if (!function_exists('external_api')) {
function external_api(string $slug): ?ExternalApi
{
@@ -138,17 +147,27 @@ if (!function_exists('external_api')) {
}
}
+/*
if (!function_exists('apis_vuexy')) {
function apis_vuexy(): ExternalApiRegistryInterface
{
return app(ExternalApiRegistryInterface::class);
}
}
+*/
// =================== NOTIFICATIONS ===================
+if (!function_exists('notify')) {
+ function notify(): KonekoNotifyManager
+ {
+ return app(KonekoNotifyManager::class);
+ }
+}
+
+/*
if (!function_exists('vuexy_notify')) {
function vuexy_notify(
string $message,
@@ -178,6 +197,7 @@ if (!function_exists('vuexy_toastr')) {
);
}
}
+*/
// =================== CATALOGS ===================
@@ -193,3 +213,37 @@ if (!function_exists('catalog')) {
return CatalogModuleRegistry::get($component);
}
}
+
+
+
+// =================== KEY VAULT ===================
+/*
+if (!function_exists('vault_value_key')) {
+ function vault_value_key(): string
+ {
+ static $cachedKey = null;
+
+ if ($cachedKey) {
+ return $cachedKey;
+ }
+
+ $path = env('VAULT_VALUE_KEY_PATH');
+
+ if (!$path || !file_exists($path)) {
+ throw new \RuntimeException("Vault Value Key file not found at {$path}");
+ }
+
+ $key = trim(file_get_contents($path));
+
+ if (Str::startsWith($key, 'base64:')) {
+ $key = base64_decode(substr($key, 7));
+ }
+
+ if (empty($key)) {
+ throw new \RuntimeException("Vault Value Key is invalid or empty.");
+ }
+
+ return $cachedKey = $key;
+ }
+}
+*/
diff --git a/src/vuexy-admin.module.php b/src/vuexy-admin.module.php
index 5acd5d7..210435f 100644
--- a/src/vuexy-admin.module.php
+++ b/src/vuexy-admin.module.php
@@ -3,24 +3,24 @@
declare(strict_types=1);
use Illuminate\Auth\Events\{Failed, Login, Logout};
-use Koneko\VuexyAdmin\Alication\Logger\KonekoSystemLogger;
-use Koneko\VuexyAdmin\Application\Cache\KonekoCacheManager;
+use Koneko\VuexyAdmin\Application\Cache\Manager\KonekoCacheManager;
+use Koneko\VuexyAdmin\Application\Config\Cast\VuexyLayoutCast;
use Koneko\VuexyAdmin\Application\Contracts\Loggers\{SecurityLoggerInterface, SystemLoggerInterface, UserInteractionLoggerInterface};
-use Koneko\VuexyAdmin\Application\Contracts\Settings\SettingsRepositoryInterface;
-use Koneko\VuexyAdmin\Application\Events\Settings\{SettingChanged, VuexyCustomizerSettingsUpdated};
+use Koneko\VuexyAdmin\Application\Settings\Contracts\SettingsRepositoryInterface;
+use Koneko\VuexyAdmin\Application\Events\Settings\VuexyCustomizerSettingsUpdated;
use Koneko\VuexyAdmin\Application\Helpers\{VuexyHelper, VuexyNotifyHelper, VuexyToastrHelper};
use Koneko\VuexyAdmin\Application\Http\Middleware\{AdminTemplateMiddleware, LocaleMiddleware, TrackSessionActivity};
+use Koneko\VuexyAdmin\Application\Jobs\Security\RotateVaultKeysJob;
use Koneko\VuexyAdmin\Application\Jobs\Users\ForceLogoutInactiveUsersJob;
use Koneko\VuexyAdmin\Application\Listeners\Authentication\{HandleFailedLogin, HandleUserLogin, HandleUserLogout};
-use Koneko\VuexyAdmin\Application\Listeners\Settings\{ApplyVuexyCustomizerSettings, SettingCacheListener};
-use Koneko\VuexyAdmin\Application\Logger\{KonekoSecurityAuditLogger, KonekoUserInteractionLogger};
-use Koneko\VuexyAdmin\Application\System\KonekoSettingManager;
+use Koneko\VuexyAdmin\Application\Listeners\Settings\ApplyVuexyCustomizerSettings;
+use Koneko\VuexyAdmin\Application\Loggers\{KonekoSecurityAuditLogger, KonekoUserInteractionLogger, KonekoSecurityLogger, KonekoSystemLogger};
+use Koneko\VuexyAdmin\Application\Settings\Manager\KonekoSettingManager;
use Koneko\VuexyAdmin\Application\UI\Livewire\Audit\LaravelLogs\LaravelLogsTable;
use Koneko\VuexyAdmin\Application\UI\Livewire\Audit\SecurityEvents\SecurityEventsTable;
use Koneko\VuexyAdmin\Application\UI\Livewire\Audit\UsersAuthLogs\UsersAuthLogsTable;
-use Koneko\VuexyAdmin\Application\UI\Livewire\Tools\Cache\{CacheFunctionsCard, CacheStatsCard, SessionStatsCard, MemcachedStatsCard, RedisStatsCard};
use Koneko\VuexyAdmin\Application\UI\Livewire\KonekoVuexy\ModuleManagement\ModuleManagementIndex;
-use Koneko\VuexyAdmin\Application\UI\Livewire\KonekoVuexy\Plugins\PluginsIndex;
+use Koneko\VuexyAdmin\Application\UI\Livewire\KonekoVuexy\Plugins\{PluginsIndex, VuexyQuicklinks};
use Koneko\VuexyAdmin\Application\UI\Livewire\Pages\Dashboards\MenuAccessCards;
use Koneko\VuexyAdmin\Application\UI\Livewire\Settings\EnvironmentVars\{EnvironmentVarsTable, EnvironmentVarsOffCanvasForm};
use Koneko\VuexyAdmin\Application\UI\Livewire\Settings\Rbac\Permissions\{PermissionsTable, PermissionOffCanvasForm};
@@ -29,15 +29,18 @@ use Koneko\VuexyAdmin\Application\UI\Livewire\Settings\Smtp\SmtpSettingsCard;
use Koneko\VuexyAdmin\Application\UI\Livewire\Settings\Users\{UsersTable, UsersCount, UserForm, UserOffCanvasForm};
use Koneko\VuexyAdmin\Application\UI\Livewire\Settings\VuexyInterface\VuexyInterfaceIndex;
use Koneko\VuexyAdmin\Application\UI\Livewire\Settings\WebInterface\{LogoOnLightBgCard, LogoOnDarkBgCard, AppDescriptionCard, AppFaviconCard};
+use Koneko\VuexyAdmin\Application\UI\Livewire\Tools\Cache\{CacheFunctionsCard, CacheStatsCard, SessionStatsCard, MemcachedStatsCard, RedisStatsCard};
use Koneko\VuexyAdmin\Application\UI\Livewire\User\Profile\{UpdateProfileInformationForm, UpdatePasswordForm, TwoFactorAuthenticationForm, LogoutOtherBrowser, DeleteUserForm};
-use Koneko\VuexyAdmin\Application\Cache\VuexyVarsBuilderService;
-use Koneko\VuexyAdmin\Application\Jobs\Security\RotateVaultKeysJob;
-use Koneko\VuexyAdmin\Application\UI\Livewire\KonekoVuexy\Plugins\VuexyQuicklinks;
use Koneko\VuexyAdmin\Application\UI\Livewire\User\Viewer\UserDetailsViewerIndex;
-use Koneko\VuexyAdmin\Console\Commands\{VuexyAvatarInitialsCommand, VuexyListCatalogsCommand, VuexyMenuBuildCommand, VuexyMenuListModulesCommand, VuexyRbacCommand, VuexySeedCommand};
+use Koneko\VuexyAdmin\Console\Commands\Geolocationg\DownloadGeoIpDatabase;
+use Koneko\VuexyAdmin\Console\Commands\Layout\VuexyMenuBuildCommand;
+use Koneko\VuexyAdmin\Console\Commands\Layout\VuexyMenuListModulesCommand;
+use Koneko\VuexyAdmin\Console\Commands\Notifications\VuexyDeviceTokenPruneCommand;
+use Koneko\VuexyAdmin\Console\Commands\Orquestator\VuexySeedCommand;
+use Koneko\VuexyAdmin\Console\Commands\RBAC\VuexyRbacCommand;
+use Koneko\VuexyAdmin\Console\Commands\UI\VuexyAvatarInitialsCommand;
use Koneko\VuexyAdmin\Models\{Setting, User};
use Koneko\VuexyAdmin\Providers\FortifyServiceProvider;
-use Koneko\VuexyAdmin\Support\Logger\KonekoSecurityLogger;
use Spatie\Permission\PermissionServiceProvider;
return [
@@ -58,9 +61,27 @@ return [
// ⚙️ Archivos de configuración del módulo
'configs' => [
- 'database' => 'config/keyvault_db.php',
- 'koneko' => 'config/koneko.php',
- 'koneko.admin' => 'config/koneko_admin.php',
+ 'auth' => 'config/auth.php',
+ 'fortify' => 'config/fortify.php',
+ 'image' => 'config/image.php',
+ 'koneko' => 'config/koneko.php',
+ 'koneko.core.layout' => 'config/koneko_layout.php',
+ 'koneko.core.ui' => 'config/koneko_ui.php',
+ 'koneko.core.logging' => 'config/koneko_logging.php',
+ 'koneko.core.security' => 'config/koneko_security.php',
+ 'koneko.core.key_vault' => 'config/koneko_key_vault.php',
+ 'database.connections.vault' => 'config/koneko_key_vault_db.php',
+ ],
+ // 📦 Configuraciones de bloques
+ 'configBlocks' => [
+ 'koneko.core.layout.vuexy' => [
+ 'component' => 'core',
+ 'group' => 'layout',
+ 'section' => 'vuexy',
+ 'sub_group' => 'customizer',
+ 'key_name' => 'vuexy-layout',
+ 'cast' => VuexyLayoutCast::class,
+ ],
],
// 🏭 Proveedores de servicio, Middleware y Aliases (runtime)
@@ -83,7 +104,7 @@ return [
'Singletons' => [
KonekoCacheManager::class,
KonekoSecurityAuditLogger::class,
- VuexyVarsBuilderService::class,
+ //KonekoAdminVarsBuilder::class,
],
// 🔗 Bindings de interfaces a servicios
@@ -101,8 +122,8 @@ return [
// 🔊 Eventos
'listeners' => [
- SettingChanged::class => SettingCacheListener::class,
- VuexyCustomizerSettingsUpdated::class => ApplyVuexyCustomizerSettings::class,
+ //SettingChanged::class => SettingCacheListener::class,
+ //VuexyCustomizerSettingsUpdated::class => ApplyVuexyCustomizerSettings::class,
Login::class => HandleUserLogin::class,
Logout::class => HandleUserLogout::class,
Failed::class => HandleFailedLogin::class,
@@ -237,12 +258,18 @@ return [
// 🛠 Comandos Artisan
'commands' => [
+ DownloadGeoIpDatabase::class,
VuexyAvatarInitialsCommand::class,
- VuexyRbacCommand::class,
- VuexySeedCommand::class,
+ VuexyDeviceTokenPruneCommand::class,
VuexyMenuBuildCommand::class,
VuexyMenuListModulesCommand::class,
- VuexyListCatalogsCommand::class,
+ VuexyRbacCommand::class,
+ VuexySeedCommand::class,
+ ],
+
+ // 📦 Scope Models
+ 'scopeModels' => [
+ 'user' => User::class,
],
// Trabajos programados