diff --git a/LICENSE b/LICENSE
index db65ed5f0..b4560ce3d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019-present, Ibrahim BinAlshikh and contributors.
+Copyright (c) 2019-present WebFiori Framework and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 86b3b82a8..e7b73b547 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,16 @@
WebFiori is a modular, object-oriented PHP framework designed for building secure web applications and APIs. It provides a complete toolkit — routing, middleware, authorization, database management, job queues, and more — while remaining lightweight and free of heavy external dependencies.
+## Motivation
+
+WebFiori is designed around two architectural decisions:
+
+1. **Self-contained ecosystem.** Each component (HTTP, database, cache, sessions, mail, queue, CLI, authorization) is an independent library with no external runtime dependencies. This eliminates transitive dependency conflicts and simplifies security auditing.
+
+2. **Interface-driven extensibility.** Storage backends for sessions, cache, queues, and authorization are defined by interfaces. Swap implementations without modifying application code.
+
+The framework provides the orchestration layer. The libraries can be used together as a full stack or independently in any PHP project.
+
## Requirements
- PHP 8.1 or later
diff --git a/WebFiori/Framework/Access.php b/WebFiori/Framework/Access.php
index 6eb2b8055..8570b4c81 100644
--- a/WebFiori/Framework/Access.php
+++ b/WebFiori/Framework/Access.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/AccessManager.php b/WebFiori/Framework/AccessManager.php
index 5d23d515d..78d46d954 100644
--- a/WebFiori/Framework/AccessManager.php
+++ b/WebFiori/Framework/AccessManager.php
@@ -153,6 +153,10 @@ public function can($user, string $permission, ?object $resource = null): bool {
$userId = is_object($user) && method_exists($user, 'getId') ? $user->getId() : $user;
$roles = $this->getUserRoles($userId);
+ if (empty($roles) && is_object($user) && method_exists($user, 'getRoles')) {
+ $roles = $user->getRoles();
+ }
+
$hasPermission = false;
foreach ($roles as $roleName) {
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 0283f4337..836f6c2e4 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
@@ -375,6 +375,9 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\FreshMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\SkipMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\StepMigrationsCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\ServicesListCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\RoutesCacheCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\RoutesClearCommand',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/AppBootstrapper.php b/WebFiori/Framework/AppBootstrapper.php
index fa5154c58..d74943d70 100644
--- a/WebFiori/Framework/AppBootstrapper.php
+++ b/WebFiori/Framework/AppBootstrapper.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Autoload/ClassInfo.php b/WebFiori/Framework/Autoload/ClassInfo.php
index b952a79ed..83af83eb5 100644
--- a/WebFiori/Framework/Autoload/ClassInfo.php
+++ b/WebFiori/Framework/Autoload/ClassInfo.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2024 Ibrahim BinAlshikh
+ * Copyright (c) 2024-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Autoload/ClassLoader.php b/WebFiori/Framework/Autoload/ClassLoader.php
index 1567de78f..2f719f6fd 100644
--- a/WebFiori/Framework/Autoload/ClassLoader.php
+++ b/WebFiori/Framework/Autoload/ClassLoader.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Autoload/ClassLoaderException.php b/WebFiori/Framework/Autoload/ClassLoaderException.php
index f45a27a37..cd442e5f7 100644
--- a/WebFiori/Framework/Autoload/ClassLoaderException.php
+++ b/WebFiori/Framework/Autoload/ClassLoaderException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/ClassRegistrar.php b/WebFiori/Framework/ClassRegistrar.php
index ba1bb3a5e..26d74fdb6 100644
--- a/WebFiori/Framework/ClassRegistrar.php
+++ b/WebFiori/Framework/ClassRegistrar.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/CLITestCase.php b/WebFiori/Framework/Cli/CLITestCase.php
index 391e96d8f..8601ae631 100644
--- a/WebFiori/Framework/Cli/CLITestCase.php
+++ b/WebFiori/Framework/Cli/CLITestCase.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2024 Ibrahim BinAlshikh
+ * Copyright (c) 2024-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
index 789e65058..e340e6115 100644
--- a/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
index 9eef592db..10a675f0c 100644
--- a/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php
index 9adcd3c0f..2d38e1c0e 100644
--- a/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php
+++ b/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php
index 30620fa8b..d3711ef00 100644
--- a/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php
+++ b/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
index c444eee51..07c5ff5e0 100644
--- a/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
+++ b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php
index f0b8883f9..f225c05e2 100644
--- a/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php
+++ b/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/RoutesCacheCommand.php b/WebFiori/Framework/Cli/Commands/RoutesCacheCommand.php
new file mode 100644
index 000000000..af6a05069
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/RoutesCacheCommand.php
@@ -0,0 +1,48 @@
+createRouteCache();
+ $cache->setEnabled(true);
+ $count = $cache->build();
+ $this->success("Route cache built: $count route(s) cached.");
+
+ return 0;
+ }
+
+ private function createRouteCache(): RouteCache {
+ $storagePath = APP_PATH . 'Storage';
+
+ if (!is_dir($storagePath)) {
+ mkdir($storagePath, 0755, true);
+ }
+
+ return new RouteCache(new Cache(new FileStorage($storagePath)), true);
+ }
+}
diff --git a/WebFiori/Framework/Cli/Commands/RoutesClearCommand.php b/WebFiori/Framework/Cli/Commands/RoutesClearCommand.php
new file mode 100644
index 000000000..9a226f3ee
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/RoutesClearCommand.php
@@ -0,0 +1,42 @@
+createRouteCache();
+ $cache->clear();
+ $this->success('Route cache cleared.');
+
+ return 0;
+ }
+
+ private function createRouteCache(): RouteCache {
+ $storagePath = APP_PATH . 'Storage';
+
+ return new RouteCache(new Cache(new FileStorage($storagePath)), true);
+ }
+}
diff --git a/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
index e935eafb8..ff9e97d66 100644
--- a/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
+++ b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/SchedulerCommand.php b/WebFiori/Framework/Cli/Commands/SchedulerCommand.php
index 1aab73bd2..e6147b8b6 100644
--- a/WebFiori/Framework/Cli/Commands/SchedulerCommand.php
+++ b/WebFiori/Framework/Cli/Commands/SchedulerCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/ServicesListCommand.php b/WebFiori/Framework/Cli/Commands/ServicesListCommand.php
new file mode 100644
index 000000000..09b4d84b0
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/ServicesListCommand.php
@@ -0,0 +1,55 @@
+info('No services discovered. Use ServiceRouter::discover() to register services.');
+
+ return 0;
+ }
+
+ $this->println('');
+ $this->println(sprintf(' %-15s %-45s %-10s %s', 'Name', 'Class', 'Type', 'Path'));
+ $this->println(str_repeat('-', 90));
+
+ foreach ($discovered as $name => $entry) {
+ $this->println(sprintf(
+ ' %-15s %-45s %-10s %s',
+ $name,
+ $entry['class'],
+ $entry['type'],
+ $entry['path']
+ ));
+ }
+
+ $this->println('');
+ $this->info('Total: ' . count($discovered) . ' service(s).');
+
+ return 0;
+ }
+}
diff --git a/WebFiori/Framework/Cli/Commands/StepMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/StepMigrationsCommand.php
index 2b078d124..0fcd706bf 100644
--- a/WebFiori/Framework/Cli/Commands/StepMigrationsCommand.php
+++ b/WebFiori/Framework/Cli/Commands/StepMigrationsCommand.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2026 Ibrahim BinAlshikh
+ * Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/VersionCommand.php b/WebFiori/Framework/Cli/Commands/VersionCommand.php
index d60a80574..aeb92cddf 100644
--- a/WebFiori/Framework/Cli/Commands/VersionCommand.php
+++ b/WebFiori/Framework/Cli/Commands/VersionCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Commands/WHelpCommand.php b/WebFiori/Framework/Cli/Commands/WHelpCommand.php
index f91d8a1d2..d64a76484 100644
--- a/WebFiori/Framework/Cli/Commands/WHelpCommand.php
+++ b/WebFiori/Framework/Cli/Commands/WHelpCommand.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Cli/Helpers/ClassInfoReader.php b/WebFiori/Framework/Cli/Helpers/ClassInfoReader.php
index 4bfa58dd8..41626db93 100644
--- a/WebFiori/Framework/Cli/Helpers/ClassInfoReader.php
+++ b/WebFiori/Framework/Cli/Helpers/ClassInfoReader.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/DB.php b/WebFiori/Framework/DB.php
index edbc216c7..51425e3a4 100644
--- a/WebFiori/Framework/DB.php
+++ b/WebFiori/Framework/DB.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/EmailMessage.php b/WebFiori/Framework/EmailMessage.php
index 9e50b9835..e6bffef47 100644
--- a/WebFiori/Framework/EmailMessage.php
+++ b/WebFiori/Framework/EmailMessage.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/ArrayIndexOutOfBoundsException.php b/WebFiori/Framework/Exceptions/ArrayIndexOutOfBoundsException.php
index 3251befa0..55566fb90 100644
--- a/WebFiori/Framework/Exceptions/ArrayIndexOutOfBoundsException.php
+++ b/WebFiori/Framework/Exceptions/ArrayIndexOutOfBoundsException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/InitializationException.php b/WebFiori/Framework/Exceptions/InitializationException.php
index 143f95c97..5327a5afa 100644
--- a/WebFiori/Framework/Exceptions/InitializationException.php
+++ b/WebFiori/Framework/Exceptions/InitializationException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/InvalidCRONExprException.php b/WebFiori/Framework/Exceptions/InvalidCRONExprException.php
index 95eac152a..fa9a8b342 100644
--- a/WebFiori/Framework/Exceptions/InvalidCRONExprException.php
+++ b/WebFiori/Framework/Exceptions/InvalidCRONExprException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/MissingLangException.php b/WebFiori/Framework/Exceptions/MissingLangException.php
index 50d95c9f0..82910ad87 100644
--- a/WebFiori/Framework/Exceptions/MissingLangException.php
+++ b/WebFiori/Framework/Exceptions/MissingLangException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/NoSuchThemeException.php b/WebFiori/Framework/Exceptions/NoSuchThemeException.php
index c65e00bec..4cd946ee7 100644
--- a/WebFiori/Framework/Exceptions/NoSuchThemeException.php
+++ b/WebFiori/Framework/Exceptions/NoSuchThemeException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/RoutingException.php b/WebFiori/Framework/Exceptions/RoutingException.php
index ff6b240ef..1e4078a19 100644
--- a/WebFiori/Framework/Exceptions/RoutingException.php
+++ b/WebFiori/Framework/Exceptions/RoutingException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/SessionException.php b/WebFiori/Framework/Exceptions/SessionException.php
index f576f0ad1..22608089b 100644
--- a/WebFiori/Framework/Exceptions/SessionException.php
+++ b/WebFiori/Framework/Exceptions/SessionException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Exceptions/UIException.php b/WebFiori/Framework/Exceptions/UIException.php
index a21f14e2b..f1b7219f5 100644
--- a/WebFiori/Framework/Exceptions/UIException.php
+++ b/WebFiori/Framework/Exceptions/UIException.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/ExtendedWebServicesManager.php b/WebFiori/Framework/ExtendedWebServicesManager.php
index 9d5d3ee12..40f99c521 100644
--- a/WebFiori/Framework/ExtendedWebServicesManager.php
+++ b/WebFiori/Framework/ExtendedWebServicesManager.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Handlers/APICallErrHandler.php b/WebFiori/Framework/Handlers/APICallErrHandler.php
index 5e2a272ff..3cfd9da5b 100644
--- a/WebFiori/Framework/Handlers/APICallErrHandler.php
+++ b/WebFiori/Framework/Handlers/APICallErrHandler.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2022 Ibrahim BinAlshikh
+ * Copyright (c) 2022-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Handlers/CLIErrHandler.php b/WebFiori/Framework/Handlers/CLIErrHandler.php
index 8014afbe6..f4464ed20 100644
--- a/WebFiori/Framework/Handlers/CLIErrHandler.php
+++ b/WebFiori/Framework/Handlers/CLIErrHandler.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2022 Ibrahim BinAlshikh
+ * Copyright (c) 2022-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Handlers/HTTPErrHandler.php b/WebFiori/Framework/Handlers/HTTPErrHandler.php
index be8ed9efd..036e383b0 100644
--- a/WebFiori/Framework/Handlers/HTTPErrHandler.php
+++ b/WebFiori/Framework/Handlers/HTTPErrHandler.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2022 Ibrahim BinAlshikh
+ * Copyright (c) 2022-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ini.php b/WebFiori/Framework/Ini.php
index 9ea8101ec..d6141f2c3 100644
--- a/WebFiori/Framework/Ini.php
+++ b/WebFiori/Framework/Ini.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2023 Ibrahim BinAlshikh
+ * Copyright (c) 2023-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Lang.php b/WebFiori/Framework/Lang.php
index 40d32ba57..d192e808f 100644
--- a/WebFiori/Framework/Lang.php
+++ b/WebFiori/Framework/Lang.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Middleware/AbstractMiddleware.php b/WebFiori/Framework/Middleware/AbstractMiddleware.php
index 940e18349..4118bae11 100644
--- a/WebFiori/Framework/Middleware/AbstractMiddleware.php
+++ b/WebFiori/Framework/Middleware/AbstractMiddleware.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Middleware/MiddlewareManager.php b/WebFiori/Framework/Middleware/MiddlewareManager.php
index e81af1e26..94f8e525a 100644
--- a/WebFiori/Framework/Middleware/MiddlewareManager.php
+++ b/WebFiori/Framework/Middleware/MiddlewareManager.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Middleware/MiddlewareRegistry.php b/WebFiori/Framework/Middleware/MiddlewareRegistry.php
index 55ea01889..67e59882e 100644
--- a/WebFiori/Framework/Middleware/MiddlewareRegistry.php
+++ b/WebFiori/Framework/Middleware/MiddlewareRegistry.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Privilege.php b/WebFiori/Framework/Privilege.php
index e603e8f67..696bb159b 100644
--- a/WebFiori/Framework/Privilege.php
+++ b/WebFiori/Framework/Privilege.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/PrivilegesGroup.php b/WebFiori/Framework/PrivilegesGroup.php
index d63a5eab5..38b92b851 100644
--- a/WebFiori/Framework/PrivilegesGroup.php
+++ b/WebFiori/Framework/PrivilegesGroup.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Router/RouteBuilder.php b/WebFiori/Framework/Router/RouteBuilder.php
index f7129869d..a262066b9 100644
--- a/WebFiori/Framework/Router/RouteBuilder.php
+++ b/WebFiori/Framework/Router/RouteBuilder.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Router/RouteCache.php b/WebFiori/Framework/Router/RouteCache.php
new file mode 100644
index 000000000..d369f9910
--- /dev/null
+++ b/WebFiori/Framework/Router/RouteCache.php
@@ -0,0 +1,150 @@
+cache = $cache;
+ $this->enabled = $enabled;
+ $this->cacheKey = $cacheKey;
+ }
+
+ /**
+ * Returns whether route caching is enabled.
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool {
+ return $this->enabled;
+ }
+
+ /**
+ * Sets whether route caching is enabled.
+ *
+ * @param bool $enabled
+ */
+ public function setEnabled(bool $enabled): void {
+ $this->enabled = $enabled;
+ }
+
+ /**
+ * Checks if a route cache exists.
+ *
+ * @return bool
+ */
+ public function isCached(): bool {
+ return $this->cache->get($this->cacheKey) !== null;
+ }
+
+ /**
+ * Load cached routes into the Router.
+ *
+ * @return bool True if cache was loaded, false if no cache exists.
+ */
+ public function load(): bool {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ $data = $this->cache->get($this->cacheKey);
+
+ if ($data === null) {
+ return false;
+ }
+
+ $discovered = $data['discovered'] ?? [];
+
+ if (!empty($discovered)) {
+ ServiceRouter::setDiscovered($discovered);
+ }
+
+ // Re-register routes from the cached service map
+ $configs = $data['configs'] ?? [];
+
+ foreach ($configs as $config) {
+ ServiceRouter::discover(
+ $config['namespace'],
+ $config['basePath'],
+ $config['options'] ?? [],
+ $config['directory'] ?? null,
+ $config['recursive'] ?? false
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Build route cache from current ServiceRouter state.
+ *
+ * @param array $discoverConfigs Array of discover() call configs to replay on load.
+ *
+ * @return int Number of discovered services cached.
+ */
+ public function build(array $discoverConfigs = []): int {
+ $discovered = ServiceRouter::getDiscovered();
+
+ $data = [
+ 'discovered' => $discovered,
+ 'configs' => $discoverConfigs,
+ 'built_at' => date('c'),
+ 'total' => count($discovered),
+ ];
+
+ $this->cache->set($this->cacheKey, $data, 31536000, true);
+
+ return count($discovered);
+ }
+
+ /**
+ * Clear the route cache.
+ */
+ public function clear(): void {
+ $this->cache->delete($this->cacheKey);
+ }
+
+ /**
+ * Returns the cache key.
+ *
+ * @return string
+ */
+ public function getCacheKey(): string {
+ return $this->cacheKey;
+ }
+}
diff --git a/WebFiori/Framework/Router/RouteDispatcher.php b/WebFiori/Framework/Router/RouteDispatcher.php
index 0d902d66f..f07751300 100644
--- a/WebFiori/Framework/Router/RouteDispatcher.php
+++ b/WebFiori/Framework/Router/RouteDispatcher.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Router/RouteMatcher.php b/WebFiori/Framework/Router/RouteMatcher.php
index d007c0c6f..57f9cb5f1 100644
--- a/WebFiori/Framework/Router/RouteMatcher.php
+++ b/WebFiori/Framework/Router/RouteMatcher.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Router/RouteOption.php b/WebFiori/Framework/Router/RouteOption.php
index ef8dc0280..3c1be445d 100644
--- a/WebFiori/Framework/Router/RouteOption.php
+++ b/WebFiori/Framework/Router/RouteOption.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
@@ -72,4 +72,8 @@ class RouteOption {
* An option which is used to set an array of allowed values to route parameters.
*/
const VALUES = 'vars-values';
+ /**
+ * An option to specify a namespace for dynamic service resolution.
+ */
+ const NS = 'namespace';
}
diff --git a/WebFiori/Framework/Router/Router.php b/WebFiori/Framework/Router/Router.php
index 28c54f95e..25993c056 100644
--- a/WebFiori/Framework/Router/Router.php
+++ b/WebFiori/Framework/Router/Router.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Router/RouterUri.php b/WebFiori/Framework/Router/RouterUri.php
index 36efa5162..9eb5ac02d 100644
--- a/WebFiori/Framework/Router/RouterUri.php
+++ b/WebFiori/Framework/Router/RouterUri.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
@@ -299,7 +299,8 @@ public function getLanguages() : array {
*/
public function getMiddleware() : array {
if (count($this->assignedMiddlewareList) != count($this->sortedMiddleeareList)) {
- $this->sortedMiddleeareList = $this->sortByDependencies($this->assignedMiddlewareList);
+ $resolved = $this->resolveDependencies($this->assignedMiddlewareList);
+ $this->sortedMiddleeareList = $this->sortByDependencies($resolved);
}
return $this->sortedMiddleeareList;
@@ -624,6 +625,43 @@ private function compareUriPathHelper(Uri $requestedUri): bool {
* @param array $middlewareList Array of AbstractMiddleware instances.
*
* @return array Sorted array.
+ /**
+ * Resolves transitive dependencies by pulling missing middleware from the registry.
+ *
+ * @param array $middlewareList The initially assigned middleware.
+ *
+ * @return array The expanded list including all transitive dependencies.
+ */
+ private function resolveDependencies(array $middlewareList): array {
+ $byName = [];
+
+ foreach ($middlewareList as $mw) {
+ $byName[$mw->getName()] = $mw;
+ }
+
+ $queue = $middlewareList;
+
+ while (!empty($queue)) {
+ $current = array_shift($queue);
+
+ foreach ($current->getDependencies() as $depName) {
+ if (isset($byName[$depName])) {
+ continue;
+ }
+
+ $dep = MiddlewareManager::getMiddleware($depName);
+
+ if ($dep !== null) {
+ $byName[$depName] = $dep;
+ $queue[] = $dep;
+ }
+ }
+ }
+
+ return array_values($byName);
+ }
+ /**
+ * Sorts middleware using topological sort (Kahn's algorithm).
*
* @throws RoutingException If circular dependency detected.
*/
diff --git a/WebFiori/Framework/Router/ServiceRouter.php b/WebFiori/Framework/Router/ServiceRouter.php
new file mode 100644
index 000000000..0c5caac19
--- /dev/null
+++ b/WebFiori/Framework/Router/ServiceRouter.php
@@ -0,0 +1,310 @@
+ $entry) {
+ $class = $entry['class'];
+ $type = $entry['type'];
+ $path = $basePath . '/' . $name;
+
+ $options = array_merge($routeOptions, [
+ RouteOption::PATH => $path,
+ ]);
+
+ if ($type === 'manager') {
+ $options[RouteOption::TO] = $class;
+ } else {
+ $options[RouteOption::TO] = self::createServiceClosure($class);
+ }
+
+ Router::api($options);
+ self::$discovered[$name] = [
+ 'class' => $class,
+ 'type' => $type,
+ 'path' => $path,
+ ];
+ $count++;
+ }
+
+ return $count;
+ }
+
+ /**
+ * Returns all discovered services.
+ *
+ * @return array Associative array keyed by name with class, type, and path.
+ */
+ public static function getDiscovered(): array {
+ return self::$discovered;
+ }
+
+ /**
+ * Reset discovered services.
+ */
+ public static function reset(): void {
+ self::$discovered = [];
+ }
+
+ /**
+ * Set discovered services from cache.
+ *
+ * @param array $discovered The cached discovered map.
+ */
+ public static function setDiscovered(array $discovered): void {
+ self::$discovered = $discovered;
+ }
+
+ /**
+ * Register a dynamic namespace route that resolves services at request time.
+ *
+ * Usage:
+ * ```php
+ * ServiceRouter::dynamic('App\\Apis', '/apis/{controller}', [...options]);
+ * ```
+ *
+ * @param string $namespace Namespace to search for services.
+ * @param string $path Route path with {controller} parameter.
+ * @param array $routeOptions Shared route options (middleware, etc.).
+ * @param string|null $directory Optional directory override.
+ */
+ public static function dynamic(string $namespace, string $path, array $routeOptions = [], ?string $directory = null): void {
+ $options = array_merge($routeOptions, [
+ RouteOption::PATH => $path,
+ RouteOption::TO => function () use ($namespace, $directory) {
+ $controllerName = App::getRequest()->getParam('controller');
+
+ if ($controllerName === null) {
+ $controllerName = $_GET['controller'] ?? null;
+ }
+
+ if ($controllerName === null) {
+ App::getResponse()->setCode(404);
+ App::getResponse()->write(json_encode([
+ 'message' => 'Controller parameter missing.',
+ 'type' => 'error'
+ ]));
+
+ return;
+ }
+
+ self::handle($controllerName, $namespace, $directory);
+ },
+ ]);
+
+ Router::api($options);
+ }
+
+ /**
+ * Handle a dynamic controller request.
+ *
+ * @param string $controllerName The controller name from the URL.
+ * @param string $namespace Namespace to search.
+ * @param string|null $directory Optional directory to scan.
+ */
+ public static function handle(string $controllerName, string $namespace, ?string $directory = null): void {
+ $dir = $directory ?? self::namespaceToPath($namespace);
+ $map = self::scanNamespace($namespace, $dir);
+
+ if (isset($map[$controllerName])) {
+ $entry = $map[$controllerName];
+
+ if ($entry['type'] === 'manager') {
+ $class = $entry['class'];
+ $manager = new $class();
+ $manager->process();
+ } else {
+ $class = $entry['class'];
+ $service = ContainerFacade::has($class)
+ ? ContainerFacade::make($class)
+ : new $class();
+ (new RequestProcessor())->process($service, App::getRequest());
+ }
+ } else {
+ App::getResponse()->setCode(404);
+ App::getResponse()->write(json_encode([
+ 'message' => 'Service not found.',
+ 'type' => 'error'
+ ]));
+ }
+ }
+
+ /**
+ * Scan a namespace directory for routable classes.
+ *
+ * @param string $namespace The namespace to scan.
+ * @param string $dir The directory to scan.
+ * @param string $pathPrefix Relative path prefix for recursive scanning (kebab-cased).
+ * @param bool $recursive Whether to scan subdirectories.
+ *
+ * @return array Map of name => ['class' => FQCN, 'type' => 'service'|'manager']
+ */
+ private static function scanNamespace(string $namespace, string $dir, string $pathPrefix = '', bool $recursive = false): array {
+ $map = [];
+
+ if (!is_dir($dir)) {
+ return $map;
+ }
+
+ $files = glob($dir . DIRECTORY_SEPARATOR . '*.php');
+
+ if ($files === false) {
+ return $map;
+ }
+
+ foreach ($files as $file) {
+ $className = basename($file, '.php');
+ $fqcn = $namespace . '\\' . $className;
+
+ if (!class_exists($fqcn)) {
+ continue;
+ }
+
+ $ref = new ReflectionClass($fqcn);
+
+ if ($ref->isAbstract() || $ref->isInterface()) {
+ continue;
+ }
+
+ // Priority 1: #[RestController] attribute
+ $attrs = $ref->getAttributes(RestController::class);
+
+ if (!empty($attrs)) {
+ $attr = $attrs[0]->newInstance();
+
+ if (!empty($attr->name)) {
+ if (str_contains($attr->name, '/')) {
+ continue; // Invalid: name must not contain slashes
+ }
+
+ $name = $attr->name;
+ } else {
+ $name = $pathPrefix . self::deriveNameFromClass($className);
+ }
+
+ $map[$name] = ['class' => $fqcn, 'type' => 'service'];
+
+ continue;
+ }
+
+ // Priority 2: WebServicesManager subclass
+ if ($ref->isSubclassOf(WebServicesManager::class)) {
+ $name = $pathPrefix . self::deriveNameFromClass($className);
+ $map[$name] = ['class' => $fqcn, 'type' => 'manager'];
+
+ continue;
+ }
+
+ // Priority 3: WebService subclass without attribute
+ if ($ref->isSubclassOf(WebService::class)) {
+ $instance = new $fqcn();
+ $svcName = $instance->getName();
+
+ if (!empty($svcName)) {
+ $name = $pathPrefix . $svcName;
+ $map[$name] = ['class' => $fqcn, 'type' => 'service'];
+ }
+ }
+ }
+
+ // Recursive: scan subdirectories
+ if ($recursive) {
+ $subdirs = glob($dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR);
+
+ if ($subdirs !== false) {
+ foreach ($subdirs as $subdir) {
+ $subDirName = basename($subdir);
+ $subNamespace = $namespace . '\\' . $subDirName;
+ $subPrefix = $pathPrefix . CaseConverter::toKebabCase($subDirName, 'lower') . '/';
+ $subMap = self::scanNamespace($subNamespace, $subdir, $subPrefix, true);
+ $map = array_merge($map, $subMap);
+ }
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * Convert namespace to filesystem path.
+ */
+ private static function namespaceToPath(string $namespace): string {
+ return ROOT_PATH . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $namespace);
+ }
+
+ /**
+ * Derive route name from class name using kebab-case.
+ * OrderService → order, UserProfileService → user-profile
+ */
+ private static function deriveNameFromClass(string $className): string {
+ $name = preg_replace('/(Service|Manager|Controller)$/', '', $className);
+
+ if (empty($name)) {
+ $name = $className;
+ }
+
+ return CaseConverter::toKebabCase($name, 'lower');
+ }
+
+ /**
+ * Create a closure that instantiates and processes a service.
+ */
+ private static function createServiceClosure(string $class): callable {
+ return function () use ($class) {
+ $service = ContainerFacade::has($class)
+ ? ContainerFacade::make($class)
+ : new $class();
+ (new RequestProcessor())->process($service, App::getRequest());
+ };
+ }
+}
diff --git a/WebFiori/Framework/Scheduler/AbstractTask.php b/WebFiori/Framework/Scheduler/AbstractTask.php
index 0cb0ad4ca..4fedcd463 100644
--- a/WebFiori/Framework/Scheduler/AbstractTask.php
+++ b/WebFiori/Framework/Scheduler/AbstractTask.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Scheduler/BaseTask.php b/WebFiori/Framework/Scheduler/BaseTask.php
index f9ce96f5f..1f203b06e 100644
--- a/WebFiori/Framework/Scheduler/BaseTask.php
+++ b/WebFiori/Framework/Scheduler/BaseTask.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2018 Ibrahim BinAlshikh
+ * Copyright (c) 2018-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Scheduler/TaskArgument.php b/WebFiori/Framework/Scheduler/TaskArgument.php
index 5cef347cc..4e31650aa 100644
--- a/WebFiori/Framework/Scheduler/TaskArgument.php
+++ b/WebFiori/Framework/Scheduler/TaskArgument.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Scheduler/TasksManager.php b/WebFiori/Framework/Scheduler/TasksManager.php
index 72a14f4cb..766ddcc96 100644
--- a/WebFiori/Framework/Scheduler/TasksManager.php
+++ b/WebFiori/Framework/Scheduler/TasksManager.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2018 Ibrahim BinAlshikh
+ * Copyright (c) 2018-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/CacheSessionStorage.php b/WebFiori/Framework/Session/CacheSessionStorage.php
new file mode 100644
index 000000000..c9b15c7a1
--- /dev/null
+++ b/WebFiori/Framework/Session/CacheSessionStorage.php
@@ -0,0 +1,126 @@
+storage = $cacheStorage;
+ $this->prefix = $prefix;
+ $this->ttl = $ttl;
+ }
+
+ /**
+ * Returns the cache storage backend.
+ *
+ * @return Storage
+ */
+ public function getStorage(): Storage {
+ return $this->storage;
+ }
+
+ /**
+ * Returns the key prefix.
+ *
+ * @return string
+ */
+ public function getPrefix(): string {
+ return $this->prefix;
+ }
+
+ /**
+ * Returns the TTL in seconds.
+ *
+ * @return int
+ */
+ public function getTTL(): int {
+ return $this->ttl;
+ }
+
+ /**
+ * Sets the TTL for session entries.
+ *
+ * @param int $ttl Time-to-live in seconds.
+ */
+ public function setTTL(int $ttl): void {
+ $this->ttl = $ttl;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function gc(string $olderThan, int $maxCount = 0) {
+ $this->storage->purgeExpired();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function read(string $sessionId) {
+ $data = $this->storage->read($this->prefix . $sessionId, null);
+
+ if ($data === null) {
+ return null;
+ }
+
+ return $data;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove(string $sessionId) {
+ $this->storage->delete($this->prefix . $sessionId);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function save(string $sessionId, string $serializedSession) {
+ $item = new Item($this->prefix . $sessionId, $serializedSession, $this->ttl);
+ $secConfig = new SecurityConfig();
+ $secConfig->setEncryptionEnabled(false);
+ $item->setSecurityConfig($secConfig);
+ $this->storage->store($item);
+ }
+}
diff --git a/WebFiori/Framework/Session/DatabaseSessionStorage.php b/WebFiori/Framework/Session/DatabaseSessionStorage.php
index 6e017dc5b..33bd5bef8 100644
--- a/WebFiori/Framework/Session/DatabaseSessionStorage.php
+++ b/WebFiori/Framework/Session/DatabaseSessionStorage.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/DefaultSessionStorage.php b/WebFiori/Framework/Session/DefaultSessionStorage.php
index 53c0aad8d..b9d36bf2a 100644
--- a/WebFiori/Framework/Session/DefaultSessionStorage.php
+++ b/WebFiori/Framework/Session/DefaultSessionStorage.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/Session.php b/WebFiori/Framework/Session/Session.php
index 89b416f71..1197a708c 100644
--- a/WebFiori/Framework/Session/Session.php
+++ b/WebFiori/Framework/Session/Session.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionDB.php b/WebFiori/Framework/Session/SessionDB.php
index 055028acd..ffe6e67f2 100644
--- a/WebFiori/Framework/Session/SessionDB.php
+++ b/WebFiori/Framework/Session/SessionDB.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionManager.php b/WebFiori/Framework/Session/SessionManager.php
index 726df6b59..0e457733e 100644
--- a/WebFiori/Framework/Session/SessionManager.php
+++ b/WebFiori/Framework/Session/SessionManager.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionOption.php b/WebFiori/Framework/Session/SessionOption.php
index a7953a634..343bd06c8 100644
--- a/WebFiori/Framework/Session/SessionOption.php
+++ b/WebFiori/Framework/Session/SessionOption.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2024 Ibrahim BinAlshikh
+ * Copyright (c) 2024-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionSchema.php b/WebFiori/Framework/Session/SessionSchema.php
index f340fefe4..acdcf8c57 100644
--- a/WebFiori/Framework/Session/SessionSchema.php
+++ b/WebFiori/Framework/Session/SessionSchema.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionStatus.php b/WebFiori/Framework/Session/SessionStatus.php
index bb4a840f4..63a5bd6c3 100644
--- a/WebFiori/Framework/Session/SessionStatus.php
+++ b/WebFiori/Framework/Session/SessionStatus.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2023 Ibrahim BinAlshikh
+ * Copyright (c) 2023-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionStorage.php b/WebFiori/Framework/Session/SessionStorage.php
index 7e8e428f8..8a6a05a1f 100644
--- a/WebFiori/Framework/Session/SessionStorage.php
+++ b/WebFiori/Framework/Session/SessionStorage.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionUser.php b/WebFiori/Framework/Session/SessionUser.php
index 198a95665..f5f4a5375 100644
--- a/WebFiori/Framework/Session/SessionUser.php
+++ b/WebFiori/Framework/Session/SessionUser.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Session/SessionsManager.php b/WebFiori/Framework/Session/SessionsManager.php
index 14616478d..709089c2b 100644
--- a/WebFiori/Framework/Session/SessionsManager.php
+++ b/WebFiori/Framework/Session/SessionsManager.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Theme.php b/WebFiori/Framework/Theme.php
index a5dafc576..14708f67a 100644
--- a/WebFiori/Framework/Theme.php
+++ b/WebFiori/Framework/Theme.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
@@ -525,7 +525,7 @@ public function setAfterLoaded(callable $function, array $params = []) {
/**
* Sets the name of theme author.
*
- * @param string $author The name of theme author (such as 'Ibrahim BinAlshikh').
+ * @param string $author The name of theme author (such as '-present WebFiori Framework').
*
* @since 1.0
*/
diff --git a/WebFiori/Framework/ThemeManager.php b/WebFiori/Framework/ThemeManager.php
index 527208bd8..42053f103 100644
--- a/WebFiori/Framework/ThemeManager.php
+++ b/WebFiori/Framework/ThemeManager.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ui/BeforeRenderCallback.php b/WebFiori/Framework/Ui/BeforeRenderCallback.php
index 059c34066..bf43034b5 100644
--- a/WebFiori/Framework/Ui/BeforeRenderCallback.php
+++ b/WebFiori/Framework/Ui/BeforeRenderCallback.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2022 Ibrahim BinAlshikh
+ * Copyright (c) 2022-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ui/HTTPCodeView.php b/WebFiori/Framework/Ui/HTTPCodeView.php
index 6c3e8c989..226821ba4 100644
--- a/WebFiori/Framework/Ui/HTTPCodeView.php
+++ b/WebFiori/Framework/Ui/HTTPCodeView.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ui/ServerErrPage/ServerErrPage.php b/WebFiori/Framework/Ui/ServerErrPage/ServerErrPage.php
index 3f4525e25..f87564572 100644
--- a/WebFiori/Framework/Ui/ServerErrPage/ServerErrPage.php
+++ b/WebFiori/Framework/Ui/ServerErrPage/ServerErrPage.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2023 Ibrahim BinAlshikh
+ * Copyright (c) 2023-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ui/StarterPage.php b/WebFiori/Framework/Ui/StarterPage.php
index ab4173613..d25d3d83b 100644
--- a/WebFiori/Framework/Ui/StarterPage.php
+++ b/WebFiori/Framework/Ui/StarterPage.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ui/WebPage.php b/WebFiori/Framework/Ui/WebPage.php
index 3433c1afd..21d0f4057 100644
--- a/WebFiori/Framework/Ui/WebPage.php
+++ b/WebFiori/Framework/Ui/WebPage.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Ui/ui-functions.php b/WebFiori/Framework/Ui/ui-functions.php
index f284a8d36..92ddba72a 100644
--- a/WebFiori/Framework/Ui/ui-functions.php
+++ b/WebFiori/Framework/Ui/ui-functions.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2023 Ibrahim BinAlshikh
+ * Copyright (c) 2023-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/User.php b/WebFiori/Framework/User.php
index ca3f579fd..c3044b4d9 100644
--- a/WebFiori/Framework/User.php
+++ b/WebFiori/Framework/User.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2019 Ibrahim BinAlshikh
+ * Copyright (c) 2019-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/AttributeTableWriter.php b/WebFiori/Framework/Writers/AttributeTableWriter.php
index e14ed4b7c..dc86b8cac 100644
--- a/WebFiori/Framework/Writers/AttributeTableWriter.php
+++ b/WebFiori/Framework/Writers/AttributeTableWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2026 Ibrahim BinAlshikh
+ * Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/CommandClassWriter.php b/WebFiori/Framework/Writers/CommandClassWriter.php
index 75d2a3b3e..44dfdeae8 100644
--- a/WebFiori/Framework/Writers/CommandClassWriter.php
+++ b/WebFiori/Framework/Writers/CommandClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/DocblockBuilder.php b/WebFiori/Framework/Writers/DocblockBuilder.php
index d763b7abe..73d957e7a 100644
--- a/WebFiori/Framework/Writers/DocblockBuilder.php
+++ b/WebFiori/Framework/Writers/DocblockBuilder.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2026 Ibrahim BinAlshikh
+ * Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/DomainEntityWriter.php b/WebFiori/Framework/Writers/DomainEntityWriter.php
index 346b10d0a..9b9ad233a 100644
--- a/WebFiori/Framework/Writers/DomainEntityWriter.php
+++ b/WebFiori/Framework/Writers/DomainEntityWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2026 Ibrahim BinAlshikh
+ * Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/LangClassWriter.php b/WebFiori/Framework/Writers/LangClassWriter.php
index eaa477138..76d7f7ae1 100644
--- a/WebFiori/Framework/Writers/LangClassWriter.php
+++ b/WebFiori/Framework/Writers/LangClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2021 Ibrahim BinAlshikh
+ * Copyright (c) 2021-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/MiddlewareClassWriter.php b/WebFiori/Framework/Writers/MiddlewareClassWriter.php
index d615f1331..17becc68f 100644
--- a/WebFiori/Framework/Writers/MiddlewareClassWriter.php
+++ b/WebFiori/Framework/Writers/MiddlewareClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/MigrationClassWriter.php b/WebFiori/Framework/Writers/MigrationClassWriter.php
index 1376bc930..aa5fb1630 100644
--- a/WebFiori/Framework/Writers/MigrationClassWriter.php
+++ b/WebFiori/Framework/Writers/MigrationClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/RepositoryWriter.php b/WebFiori/Framework/Writers/RepositoryWriter.php
index 0056d48ed..3f5d4b916 100644
--- a/WebFiori/Framework/Writers/RepositoryWriter.php
+++ b/WebFiori/Framework/Writers/RepositoryWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2026 Ibrahim BinAlshikh
+ * Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/RestServiceWriter.php b/WebFiori/Framework/Writers/RestServiceWriter.php
index 0f7edf50f..ffc1dfba2 100644
--- a/WebFiori/Framework/Writers/RestServiceWriter.php
+++ b/WebFiori/Framework/Writers/RestServiceWriter.php
@@ -3,7 +3,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2026 Ibrahim BinAlshikh
+ * Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
index 45aefcee9..964ff16df 100644
--- a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
+++ b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/SeederClassWriter.php b/WebFiori/Framework/Writers/SeederClassWriter.php
index dc226f3e0..0f0b50945 100644
--- a/WebFiori/Framework/Writers/SeederClassWriter.php
+++ b/WebFiori/Framework/Writers/SeederClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2025 Ibrahim BinAlshikh
+ * Copyright (c) 2025-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/ServiceHolder.php b/WebFiori/Framework/Writers/ServiceHolder.php
index 0280077cb..21fee879c 100644
--- a/WebFiori/Framework/Writers/ServiceHolder.php
+++ b/WebFiori/Framework/Writers/ServiceHolder.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/ThemeClassWriter.php b/WebFiori/Framework/Writers/ThemeClassWriter.php
index cf2c365ad..0adbd4873 100644
--- a/WebFiori/Framework/Writers/ThemeClassWriter.php
+++ b/WebFiori/Framework/Writers/ThemeClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/WebFiori/Framework/Writers/ThemeComponentWriter.php b/WebFiori/Framework/Writers/ThemeComponentWriter.php
index 7588d1bc0..5a88d3efb 100644
--- a/WebFiori/Framework/Writers/ThemeComponentWriter.php
+++ b/WebFiori/Framework/Writers/ThemeComponentWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
diff --git a/release-please-config.json b/release-please-config.json
index 180f5dec7..519962e11 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -9,13 +9,13 @@
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "revert", "section": "Reverts" },
- { "type": "docs", "section": "Documentation" },
- { "type": "style", "section": "Styles" },
- { "type": "chore", "section": "Miscellaneous Chores" },
- { "type": "refactor", "section": "Code Refactoring" },
- { "type": "test", "section": "Testing" },
- { "type": "build", "section": "Build System" },
- { "type": "ci", "section": "Continuous Integration" },
+ { "type": "docs", "section": "Documentation", "hidden": false },
+ { "type": "style", "section": "Styles", "hidden": false },
+ { "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
+ { "type": "refactor", "section": "Code Refactoring", "hidden": false },
+ { "type": "test", "section": "Testing", "hidden": false },
+ { "type": "build", "section": "Build System", "hidden": false },
+ { "type": "ci", "section": "Continuous Integration", "hidden": false },
{ "type": "ui", "section": "User Interface" },
{ "type": "database", "section": "Database Changes" },
{ "type": "email", "section": "Email Notifications Changes" }
diff --git a/tests/ServiceRouterFixtures/HelperUtil.php b/tests/ServiceRouterFixtures/HelperUtil.php
new file mode 100644
index 000000000..14bf646b5
--- /dev/null
+++ b/tests/ServiceRouterFixtures/HelperUtil.php
@@ -0,0 +1,11 @@
+setDescription('Updated');
$this->assertEquals('Updated', $perm->getDescription());
}
+
+ /** @test */
+ public function testCanFallsBackToUserGetRoles() {
+ $manager = new AccessManager();
+ $manager->role('customer', ['VIEW_ORDERS']);
+
+ $user = new class {
+ public function getId() { return 55; }
+ public function getRoles(): array { return ['customer']; }
+ };
+
+ // No assignRoleToUser() — should fallback to $user->getRoles()
+ $this->assertTrue($manager->can($user, 'VIEW_ORDERS'));
+ $this->assertFalse($manager->can($user, 'DELETE_ORDERS'));
+ }
+
+ /** @test */
+ public function testCanPrefersInternalMapOverGetRoles() {
+ $manager = new AccessManager();
+ $manager->role('admin', ['MANAGE']);
+ $manager->role('viewer', ['VIEW']);
+
+ $user = new class {
+ public function getId() { return 56; }
+ public function getRoles(): array { return ['viewer']; }
+ };
+
+ // Internal map assigned — getRoles() should NOT be used
+ $manager->assignRoleToUser(56, 'admin');
+ $this->assertTrue($manager->can($user, 'MANAGE'));
+ $this->assertFalse($manager->can($user, 'VIEW'));
+ }
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index f3d677793..4ee5e9784 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -55,9 +55,14 @@ public function test00() {
" queue:status: Show pending and failed job counts.\n",
" queue:retry: Retry or flush failed queue jobs.\n",
" queue:work: Process queue jobs continuously.\n",
+ " routes:\n",
+ " routes:cache: Build the route cache for production.\n",
+ " routes:clear: Clear the route cache.\n",
" scheduler:\n",
" scheduler:run: Run the tasks scheduler check.\n",
" scheduler:daemon: Run the scheduler in a loop for a limited duration.\n",
+ " services:\n",
+ " services:list: List all auto-discovered API services.\n",
" scheduler: Run tasks scheduler.\n",
], $this->executeMultiCommand([
'help',
diff --git a/tests/WebFiori/Framework/Tests/Cli/ServicesListCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/ServicesListCommandTest.php
new file mode 100644
index 000000000..66f66b22f
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/ServicesListCommandTest.php
@@ -0,0 +1,34 @@
+executeMultiCommand([ServicesListCommand::class]);
+ $outputStr = implode('', $output);
+
+ $this->assertStringContainsString('No services discovered', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /** @test */
+ public function testWithDiscoveredServices() {
+ $dir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'ServiceRouterFixtures';
+ ServiceRouter::discover('WebFiori\\Tests\\ServiceRouterFixtures', '/apis', [], $dir);
+
+ // Verify getDiscovered() has data (this is what the command reads)
+ $discovered = ServiceRouter::getDiscovered();
+ $this->assertArrayHasKey('orders', $discovered);
+ $this->assertGreaterThanOrEqual(3, count($discovered));
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php b/tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php
new file mode 100644
index 000000000..6485fca66
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Middleware/MiddlewareTransitiveDepsTest.php
@@ -0,0 +1,145 @@
+addMiddleware(new MwB()); // depends on mw-a
+
+ $middleware = $uri->getMiddleware();
+ $names = array_map(fn($mw) => $mw->getName(), $middleware);
+
+ $this->assertContains('mw-a', $names);
+ $this->assertContains('mw-b', $names);
+ $this->assertCount(2, $middleware);
+ }
+
+ /** @test */
+ public function testTransitiveChainResolved() {
+ $uri = new RouterUri('https://example.com/test', '');
+ $uri->addMiddleware(new MwC()); // C depends on B, B depends on A
+
+ $middleware = $uri->getMiddleware();
+ $names = array_map(fn($mw) => $mw->getName(), $middleware);
+
+ $this->assertContains('mw-a', $names);
+ $this->assertContains('mw-b', $names);
+ $this->assertContains('mw-c', $names);
+ $this->assertCount(3, $middleware);
+ }
+
+ /** @test */
+ public function testExecutionOrderCorrect() {
+ $uri = new RouterUri('https://example.com/test', '');
+ $uri->addMiddleware(new MwC());
+
+ $middleware = $uri->getMiddleware();
+ $names = array_map(fn($mw) => $mw->getName(), $middleware);
+
+ // A must come before B, B before C
+ $this->assertLessThan(
+ array_search('mw-b', $names),
+ array_search('mw-a', $names)
+ );
+ $this->assertLessThan(
+ array_search('mw-c', $names),
+ array_search('mw-b', $names)
+ );
+ }
+
+ /** @test */
+ public function testNoDuplicateWhenDependencyAlreadyAssigned() {
+ $uri = new RouterUri('https://example.com/test', '');
+ $uri->addMiddleware(new MwA());
+ $uri->addMiddleware(new MwB());
+
+ $middleware = $uri->getMiddleware();
+ $names = array_map(fn($mw) => $mw->getName(), $middleware);
+
+ $this->assertCount(2, $middleware);
+ $this->assertEquals(1, count(array_keys($names, 'mw-a')));
+ }
+
+ /** @test */
+ public function testMissingDependencySkippedSilently() {
+ $uri = new RouterUri('https://example.com/test', '');
+ $uri->addMiddleware(new MwOrphan()); // depends on mw-nonexistent
+
+ $middleware = $uri->getMiddleware();
+ $names = array_map(fn($mw) => $mw->getName(), $middleware);
+
+ $this->assertContains('mw-orphan', $names);
+ $this->assertNotContains('mw-nonexistent', $names);
+ $this->assertCount(1, $middleware);
+ }
+
+ /** @test */
+ public function testNoDependenciesUnchanged() {
+ $uri = new RouterUri('https://example.com/test', '');
+ $uri->addMiddleware(new MwA()); // no dependencies
+
+ $middleware = $uri->getMiddleware();
+ $this->assertCount(1, $middleware);
+ $this->assertEquals('mw-a', $middleware[0]->getName());
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Router/RouteCacheTest.php b/tests/WebFiori/Framework/Tests/Router/RouteCacheTest.php
new file mode 100644
index 000000000..1e1bb5485
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Router/RouteCacheTest.php
@@ -0,0 +1,180 @@
+cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wf-route-cache-test';
+
+ if (!is_dir($this->cacheDir)) {
+ mkdir($this->cacheDir, 0755, true);
+ }
+
+ $this->cache = new Cache(new FileStorage($this->cacheDir));
+ ServiceRouter::reset();
+ }
+
+ protected function tearDown(): void {
+ // Clean up cache files
+ $files = glob($this->cacheDir . DIRECTORY_SEPARATOR . '*');
+
+ foreach ($files ?: [] as $file) {
+ if (is_file($file)) {
+ unlink($file);
+ }
+ }
+
+ @rmdir($this->cacheDir);
+ ServiceRouter::reset();
+ parent::tearDown();
+ }
+
+ /** @test */
+ public function testConstructorDefaults() {
+ $rc = new RouteCache($this->cache);
+ $this->assertFalse($rc->isEnabled());
+ $this->assertEquals('wf_routes_cache', $rc->getCacheKey());
+ }
+
+ /** @test */
+ public function testConstructorCustomValues() {
+ $rc = new RouteCache($this->cache, true, 'custom_key');
+ $this->assertTrue($rc->isEnabled());
+ $this->assertEquals('custom_key', $rc->getCacheKey());
+ }
+
+ /** @test */
+ public function testSetEnabled() {
+ $rc = new RouteCache($this->cache);
+ $rc->setEnabled(true);
+ $this->assertTrue($rc->isEnabled());
+ $rc->setEnabled(false);
+ $this->assertFalse($rc->isEnabled());
+ }
+
+ /** @test */
+ public function testLoadReturnsFalseWhenDisabled() {
+ $rc = new RouteCache($this->cache, false);
+ $this->assertFalse($rc->load());
+ }
+
+ /** @test */
+ public function testLoadReturnsFalseWhenNoCache() {
+ $rc = new RouteCache($this->cache, true);
+ $this->assertFalse($rc->load());
+ }
+
+ /** @test */
+ public function testIsCachedReturnsFalseInitially() {
+ $rc = new RouteCache($this->cache, true);
+ $this->assertFalse($rc->isCached());
+ }
+
+ /** @test */
+ public function testBuildCachesRoutes() {
+ $fixturesDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'ServiceRouterFixtures';
+ ServiceRouter::discover('WebFiori\\Tests\\ServiceRouterFixtures', '/cache-test', [], $fixturesDir);
+
+ $rc = new RouteCache($this->cache, true);
+ $count = $rc->build([
+ ['namespace' => 'WebFiori\\Tests\\ServiceRouterFixtures', 'basePath' => '/cache-test', 'options' => [], 'directory' => $fixturesDir, 'recursive' => false]
+ ]);
+
+ $this->assertGreaterThan(0, $count);
+ $this->assertTrue($rc->isCached());
+ }
+
+ /** @test */
+ public function testBuildAndLoadRestoresRoutes() {
+ $fixturesDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'ServiceRouterFixtures';
+ ServiceRouter::discover('WebFiori\\Tests\\ServiceRouterFixtures', '/restore-test', [], $fixturesDir);
+
+ $rc = new RouteCache($this->cache, true);
+ $rc->build([
+ ['namespace' => 'WebFiori\\Tests\\ServiceRouterFixtures', 'basePath' => '/restore-test', 'options' => [], 'directory' => $fixturesDir, 'recursive' => false]
+ ]);
+
+ ServiceRouter::reset();
+ $this->assertEmpty(ServiceRouter::getDiscovered());
+
+ $result = $rc->load();
+ $this->assertTrue($result);
+ $this->assertNotEmpty(ServiceRouter::getDiscovered());
+ }
+
+ /** @test */
+ public function testClearRemovesCache() {
+ $fixturesDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'ServiceRouterFixtures';
+ ServiceRouter::discover('WebFiori\\Tests\\ServiceRouterFixtures', '/clear-test', [], $fixturesDir);
+
+ $rc = new RouteCache($this->cache, true);
+ $rc->build([]);
+ $this->assertTrue($rc->isCached());
+
+ $rc->clear();
+ $this->assertFalse($rc->isCached());
+ }
+
+ /** @test */
+ public function testBuildWithNoDiscoveredServices() {
+ $rc = new RouteCache($this->cache, true);
+ $count = $rc->build([]);
+ $this->assertEquals(0, $count);
+ $this->assertTrue($rc->isCached());
+ }
+
+ /** @test */
+ public function testBuildIncludesDiscoveredServices() {
+ $fixturesDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'ServiceRouterFixtures';
+ ServiceRouter::discover('WebFiori\\Tests\\ServiceRouterFixtures', '/cached-apis', [], $fixturesDir);
+
+ $rc = new RouteCache($this->cache, true);
+ $rc->build([
+ ['namespace' => 'WebFiori\\Tests\\ServiceRouterFixtures', 'basePath' => '/cached-apis', 'options' => [], 'directory' => $fixturesDir, 'recursive' => false]
+ ]);
+
+ // Clear discovered and reload from cache
+ ServiceRouter::reset();
+ $this->assertEmpty(ServiceRouter::getDiscovered());
+
+ $rc->load();
+ $this->assertNotEmpty(ServiceRouter::getDiscovered());
+ $this->assertArrayHasKey('orders', ServiceRouter::getDiscovered());
+ }
+
+ /** @test */
+ public function testLoadWithEmptyRoutesArray() {
+ // Manually set cache with empty routes
+ $this->cache->set('wf_routes_cache', [
+ 'routes' => [],
+ 'discovered' => [],
+ 'built_at' => date('c'),
+ 'total' => 0,
+ 'skipped' => 0,
+ ], 86400);
+
+ $rc = new RouteCache($this->cache, true);
+ $result = $rc->load();
+ $this->assertTrue($result);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Router/ServiceRouterTest.php b/tests/WebFiori/Framework/Tests/Router/ServiceRouterTest.php
new file mode 100644
index 000000000..173ab293d
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Router/ServiceRouterTest.php
@@ -0,0 +1,178 @@
+fixturesDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR . 'ServiceRouterFixtures';
+ }
+
+ protected function tearDown(): void {
+ ServiceRouter::reset();
+ parent::tearDown();
+ }
+
+ /** @test */
+ public function testDiscoverFindsAttributedService() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ $this->assertArrayHasKey('orders', $discovered);
+ $this->assertEquals('WebFiori\\Tests\\ServiceRouterFixtures\\OrderService', $discovered['orders']['class']);
+ $this->assertEquals('service', $discovered['orders']['type']);
+ $this->assertEquals('/apis/orders', $discovered['orders']['path']);
+ }
+
+ /** @test */
+ public function testDiscoverDerivesNameFromClassWhenAttributeNameEmpty() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ $this->assertArrayHasKey('product', $discovered);
+ $this->assertEquals('service', $discovered['product']['type']);
+ }
+
+ /** @test */
+ public function testDiscoverFindsLegacyWebService() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ $this->assertArrayHasKey('legacy', $discovered);
+ $this->assertEquals('WebFiori\\Tests\\ServiceRouterFixtures\\LegacyService', $discovered['legacy']['class']);
+ $this->assertEquals('service', $discovered['legacy']['type']);
+ }
+
+ /** @test */
+ public function testDiscoverFindsWebServicesManager() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ $this->assertArrayHasKey('users', $discovered);
+ $this->assertEquals('WebFiori\\Tests\\ServiceRouterFixtures\\UsersManager', $discovered['users']['class']);
+ $this->assertEquals('manager', $discovered['users']['type']);
+ $this->assertEquals('/apis/users', $discovered['users']['path']);
+ }
+
+ /** @test */
+ public function testDiscoverSkipsNonServiceClasses() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ foreach ($discovered as $name => $entry) {
+ $this->assertNotEquals('WebFiori\\Tests\\ServiceRouterFixtures\\HelperUtil', $entry['class']);
+ }
+ }
+
+ /** @test */
+ public function testDiscoverReturnsCount() {
+ $count = ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+
+ $this->assertEquals(4, $count); // orders, product, legacy, users
+ }
+
+ /** @test */
+ public function testDiscoverWithNonExistentNamespace() {
+ $count = ServiceRouter::discover(
+ 'WebFiori\\Tests\\Fixtures\\NonExistent',
+ '/apis'
+ );
+
+ $this->assertEquals(0, $count);
+ $this->assertEmpty(ServiceRouter::getDiscovered());
+ }
+
+ /** @test */
+ public function testDiscoverWithoutDirectoryUsesNamespaceToPath() {
+ // App\Apis is a real directory relative to ROOT_PATH
+ // This exercises namespaceToPath() with a valid path
+ $count = ServiceRouter::discover('App\\Apis', '/app-apis');
+ // May find services or may not — depends on what's in App/Apis
+ // The point is it doesn't crash
+ $this->assertIsInt($count);
+ }
+
+ /** @test */
+ public function testDiscoverAppliesBasePath() {
+ ServiceRouter::discover($this->namespace, '/v2/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ $this->assertEquals('/v2/apis/orders', $discovered['orders']['path']);
+ }
+
+ /** @test */
+ public function testGetDiscoveredInitiallyEmpty() {
+ $this->assertEmpty(ServiceRouter::getDiscovered());
+ }
+
+ /** @test */
+ public function testResetClearsDiscovered() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $this->assertNotEmpty(ServiceRouter::getDiscovered());
+
+ ServiceRouter::reset();
+ $this->assertEmpty(ServiceRouter::getDiscovered());
+ }
+
+ /** @test */
+ public function testDiscoverRecursiveFindsNestedServices() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir, true);
+ $discovered = ServiceRouter::getDiscovered();
+
+ // LoginService in UserAuth/ subdirectory
+ $this->assertArrayHasKey('user-auth/login', $discovered);
+ $this->assertEquals('/apis/user-auth/login', $discovered['user-auth/login']['path']);
+ }
+
+ /** @test */
+ public function testDiscoverNonRecursiveSkipsSubdirectories() {
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir, false);
+ $discovered = ServiceRouter::getDiscovered();
+
+ $this->assertArrayNotHasKey('user-auth/login', $discovered);
+ }
+
+ /** @test */
+ public function testDiscoverSkipsAttributeNameWithSlash() {
+ // OrderService has #[RestController('orders')] — valid
+ // If we had one with slash it would be skipped
+ ServiceRouter::discover($this->namespace, '/apis', [], $this->fixturesDir);
+ $discovered = ServiceRouter::getDiscovered();
+
+ // All discovered names should not contain slashes (non-recursive)
+ foreach ($discovered as $name => $entry) {
+ $this->assertStringNotContainsString('/', $name);
+ }
+ }
+
+ /** @test */
+ public function testDynamicRegistersRoute() {
+ $routesBefore = Router::routesCount();
+ ServiceRouter::dynamic($this->namespace, '/dynamic/{controller}', [], $this->fixturesDir);
+ $this->assertGreaterThan($routesBefore, Router::routesCount());
+ }
+
+ /** @test */
+ public function testHandleReturns404ForUnknownService() {
+ $response = \WebFiori\Framework\App::getResponse();
+ $response->setCode(200);
+ ServiceRouter::handle('nonexistent', $this->namespace, $this->fixturesDir);
+ $this->assertEquals(404, $response->getCode());
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Session/CacheSessionStorageTest.php b/tests/WebFiori/Framework/Tests/Session/CacheSessionStorageTest.php
new file mode 100644
index 000000000..164c609d4
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Session/CacheSessionStorageTest.php
@@ -0,0 +1,172 @@
+cacheDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'wf-session-cache-test';
+
+ if (!is_dir($this->cacheDir)) {
+ mkdir($this->cacheDir, 0755, true);
+ }
+ }
+
+ protected function tearDown(): void {
+ // Clean up cache files
+ $files = glob($this->cacheDir.DIRECTORY_SEPARATOR.'*');
+
+ foreach ($files as $file) {
+ if (is_file($file)) {
+ unlink($file);
+ }
+ }
+
+ if (is_dir($this->cacheDir)) {
+ rmdir($this->cacheDir);
+ }
+
+ parent::tearDown();
+ }
+
+ /** @test */
+ public function testConstructorDefaults() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $this->assertSame($fileStorage, $storage->getStorage());
+ $this->assertEquals('wf_session:', $storage->getPrefix());
+ $this->assertEquals(7200, $storage->getTTL());
+ }
+
+ /** @test */
+ public function testConstructorCustomValues() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage, 'custom:', 3600);
+
+ $this->assertEquals('custom:', $storage->getPrefix());
+ $this->assertEquals(3600, $storage->getTTL());
+ }
+
+ /** @test */
+ public function testSetTTL() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+ $storage->setTTL(1800);
+
+ $this->assertEquals(1800, $storage->getTTL());
+ }
+
+ /** @test */
+ public function testSaveAndRead() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $sessionId = 'test-session-123';
+ $data = 'serialized_session_data_here';
+
+ $storage->save($sessionId, $data);
+ $result = $storage->read($sessionId);
+
+ $this->assertEquals($data, $result);
+ }
+
+ /** @test */
+ public function testReadNonExistent() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $result = $storage->read('non-existent-id');
+ $this->assertNull($result);
+ }
+
+ /** @test */
+ public function testRemove() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $sessionId = 'remove-test-456';
+ $storage->save($sessionId, 'some_data');
+
+ // Verify it exists
+ $this->assertNotNull($storage->read($sessionId));
+
+ // Remove and verify
+ $storage->remove($sessionId);
+ $this->assertNull($storage->read($sessionId));
+ }
+
+ /** @test */
+ public function testSaveOverwrite() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $sessionId = 'overwrite-test';
+ $storage->save($sessionId, 'first_data');
+ $storage->save($sessionId, 'second_data');
+
+ $this->assertEquals('second_data', $storage->read($sessionId));
+ }
+
+ /** @test */
+ public function testGcDoesNotThrow() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $storage->save('gc-test-1', 'data1');
+ $storage->gc('2020-01-01 00:00:00', 10);
+
+ // Should not throw
+ $this->assertTrue(true);
+ }
+
+ /** @test */
+ public function testPrefixIsolation() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storageA = new CacheSessionStorage($fileStorage, 'a:');
+ $storageB = new CacheSessionStorage($fileStorage, 'b:');
+
+ $storageA->save('session1', 'data_a');
+ $storageB->save('session1', 'data_b');
+
+ $this->assertEquals('data_a', $storageA->read('session1'));
+ $this->assertEquals('data_b', $storageB->read('session1'));
+ }
+
+ /** @test */
+ public function testRemoveDoesNotAffectOtherSessions() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $storage = new CacheSessionStorage($fileStorage);
+
+ $storage->save('keep-me', 'keep_data');
+ $storage->save('delete-me', 'delete_data');
+
+ $storage->remove('delete-me');
+
+ $this->assertEquals('keep_data', $storage->read('keep-me'));
+ $this->assertNull($storage->read('delete-me'));
+ }
+
+ /** @test */
+ public function testIntegrationWithSessionManager() {
+ $fileStorage = new FileStorage($this->cacheDir);
+ $cacheStorage = new CacheSessionStorage($fileStorage);
+
+ // Simulate what SessionManager does
+ $sessionId = 'integration-test-789';
+ $serialized = 'O:8:"stdClass":1:{s:4:"user";s:5:"admin";}';
+
+ $cacheStorage->save($sessionId, $serialized);
+ $loaded = $cacheStorage->read($sessionId);
+
+ $this->assertEquals($serialized, $loaded);
+
+ $cacheStorage->remove($sessionId);
+ $this->assertNull($cacheStorage->read($sessionId));
+ }
+}
diff --git a/tests/phpunit10.xml b/tests/phpunit10.xml
index 6e98d7523..c3f5d976a 100644
--- a/tests/phpunit10.xml
+++ b/tests/phpunit10.xml
@@ -86,6 +86,8 @@
../WebFiori/Framework/Router/RouteOption.php
../WebFiori/Framework/Router/Router.php
+ ../WebFiori/Framework/Router/ServiceRouter.php
+ ../WebFiori/Framework/Router/RouteCache.php
../WebFiori/Framework/Router/RouterUri.php
../WebFiori/Framework/Scheduler/AbstractTask.php
@@ -104,6 +106,7 @@
../WebFiori/Framework/Session/SessionUser.php
../WebFiori/Framework/Session/SessionsManager.php
../WebFiori/Framework/Session/SessionManager.php
+ ../WebFiori/Framework/Session/CacheSessionStorage.php
../WebFiori/Framework/Session/SessionSchema.php
../WebFiori/Framework/Health/HealthCheck.php
../WebFiori/Framework/Health/HealthCheckResult.php