From 9051c34725c96a0b3320dca624e318bf1b3a9a52 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 19 Aug 2025 12:30:57 -0700 Subject: [PATCH 001/161] Updated DebugErrorHandler. Signed-off-by: Joshua Parker --- {Console => Contracts/Console}/Kernel.php | 0 Contracts/{ => Http}/Kernel.php | 0 Http/Kernel.php | 7 ++++--- 3 files changed, 4 insertions(+), 3 deletions(-) rename {Console => Contracts/Console}/Kernel.php (100%) rename Contracts/{ => Http}/Kernel.php (100%) diff --git a/Console/Kernel.php b/Contracts/Console/Kernel.php similarity index 100% rename from Console/Kernel.php rename to Contracts/Console/Kernel.php diff --git a/Contracts/Kernel.php b/Contracts/Http/Kernel.php similarity index 100% rename from Contracts/Kernel.php rename to Contracts/Http/Kernel.php diff --git a/Http/Kernel.php b/Http/Kernel.php index b36075d..83be2f4 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -5,7 +5,7 @@ namespace Codefy\Framework\Http; use Codefy\Framework\Application; -use Codefy\Framework\Contracts\Kernel as HttpKernel; +use Codefy\Framework\Contracts\Http\Kernel as HttpKernel; use Exception; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Qubus\Error\Handlers\DebugErrorHandler; @@ -17,6 +17,7 @@ use function Codefy\Framework\Helpers\public_path; use function Codefy\Framework\Helpers\router_basepath; +use function Qubus\Config\Helpers\env; use function Qubus\Security\Helpers\__observer; use function sprintf; use function version_compare; @@ -56,7 +57,7 @@ public function codefy(): Application */ protected function dispatchRouter(): bool { - return (new HttpPublisher())->publish( + return new HttpPublisher()->publish( content: $this->router->match( serverRequest: ServerRequest::fromGlobals( server: $_SERVER, @@ -116,7 +117,7 @@ protected function bootstrappers(): array protected function registerErrorHandler(): ErrorHandler { if ($this->codefy()->hasDebugModeEnabled()) { - return new DebugErrorHandler(); + return new DebugErrorHandler(title: env(key: 'APP_NAME', default: 'CodefyPHP') . ' Error'); } return new ProductionErrorHandler(); From 746c824e1dddb026a3c81bb7b95a8e7624a308c0 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 19 Aug 2025 12:32:47 -0700 Subject: [PATCH 002/161] Relocated interfaces for both Kernels. Signed-off-by: Joshua Parker --- Contracts/Console/Kernel.php | 4 ++-- Contracts/Http/Kernel.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Contracts/Console/Kernel.php b/Contracts/Console/Kernel.php index 2abad8d..6f9a7aa 100644 --- a/Contracts/Console/Kernel.php +++ b/Contracts/Console/Kernel.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Codefy\Framework\Console; +namespace Codefy\Framework\Contracts\Console; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -47,5 +47,5 @@ public function bootstrap(): void; * @param bool|OutputInterface|null $outputBuffer * @return int */ - public function call(string $command, array $parameters = [], bool|OutputInterface $outputBuffer = null): int; + public function call(string $command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int; } diff --git a/Contracts/Http/Kernel.php b/Contracts/Http/Kernel.php index abd6bf2..0d29cac 100644 --- a/Contracts/Http/Kernel.php +++ b/Contracts/Http/Kernel.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Codefy\Framework\Contracts; +namespace Codefy\Framework\Contracts\Http; use Codefy\Framework\Application; From 82a73b3df2b95072049f5bd66bdc45011fdc48f2 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 19 Aug 2025 12:36:51 -0700 Subject: [PATCH 003/161] PHP 8.4 updates. Signed-off-by: Joshua Parker --- Application.php | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/Application.php b/Application.php index 03eae67..90f623f 100644 --- a/Application.php +++ b/Application.php @@ -69,11 +69,11 @@ final class Application extends Container { use InvokerAware; - public const APP_VERSION = '2.1.5'; + public const string APP_VERSION = '3.0.0'; - public const MIN_PHP_VERSION = '8.2'; + public const string MIN_PHP_VERSION = '8.4'; - public const DS = DIRECTORY_SEPARATOR; + public const string DS = DIRECTORY_SEPARATOR; /** * The current globally available Application (if any). @@ -82,9 +82,21 @@ final class Application extends Container */ public static ?Application $APP = null; - public string $charset = 'UTF-8'; + // phpcs:disable + public string $charset { + get => $this->charset = 'UTF-8'; + set(string $charset) { + $this->charset = $charset; + } + } - public string $locale = 'en'; + public string $locale { + get => $this->locale = 'en'; + set(string $locale) { + $this->locale = $locale; + } + } + // phpcs:enable public string $controllerNamespace = 'App\\Infrastructure\\Http\\Controllers'; @@ -174,10 +186,10 @@ private function registerPropertyBindings(): void */ protected static function inferBasePath(): ?string { - $basePath = (new BasePathDetector())->getBasePath(); + $basePath = new BasePathDetector()->getBasePath(); return match (true) { - (env('APP_BASE_PATH') !== null && env('APP_BASE_PATH') !== false) => env('APP_BASE_PATH'), + (env(key: 'APP_BASE_PATH') !== null && env(key: 'APP_BASE_PATH') !== false) => env(key: 'APP_BASE_PATH'), $basePath !== '' => $basePath, default => dirname(path: __FILE__, levels: 2), }; @@ -186,7 +198,7 @@ protected static function inferBasePath(): ?string /** * FileLogger * - * @throws ReflectionException + * @throws ReflectionException|TypeException */ public static function getLogger(): LoggerInterface { @@ -792,7 +804,6 @@ protected function coreAliases(): array 'codefy' => self::class, \Qubus\Routing\Interfaces\Collector::class => \Qubus\Routing\Route\RouteCollector::class, 'router' => \Qubus\Routing\Router::class, - \Codefy\Framework\Contracts\Kernel::class => \Codefy\Framework\Http\Kernel::class, \Codefy\Framework\Contracts\RoutingController::class => \Codefy\Framework\Http\BaseController::class, \League\Flysystem\FilesystemOperator::class => \Qubus\FileSystem\FileSystem::class, \League\Flysystem\FilesystemAdapter::class => \Qubus\FileSystem\Adapter\LocalFlysystemAdapter::class, @@ -818,12 +829,13 @@ protected function coreAliases(): array /** * Load environment file(s). * + * @param string $basePath * @return void */ - private function loadEnvironment(): void + private static function loadEnvironment(string $basePath): void { $dotenv = Dotenv::createImmutable( - paths: $this->basePath(), + paths: $basePath, names: ['.env','.env.local','.env.staging','.env.development','.env.production'], shortCircuit: false ); @@ -910,6 +922,8 @@ public static function getInstance(?string $path = null): self ); } + self::loadEnvironment($basePath); + return self::$APP; } } From d778595437265ab197a487564dffc418b88c2e2a Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:39:26 -0700 Subject: [PATCH 004/161] Updateed Auth to PHP 8.4 Signed-off-by: Joshua Parker --- Auth/Rbac/Entity/Permission.php | 14 +- Auth/Rbac/Entity/RbacPermission.php | 30 ++-- Auth/Rbac/Entity/RbacRole.php | 35 ++-- Auth/Rbac/Entity/Role.php | 14 +- Auth/Rbac/Guard.php | 8 +- Auth/Rbac/Rbac.php | 14 +- Auth/Rbac/Resource/BaseStorageResource.php | 30 ++-- Auth/Rbac/Resource/FileResource.php | 151 ++++++++++++++++++ Auth/Repository/AuthUserRepository.php | 3 +- Auth/Repository/PdoRepository.php | 15 +- Auth/UserSession.php | 16 +- .../Auth}/AuthenticationMiddleware.php | 2 +- .../Auth}/UserSessionMiddleware.php | 5 +- 13 files changed, 221 insertions(+), 116 deletions(-) create mode 100644 Auth/Rbac/Resource/FileResource.php rename {Auth/Middleware => Http/Middleware/Auth}/AuthenticationMiddleware.php (94%) rename {Auth/Middleware => Http/Middleware/Auth}/UserSessionMiddleware.php (90%) diff --git a/Auth/Rbac/Entity/Permission.php b/Auth/Rbac/Entity/Permission.php index 5fd6638..7efcda7 100644 --- a/Auth/Rbac/Entity/Permission.php +++ b/Auth/Rbac/Entity/Permission.php @@ -6,15 +6,11 @@ interface Permission { - /** - * @return string - */ - public function getName(): string; + //phpcs:disable + public string $name { get; } - /** - * @return string - */ - public function getDescription(): string; + public string $description { get; } + //phpcs:enable /** * @param Permission $permission @@ -45,5 +41,5 @@ public function getRuleClass(): ?string; * @param array|null $params * @return bool */ - public function checkAccess(array $params = null): bool; + public function checkAccess(?array $params = null): bool; } diff --git a/Auth/Rbac/Entity/RbacPermission.php b/Auth/Rbac/Entity/RbacPermission.php index d33704f..5638e24 100644 --- a/Auth/Rbac/Entity/RbacPermission.php +++ b/Auth/Rbac/Entity/RbacPermission.php @@ -16,40 +16,30 @@ class RbacPermission implements Permission protected array $childrenNames = []; protected ?string $ruleClass = ''; + //phpcs:disable /** - * @param string $permissionName + * @param string $name * @param string $description * @param StorageResource $rbacStorageCollection */ public function __construct( - protected string $permissionName, - protected string $description, + public private(set) string $name { + get => $this->name; + }, + public private(set) string $description { + get => $this->description; + }, protected StorageResource $rbacStorageCollection ) { } - - /** - * @return string - */ - public function getName(): string - { - return $this->permissionName; - } - - /** - * @return string - */ - public function getDescription(): string - { - return $this->description; - } + //phpcs.enable /** * @param Permission $permission */ public function addChild(Permission $permission): void { - $this->childrenNames[$permission->getName()] = true; + $this->childrenNames[$permission->name] = true; } /** diff --git a/Auth/Rbac/Entity/RbacRole.php b/Auth/Rbac/Entity/RbacRole.php index 86f48af..9c24972 100644 --- a/Auth/Rbac/Entity/RbacRole.php +++ b/Auth/Rbac/Entity/RbacRole.php @@ -12,43 +12,32 @@ class RbacRole implements Role { protected array $childrenNames = []; - protected array $permissionNames = []; + //phpcs:disable /** - * @param string $roleName + * @param string $name * @param string $description * @param StorageResource $rbacStorageCollection */ public function __construct( - protected string $roleName, - protected string $description, + public private(set) string $name { + get => $this->name; + }, + public private(set) string $description { + get => $this->description; + }, protected StorageResource $rbacStorageCollection ) { } - - /** - * @return string - */ - public function getName(): string - { - return $this->roleName; - } - - /** - * @return string - */ - public function getDescription(): string - { - return $this->description; - } + //phpcs:enable /** * @param Role $role */ public function addChild(Role $role): void { - $this->childrenNames[$role->getName()] = true; + $this->childrenNames[$role->name] = true; } /** @@ -77,7 +66,7 @@ public function getChildren(): array */ public function addPermission(Permission $permission): void { - $this->permissionNames[$permission->getName()] = true; + $this->permissionNames[$permission->name] = true; } /** @@ -137,7 +126,7 @@ public function checkAccess(string $permissionName, ?array $params = null): bool protected function collectChildrenPermissions(Permission $permission, &$result): void { foreach ($permission->getChildren() as $childPermission) { - $childPermissionName = $childPermission->getName(); + $childPermissionName = $childPermission->name; if (!isset($result[$childPermissionName])) { $result[$childPermissionName] = $childPermission; $this->collectChildrenPermissions($childPermission, $result); diff --git a/Auth/Rbac/Entity/Role.php b/Auth/Rbac/Entity/Role.php index ef0685e..da53754 100644 --- a/Auth/Rbac/Entity/Role.php +++ b/Auth/Rbac/Entity/Role.php @@ -8,15 +8,11 @@ interface Role { - /** - * @return string - */ - public function getName(): string; + //phpcs:disable + public string $name { get; } - /** - * @return string - */ - public function getDescription(): string; + public string $description { get; } + //phpcs:enable /** * @param Role $role @@ -55,5 +51,5 @@ public function getPermissions(bool $withChildren = false): array; * @return bool * @throws SentinelException */ - public function checkAccess(string $permissionName, array $params = null): bool; + public function checkAccess(string $permissionName, ?array $params = null): bool; } diff --git a/Auth/Rbac/Guard.php b/Auth/Rbac/Guard.php index bbfc20c..826aeaa 100644 --- a/Auth/Rbac/Guard.php +++ b/Auth/Rbac/Guard.php @@ -20,13 +20,17 @@ public function addRole(string $name, string $description = ''): Role; */ public function addPermission(string $name, string $description = ''): Permission; - public function getRoles(): array; + //phpcs:disable + public array $roles { &get; } + //phpcs:enable public function getRole(string $name): Role|null; public function deleteRole(string $name): void; - public function getPermissions(): array; + //phpcs:disable + public array $permissions { &get; } + //phpcs:enable public function getPermission(string $name): Permission|null; diff --git a/Auth/Rbac/Rbac.php b/Auth/Rbac/Rbac.php index ff1a96e..69cdbd8 100644 --- a/Auth/Rbac/Rbac.php +++ b/Auth/Rbac/Rbac.php @@ -34,13 +34,14 @@ public function addPermission(string $name, string $description = ''): Permissio return $this->storageResource->addPermission($name, $description); } + //phpcs:disable /** * @return Role[] */ - public function getRoles(): array - { - return $this->storageResource->getRoles(); + public array $roles = [] { + &get => $this->roles; } + //phpcs:enable /** * @param string $name @@ -59,13 +60,14 @@ public function deleteRole(string $name): void $this->storageResource->deleteRole($name); } + //phpcs:disable /** * @return Permission[] */ - public function getPermissions(): array - { - return $this->storageResource->getPermissions(); + public array $permissions = [] { + &get => $this->permissions; } + //phpcs:enable /** * @param string $name diff --git a/Auth/Rbac/Resource/BaseStorageResource.php b/Auth/Rbac/Resource/BaseStorageResource.php index 095d1a0..2531027 100644 --- a/Auth/Rbac/Resource/BaseStorageResource.php +++ b/Auth/Rbac/Resource/BaseStorageResource.php @@ -12,9 +12,15 @@ abstract class BaseStorageResource implements StorageResource { - protected array $roles = []; + //phpcs:disable + public protected(set) array $roles = [] { + &get => $this->roles; + } - protected array $permissions = []; + public protected(set) array $permissions = [] { + &get => $this->permissions; + } + //phpcs:enable /** * @throws SentinelException @@ -24,7 +30,7 @@ public function addRole(string $name, string $description = ''): Role if (isset($this->roles[$name])) { throw new SentinelException(message: 'Role already exists.'); } - $role = new RbacRole(roleName: $name, description: $description, rbacStorageCollection: $this); + $role = new RbacRole(name: $name, description: $description, rbacStorageCollection: $this); $this->roles[$name] = $role; return $role; } @@ -36,7 +42,7 @@ public function addPermission(string $name, string $description = ''): Permissio } $permission = new RbacPermission( - permissionName: $name, + name: $name, description: $description, rbacStorageCollection: $this ); @@ -45,11 +51,6 @@ public function addPermission(string $name, string $description = ''): Permissio return $permission; } - public function getRoles(): array - { - return $this->roles; - } - public function getRole(string $name): ?Role { return $this->roles[$name] ?? null; @@ -59,16 +60,11 @@ public function deleteRole(string $name): void { unset($this->roles[$name]); - foreach ($this->getRoles() as $role) { + foreach ($this->roles as $role) { $role->removeChild($name); } } - public function getPermissions(): array - { - return $this->permissions; - } - public function getPermission(string $name): ?Permission { return $this->permissions[$name] ?? null; @@ -78,11 +74,11 @@ public function deletePermission(string $name): void { unset($this->permissions[$name]); - foreach ($this->getRoles() as $role) { + foreach ($this->roles as $role) { $role->removePermission(permissionName: $name); } - foreach ($this->getPermissions() as $permission) { + foreach ($this->permissions as $permission) { $permission->removeChild(permissionName: $name); } } diff --git a/Auth/Rbac/Resource/FileResource.php b/Auth/Rbac/Resource/FileResource.php new file mode 100644 index 0000000..2819f68 --- /dev/null +++ b/Auth/Rbac/Resource/FileResource.php @@ -0,0 +1,151 @@ +file = $file; + } + + /** + * @throws SentinelException + * @throws FilesystemException|TypeException + */ + public function load(): void + { + $this->clear(); + + if (!file_exists($this->file) || (!$data = LocalStorage::disk()->read(json_decode($this->file, true)))) { + $data = []; + } + + $this->restorePermissions($data['permissions'] ?? []); + $this->restoreRoles($data['roles'] ?? []); + } + + /** + * @throws FilesystemException + * @throws TypeException|FilesystemException + */ + public function save(): void + { + $data = [ + 'roles' => [], + 'permissions' => [], + ]; + foreach ($this->roles as $role) { + $data['roles'][$role->getName()] = $this->roleToRow($role); + } + foreach ($this->permissions as $permission) { + $data['permissions'][$permission->getName()] = $this->permissionToRow($permission); + } + + LocalStorage::disk()->write($this->file, json_encode(value: $data, flags: JSON_PRETTY_PRINT)); + } + + protected function roleToRow(Role $role): array + { + $result = []; + $result['name'] = $role->getName(); + $result['description'] = $role->getDescription(); + $childrenNames = []; + foreach ($role->getChildren() as $child) { + $childrenNames[] = $child->getName(); + } + $result['children'] = $childrenNames; + $permissionNames = []; + foreach ($role->getPermissions() as $permission) { + $permissionNames[] = $permission->getName(); + } + $result['permissions'] = $permissionNames; + return $result; + } + + protected function permissionToRow(Permission $permission): array + { + $result = []; + $result['name'] = $permission->getName(); + $result['description'] = $permission->getDescription(); + $childrenNames = []; + foreach ($permission->getChildren() as $child) { + $childrenNames[] = $child->getName(); + } + $result['children'] = $childrenNames; + $result['ruleClass'] = $permission->getRuleClass(); + return $result; + } + + /** + * @throws SentinelException + */ + protected function restorePermissions(array $permissionsData): void + { + /** @var string[][] $permChildrenNames */ + $permChildrenNames = []; + + foreach ($permissionsData as $pData) { + $permission = $this->addPermission($pData['name'] ?? '', $pData['description'] ?? ''); + $permission->setRuleClass($pData['ruleClass'] ?? ''); + $permChildrenNames[$permission->getName()] = $pData['children'] ?? []; + } + + foreach ($permChildrenNames as $permissionName => $childrenNames) { + foreach ($childrenNames as $childName) { + $permission = $this->getPermission($permissionName); + $child = $this->getPermission($childName); + if ($permission && $child) { + $permission->addChild($child); + } + } + } + } + + /** + * @throws SentinelException + */ + protected function restoreRoles($rolesData): void + { + /** @var string[][] $rolesChildrenNames */ + $rolesChildrenNames = []; + + foreach ($rolesData as $rData) { + $role = $this->addRole($rData['name'] ?? '', $rData['description'] ?? ''); + $rolesChildrenNames[$role->getName()] = $rData['children'] ?? []; + $permissionNames = $rData['permissions'] ?? []; + foreach ($permissionNames as $permissionName) { + if ($permission = $this->getPermission($permissionName)) { + $role->addPermission($permission); + } + } + } + + foreach ($rolesChildrenNames as $roleName => $childrenNames) { + foreach ($childrenNames as $childName) { + $role = $this->getRole($roleName); + $child = $this->getRole($childName); + if ($role && $child) { + $role->addChild($child); + } + } + } + } +} diff --git a/Auth/Repository/AuthUserRepository.php b/Auth/Repository/AuthUserRepository.php index a28e0b4..98ad99b 100644 --- a/Auth/Repository/AuthUserRepository.php +++ b/Auth/Repository/AuthUserRepository.php @@ -5,6 +5,7 @@ namespace Codefy\Framework\Auth\Repository; use Qubus\Http\Session\SessionEntity; +use SensitiveParameter; interface AuthUserRepository { @@ -17,5 +18,5 @@ interface AuthUserRepository * @param string|null $password * @return SessionEntity|null */ - public function authenticate(string $credential, ?string $password = null): ?SessionEntity; + public function authenticate(string $credential, #[SensitiveParameter] ?string $password = null): ?SessionEntity; } diff --git a/Auth/Repository/PdoRepository.php b/Auth/Repository/PdoRepository.php index ba69258..61ca969 100644 --- a/Auth/Repository/PdoRepository.php +++ b/Auth/Repository/PdoRepository.php @@ -9,6 +9,7 @@ use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; use Qubus\Http\Session\SessionEntity; +use SensitiveParameter; use function sprintf; @@ -22,7 +23,7 @@ public function __construct(private PDO $pdo, protected ConfigContainer $config) * @inheritdoc * @throws Exception */ - public function authenticate(string $credential, #[\SensitiveParameter] ?string $password = null): ?SessionEntity + public function authenticate(string $credential, #[SensitiveParameter] ?string $password = null): ?SessionEntity { $fields = $this->config->getConfigKey(key: 'auth.pdo.fields'); @@ -50,7 +51,6 @@ public function authenticate(string $credential, #[\SensitiveParameter] ?string if (Password::verify(password: $password ?? '', hash: $passwordHash)) { $user = new class () implements SessionEntity { public ?string $token = null; - public ?string $role = null; public function withToken(?string $token = null): self { @@ -58,21 +58,14 @@ public function withToken(?string $token = null): self return $this; } - public function withRole(?string $role = null): self - { - $this->role = $role; - return $this; - } - public function isEmpty(): bool { - return !empty($this->token) && !empty($this->role); + return !empty($this->token); } }; $user - ->withToken($result->token) - ->withRole($result->role); + ->withToken($result->token); return $user; } diff --git a/Auth/UserSession.php b/Auth/UserSession.php index afbf314..8efdd8b 100644 --- a/Auth/UserSession.php +++ b/Auth/UserSession.php @@ -8,9 +8,7 @@ class UserSession implements SessionEntity { - public ?string $token = null; - - public ?string $role = null; + public private(set) ?string $token = null; public function withToken(?string $token = null): self { @@ -18,25 +16,15 @@ public function withToken(?string $token = null): self return $this; } - public function withRole(?string $role = null): self - { - $this->role = $role; - return $this; - } - public function clear(): void { if (!empty($this->token)) { unset($this->token); } - - if (!empty($this->role)) { - unset($this->role); - } } public function isEmpty(): bool { - return empty($this->token) && empty($this->role); + return empty($this->token); } } diff --git a/Auth/Middleware/AuthenticationMiddleware.php b/Http/Middleware/Auth/AuthenticationMiddleware.php similarity index 94% rename from Auth/Middleware/AuthenticationMiddleware.php rename to Http/Middleware/Auth/AuthenticationMiddleware.php index 2283cc6..6103936 100644 --- a/Auth/Middleware/AuthenticationMiddleware.php +++ b/Http/Middleware/Auth/AuthenticationMiddleware.php @@ -15,7 +15,7 @@ final class AuthenticationMiddleware implements MiddlewareInterface { - public const AUTH_ATTRIBUTE = 'USERSESSION'; + public const string AUTH_ATTRIBUTE = 'USERSESSION'; public function __construct(protected Sentinel $auth) { diff --git a/Auth/Middleware/UserSessionMiddleware.php b/Http/Middleware/Auth/UserSessionMiddleware.php similarity index 90% rename from Auth/Middleware/UserSessionMiddleware.php rename to Http/Middleware/Auth/UserSessionMiddleware.php index e198e98..697f7f5 100644 --- a/Auth/Middleware/UserSessionMiddleware.php +++ b/Http/Middleware/Auth/UserSessionMiddleware.php @@ -16,7 +16,7 @@ final class UserSessionMiddleware implements MiddlewareInterface { - public const SESSION_ATTRIBUTE = 'USERSESSION'; + public const string SESSION_ATTRIBUTE = 'USERSESSION'; public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService) { @@ -39,8 +39,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** @var UserSession $user */ $user = $session->get(type: UserSession::class); $user - ->withToken(token: $userDetails->token) - ->withRole(role: $userDetails->role); + ->withToken(token: $userDetails->token); $request = $request->withAttribute(self::SESSION_ATTRIBUTE, $user); From 93197e71c5d08ee7875797c68f11747d2adfb46e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:40:44 -0700 Subject: [PATCH 005/161] Enhanced registration of service providers. Signed-off-by: Joshua Parker --- Bootstrap/RegisterProviders.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Bootstrap/RegisterProviders.php b/Bootstrap/RegisterProviders.php index 73475bf..b17f81c 100644 --- a/Bootstrap/RegisterProviders.php +++ b/Bootstrap/RegisterProviders.php @@ -24,7 +24,7 @@ final class RegisterProviders */ public function bootstrap(Application $app): void { - $this->mergeAdditionalProviders($app); + $this->mergeAdditionalProviders(app: $app); $app->registerConfiguredServiceProviders(); } @@ -41,18 +41,13 @@ protected function mergeAdditionalProviders(Application $app): void $config = $app->make(name: 'codefy.config'); $arrayMerge = array_unique( - array_merge( + array: array_merge( self::$merge, $config->getConfigKey(key: 'app.providers'), ) ); - $config->setConfigKey( - 'app', - [ - 'providers' => $arrayMerge, - ] - ); + $config->setConfigKey(key: 'app', value: ['providers' => $arrayMerge,]); } /** @@ -65,9 +60,9 @@ protected function mergeAdditionalProviders(Application $app): void public static function merge(array $providers): void { self::$merge = array_values( - array_filter( - array_unique( - array_merge(self::$merge, $providers) + array: array_filter( + array: array_unique( + array: array_merge(self::$merge, $providers) ) ) ); From 0697d2fb5b62ea9196d96cea17c844f64180363a Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:41:27 -0700 Subject: [PATCH 006/161] Enabled Kernel aliases. Signed-off-by: Joshua Parker --- Configuration/ApplicationBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Configuration/ApplicationBuilder.php b/Configuration/ApplicationBuilder.php index 8cf41dc..9764cd7 100644 --- a/Configuration/ApplicationBuilder.php +++ b/Configuration/ApplicationBuilder.php @@ -22,12 +22,12 @@ public function __construct(protected Application $app) public function withKernels(): self { $this->app->singleton( - \Codefy\Framework\Contracts\Kernel::class, + \Codefy\Framework\Contracts\Http\Kernel::class, fn() => $this->app->make(name: \Codefy\Framework\Http\Kernel::class) ); $this->app->singleton( - key: \Codefy\Framework\Console\Kernel::class, + key: \Codefy\Framework\Contracts\Console\Kernel::class, value: fn() => $this->app->make(name: \Codefy\Framework\Console\ConsoleKernel::class) ); From 98e597e6e369787b92eaccb9158625645c2cac30 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:41:54 -0700 Subject: [PATCH 007/161] Fixed nullable. Signed-off-by: Joshua Parker --- Console/ConsoleApplication.php | 6 +++--- Console/ConsoleKernel.php | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Console/ConsoleApplication.php b/Console/ConsoleApplication.php index 864730e..137cd38 100644 --- a/Console/ConsoleApplication.php +++ b/Console/ConsoleApplication.php @@ -24,7 +24,7 @@ public function __construct(protected Application $codefy) parent::__construct(name: 'CodefyPHP', version: Application::APP_VERSION); } - public function run(InputInterface $input = null, OutputInterface $output = null): int + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { $this->getCommandName(input: $input = $input ?: new ArgvInput()); @@ -34,7 +34,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null /** * @throws Exception */ - public function call($command, array $parameters = [], bool|OutputInterface $outputBuffer = null): int + public function call($command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int { [$command, $input] = $this->parseCommand(command: $command, parameters: $parameters); @@ -75,7 +75,7 @@ protected function parseCommand(string $command, array $parameters): array */ public function output(): string { - return $this->lastOutput && method_exists($this->lastOutput, 'fetch') + return $this->lastOutput && method_exists(object_or_class: $this->lastOutput, method: 'fetch') ? $this->lastOutput->fetch() : ''; } diff --git a/Console/ConsoleKernel.php b/Console/ConsoleKernel.php index 57b1c7a..0b2350f 100644 --- a/Console/ConsoleKernel.php +++ b/Console/ConsoleKernel.php @@ -6,6 +6,7 @@ use Codefy\Framework\Application; use Codefy\Framework\Console\ConsoleApplication as Codex; +use Codefy\Framework\Contracts\Console\Kernel; use Codefy\Framework\Factory\FileLoggerSmtpFactory; use Codefy\Framework\Scheduler\Mutex\Locker; use Codefy\Framework\Scheduler\Schedule; @@ -153,7 +154,7 @@ protected function getCodex(): Codex * @throws CommandNotFoundException * @throws Exception */ - public function call(string $command, array $parameters = [], bool|OutputInterface $outputBuffer = null): int + public function call(string $command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int { $this->bootstrap(); From 16b81fa6dce9902693ea2ffa5f274d2135033bec Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:42:33 -0700 Subject: [PATCH 008/161] Updated docblock exceptions. Signed-off-by: Joshua Parker --- Factory/FileLoggerFactory.php | 3 ++- Factory/PHPMailerSmtpFactory.php | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Factory/FileLoggerFactory.php b/Factory/FileLoggerFactory.php index c9b4e58..8b81281 100644 --- a/Factory/FileLoggerFactory.php +++ b/Factory/FileLoggerFactory.php @@ -9,6 +9,7 @@ use Codefy\Framework\Support\LocalStorage; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Qubus\Exception\Data\TypeException; use Qubus\Log\Logger; use Qubus\Log\Loggers\FileLogger; use ReflectionException; @@ -19,7 +20,7 @@ class FileLoggerFactory implements LoggerFactory use FileLoggerAware; /** - * @throws ReflectionException + * @throws ReflectionException|TypeException */ public static function getLogger(): LoggerInterface { diff --git a/Factory/PHPMailerSmtpFactory.php b/Factory/PHPMailerSmtpFactory.php index 70f276e..0d484c4 100644 --- a/Factory/PHPMailerSmtpFactory.php +++ b/Factory/PHPMailerSmtpFactory.php @@ -6,16 +6,12 @@ use Codefy\Framework\Contracts\MailerFactory; use Codefy\Framework\Support\CodefyMailer; -use Qubus\Exception\Data\TypeException; use Qubus\Mail\Mailer; use function Codefy\Framework\Helpers\app; class PHPMailerSmtpFactory implements MailerFactory { - /** - * @throws TypeException - */ public static function create(): Mailer { return new CodefyMailer(config: app(name: 'codefy.config')); From bfe2a78bcbb3eb753ef5edf355b554927ce66ca8 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:44:40 -0700 Subject: [PATCH 009/161] Updated QueryBuilder function and removed unecessary import. Signed-off-by: Joshua Parker --- Helpers/core.php | 10 +++++----- Helpers/path.php | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Helpers/core.php b/Helpers/core.php index 965be11..e40a002 100644 --- a/Helpers/core.php +++ b/Helpers/core.php @@ -12,7 +12,7 @@ use Qubus\Dbal\Connection; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; -use Qubus\Expressive\OrmBuilder; +use Qubus\Expressive\QueryBuilder; use Qubus\Routing\Exceptions\NamedRouteNotFoundException; use Qubus\Routing\Exceptions\RouteParamFailedConstraintException; use Qubus\Routing\Router; @@ -97,14 +97,14 @@ function env(string $key, mixed $default = null): mixed } /** - * OrmBuilder database instance. + * QueryBuilder database instance. * - * @return OrmBuilder|null + * @return QueryBuilder|null * @throws Exception */ -function orm(): ?OrmBuilder +function orm(): ?QueryBuilder { - return Codefy::$PHP->getDB(); + return Codefy::$PHP->getDb(); } /** diff --git a/Helpers/path.php b/Helpers/path.php index 94accde..fb3b0a8 100644 --- a/Helpers/path.php +++ b/Helpers/path.php @@ -5,7 +5,6 @@ namespace Codefy\Framework\Helpers; use Codefy\Framework\Application; -use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; use function implode; From e0639fdc971b4ec6522b457f4cb2f7b40259cf69 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:46:03 -0700 Subject: [PATCH 010/161] Libraries moved here from skeleton. Signed-off-by: Joshua Parker --- Http/BaseController.php | 6 +- Http/Errors/HttpRequestError.php | 15 + Http/HttpClient.php | 214 ++++++++++++ Http/Kernel.php | 2 +- .../Auth/AuthenticationMiddleware.php | 2 +- .../Auth/ExpireUserSessionMiddleware.php | 58 ++++ .../Auth/UserAuthorizationMiddleware.php | 62 ++++ .../Middleware/Auth/UserSessionMiddleware.php | 3 +- .../Cache/CacheExpiresMiddleware.php | 21 ++ Http/Middleware/Cache/CacheMiddleware.php | 11 + .../Cache/CachePreventionMiddleware.php | 11 + .../Cache/ClearSiteDataMiddleware.php | 20 ++ Http/Middleware/CorsMiddleware.php | 52 +++ .../Csrf/CsrfProtectionMiddleware.php | 119 +++++++ Http/Middleware/Csrf/CsrfSession.php | 41 +++ Http/Middleware/Csrf/CsrfTokenMiddleware.php | 90 +++++ .../Middleware/Csrf/Traits/CsrfTokenAware.php | 56 +++ Http/Middleware/Csrf/helpers.php | 15 + .../ContentSecurityPolicyMiddleware.php | 35 ++ .../SecureHeaders/SecureHeaders.php | 327 ++++++++++++++++++ Http/Middleware/Spam/HoneyPotMiddleware.php | 79 +++++ .../Spam/ReferrerSpamMiddleware.php | 23 ++ 22 files changed, 1254 insertions(+), 8 deletions(-) create mode 100644 Http/Errors/HttpRequestError.php create mode 100644 Http/HttpClient.php create mode 100644 Http/Middleware/Auth/ExpireUserSessionMiddleware.php create mode 100644 Http/Middleware/Auth/UserAuthorizationMiddleware.php create mode 100644 Http/Middleware/Cache/CacheExpiresMiddleware.php create mode 100644 Http/Middleware/Cache/CacheMiddleware.php create mode 100644 Http/Middleware/Cache/CachePreventionMiddleware.php create mode 100644 Http/Middleware/Cache/ClearSiteDataMiddleware.php create mode 100644 Http/Middleware/CorsMiddleware.php create mode 100644 Http/Middleware/Csrf/CsrfProtectionMiddleware.php create mode 100644 Http/Middleware/Csrf/CsrfSession.php create mode 100644 Http/Middleware/Csrf/CsrfTokenMiddleware.php create mode 100644 Http/Middleware/Csrf/Traits/CsrfTokenAware.php create mode 100644 Http/Middleware/Csrf/helpers.php create mode 100644 Http/Middleware/SecureHeaders/ContentSecurityPolicyMiddleware.php create mode 100644 Http/Middleware/SecureHeaders/SecureHeaders.php create mode 100644 Http/Middleware/Spam/HoneyPotMiddleware.php create mode 100644 Http/Middleware/Spam/ReferrerSpamMiddleware.php diff --git a/Http/BaseController.php b/Http/BaseController.php index 2234e34..2052b7f 100644 --- a/Http/BaseController.php +++ b/Http/BaseController.php @@ -10,19 +10,15 @@ use Qubus\Http\Session\SessionService; use Qubus\Routing\Controller\Controller; use Qubus\Routing\Router; -use Qubus\View\Native\NativeLoader; use Qubus\View\Renderer; -use function Codefy\Framework\Helpers\config; - class BaseController extends Controller implements RoutingController { public function __construct( protected SessionService $sessionService, protected Router $router, - protected ?Renderer $view = null, + protected Renderer $view, ) { - $this->setView(view: $view ?? new NativeLoader(config('view.path'))); } /** diff --git a/Http/Errors/HttpRequestError.php b/Http/Errors/HttpRequestError.php new file mode 100644 index 0000000..afa34f1 --- /dev/null +++ b/Http/Errors/HttpRequestError.php @@ -0,0 +1,15 @@ + Filter::getInstance()->applyFilter('http.request.timeout', 10, $uri), + /** + * Filters the number of seconds to wait while trying to connect to a server. + * Use 0 to wait indefinitely. Default: 10. + * + * @param float $connect_timeout Number of seconds. Default: 10. + * @param string|UriInterface $uri URI object or string. + */ + 'connect_timeout' => Filter::getInstance()->applyFilter('http.request.connect.timeout', 10, $uri), + /** + * Filters the version of the HTTP protocol used in a request. + * + * @param string $version HTTP protocol version used (usually '1.1', '1.0' or '2'). + * Default: 1.1. + * @param string|UriInterface $uri URI object or string. + */ + 'version' => Filter::getInstance()->applyFilter('http.request.version', '1.1', $uri), + /** + * Filters the redirect behavior of a request. + * + * @param bool|array $allow_redirects The redirect behavior of a request. Default: false. + * @param string|UriInterface $uri URI object or string. + */ + 'allow_redirects ' => Filter::getInstance()->applyFilter( + 'http.request.allow.redirects', + false, + $uri + ), + 'headers' => [], + 'body' => null, + 'delay' => null, + 'http_errors' => true, + 'proxy' => false, + 'stream' => false, + + ]; + + // Pre-parse for the HEAD checks. + $options = ArgsParser::parse($options); + // By default, HEAD requests do not cause redirections. + if (isset($options['method']) && $options['method'] === RequestMethod::HEAD) { + $defaults['allow_redirects'] = false; + } + + $parsedArgs = ArgsParser::parse($options, $defaults); + /** + * Filters the arguments used in an HTTP request. + * + * @param array $parsedArgs An array of HTTP request arguments. + * @param string|UriInterface $uri URI object or string. + */ + $parsedArgs = Filter::getInstance()->applyFilter('http.request.args', $parsedArgs, $uri); + /** + * Filters the preemptive return value of an HTTP request. + * + * Returning a non-false value from the filter will short-circuit the HTTP request and return + * early with that value. A filter should return one of: + * + * - An array containing 'headers', 'body', and 'response' elements + * - A HttpRequestError instance + * - bool false to avoid short-circuiting the response + * + * Returning any other value may result in unexpected behavior. + * + * @param false|array|HttpRequestError $response A preemptive return value of an HTTP request. Default false. + * @param array $parsedArgs HTTP request arguments. + * @param string|UriInterface $uri URI object or string. + */ + $preempt = Filter::getInstance()->applyFilter('http.request.preempt', false, $parsedArgs, $uri); + if ($preempt !== false) { + return $preempt; + } + + $parsedUrl = parse_url($uri); + if (empty($parsedUrl) || !isset($parsedUrl['scheme'])) { + $response = new HttpRequestError('A valid URL was not provided.', 405); + Action::getInstance()->doAction( + 'http_api_debug', + $response, + 'response', + \Qubus\Http\Request::class, + $parsedArgs, + $uri + ); + return JsonResponseFactory::create($response->getMessage(), (int) $response->getCode()); + } + + if (is_null__($parsedArgs['headers'])) { + $parsedArgs['headers'] = []; + } + + $response = parent::request($method, $uri, $parsedArgs); + + /** + * Fires after an HTTP API response is received and before the response is returned. + * + * @param ResponseInterface|mixed $response HTTP response. + * @param string $context Context under which the hook is fired. + * @param string $class HTTP transport used. + * @param array $parsedArgs HTTP request arguments. + * @param string|UriInterface $uri URI object or string. + */ + Action::getInstance()->doAction( + 'http_api_debug', + $response, + 'response', + \Qubus\Http\Request::class, + $parsedArgs, + $uri + ); + + /** + * Filters a successful HTTP API response immediately before the response is returned. + * + * @param ResponseInterface $response HTTP response. + * @param array $parsedArgs HTTP request arguments. + * @param string|UriInterface $uri URI object or string. + */ + return Filter::getInstance()->applyFilter('http.request.response', $response, $parsedArgs, $uri); + } +} diff --git a/Http/Kernel.php b/Http/Kernel.php index 83be2f4..8a70ee0 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -42,7 +42,7 @@ public function __construct(Application $codefy, Router $router) $this->router = $router; $this->router->setBaseMiddleware(middleware: $this->codefy->getBaseMiddlewares()); $this->router->setBasePath(basePath: router_basepath(path: public_path())); - $this->router->setDefaultNamespace(namespace: $this->codefy->getControllerNamespace()); + $this->router->setDefaultNamespace(namespace: $this->codefy->controllerNamespace); $this->registerErrorHandler(); } diff --git a/Http/Middleware/Auth/AuthenticationMiddleware.php b/Http/Middleware/Auth/AuthenticationMiddleware.php index 6103936..c611761 100644 --- a/Http/Middleware/Auth/AuthenticationMiddleware.php +++ b/Http/Middleware/Auth/AuthenticationMiddleware.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Codefy\Framework\Auth\Middleware; +namespace Codefy\Framework\Http\Middleware\Auth; use Codefy\Framework\Auth\Sentinel; use Psr\Http\Message\ResponseInterface; diff --git a/Http/Middleware/Auth/ExpireUserSessionMiddleware.php b/Http/Middleware/Auth/ExpireUserSessionMiddleware.php new file mode 100644 index 0000000..a3f1df5 --- /dev/null +++ b/Http/Middleware/Auth/ExpireUserSessionMiddleware.php @@ -0,0 +1,58 @@ +sessionService::$options = [ + 'cookie-name' => 'USERSESSID', + 'cookie-lifetime' => 0, + ]; + $session = $this->sessionService->makeSession($request); + + /** @var UserSession $user */ + $user = $session->get(type: UserSession::class); + $user + ->withToken(token: null); + + $request = $request->withAttribute(self::SESSION_ATTRIBUTE, $session); + + $response = $handler->handle($request); + + $response = CookiesResponse::set( + response: $response, + setCookieCollection: $this->sessionService->cookie->make( + name: 'USERSESSID', + value: '', + maxAge: 0 + ) + ); + + return $this->sessionService->commitSession($response, $session); + } +} diff --git a/Http/Middleware/Auth/UserAuthorizationMiddleware.php b/Http/Middleware/Auth/UserAuthorizationMiddleware.php new file mode 100644 index 0000000..5bbc72e --- /dev/null +++ b/Http/Middleware/Auth/UserAuthorizationMiddleware.php @@ -0,0 +1,62 @@ +isLoggedIn($request))) { + return $handler->handle($request->withHeader(self::HEADER_HTTP_STATUS_CODE, 'ok')); + } + + return $handler->handle($request->withHeader(self::HEADER_HTTP_STATUS_CODE, 'not_authorized')); + } + + /** + * @throws TypeException + * @throws Exception + */ + private function isLoggedIn(ServerRequestInterface $request): bool + { + $this->sessionService::$options = [ + 'cookie-name' => 'USERSESSID', + ]; + $session = $this->sessionService->makeSession($request); + + /** @var UserSession $user */ + $user = $session->get(type: UserSession::class); + if ($user->isEmpty()) { + return false; + } + + return true; + } +} diff --git a/Http/Middleware/Auth/UserSessionMiddleware.php b/Http/Middleware/Auth/UserSessionMiddleware.php index 697f7f5..e6af5de 100644 --- a/Http/Middleware/Auth/UserSessionMiddleware.php +++ b/Http/Middleware/Auth/UserSessionMiddleware.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace Codefy\Framework\Auth\Middleware; +namespace Codefy\Framework\Http\Middleware\Auth; use Codefy\Framework\Auth\UserSession; +use Codefy\Framework\Http\Middleware\Auth\AuthenticationMiddleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; diff --git a/Http/Middleware/Cache/CacheExpiresMiddleware.php b/Http/Middleware/Cache/CacheExpiresMiddleware.php new file mode 100644 index 0000000..1f808fd --- /dev/null +++ b/Http/Middleware/Cache/CacheExpiresMiddleware.php @@ -0,0 +1,21 @@ +defaultExpires($this->configContainer->getConfigKey(key: 'http-cache.default')); + parent::__construct($this->configContainer->getConfigKey(key: 'http-cache.expires')); + } +} diff --git a/Http/Middleware/Cache/CacheMiddleware.php b/Http/Middleware/Cache/CacheMiddleware.php new file mode 100644 index 0000000..a561393 --- /dev/null +++ b/Http/Middleware/Cache/CacheMiddleware.php @@ -0,0 +1,11 @@ +configContainer->getConfigKey(key: 'http-cache.types')); + } +} diff --git a/Http/Middleware/CorsMiddleware.php b/Http/Middleware/CorsMiddleware.php new file mode 100644 index 0000000..b10bf8a --- /dev/null +++ b/Http/Middleware/CorsMiddleware.php @@ -0,0 +1,52 @@ +handle($request); + + $origin = $response->getHeaderLine('origin'); + + if ($this->configContainer->getConfigKey(key: 'cors.access-control-allow-origin')[0] !== '*') { + if ( + !$origin || !in_array( + $origin, + $this->configContainer->getConfigKey(key: 'cors.access-control-allow-origin') + ) + ) { + return $response; + } + } + + $headers = $this->configContainer->getConfigKey(key: 'cors'); + + foreach ($headers as $key => $value) { + $response = $response->withAddedHeader($key, implode(separator: ',', array: $value)); + } + + return $response; + } +} diff --git a/Http/Middleware/Csrf/CsrfProtectionMiddleware.php b/Http/Middleware/Csrf/CsrfProtectionMiddleware.php new file mode 100644 index 0000000..317c511 --- /dev/null +++ b/Http/Middleware/Csrf/CsrfProtectionMiddleware.php @@ -0,0 +1,119 @@ +handle($request); + + if (true === $this->needsProtection($request) && false === $this->tokensMatch($request)) { + return JsonResponseFactory::create( + data: 'Bad CSRF Token.', + status: $this->configContainer->getConfigKey(key: 'csrf.error_status_code') + ); + } + + return $response; + } catch (\Exception $e) { + return $handler->handle($request); + } + } + + /** + * Check for methods not defined as safe. + * + * @param ServerRequestInterface $request + * @return bool + */ + private function needsProtection(ServerRequestInterface $request): bool + { + return RequestMethod::isSafe($request->getMethod()) === false; + } + + /** + * @throws Exception + */ + private function tokensMatch(ServerRequestInterface $request): bool + { + $expected = $this->fetchToken($request); + $provided = $this->getTokenFromRequest($request); + + return hash_equals($expected, $provided); + } + + + /** + * @throws Exception + * @throws \Exception + */ + private function fetchToken(ServerRequestInterface $request): string + { + $token = $request->getAttribute(CsrfTokenMiddleware::SESSION_ATTRIBUTE); + + // Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format. + if ( + is_string($token) && + ctype_alnum($token) && + strlen($token) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') + ) { + return $token; + } + + return ''; + } + + /** + * @throws Exception + */ + private function getTokenFromRequest(ServerRequestInterface $request): string + { + if ($request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header'))) { + return (string) $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); + } + + // Handle the case for a POST form. + $body = $request->getParsedBody(); + + if ( + is_array( + $body + ) && + isset($body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')]) && + is_string($body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')]) + ) { + return $body[$this->configContainer->getConfigKey(key: 'csrf.csrf_token')]; + } + + return ''; + } +} diff --git a/Http/Middleware/Csrf/CsrfSession.php b/Http/Middleware/Csrf/CsrfSession.php new file mode 100644 index 0000000..df3f6f3 --- /dev/null +++ b/Http/Middleware/Csrf/CsrfSession.php @@ -0,0 +1,41 @@ +csrfToken = $csrfToken; + + return $this; + } + + public function equals(string $token): bool + { + return !is_null__($this->csrfToken) && $this->csrfToken === $token; + } + + public function csrfToken(): string|null + { + return $this->csrfToken; + } + + public function clear(): void + { + if (!empty($this->csrfToken)) { + unset($this->csrfToken); + } + } + + public function isEmpty(): bool + { + return empty($this->csrfToken); + } +} diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/Http/Middleware/Csrf/CsrfTokenMiddleware.php new file mode 100644 index 0000000..b805803 --- /dev/null +++ b/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -0,0 +1,90 @@ +' . "\n", + self::$current->getFieldAttr(), + self::$current->token + ); + } + + /** + * @throws Exception + */ + public function getFieldAttr(): string + { + return $this->configContainer->getConfigKey(key: 'csrf.csrf_token', default: '_token'); + } + + /** + * @inheritDoc + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + $this->sessionService::$options = [ + 'cookie-name' => 'CSRFSESSID', + 'cookie-lifetime' => (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400), + ]; + + $session = $this->sessionService->makeSession($request); + + $this->token = $this->prepareToken(session: $session); + + /** + * If true, the application will do a header check, if not, + * it will expect data submitted via an HTML form tag. + */ + if ($this->configContainer->getConfigKey(key: 'csrf.request_header') === true) { + $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); + } + + $response = $handler->handle( + $request + ->withAttribute(self::SESSION_ATTRIBUTE, $this->token) + ); + + /** @var CsrfSession $csrf */ + $csrf = $session->get(CsrfSession::class); + $csrf->withCsrfToken($this->token); + + return $this->sessionService->commitSession($response, $session); + } catch (\Exception $e) { + return $handler->handle($request); + } + } +} diff --git a/Http/Middleware/Csrf/Traits/CsrfTokenAware.php b/Http/Middleware/Csrf/Traits/CsrfTokenAware.php new file mode 100644 index 0000000..b19a89b --- /dev/null +++ b/Http/Middleware/Csrf/Traits/CsrfTokenAware.php @@ -0,0 +1,56 @@ +configContainer->getConfigKey(key: 'csrf.salt'); + + return sha1(string: uniqid(prefix: sha1(string: $salt), more_entropy: true)); + } + + /** + * @throws Exception + */ + protected function prepareToken(HttpSession $session): string + { + // Try to retrieve an existing token from the session. + $token = $session->clientSessionId(); + + // If token isn't present in the session, we generate a new token. + if ($token === '') { + $token = $this->generateToken(); + } + return hash_hmac(algo: 'sha256', data: $token, key: $this->configContainer->getConfigKey(key: 'csrf.salt')); + } + + /** + * @throws Exception + */ + protected function hashEquals(string $knownString, string $userString): bool + { + return hash_equals( + $knownString, + hash_hmac( + algo: 'sha256', + data: $userString, + key: $this->configContainer->getConfigKey(key: 'csrf.salt') + ) + ); + } +} diff --git a/Http/Middleware/Csrf/helpers.php b/Http/Middleware/Csrf/helpers.php new file mode 100644 index 0000000..d3ffdc2 --- /dev/null +++ b/Http/Middleware/Csrf/helpers.php @@ -0,0 +1,15 @@ +handle($request); + + $headers = new SecureHeaders($this->configContainer->getConfigKey(key: 'headers'))->headers(); + foreach ($headers as $key => $value) { + $response = $response->withHeader($key, $value); + } + + return $response; + } +} diff --git a/Http/Middleware/SecureHeaders/SecureHeaders.php b/Http/Middleware/SecureHeaders/SecureHeaders.php new file mode 100644 index 0000000..d297278 --- /dev/null +++ b/Http/Middleware/SecureHeaders/SecureHeaders.php @@ -0,0 +1,327 @@ + [], + 'style' => [], + ]; + + public function __construct(protected array $config = []) + { + } + + public function headers(): array + { + if (! $this->compiled) { + $this->compile(); + } + + return $this->headers; + } + + protected function compile(): void + { + $this->headers = array_merge( + $this->csp(), + $this->expectCT(), + $this->hsts(), + $this->permissionsPolicy(), + $this->miscellaneous(), + $this->clearSiteData(), + ); + + $this->compiled = true; + } + + protected function csp(): array + { + if (isset($this->config['custom-csp'])) { + if (empty($this->config['custom-csp'])) { + return []; + } + + return ['Content-Security-Policy' => $this->config['custom-csp']]; + } + + $config = $this->config['csp'] ?? []; + + if (!($config['enable'] ?? false)) { + return []; + } + + $config['script-src']['nonces'] = self::$nonces['script']; + + $config['style-src']['nonces'] = self::$nonces['style']; + + $csp = new CSPBuilder($config); + + return $csp->getHeaderArray(legacy: false); + } + + /** + * Strict Transport Security. + * + * @return array|string[] + */ + protected function hsts(): array + { + if (! $this->config['hsts']['enable']) { + return []; + } + + $hsts = sprintf('max-age=%s; preload;', $this->config['hsts']['max-age']); + + if ($this->config['hsts']['include-sub-domains']) { + $hsts .= ' includeSubDomains;'; + } + + return ['Strict-Transport-Security' => $hsts]; + } + + protected function expectCT(): array + { + $config = $this->config['expect-ct'] ?? []; + + if (!($config['enable'] ?? false)) { + return []; + } + + $build[] = $this->maxAge(); + + if ($this->config['enforce'] ?? false) { + $build[] = 'enforce'; + } + + if (!empty($this->config['report-uri'])) { + $build[] = $this->reportUri(); + } + + return ['Expect-CT' => implode(separator: ', ', array: array_filter($build))]; + } + + /** + * Generate Clear-Site-Data header. + * + * @return array + */ + protected function clearSiteData(): array + { + $config = $this->config['clear-site-data'] ?? []; + + if (!($config['enable'] ?? false)) { + return []; + } + + if ($config['all'] ?? false) { + return ['"*"']; + } + + $targets = array_intersect_key($config, [ + 'cache' => true, + 'cookies' => true, + 'storage' => true, + 'executionContexts' => true, + ]); + + $needs = array_filter($targets); + + $build = array_map(function (string $directive) { + return sprintf('"%s"', $directive); + }, array_keys($needs)); + + return ['Clear-Site-Data' => implode(separator: ', ', array: $build)]; + } + + protected function permissionsPolicy(): array + { + $config = $this->config['permissions-policy'] ?? []; + + if (!($config['enable'] ?? false)) { + return []; + } + + $build = []; + + foreach ($config as $name => $c) { + if ($name === 'enable') { + continue; + } + + if (empty($val = $this->directive($c))) { + continue; + } + + $build[] = sprintf('%s=%s', $name, $val); + } + + return ['Permissions-Policy' => $build]; + } + + /** + * Get Miscellaneous headers. + * + * @return array + */ + protected function miscellaneous(): array + { + return [ + 'X-Content-Type-Options' => $this->config['x-content-type-options'], + 'X-Download-Options' => $this->config['x-download-options'], + 'X-Frame-Options' => $this->config['x-frame-options'], + 'X-Permitted-Cross-Domain-Policies' => $this->config['x-permitted-cross-domain-policies'], + 'X-Powered-By' => $this->config['x-powered-by'] ?? ($this->config['x-power-by'] ?? + sprintf('CodefyPHP-%s', Application::APP_VERSION)), + 'X-XSS-Protection' => $this->config['x-xss-protection'], + 'Referrer-Policy' => $this->config['referrer-policy'], + 'Server' => $this->config['server'], + 'Cross-Origin-Embedder-Policy' => $this->config['cross-origin-embedder-policy'] ?? '', + 'Cross-Origin-Opener-Policy' => $this->config['cross-origin-opener-policy'] ?? '', + 'Cross-Origin-Resource-Policy' => $this->config['cross-origin-resource-policy'] ?? '', + ]; + } + + protected function maxAge(): string + { + $origin = $this->config['max-age'] ?? 1800; + + // convert to int + $age = intval(value: $origin); + + // prevent negative value + $val = max($age, 0); + + return sprintf('max-age=%d', $val); + } + + /** + * Get report-uri directive. + */ + protected function reportUri(): string + { + $uri = filter_var(value: $this->config['report-uri'], filter: FILTER_VALIDATE_URL); + + if ($uri === false) { + return ''; + } + + return sprintf('report-uri="%s"', $uri); + } + + /** + * Parse a specific permission policy value. + * + * @param array $config + * @return string + */ + protected function directive(array $config): string + { + if ($config['none'] ?? false) { + return '()'; + } elseif ($config['*'] ?? false) { + return '*'; + } + + $origins = $this->origins(origins: $config['origins'] ?? []); + + if ($config['self'] ?? false) { + array_unshift($origins, 'self'); + } + + return sprintf('(%s)', implode(separator: ' ', array: $origins)); + } + + /** + * Get valid origins. + */ + protected function origins(array $origins): array + { + // prevent user leave spaces by mistake + $trimmed = array_map(callback: 'trim', array: $origins); + + // filter array using FILTER_VALIDATE_URL + $filters = filter_var_array(array: $trimmed, options: FILTER_VALIDATE_URL); + + // get valid value + $passes = array_filter(array: $filters); + + // ensure indexes are numerically + $urls = array_values(array: $passes); + + return array_map(callback: function (string $url) { + return sprintf('"%s"', $url); + }, array: $urls); + } + + /** + * Generate random nonce value for the current request. + * + * @throws Exception + */ + public static function nonce(string $target = 'script'): string + { + $nonce = base64_encode(string: bin2hex(string: random_bytes(length: 8))); + + self::$nonces[$target][] = $nonce; + + return $nonce; + } + + /** + * Remove a specific nonce value or flush all nonces for the given target. + * + * @param string|null $target + * @param string|null $nonce + * @return void + */ + public static function removeNonce(string $target = null, string $nonce = null): void + { + if ($target === null) { + self::$nonces['script'] = self::$nonces['style'] = []; + } elseif (isset(self::$nonces[$target])) { + if ($nonce === null) { + self::$nonces[$target] = []; + } elseif (false !== ($idx = array_search(needle: $nonce, haystack: self::$nonces[$target]))) { + unset(self::$nonces[$target][$idx]); + } + } + } +} diff --git a/Http/Middleware/Spam/HoneyPotMiddleware.php b/Http/Middleware/Spam/HoneyPotMiddleware.php new file mode 100644 index 0000000..3613b79 --- /dev/null +++ b/Http/Middleware/Spam/HoneyPotMiddleware.php @@ -0,0 +1,79 @@ +attrName = $attrName; + self::$current = $this; + } + + public static function getField(?string $name = null, ?string $label = null): string + { + $label = is_null__(var: $label) ? 'Honeypot Captcha' : esc_attr(string: $label); + + return sprintf( + '' . "\n", + $name ?? self::$current->attrName, + $label + ); + } + + public static function getHiddenField(?string $name = null): string + { + return sprintf( + '' . "\n", + $name ?? self::$current->attrName, + ); + } + + /** + * @inheritDoc + * @throws Exception + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if (!$this->isValid($request)) { + return JsonResponseFactory::create( + data: 'Form submission error.', + status: 403 + ); + } + + return $handler->handle($request); + } + + private function isValid(ServerRequestInterface $request): bool + { + $method = strtoupper(string: $request->getMethod()); + + if (in_array(needle: $method, haystack: ['GET', 'HEAD', 'CONNECT', 'TRACE', 'OPTIONS'], strict: true)) { + return true; + } + + $data = $request->getParsedBody(); + + return isset($data[$this->attrName]) && $data[$this->attrName] === ''; + } +} diff --git a/Http/Middleware/Spam/ReferrerSpamMiddleware.php b/Http/Middleware/Spam/ReferrerSpamMiddleware.php new file mode 100644 index 0000000..f754f13 --- /dev/null +++ b/Http/Middleware/Spam/ReferrerSpamMiddleware.php @@ -0,0 +1,23 @@ +configContainer->getConfigKey(key: 'referrer-spam.blacklist'), $responseFactory); + } +} From cab8c08efedc64badf0eb59de1bf2789d9870eb0 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:47:56 -0700 Subject: [PATCH 011/161] Edits based on new features in PHP 8.4. Signed-off-by: Joshua Parker --- Migration/Adapter/DbalMigrationAdapter.php | 2 +- Pipeline/PipelineBuilder.php | 4 +- Scheduler/Event/TaskCompleted.php | 2 +- Scheduler/Event/TaskFailed.php | 2 +- Scheduler/Event/TaskSkipped.php | 2 +- Scheduler/Event/TaskStarted.php | 2 +- Scheduler/Schedule.php | 53 +++++++++------------- Scheduler/Traits/ExpressionAware.php | 26 ++--------- Scheduler/Traits/ScheduleValidateAware.php | 3 +- View/FenomView.php | 4 +- 10 files changed, 37 insertions(+), 63 deletions(-) diff --git a/Migration/Adapter/DbalMigrationAdapter.php b/Migration/Adapter/DbalMigrationAdapter.php index 5b277a4..3afd123 100644 --- a/Migration/Adapter/DbalMigrationAdapter.php +++ b/Migration/Adapter/DbalMigrationAdapter.php @@ -52,7 +52,7 @@ public function up(Migration $migration): MigrationAdapter ->values( [ 'version' => $migration->getVersion(), - 'recorded_on' => (new QubusDateTimeImmutable(time: 'now'))->format(format: 'Y-m-d h:i:s') + 'recorded_on' => new QubusDateTimeImmutable(time: 'now')->format(format: 'Y-m-d h:i:s') ] )->execute(); diff --git a/Pipeline/PipelineBuilder.php b/Pipeline/PipelineBuilder.php index d5de34e..2006e7d 100644 --- a/Pipeline/PipelineBuilder.php +++ b/Pipeline/PipelineBuilder.php @@ -4,7 +4,7 @@ namespace Codefy\Framework\Pipeline; -use Codefy\Framework\Application; +use Codefy\Framework\Codefy; final class PipelineBuilder { @@ -24,6 +24,6 @@ public function pipe(callable $pipe): self public function build(): Chainable { - return new Pipeline(Application::$APP->getContainer()); + return new Pipeline(Codefy::$PHP->getContainer()); } } diff --git a/Scheduler/Event/TaskCompleted.php b/Scheduler/Event/TaskCompleted.php index 2988675..f08e14a 100644 --- a/Scheduler/Event/TaskCompleted.php +++ b/Scheduler/Event/TaskCompleted.php @@ -9,7 +9,7 @@ final class TaskCompleted extends BaseEvent { - public const EVENT_NAME = 'task.completed'; + public const string EVENT_NAME = 'task.completed'; private Task $task; diff --git a/Scheduler/Event/TaskFailed.php b/Scheduler/Event/TaskFailed.php index ceaf433..56d1b88 100644 --- a/Scheduler/Event/TaskFailed.php +++ b/Scheduler/Event/TaskFailed.php @@ -9,7 +9,7 @@ final class TaskFailed extends BaseEvent { - public const EVENT_NAME = 'task.failed'; + public const string EVENT_NAME = 'task.failed'; private Task $task; diff --git a/Scheduler/Event/TaskSkipped.php b/Scheduler/Event/TaskSkipped.php index ca60923..797d15e 100644 --- a/Scheduler/Event/TaskSkipped.php +++ b/Scheduler/Event/TaskSkipped.php @@ -8,5 +8,5 @@ final class TaskSkipped extends BaseEvent { - public const EVENT_NAME = 'task.skipped'; + public const string EVENT_NAME = 'task.skipped'; } diff --git a/Scheduler/Event/TaskStarted.php b/Scheduler/Event/TaskStarted.php index f2279ff..4eccfe1 100644 --- a/Scheduler/Event/TaskStarted.php +++ b/Scheduler/Event/TaskStarted.php @@ -9,7 +9,7 @@ final class TaskStarted extends BaseEvent { - public const EVENT_NAME = 'task.started'; + public const string EVENT_NAME = 'task.started'; private Task $task; diff --git a/Scheduler/Schedule.php b/Scheduler/Schedule.php index 91551dc..7246431 100644 --- a/Scheduler/Schedule.php +++ b/Scheduler/Schedule.php @@ -27,20 +27,29 @@ class Schedule { use LiteralAware; - public const SUNDAY = 0; - public const MONDAY = 1; - public const TUESDAY = 2; - public const WEDNESDAY = 3; - public const THURSDAY = 4; - public const FRIDAY = 5; - public const SATURDAY = 6; - + public const int SUNDAY = 0; + public const int MONDAY = 1; + public const int TUESDAY = 2; + public const int WEDNESDAY = 3; + public const int THURSDAY = 4; + public const int FRIDAY = 5; + public const int SATURDAY = 6; + + //phpcs:disable /** @var Processor[] $processors */ - protected array $processors = []; + protected array $processors = [] { + &get => $this->processors; + } - protected array $executedProcessors = []; + public protected(set) array $executedProcessors = [] { + &get => $this->executedProcessors; + } + + public protected(set) array $failedProcessors = [] { + &get => $this->failedProcessors; + } - protected array $failedProcessors = []; + //phpcs:enable public function __construct(public readonly QubusDateTimeZone $timeZone, public readonly Locker $mutex) { @@ -153,16 +162,6 @@ private function pushExecutedProcessor(Processor $processor): Processor return $processor; } - /** - * Get the executed processes. - * - * @return array - */ - public function getExecutedProcessors(): array - { - return $this->executedProcessors; - } - /** * Push a failed process. */ @@ -173,21 +172,13 @@ private function pushFailedProcessor(Processor $processor, Exception $ex): Proce return $processor; } - /** - * Get the failed jobs. - * - * @return FailedProcessor[] - */ - public function getFailedProcessors(): array - { - return $this->failedProcessors; - } - /** * Compile the Task command. */ protected function compileArguments(array $args = []): string { + $compiled = ''; + // Sanitize command arguments. foreach ($args as $key => $value) { $compiled = ' ' . escapeshellarg($key); diff --git a/Scheduler/Traits/ExpressionAware.php b/Scheduler/Traits/ExpressionAware.php index 3a5f648..41972c8 100644 --- a/Scheduler/Traits/ExpressionAware.php +++ b/Scheduler/Traits/ExpressionAware.php @@ -40,7 +40,7 @@ trait ExpressionAware * * @var array */ - protected $fieldsPosition = [ + protected array $fieldsPosition = [ 'minute' => CronExpression::MINUTE, 'hour' => CronExpression::HOUR, 'day' => CronExpression::DAY, @@ -66,19 +66,11 @@ public function cron(string $expression = '* * * * *'): self */ public function filtersPass(): bool { - foreach ($this->filters as $callback) { - if (! $this->call($callback)) { - return false; - } - } - - foreach ($this->rejects as $callback) { - if ($this->call($callback)) { - return false; - } + if (array_any($this->filters, fn($callback) => !$this->call($callback))) { + return false; } - return true; + return array_all($this->rejects, fn($callback) => !$this->call($callback)); } /** @@ -570,7 +562,7 @@ protected function expressionPasses(string|DateTimeZone|null $timezone = null): $now = $now->setTimezone($timezone); if ($this->timezone) { - $taskTimeZone = is_object($this->timezone) && $this->timezone instanceof DateTimeZone + $taskTimeZone = $this->timezone instanceof DateTimeZone ? $this->timezone ->getName() : $this->timezone; @@ -591,14 +583,6 @@ protected function expressionPasses(string|DateTimeZone|null $timezone = null): */ protected function spliceIntoPosition(int $position, string $value): self { - /*if ($this->expression instanceof CronExpression) { - $expression = $this->expression->getExpression(); - } - - if (is_string($this->expression)) { - $expression = $this->expression; - }*/ - $expression = match (true) { $this->expression instanceof CronExpression => $this->expression->getExpression(), is_string($this->expression) => $this->expression diff --git a/Scheduler/Traits/ScheduleValidateAware.php b/Scheduler/Traits/ScheduleValidateAware.php index b0721bb..d8a8c7b 100644 --- a/Scheduler/Traits/ScheduleValidateAware.php +++ b/Scheduler/Traits/ScheduleValidateAware.php @@ -57,8 +57,7 @@ protected static function range(int|string|array|null $value = null, int $min = } } - $value = implode(',', $value); - return $value; + return implode(',', $value); } if ( diff --git a/View/FenomView.php b/View/FenomView.php index 334fd39..f9c5c2d 100644 --- a/View/FenomView.php +++ b/View/FenomView.php @@ -24,9 +24,9 @@ final class FenomView implements Renderer */ public function __construct(protected ConfigContainer $configContainer) { - $this->fenom = (new Fenom( + $this->fenom = new Fenom( provider: new Provider(template_dir: $this->configContainer->getConfigKey(key: 'view.path')) - ))->setCompileDir( + )->setCompileDir( dir: $this->configContainer->getConfigKey(key: 'view.cache') )->setOptions(options: $this->configContainer->getConfigKey(key: 'view.options')); } From 45eaf3434163dad88bdc2e192c4af578b55b7bdd Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:48:25 -0700 Subject: [PATCH 012/161] More libraries moved over from skeleton. Signed-off-by: Joshua Parker --- Support/ArgsParser.php | 73 +++++++++++++++++++++ Support/CodefyMailer.php | 2 +- Support/Paths.php | 6 +- Support/RequestMethod.php | 63 ++++++++++++++++++ Support/SeoFactory.php | 91 ++++++++++++++++++++++++++ Support/StringParser.php | 66 +++++++++++++++++++ Support/Traits/DbTransactionsAware.php | 12 ++-- 7 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 Support/ArgsParser.php create mode 100644 Support/RequestMethod.php create mode 100644 Support/SeoFactory.php create mode 100644 Support/StringParser.php diff --git a/Support/ArgsParser.php b/Support/ArgsParser.php new file mode 100644 index 0000000..15f5b60 --- /dev/null +++ b/Support/ArgsParser.php @@ -0,0 +1,73 @@ + $value) { + if (array_key_exists($key, $defaults) && is_array($defaults[$key]) && is_array($value)) { + $defaults[$key] = self::deepMerge($defaults[$key], $value); + } else { + $defaults[$key] = $value; + } + } + + return $defaults; + } +} diff --git a/Support/CodefyMailer.php b/Support/CodefyMailer.php index b0b8ef5..02cbc8a 100644 --- a/Support/CodefyMailer.php +++ b/Support/CodefyMailer.php @@ -9,5 +9,5 @@ final class CodefyMailer extends QubusMailer { - public const VERSION = Application::APP_VERSION; + public const string VERSION = Application::APP_VERSION; } diff --git a/Support/Paths.php b/Support/Paths.php index c793910..30e00d6 100644 --- a/Support/Paths.php +++ b/Support/Paths.php @@ -21,7 +21,11 @@ */ final class Paths { - private array $paths = []; + //phpcs:disable + private array $paths = [] { + &get => $this->paths; + } + //phpcs:enable /** * @throws TypeException diff --git a/Support/RequestMethod.php b/Support/RequestMethod.php new file mode 100644 index 0000000..a04ad79 --- /dev/null +++ b/Support/RequestMethod.php @@ -0,0 +1,63 @@ + $this->useTransaction; + } + //phpcs:enable /** * Enable transaction in pipeline. @@ -35,7 +39,7 @@ protected function beginTransaction(): void return; } - Codefy::$PHP->getDB()->beginTransaction(); + Codefy::$PHP->getDb()->beginTransaction(); } /** @@ -49,7 +53,7 @@ protected function commitTransaction(): void return; } - Codefy::$PHP->getDB()->commit(); + Codefy::$PHP->getDb()->commit(); } /** @@ -63,6 +67,6 @@ protected function rollbackTransaction(): void return; } - Codefy::$PHP->getDB()->rollback(); + Codefy::$PHP->getDb()->rollback(); } } From 269745b35f003d988c6d0fb82dda14b31a087064 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:48:51 -0700 Subject: [PATCH 013/161] More tests added. Signed-off-by: Joshua Parker --- tests/ApplicationTest.php | 55 ++++++++++++++++++++++++++++ tests/Auth/PermissionTest.php | 24 ++++++------- tests/Auth/RbacTest.php | 66 ++++++++++++++-------------------- tests/Auth/RoleTest.php | 20 +++++------ tests/Auth/UserSessionTest.php | 35 ++++++++++++++++++ tests/Pipes/PipelineTest.php | 4 +-- tests/vendor/bootstrap.php | 1 + 7 files changed, 141 insertions(+), 64 deletions(-) create mode 100644 tests/ApplicationTest.php create mode 100644 tests/Auth/UserSessionTest.php diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php new file mode 100644 index 0000000..6f6aaa5 --- /dev/null +++ b/tests/ApplicationTest.php @@ -0,0 +1,55 @@ +charset; + Assert::assertEquals(expected: 'UTF-8', actual: $charset); +}); + +it(description: 'sets charset value.', closure: function () { + $app = Application::getInstance(); + $app->charset = 'iso-8859-1'; + Assert::assertSame(expected: 'ISO-8859-1', actual: $app->charset); +}); + +it(description: 'gets default locale value.', closure: function () { + $locale = Application::getInstance()->locale; + Assert::assertEquals(expected: 'en', actual: $locale); +}); + +it(description: 'sets locale value.', closure: function () { + $app = Application::getInstance(); + $app->locale = 'es-ES'; + Assert::assertSame(expected: 'es-ES', actual: $app->locale); + + $app->withLocale(locale: 'es-ES'); + Assert::assertSame(expected: 'es-ES', actual: $app->locale); +}); + +it(description: 'gets default controller namespace value.', closure: function () { + $namespace = Application::getInstance()->controllerNamespace; + Assert::assertEquals(expected: 'App\\Infrastructure\\Http\\Controllers', actual: $namespace); +}); + +it(description: 'sets controller namespace value.', closure: function () { + $app = Application::getInstance(); + $app->controllerNamespace = 'Temp\\App\\Http\\Controllers'; + Assert::assertSame(expected: 'Temp\\App\\Http\\Controllers', actual: $app->controllerNamespace); + + $app->withControllerNamespace(namespace: 'Temp\\App\\Http\\Controllers'); + Assert::assertSame(expected: 'Temp\\App\\Http\\Controllers', actual: $app->controllerNamespace); +}); + +it(description: 'gets default booted value.', closure: function () { + $booted = Application::getInstance()->booted; + Assert::assertEquals(expected: false, actual: $booted); +}); + +it(description: 'sets booted value.', closure: function () { + $app = Application::getInstance(); + $app->setBooted(bool: true); + Assert::assertSame(expected: true, actual: $app->booted); +}); diff --git a/tests/Auth/PermissionTest.php b/tests/Auth/PermissionTest.php index 4a81b5d..11cca1c 100644 --- a/tests/Auth/PermissionTest.php +++ b/tests/Auth/PermissionTest.php @@ -6,35 +6,35 @@ $resource = Mockery::mock(\Codefy\Framework\Auth\Rbac\Resource\StorageResource::class); -it('should get the permission name.', function () use ($resource) { +it(description: 'should get the permission name.', closure: function () use ($resource) { $permission = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'admin:edit', + name: 'admin:edit', description: 'Edit permission.', rbacStorageCollection: $resource ); - Assert::assertEquals('admin:edit', $permission->getName()); + Assert::assertEquals('admin:edit', $permission->name); }); -it('should get permission description.', function () use ($resource) { +it(description: 'should get permission description.', closure: function () use ($resource) { $permission = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'admin:edit', + name: 'admin:edit', description: 'Edit permission.', rbacStorageCollection: $resource ); - Assert::assertEquals('Edit permission.', $permission->getDescription()); + Assert::assertEquals('Edit permission.', $permission->description); }); -it('should get children.', function () use ($resource) { +it(description: 'should get children.', closure: function () use ($resource) { $permission2 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'perm:perm2', + name: 'perm:perm2', description: 'desc2', rbacStorageCollection: $resource ); $permission3 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'perm:perm3', + name: 'perm:perm3', description: 'desc3', rbacStorageCollection: $resource ); @@ -43,7 +43,7 @@ $resource->shouldReceive('getPermission')->andReturn($permission2, $permission3); $permission1 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'perm:perm1', + name: 'perm:perm1', description: 'desc1', rbacStorageCollection: $resource ); @@ -64,9 +64,9 @@ Assert::assertEquals(['perm:perm3' => $permission3], $permission1->getChildren()); }); -it('should set and get rule.', function () use ($resource) { +it(description: 'should set and get rule.', closure: function () use ($resource) { $permission = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'admin:edit', + name: 'admin:edit', description: 'Edit permission.', rbacStorageCollection: $resource ); diff --git a/tests/Auth/RbacTest.php b/tests/Auth/RbacTest.php index 1b11d90..65bdeee 100644 --- a/tests/Auth/RbacTest.php +++ b/tests/Auth/RbacTest.php @@ -5,35 +5,27 @@ use Codefy\Framework\Auth\Rbac\Entity\Permission; use Codefy\Framework\Auth\Rbac\Entity\Role; use Codefy\Framework\Auth\Rbac\Rbac; +use Codefy\Framework\Auth\Rbac\Resource\FileResource; use PHPUnit\Framework\Assert; -$resource = Mockery::mock(\Codefy\Framework\Auth\Rbac\Resource\StorageResource::class); +$resource = new FileResource(file: 'rbac.json'); it('should create instance.', function () use ($resource) { - $resource->shouldReceive('load'); - new Rbac($resource); - Assert::assertTrue(true); + Assert::assertTrue(condition: true); }); it('should create role and return Role instance.', function () use ($resource) { - $resource->shouldReceive('load'); - $resource->shouldReceive('addRole') - ->with('role1', 'desc1') - ->andReturn(Mockery::mock(Role::class)); - $rbac = new Rbac($resource); - Assert::assertInstanceOf(Role::class, $rbac->addRole(name: 'role1', description: 'desc1')); + Assert::assertInstanceOf( + expected: Role::class, + actual: $rbac->addRole(name: 'admin', description: 'Administrator') + ); }); it('should create Permission and return Permission instance.', function () use ($resource) { - $resource->shouldReceive('load'); - $resource->shouldReceive('addPermission') - ->with('dashboard:view', 'Access dashboard.') - ->andReturn(Mockery::mock(Permission::class)); - $rbac = new Rbac($resource); Assert::assertInstanceOf( @@ -42,44 +34,38 @@ ); }); -it('should get roles.', function () use ($resource) { - $resource->shouldReceive('load'); - $resource->shouldReceive('getRoles') - ->andReturn([]); - +it('should get empty array for roles.', function () use ($resource) { $rbac = new Rbac($resource); - Assert::assertEquals([], $rbac->getRoles()); + Assert::assertEquals([], $rbac->roles); }); it('should get role.', function () use ($resource) { - $resource->shouldReceive('load'); - $resource->shouldReceive('getRole') - ->with('role1') - ->andReturn(Mockery::mock(Role::class)); - $rbac = new Rbac($resource); - - Assert::assertInstanceOf(Role::class, $rbac->getRole('role1')); + $perm1 = $rbac->addPermission(name: 'create:post', description: 'Can create posts'); + $perm2 = $rbac->addPermission(name: 'moderate:post', description: 'Can moderate posts'); + $perm3 = $rbac->addPermission(name: 'update:post', description: 'Can update posts'); + $perm4 = $rbac->addPermission(name: 'delete:post', description: 'Can delete posts'); + $perm2->addChild($perm3); + $perm2->addChild($perm4); + + $adminRole = $rbac->addRole(name: 'admin'); + $moderatorRole = $rbac->addRole(name: 'moderator'); + $authorRole = $rbac->addRole(name: 'author'); + $adminRole->addChild($moderatorRole); + + Assert::assertInstanceOf(Role::class, $rbac->getRole(name: 'admin')); }); -it('should get permissions.', function () use ($resource) { - $resource->shouldReceive('load'); - $resource->shouldReceive('getPermissions') - ->andReturn([]); - +it('should get empty array for permissions.', function () use ($resource) { $rbac = new Rbac($resource); - Assert::assertEquals([], $rbac->getPermissions()); + Assert::assertEquals(expected: [], actual: $rbac->permissions); }); it('should get permission.', function () use ($resource) { - $resource->shouldReceive('load'); - $resource->shouldReceive('getPermission') - ->with('perm1') - ->andReturn(Mockery::mock(Permission::class)); - $rbac = new Rbac($resource); + $perm1 = $rbac->addPermission(name: 'create:post', description: 'Can create posts'); - Assert::assertInstanceOf(Permission::class, $rbac->getPermission(name: 'perm1')); + Assert::assertInstanceOf(expected: Permission::class, actual: $rbac->getPermission(name: 'create:post')); }); diff --git a/tests/Auth/RoleTest.php b/tests/Auth/RoleTest.php index 477633f..821129f 100644 --- a/tests/Auth/RoleTest.php +++ b/tests/Auth/RoleTest.php @@ -8,33 +8,33 @@ it('should create Role instance.', function () use ($resource) { $role = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole( - roleName: 'admin', + name: 'admin', description: 'Super administrator.', rbacStorageCollection: $resource ); - Assert::assertEquals('admin', $role->getName()); + Assert::assertEquals('admin', $role->name); }); it('should get the role description', function () use ($resource) { $role = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole( - roleName: 'admin', + name: 'admin', description: 'Super administrator.', rbacStorageCollection: $resource ); - Assert::assertEquals('Super administrator.', $role->getDescription()); + Assert::assertEquals('Super administrator.', $role->description); }); it('should get children.', function () use ($resource) { $role2 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole( - roleName: 'role2', + name: 'role2', description: 'desc2', rbacStorageCollection: $resource ); $role3 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole( - roleName: 'role3', + name: 'role3', description: 'desc3', rbacStorageCollection: $resource ); @@ -43,7 +43,7 @@ $resource->shouldReceive('getRole')->andReturn($role2, $role3); $role1 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole( - roleName: 'role1', + name: 'role1', description: 'desc1', rbacStorageCollection: $resource ); @@ -66,13 +66,13 @@ it('should get permissions.', function () use ($resource) { $permission2 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'perm:perm2', + name: 'perm:perm2', description: 'desc2', rbacStorageCollection: $resource ); $permission3 = new \Codefy\Framework\Auth\Rbac\Entity\RbacPermission( - permissionName: 'perm:perm3', + name: 'perm:perm3', description: 'desc3', rbacStorageCollection: $resource ); @@ -81,7 +81,7 @@ $resource->shouldReceive('getPermission')->andReturn($permission2, $permission3); $role1 = new \Codefy\Framework\Auth\Rbac\Entity\RbacRole( - roleName: 'admin', + name: 'admin', description: 'Super administrator.', rbacStorageCollection: $resource ); diff --git a/tests/Auth/UserSessionTest.php b/tests/Auth/UserSessionTest.php new file mode 100644 index 0000000..1e15397 --- /dev/null +++ b/tests/Auth/UserSessionTest.php @@ -0,0 +1,35 @@ +token; + + Assert::assertSame(expected: null, actual: $token); +}); + +it('should throw error when setting token property.', function () { + $usession = new UserSession(); + $usession->token = 'test'; +})->throws(Error::class); + +it('should set token and return a UserSession object.', function () { + $usession = new UserSession(); + $usession->withToken(token: '426d9e8f-b8f7-4be0-a383-324ee3c3ea4d'); + + Assert::assertSame(expected: '426d9e8f-b8f7-4be0-a383-324ee3c3ea4d', actual: $usession->token); + Assert::assertInstanceOf(expected: UserSession::class, actual: $usession); + Assert::assertIsObject($usession); +}); + +it('should be an empty token after clearing.', function () { + $usession = new UserSession(); + $usession->withToken(token: '426d9e8f-b8f7-4be0-a383-324ee3c3ea4d'); + + Assert::assertFalse(condition: $usession->isEmpty()); + + $usession->clear(); + + Assert::assertTrue(condition: $usession->isEmpty()); +}); diff --git a/tests/Pipes/PipelineTest.php b/tests/Pipes/PipelineTest.php index 9dcde8b..f4527e4 100644 --- a/tests/Pipes/PipelineTest.php +++ b/tests/Pipes/PipelineTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Codefy\Framework\Application; +use Codefy\Framework\Codefy; use Codefy\Framework\Pipeline\Pipeline; use Codefy\Framework\Pipeline\PipelineBuilder; use Codefy\Framework\tests\Pipes\PipeFour; @@ -86,8 +87,7 @@ }); it('accepts invokable class as pipe using PipelineBuilder.', function () { - $builder = (new PipelineBuilder()) - ->pipe(new PipeFour()); + $builder = Codefy::$PHP->pipeline->pipe(new PipeFour()); $pipeline = $builder->build(); diff --git a/tests/vendor/bootstrap.php b/tests/vendor/bootstrap.php index 51d7213..adf1144 100644 --- a/tests/vendor/bootstrap.php +++ b/tests/vendor/bootstrap.php @@ -5,6 +5,7 @@ try { return Application::configure(['basePath' => dirname(path: __DIR__, levels: 2)]) + ->withKernels() ->withProviders([ // fill in custom providers ]) From 4b10f799d8798aabc031cda2e805f0f1e2ba9e7b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:49:08 -0700 Subject: [PATCH 014/161] Updated composer packages. Signed-off-by: Joshua Parker --- composer.json | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 249e7c6..dacf2a9 100644 --- a/composer.json +++ b/composer.json @@ -11,15 +11,20 @@ } ], "require": { - "php": ">=8.2", + "php": ">=8.4", "ext-pdo": "*", - "codefyphp/domain-driven-core": "^1", + "codefyphp/domain-driven-core": "^3.0.x-dev", "dragonmantank/cron-expression": "^3", + "guzzlehttp/guzzle": "^7.10", + "melbahja/seo": "^2.1", + "middlewares/cache": "^3.1", + "middlewares/referrer-spam": "^2.1", + "paragonie/csp-builder": "^3", "qubus/cache": "^3", "qubus/error": "^2", "qubus/event-dispatcher": "^3", "qubus/exception": "^3", - "qubus/expressive": "^1", + "qubus/expressive": "^2", "qubus/filesystem": "^3", "qubus/inheritance": "^3", "qubus/injector": "^3", @@ -28,8 +33,8 @@ "qubus/security": "^3", "qubus/support": "^3", "qubus/view": "^2", - "symfony/console": "^6", - "symfony/options-resolver": "^6" + "symfony/console": "^7", + "symfony/options-resolver": "^7" }, "autoload": { "psr-4": { @@ -37,20 +42,20 @@ }, "files": [ "Helpers/core.php", - "Helpers/path.php" + "Helpers/path.php", + "Http/Middleware/Csrf/helpers.php" ] }, "require-dev": { "fenom/fenom": "^3.0", "fenom/providers-collection": "^1.0", "foil/foil": "^0.6.7", - "mockery/mockery": "^1.3.1", - "pestphp/pest": "^1.22", - "pestphp/pest-plugin-mock": "^1.0", - "qubus/qubus-coding-standard": "^1.1" + "mockery/mockery": "^1", + "pestphp/pest": "^3", + "qubus/qubus-coding-standard": "^1" }, "scripts": { - "test": "XDEBUG_MODE=coverage vendor/bin/pest --coverage --min=50 --colors=always", + "test": "vendor/bin/pest", "cs-check": "phpcs", "cs-fix": "phpcbf" }, From 79f9ac36920eb413daf43faf5180a1b47777e834 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:49:25 -0700 Subject: [PATCH 015/161] Updated configuration. Signed-off-by: Joshua Parker --- phpunit.xml | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 2ec8627..08a27fd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,21 +1,17 @@ - - - - ./tests - - - - - ./vendor - ./tests - - - ./ - - + + + + ./tests + + + + + ./ + + + ./vendor + ./tests + + From 02714430b228071b8b0ad59bb4da8d4f4a0ea270 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:49:57 -0700 Subject: [PATCH 016/161] Added new .env files to avoid warnings. Signed-off-by: Joshua Parker --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index a0d7e73..e50a0bc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,13 @@ Scheduler/Tests storage /vendor/ .env +.env.development +.env.local +.env.production +.env.staging .phpcs-cache .php-cs-fixer.cache +.phpunit.cache .phpunit.result.cache composer.lock index.php \ No newline at end of file From 8121584801395831bcb71ed4cbcc5c30a594a28d Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 23 Aug 2025 22:50:21 -0700 Subject: [PATCH 017/161] Updated with PHP 8.4 changes. Signed-off-by: Joshua Parker --- Application.php | 201 ++++++++++++++++++++++++++---------------------- 1 file changed, 111 insertions(+), 90 deletions(-) diff --git a/Application.php b/Application.php index 90f623f..f3e60dd 100644 --- a/Application.php +++ b/Application.php @@ -23,7 +23,7 @@ use Qubus\EventDispatcher\EventDispatcher; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; -use Qubus\Expressive\OrmBuilder; +use Qubus\Expressive\QueryBuilder; use Qubus\Http\Cookies\Factory\HttpCookieFactory; use Qubus\Http\Session\Flash; use Qubus\Http\Session\PhpSession; @@ -46,30 +46,15 @@ use function Qubus\Config\Helpers\env; use function Qubus\Support\Helpers\is_null__; use function rtrim; +use function strtoupper; use const DIRECTORY_SEPARATOR; -/** - * @property-read ServerRequestInterface $request - * @property-read ResponseInterface $response - * @property-read Assets $assets - * @property-read Mailer $mailer - * @property-read PhpSession $session - * @property-read Flash $flash - * @property-read EventDispatcher $event - * @property-read HttpCookieFactory $httpCookie - * @property-read LocalStorage $localStorage - * @property-read ConfigContainer $configContainer - * @property-read PipelineBuilder $pipeline - * @property-read Observer $hook - * @property-read StringHelper $string - * @property-read ArrayHelper $array - */ final class Application extends Container { use InvokerAware; - public const string APP_VERSION = '3.0.0'; + public const string APP_VERSION = '3.0.0-beta.1'; public const string MIN_PHP_VERSION = '8.4'; @@ -83,46 +68,65 @@ final class Application extends Container public static ?Application $APP = null; // phpcs:disable - public string $charset { - get => $this->charset = 'UTF-8'; - set(string $charset) { - $this->charset = $charset; - } + public string $charset = 'UTF-8' { + get => $this->charset; + set(string $charset) => $this->charset = strtoupper($charset); } - public string $locale { - get => $this->locale = 'en'; - set(string $locale) { - $this->locale = $locale; - } + public string $locale = 'en' { + get => $this->locale; + set(string $locale) => $this->locale = $locale; } - // phpcs:enable - public string $controllerNamespace = 'App\\Infrastructure\\Http\\Controllers'; + public string $controllerNamespace = 'App\\Infrastructure\\Http\\Controllers' { + get => $this->controllerNamespace; + set(string $controllerNamespace) => $this->controllerNamespace = $controllerNamespace; + } public static string $ROOT_PATH = ''; - protected string $basePath = ''; + private string $basePath = '' { + get => $this->basePath; + } - protected ?string $appPath = null; + private ?string $appPath = null { + get => $this->appPath; + } - protected array $serviceProviders = []; + private array $serviceProviders = [] { + &get => $this->serviceProviders; + } - protected array $serviceProvidersRegistered = []; + private array $serviceProvidersRegistered = [] { + &get => $this->serviceProvidersRegistered; + } - protected array $baseMiddlewares = []; + private array $baseMiddlewares = [] { + &get => $this->baseMiddlewares; + } - private bool $booted = false; + public private(set) bool $booted = false; - private bool $hasBeenBootstrapped = false; + private bool $hasBeenBootstrapped = false { + get => $this->hasBeenBootstrapped; + } - protected array $bootingCallbacks = []; + private array $bootingCallbacks = [] { + &get => $this->bootingCallbacks; + } - protected array $bootedCallbacks = []; + private array $bootedCallbacks = [] { + &get => $this->bootedCallbacks; + } - protected array $registeredCallbacks = []; + private array $registeredCallbacks = [] { + &get => $this->registeredCallbacks; + } - private array $param = []; + private array $param = [] { + &get => $this->param; + } + // phpcs:enable /** * @throws TypeException @@ -136,7 +140,8 @@ public function __construct(array $params) parent::__construct(InjectorFactory::create(config: $this->coreAliases())); $this->registerBaseBindings(); $this->registerDefaultServiceProviders(); - $this->registerPropertyBindings(); + + Codefy::$PHP = $this::getInstance(); } private function registerBaseBindings(): void @@ -146,38 +151,6 @@ private function registerBaseBindings(): void $this->alias(original: 'app', alias: self::class); } - /** - * Dynamically created properties. - * - * @return void - * @throws TypeException - */ - private function registerPropertyBindings(): void - { - $contracts = [ - 'request' => ServerRequestInterface::class, - 'response' => ResponseInterface::class, - 'assets' => Assets::class, - 'mailer' => Mailer::class, - 'session' => PhpSession::class, - 'flash' => Flash::class, - 'event' => EventDispatcher::class, - 'httpCookie' => HttpCookieFactory::class, - 'localStorage' => Support\LocalStorage::class, - 'configContainer' => ConfigContainer::class, - 'pipeline' => PipelineBuilder::class, - 'hook' => Observer::class, - 'string' => StringHelper::class, - 'array' => ArrayHelper::class, - ]; - - foreach ($contracts as $property => $name) { - $this->{$property} = $this->make(name: $name); - } - - Codefy::$PHP = $this::getInstance(); - } - /** * Infer the application's base directory * from the environment and server. @@ -242,12 +215,12 @@ public function getDbConnection(): Connection } /** - * @return OrmBuilder|null + * @return QueryBuilder|null * @throws Exception */ - public function getDB(): ?OrmBuilder + public function getDb(): ?QueryBuilder { - return new OrmBuilder(connection: $this->getDbConnection()); + return QueryBuilder::fromInstance($this->getDbConnection()); } /** @@ -317,16 +290,11 @@ public function getContainer(): ServiceContainer|ContainerInterface return $this; } - public function setBoot(bool $bool = false): void + public function setBooted(bool $bool = false): void { $this->booted = $bool; } - public function isBooted(): bool - { - return $this->booted; - } - /** * @return void * @throws TypeException @@ -410,7 +378,7 @@ public function registerServiceProvider( $this->markServiceProviderAsRegistered(registered: $registered, serviceProvider: $serviceProvider); // If application is booted, call the boot method on the service provider // if it exists. - if ($this->isBooted()) { + if ($this->booted) { $this->bootServiceProvider(provider: $serviceProvider); }; @@ -508,7 +476,7 @@ public function booted(callable $callback): void { $this->bootedCallbacks[] = $callback; - if ($this->isBooted()) { + if ($this->booted) { $callback($this); } } @@ -723,11 +691,6 @@ public function withControllerNamespace(string $namespace): void $this->controllerNamespace = $namespace; } - public function getControllerNamespace(): string - { - return $this->controllerNamespace; - } - public function withBaseMiddlewares(array $middlewares): void { $this->baseMiddlewares = $middlewares; @@ -926,4 +889,62 @@ public static function getInstance(?string $path = null): self return self::$APP; } + + //phpcs:disable + public private(set) ServerRequestInterface $request { + get => $this->request ?? $this->make(name: ServerRequestInterface::class); + } + + public private(set) ResponseInterface $response { + get => $this->response ?? $this->make(name: ResponseInterface::class); + } + + public private(set) Assets $assets { + get => $this->assets ?? $this->make(name: Assets::class); + } + + public private(set) Mailer $mailer { + get => $this->mailer ?? $this->make(name: Mailer::class); + } + + public private(set) PhpSession $session { + get => $this->session ?? $this->make(name: PhpSession::class); + } + + public private(set) Flash $flash { + get => $this->flash ?? $this->make(name: Flash::class); + } + + public private(set) EventDispatcher $event { + get => $this->event ?? $this->make(name: EventDispatcher::class); + } + + public private(set) HttpCookieFactory $httpCookie { + get => $this->httpCookie ?? $this->make(name: HttpCookieFactory::class); + } + + public private(set) LocalStorage $localStorage { + get => $this->localStorage ?? $this->make(name: LocalStorage::class); + } + + public private(set) ConfigContainer $configContainer { + get => $this->configContainer ?? $this->make(name: ConfigContainer::class); + } + + public private(set) PipelineBuilder $pipeline { + get => $this->pipeline ?? $this->make(name: PipelineBuilder::class); + } + + public private(set) Observer $hook { + get => $this->hook ?? $this->make(name: Observer::class); + } + + public private(set) StringHelper $string { + get => $this->string ?? $this->make(name: StringHelper::class); + } + + public private(set) ArrayHelper $array { + get => $this->array ?? $this->make(name: ArrayHelper::class); + } + //phpcs:disable } From ff2f879f7943a8a96f5f2b31ad83079c01729b22 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 16:21:31 -0700 Subject: [PATCH 018/161] Updated cookie lifetimes. Signed-off-by: Joshua Parker --- .../Middleware/Auth/UserSessionMiddleware.php | 12 +++++++----- .../Csrf/CsrfProtectionMiddleware.php | 11 ++++++----- Http/Middleware/Csrf/CsrfTokenMiddleware.php | 19 ++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Http/Middleware/Auth/UserSessionMiddleware.php b/Http/Middleware/Auth/UserSessionMiddleware.php index e6af5de..2b70e57 100644 --- a/Http/Middleware/Auth/UserSessionMiddleware.php +++ b/Http/Middleware/Auth/UserSessionMiddleware.php @@ -28,19 +28,21 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface try { $userDetails = $request->getAttribute(AuthenticationMiddleware::AUTH_ATTRIBUTE); + $expire = isset($request->getParsedBody()['rememberme']) + && $request->getParsedBody()['rememberme'] === 'yes' + ? $this->configContainer->getConfigKey(key: 'cookies.remember') + : $this->configContainer->getConfigKey(key: 'cookies.lifetime'); + $this->sessionService::$options = [ 'cookie-name' => 'USERSESSID', - 'cookie-lifetime' => (int) $this->configContainer->getConfigKey( - key: 'cookies.lifetime', - default: 86400 - ), + 'cookie-lifetime' => (int) $expire, ]; $session = $this->sessionService->makeSession($request); /** @var UserSession $user */ $user = $session->get(type: UserSession::class); $user - ->withToken(token: $userDetails->token); + ->withToken(token: $userDetails->token); $request = $request->withAttribute(self::SESSION_ATTRIBUTE, $user); diff --git a/Http/Middleware/Csrf/CsrfProtectionMiddleware.php b/Http/Middleware/Csrf/CsrfProtectionMiddleware.php index 317c511..c85eb85 100644 --- a/Http/Middleware/Csrf/CsrfProtectionMiddleware.php +++ b/Http/Middleware/Csrf/CsrfProtectionMiddleware.php @@ -78,15 +78,16 @@ private function tokensMatch(ServerRequestInterface $request): bool */ private function fetchToken(ServerRequestInterface $request): string { - $token = $request->getAttribute(CsrfTokenMiddleware::SESSION_ATTRIBUTE); + /** @var CsrfSession $csrf */ + $csrf = $request->getAttribute(CsrfTokenMiddleware::CSRF_SESSION_ATTRIBUTE); // Ensure the token stored previously by the CsrfTokenMiddleware is present and has a valid format. if ( - is_string($token) && - ctype_alnum($token) && - strlen($token) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') + is_string($csrf->csrfToken()) && + ctype_alnum($csrf->csrfToken()) && + strlen($csrf->csrfToken()) === $this->configContainer->getConfigKey(key: 'csrf.csrf_token_length') ) { - return $token; + return $csrf->csrfToken(); } return ''; diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/Http/Middleware/Csrf/CsrfTokenMiddleware.php index b805803..c889058 100644 --- a/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -19,7 +19,7 @@ class CsrfTokenMiddleware implements MiddlewareInterface { use CsrfTokenAware; - public const string SESSION_ATTRIBUTE = 'CSRF_TOKEN'; + public const string CSRF_SESSION_ATTRIBUTE = 'CSRF_TOKEN'; public static CsrfTokenMiddleware $current; @@ -36,9 +36,9 @@ public function __construct(protected ConfigContainer $configContainer, protecte public static function getField(): string { return sprintf( - '' . "\n", - self::$current->getFieldAttr(), - self::$current->token + '' . "\n", + self::$current->getFieldAttr(), + self::$current->token ); } @@ -73,15 +73,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); } + /** @var CsrfSession $csrf */ + $csrf = $session->get(CsrfSession::class); + $csrf + ->withCsrfToken($this->token); + $response = $handler->handle( $request - ->withAttribute(self::SESSION_ATTRIBUTE, $this->token) + ->withAttribute(self::CSRF_SESSION_ATTRIBUTE, $csrf) ); - /** @var CsrfSession $csrf */ - $csrf = $session->get(CsrfSession::class); - $csrf->withCsrfToken($this->token); - return $this->sessionService->commitSession($response, $session); } catch (\Exception $e) { return $handler->handle($request); From e903d8cf324dbc2d2a072cc5a5892508931b6e03 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 16:42:25 -0700 Subject: [PATCH 019/161] Fixed issue with returning SessionEntity. Signed-off-by: Joshua Parker --- Http/Middleware/Auth/UserSessionMiddleware.php | 1 - Http/Middleware/Csrf/CsrfTokenMiddleware.php | 1 - 2 files changed, 2 deletions(-) diff --git a/Http/Middleware/Auth/UserSessionMiddleware.php b/Http/Middleware/Auth/UserSessionMiddleware.php index 2b70e57..b85a435 100644 --- a/Http/Middleware/Auth/UserSessionMiddleware.php +++ b/Http/Middleware/Auth/UserSessionMiddleware.php @@ -39,7 +39,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ]; $session = $this->sessionService->makeSession($request); - /** @var UserSession $user */ $user = $session->get(type: UserSession::class); $user ->withToken(token: $userDetails->token); diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/Http/Middleware/Csrf/CsrfTokenMiddleware.php index c889058..887d1a4 100644 --- a/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -73,7 +73,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); } - /** @var CsrfSession $csrf */ $csrf = $session->get(CsrfSession::class); $csrf ->withCsrfToken($this->token); From fc8f146eaced0bb778fd0c2a7ea2254c79dc040e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 16:45:56 -0700 Subject: [PATCH 020/161] Fixed implementing SessionEntity in CsrfSession.php. Signed-off-by: Joshua Parker --- Http/Middleware/Auth/UserSessionMiddleware.php | 1 + Http/Middleware/Csrf/CsrfSession.php | 4 +++- Http/Middleware/Csrf/CsrfTokenMiddleware.php | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Http/Middleware/Auth/UserSessionMiddleware.php b/Http/Middleware/Auth/UserSessionMiddleware.php index b85a435..2b70e57 100644 --- a/Http/Middleware/Auth/UserSessionMiddleware.php +++ b/Http/Middleware/Auth/UserSessionMiddleware.php @@ -39,6 +39,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface ]; $session = $this->sessionService->makeSession($request); + /** @var UserSession $user */ $user = $session->get(type: UserSession::class); $user ->withToken(token: $userDetails->token); diff --git a/Http/Middleware/Csrf/CsrfSession.php b/Http/Middleware/Csrf/CsrfSession.php index df3f6f3..ab02675 100644 --- a/Http/Middleware/Csrf/CsrfSession.php +++ b/Http/Middleware/Csrf/CsrfSession.php @@ -4,9 +4,11 @@ namespace Codefy\Framework\Http\Middleware\Csrf; +use Qubus\Http\Session\SessionEntity; + use function Qubus\Support\Helpers\is_null__; -class CsrfSession +class CsrfSession implements SessionEntity { public ?string $csrfToken = null; diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/Http/Middleware/Csrf/CsrfTokenMiddleware.php index 887d1a4..c889058 100644 --- a/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -73,6 +73,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $request = $request->withHeader($this->configContainer->getConfigKey(key: 'csrf.header'), $this->token); } + /** @var CsrfSession $csrf */ $csrf = $session->get(CsrfSession::class); $csrf ->withCsrfToken($this->token); From 4fa3ccc303903cce28d5d3047ed5e6fe6cfd6f92 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 17:12:25 -0700 Subject: [PATCH 021/161] Updated properties in Application.php. Signed-off-by: Joshua Parker --- Application.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Application.php b/Application.php index f3e60dd..80790a4 100644 --- a/Application.php +++ b/Application.php @@ -68,11 +68,25 @@ final class Application extends Container public static ?Application $APP = null; // phpcs:disable + /** + * @var string Charset of the application. + */ public string $charset = 'UTF-8' { get => $this->charset; set(string $charset) => $this->charset = strtoupper($charset); } + /** + * @var string Language of the application. + */ + public string $language = 'en-US' { + get => $this->language; + set(string $language) => $this->language = strtoupper($language); + } + + /** + * @var string Language of translatable source files. + */ public string $locale = 'en' { get => $this->locale; set(string $locale) => $this->locale = $locale; From 14c4d99069a17cbf8a82d14868bfab9cfd9fd8d9 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 18:20:19 -0700 Subject: [PATCH 022/161] Trying a fix continuous generation of CSRF token. Signed-off-by: Joshua Parker --- Http/Middleware/Csrf/CsrfTokenMiddleware.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/Http/Middleware/Csrf/CsrfTokenMiddleware.php index c889058..2626bc9 100644 --- a/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -65,6 +65,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $this->token = $this->prepareToken(session: $session); + if ( + $request->hasHeader($this->configContainer->getConfigKey(key: 'csrf.header')) + && $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')) !== '' + ) { + $this->token = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); + } + /** * If true, the application will do a header check, if not, * it will expect data submitted via an HTML form tag. From d7fc543dbb44610407a4e8207dcb39de092a5e79 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 18:38:16 -0700 Subject: [PATCH 023/161] Set token property to nullable. Signed-off-by: Joshua Parker --- Http/Middleware/Csrf/CsrfTokenMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/Http/Middleware/Csrf/CsrfTokenMiddleware.php index 2626bc9..8c9c6e1 100644 --- a/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -23,7 +23,7 @@ class CsrfTokenMiddleware implements MiddlewareInterface public static CsrfTokenMiddleware $current; - private string $token; + private ?string $token = null; public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService) { From 2e9775ab29519ec3783db99dedd210e33f145627 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 21:56:16 -0700 Subject: [PATCH 024/161] Added rate limiter and middleware. Signed-off-by: Joshua Parker --- Http/Middleware/ThrottleMiddleware.php | 57 ++++++++++ Http/Throttle/Condition.php | 32 ++++++ Http/Throttle/Interval.php | 26 +++++ Http/Throttle/RateException.php | 37 +++++++ Http/Throttle/RateLimiter.php | 148 +++++++++++++++++++++++++ 5 files changed, 300 insertions(+) create mode 100644 Http/Middleware/ThrottleMiddleware.php create mode 100644 Http/Throttle/Condition.php create mode 100644 Http/Throttle/Interval.php create mode 100644 Http/Throttle/RateException.php create mode 100644 Http/Throttle/RateLimiter.php diff --git a/Http/Middleware/ThrottleMiddleware.php b/Http/Middleware/ThrottleMiddleware.php new file mode 100644 index 0000000..281d63b --- /dev/null +++ b/Http/Middleware/ThrottleMiddleware.php @@ -0,0 +1,57 @@ +rateLimiter->add( + new Condition( + $this->configContainer->getConfigKey(key: 'throttle.ttl'), + $this->configContainer->getConfigKey(key: 'throttle.max_attempts'), + ) + ); + + $identifier = $request->getHeaderLine($this->configContainer->getConfigKey(key: 'csrf.header')); + + try { + $this->rateLimiter->increment($identifier); + } catch (RateException $e) { + $condition = $e->condition; + return JsonResponseFactory::create( + data: sprintf( + 'You can only make %d requests in %d seconds', + $condition->limit, + $condition->ttl + ), + status: 404 + ); + } + + return $handler->handle($request); + } +} diff --git a/Http/Throttle/Condition.php b/Http/Throttle/Condition.php new file mode 100644 index 0000000..e248d54 --- /dev/null +++ b/Http/Throttle/Condition.php @@ -0,0 +1,32 @@ + $this->ttl; + set(int $value) => $this->ttl = $value; + }, + public private(set) int $limit { + get => $this->limit; + set(int $value) => $this->limit = $value; + } + ) { + } + + /** + * @return string + */ + public function __toString(): string + { + return (string) $this->ttl; + } +} diff --git a/Http/Throttle/Interval.php b/Http/Throttle/Interval.php new file mode 100644 index 0000000..e8c2c0f --- /dev/null +++ b/Http/Throttle/Interval.php @@ -0,0 +1,26 @@ + $this->expiresAt; + set(int $value) => $this->expiresAt = time() + $value; + }, + public int $count = 0 { + get => $this->count; + set(int $value) => $this->count = $value; + } + ) { + } +} diff --git a/Http/Throttle/RateException.php b/Http/Throttle/RateException.php new file mode 100644 index 0000000..f1a85aa --- /dev/null +++ b/Http/Throttle/RateException.php @@ -0,0 +1,37 @@ +condition; + } + } + + /** + * @param string $identifier + * @param Condition $condition + */ + public function __construct(string $identifier, Condition $condition) + { + $this->condition = $condition; + + parent::__construct( + sprintf( + 'Rate %d in %d seconds was exceeded by "%s"', + $condition->limit, + $condition->ttl, + $identifier + ) + ); + } +} diff --git a/Http/Throttle/RateLimiter.php b/Http/Throttle/RateLimiter.php new file mode 100644 index 0000000..770ff48 --- /dev/null +++ b/Http/Throttle/RateLimiter.php @@ -0,0 +1,148 @@ +cache = $cache; + } + + /** + * @param Condition $condition + * @return RateLimiter + */ + public function add(Condition $condition): RateLimiter + { + foreach ($this->conditions as $existing) { + if ($condition->ttl === $existing->ttl) { + throw new LogicException( + sprintf('This instance already has a condition with a ttl of %d', $existing->ttl) + ); + } elseif ($condition->ttl > $existing->ttl && $condition->limit <= $existing->limit) { + throw new LogicException( + sprintf( + 'Adding a condition of ttl %d, limit %d will never be reached due to existing condition of ttl %d, limit %d', + $condition->ttl, + $condition->limit, + $existing->ttl, + $existing->limit + ) + ); + } elseif ($condition->ttl < $existing->ttl && $condition->limit >= $existing->limit) { + throw new LogicException( + sprintf( + 'Adding a condition of ttl %d, limit %d will prevent existing condition of ttl %d, limit %d from being reached', + $condition->ttl, + $condition->limit, + $existing->ttl, + $existing->limit + ) + ); + } + } + $this->conditions[] = $condition; + + return $this; + } + + /** + * @param string $identifier + * @param int $count + * @return $this + * @throws RateException + * @throws InvalidArgumentException + */ + public function increment(string $identifier, int $count = 1): RateLimiter + { + foreach ($this->conditions as $condition) { + $item = $this->getItem($identifier, $condition); + if ($item->isHit()) { + /** @var Interval $interval */ + $interval = $item->get(); + } else { + $interval = new Interval($condition->ttl); + $item->expiresAfter($condition->ttl); + } + $interval->count += $count; + $item->set($interval); + $this->cache->save($item); + + if ($interval->count > $condition->limit) { + throw new RateException($identifier, $condition); + } + } + + return $this; + } + + /** + * @param string $identifier + * @return Interval[] + * @throws InvalidArgumentException + */ + public function getIntervals(string $identifier): array + { + $intervals = []; + foreach ($this->conditions as $condition) { + $item = $this->getItem($identifier, $condition); + $interval = $item->isHit() ? $item->get() : new Interval($condition->ttl); + $intervals[] = $interval; + } + + return $intervals; + } + + /** + * Reset counter + * + * @param string $identifier + * @return $this + * @throws InvalidArgumentException + */ + public function reset(string $identifier): RateLimiter + { + foreach ($this->conditions as $condition) { + $key = sprintf('%s-%s', $identifier, $condition); + $this->cache->deleteItem(md5($key)); + } + + return $this; + } + + /** + * @param string $identifier + * @param Condition $condition + * @return CacheItemInterface + * @throws InvalidArgumentException + */ + private function getItem(string $identifier, Condition $condition): CacheItemInterface + { + $key = sprintf('%s-%s', $identifier, $condition); + return $this->cache->getItem(md5($key)); + } +} From e3c01416f04ef13b754fa75d91039f84231dbeea Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 22:30:12 -0700 Subject: [PATCH 025/161] Fixed issue with Rate Limiting not expiring. Signed-off-by: Joshua Parker --- Http/Throttle/RateLimiter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Throttle/RateLimiter.php b/Http/Throttle/RateLimiter.php index 770ff48..dd1ac3b 100644 --- a/Http/Throttle/RateLimiter.php +++ b/Http/Throttle/RateLimiter.php @@ -86,8 +86,8 @@ public function increment(string $identifier, int $count = 1): RateLimiter $interval = $item->get(); } else { $interval = new Interval($condition->ttl); - $item->expiresAfter($condition->ttl); } + $item->expiresAfter($condition->ttl); $interval->count += $count; $item->set($interval); $this->cache->save($item); From cdeb80c600f86e85ccf7e264ab8de36b19ccaf60 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 24 Aug 2025 22:32:50 -0700 Subject: [PATCH 026/161] Updated status code for ThrottleMiddleware. Signed-off-by: Joshua Parker --- Http/Middleware/ThrottleMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Middleware/ThrottleMiddleware.php b/Http/Middleware/ThrottleMiddleware.php index 281d63b..d05e590 100644 --- a/Http/Middleware/ThrottleMiddleware.php +++ b/Http/Middleware/ThrottleMiddleware.php @@ -48,7 +48,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $condition->limit, $condition->ttl ), - status: 404 + status: 401 ); } From 68f93d15089d03d0bd0b38bd025cbe72bd766ba7 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 25 Aug 2025 13:36:49 -0700 Subject: [PATCH 027/161] Added content caching middleware. Signed-off-by: Joshua Parker --- Http/Middleware/ContentCacheMiddleware.php | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Http/Middleware/ContentCacheMiddleware.php diff --git a/Http/Middleware/ContentCacheMiddleware.php b/Http/Middleware/ContentCacheMiddleware.php new file mode 100644 index 0000000..2bc402d --- /dev/null +++ b/Http/Middleware/ContentCacheMiddleware.php @@ -0,0 +1,107 @@ +handle($request); + + if (RequestMethod::GET !== $request->getMethod()) { + return $response; + } + + if ($html = $this->getCachedResponseHtml($request)) { + return $this->buildResponse($html, $response); + } + + if (200 === $response->getStatusCode()) { + $this->cacheResponse($request, $response); + } + + return $response; + } + + protected function buildResponse($html, ResponseInterface $response): ResponseInterface + { + $body = new Stream('php://memory', 'w'); + $body->write($html); + + return $response->withBody($body); + } + + protected function createKeyFromRequest(RequestInterface $request): string + { + $cacheItemKeyFactory = function (RequestInterface $request): string { + static $key = null; + if (null === $key) { + $uri = $request->getUri(); + $slugify = new Slugify(); + $key = $slugify->slugify( + string: trim( + string: $uri->getPath(), + characters: '/' + ) . ($uri->getQuery() ? '?' . $uri->getQuery() : '') + ); + } + + return md5($key); + }; + + return $cacheItemKeyFactory($request); + } + + /** + * @throws InvalidArgumentException + */ + protected function getCachedResponseHtml(RequestInterface $request): ResponseInterface + { + return $this + ->cacheItemPool + ->getItem($this->createKeyFromRequest($request)) + ->get(); + } + + protected function createCacheItem(string $key): Closure + { + return fn() => new Item($key); + } + + protected function cacheResponse(RequestInterface $request, ResponseInterface $response): void + { + $cacheItem = $this->createCacheItem($this->createKeyFromRequest($request)); + $value = (string) $response->getBody(); + $cacheItem->set($value); + + $this + ->cacheItemPool + ->save($cacheItem); + } +} From ee8d0d4ff1b88cd5df124d0538e31fec857cf080 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 25 Aug 2025 13:51:44 -0700 Subject: [PATCH 028/161] Fixed bug in content cache middleware. Signed-off-by: Joshua Parker --- Http/Middleware/ContentCacheMiddleware.php | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Http/Middleware/ContentCacheMiddleware.php b/Http/Middleware/ContentCacheMiddleware.php index 2bc402d..9e86bd4 100644 --- a/Http/Middleware/ContentCacheMiddleware.php +++ b/Http/Middleware/ContentCacheMiddleware.php @@ -4,7 +4,6 @@ namespace Codefy\Framework\Http\Middleware; -use Closure; use Cocur\Slugify\Slugify; use Codefy\Framework\Support\RequestMethod; use Laminas\Diactoros\Stream; @@ -81,7 +80,7 @@ protected function createKeyFromRequest(RequestInterface $request): string /** * @throws InvalidArgumentException */ - protected function getCachedResponseHtml(RequestInterface $request): ResponseInterface + protected function getCachedResponseHtml(RequestInterface $request) { return $this ->cacheItemPool @@ -89,19 +88,14 @@ protected function getCachedResponseHtml(RequestInterface $request): ResponseInt ->get(); } - protected function createCacheItem(string $key): Closure - { - return fn() => new Item($key); - } - protected function cacheResponse(RequestInterface $request, ResponseInterface $response): void { - $cacheItem = $this->createCacheItem($this->createKeyFromRequest($request)); + $cacheItem = new Item($this->createKeyFromRequest($request)); $value = (string) $response->getBody(); $cacheItem->set($value); $this - ->cacheItemPool - ->save($cacheItem); + ->cacheItemPool + ->save($cacheItem); } } From 89f98938e867751a7db0d0ea7056d57e11c29580 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 25 Aug 2025 20:45:21 -0700 Subject: [PATCH 029/161] Fixed line length. Signed-off-by: Joshua Parker --- Http/Throttle/RateLimiter.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Http/Throttle/RateLimiter.php b/Http/Throttle/RateLimiter.php index dd1ac3b..ec808f1 100644 --- a/Http/Throttle/RateLimiter.php +++ b/Http/Throttle/RateLimiter.php @@ -46,7 +46,8 @@ public function add(Condition $condition): RateLimiter } elseif ($condition->ttl > $existing->ttl && $condition->limit <= $existing->limit) { throw new LogicException( sprintf( - 'Adding a condition of ttl %d, limit %d will never be reached due to existing condition of ttl %d, limit %d', + 'Adding a condition of ttl %d, limit %d will never be + reached due to existing condition of ttl %d, limit %d', $condition->ttl, $condition->limit, $existing->ttl, @@ -56,7 +57,8 @@ public function add(Condition $condition): RateLimiter } elseif ($condition->ttl < $existing->ttl && $condition->limit >= $existing->limit) { throw new LogicException( sprintf( - 'Adding a condition of ttl %d, limit %d will prevent existing condition of ttl %d, limit %d from being reached', + 'Adding a condition of ttl %d, limit %d will prevent existing + condition of ttl %d, limit %d from being reached', $condition->ttl, $condition->limit, $existing->ttl, From 3cb38e993b472be3ef91253db9159ef73b7e287c Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 25 Aug 2025 20:45:55 -0700 Subject: [PATCH 030/161] Renamed database static function. Signed-off-by: Joshua Parker --- Helpers/core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helpers/core.php b/Helpers/core.php index e40a002..386577d 100644 --- a/Helpers/core.php +++ b/Helpers/core.php @@ -102,7 +102,7 @@ function env(string $key, mixed $default = null): mixed * @return QueryBuilder|null * @throws Exception */ -function orm(): ?QueryBuilder +function qb(): ?QueryBuilder { return Codefy::$PHP->getDb(); } From 9736ef78efbb9b220f1bf341fc799c77448bd008 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 25 Aug 2025 21:47:44 -0700 Subject: [PATCH 031/161] Added debugbar middleware, js, css, and html minify middleware. Signed-off-by: Joshua Parker --- Http/Middleware/CssMinifierMiddleware.php | 17 ++ Http/Middleware/DebugBarMiddleware.php | 239 +++++++++++++++++++++ Http/Middleware/HtmlMinifierMiddleware.php | 17 ++ Http/Middleware/JsMinifierMiddleware.php | 17 ++ composer.json | 2 + 5 files changed, 292 insertions(+) create mode 100644 Http/Middleware/CssMinifierMiddleware.php create mode 100644 Http/Middleware/DebugBarMiddleware.php create mode 100644 Http/Middleware/HtmlMinifierMiddleware.php create mode 100644 Http/Middleware/JsMinifierMiddleware.php diff --git a/Http/Middleware/CssMinifierMiddleware.php b/Http/Middleware/CssMinifierMiddleware.php new file mode 100644 index 0000000..91bc078 --- /dev/null +++ b/Http/Middleware/CssMinifierMiddleware.php @@ -0,0 +1,17 @@ +debugBarRenderer = $debugBarRenderer; + $this->responseFactory = $responseFactory; + $this->streamFactory = $streamFactory ?? new StreamFactory(); + } + + /** + * @inheritDoc + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ($staticFile = $this->getStaticFile($request->getUri())) { + return $staticFile; + } + + $response = $handler->handle($request); + + if ($this->shouldReturnResponse($request, $response)) { + return $response; + } + + if ($this->isHtmlResponse($response)) { + return $this->attachDebugBarToHtmlResponse($response); + } + + return $this->prepareHtmlResponseWithDebugBar($response); + } + + private function shouldReturnResponse(ServerRequestInterface $request, ResponseInterface $response): bool + { + $forceHeaderValue = $request->getHeaderLine(self::FORCE_KEY); + $forceCookieValue = $request->getCookieParams()[self::FORCE_KEY] ?? ''; + $forceAttributeValue = $request->getAttribute(self::FORCE_KEY, ''); + $isForceEnable = in_array('true', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true); + $isForceDisable = in_array('false', [$forceHeaderValue, $forceCookieValue, $forceAttributeValue], true); + + return $isForceDisable + || (!$isForceEnable && ($this->isRedirect($response) || !$this->isHtmlAccepted($request))); + } + + private function prepareHtmlResponseWithDebugBar(ResponseInterface $response): ResponseInterface + { + $head = $this->debugBarRenderer->renderHead(); + $body = $this->debugBarRenderer->render(); + $outResponseBody = $this->serializeResponse($response); + $template = '%s

DebugBar

Response:

%s
%s'; + $escapedOutResponseBody = htmlspecialchars($outResponseBody); + $result = sprintf($template, $head, $escapedOutResponseBody, $body); + + $stream = $this->streamFactory->createStream($result); + + return $this->responseFactory->createResponse() + ->withBody($stream) + ->withAddedHeader('Content-type', 'text/html'); + } + + private function attachDebugBarToHtmlResponse(ResponseInterface $response): ResponseInterface + { + $head = $this->debugBarRenderer->renderHead(); + $body = $this->debugBarRenderer->render(); + $responseBody = $response->getBody(); + + if (! $responseBody->eof() && $responseBody->isSeekable()) { + $responseBody->seek(0, SEEK_END); + } + $responseBody->write($head . $body); + + return $response; + } + + private function getStaticFile(UriInterface $uri): ?ResponseInterface + { + $path = $this->extractPath($uri); + + if (!str_starts_with($path, $this->debugBarRenderer->getBaseUrl())) { + return null; + } + + $pathToFile = substr($path, strlen($this->debugBarRenderer->getBaseUrl())); + + $fullPathToFile = $this->debugBarRenderer->getBasePath() . $pathToFile; + + if (!file_exists($fullPathToFile)) { + return null; + } + + $contentType = $this->getContentTypeByFileName($fullPathToFile); + $stream = $this->streamFactory->createStreamFromResource(fopen($fullPathToFile, 'rb')); + + return $this->responseFactory->createResponse() + ->withBody($stream) + ->withAddedHeader('Content-type', $contentType); + } + + private function extractPath(UriInterface $uri): string + { + return $uri->getPath(); + } + + private function getContentTypeByFileName(string $filename): string + { + $ext = pathinfo($filename, PATHINFO_EXTENSION); + + $map = [ + 'css' => 'text/css', + 'js' => 'text/javascript', + 'otf' => 'font/opentype', + 'eot' => 'application/vnd.ms-fontobject', + 'svg' => 'image/svg+xml', + 'ttf' => 'application/font-sfnt', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + ]; + + return $map[$ext] ?? 'text/plain'; + } + + private function isHtmlResponse(ResponseInterface $response): bool + { + return $this->isHtml($response, 'Content-Type'); + } + + private function isHtmlAccepted(ServerRequestInterface $request): bool + { + return $this->isHtml($request, 'Accept'); + } + + private function isHtml(MessageInterface $message, string $headerName): bool + { + return str_contains($message->getHeaderLine($headerName), 'text/html'); + } + + private function isRedirect(ResponseInterface $response): bool + { + $statusCode = $response->getStatusCode(); + + return $statusCode >= 300 && $statusCode < 400 && $response->getHeaderLine('Location') !== ''; + } + + private function serializeResponse(ResponseInterface $response): string + { + $reasonPhrase = $response->getReasonPhrase(); + $headers = $this->serializeHeaders($response->getHeaders()); + $format = 'HTTP/%s %d%s%s%s'; + + if (! empty($headers)) { + $headers = "\r\n" . $headers; + } + + $headers .= "\r\n\r\n"; + + return sprintf( + $format, + $response->getProtocolVersion(), + $response->getStatusCode(), + ($reasonPhrase ? ' ' . $reasonPhrase : ''), + $headers, + $response->getBody() + ); + } + + /** + * @param array> $headers + */ + private function serializeHeaders(array $headers): string + { + $lines = []; + foreach ($headers as $header => $values) { + $normalized = $this->filterHeader($header); + foreach ($values as $value) { + $lines[] = sprintf('%s: %s', $normalized, $value); + } + } + + return implode("\r\n", $lines); + } + + private function filterHeader(string $header): string + { + $filtered = str_replace('-', ' ', $header); + $filtered = ucwords($filtered); + return str_replace(' ', '-', $filtered); + } +} diff --git a/Http/Middleware/HtmlMinifierMiddleware.php b/Http/Middleware/HtmlMinifierMiddleware.php new file mode 100644 index 0000000..70bba6c --- /dev/null +++ b/Http/Middleware/HtmlMinifierMiddleware.php @@ -0,0 +1,17 @@ + Date: Tue, 26 Aug 2025 06:38:46 -0700 Subject: [PATCH 032/161] Bumped version value. Signed-off-by: Joshua Parker --- Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application.php b/Application.php index 80790a4..54d3477 100644 --- a/Application.php +++ b/Application.php @@ -54,7 +54,7 @@ final class Application extends Container { use InvokerAware; - public const string APP_VERSION = '3.0.0-beta.1'; + public const string APP_VERSION = '3.0.0-beta.2'; public const string MIN_PHP_VERSION = '8.4'; From 26d14856074b4693d1a2e7a5de2bea81b8fc7803 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 26 Aug 2025 17:43:46 -0700 Subject: [PATCH 033/161] Fixed nullable in SecureHeaders.php. Signed-off-by: Joshua Parker --- Http/Middleware/SecureHeaders/SecureHeaders.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Middleware/SecureHeaders/SecureHeaders.php b/Http/Middleware/SecureHeaders/SecureHeaders.php index d297278..7aafc60 100644 --- a/Http/Middleware/SecureHeaders/SecureHeaders.php +++ b/Http/Middleware/SecureHeaders/SecureHeaders.php @@ -312,7 +312,7 @@ public static function nonce(string $target = 'script'): string * @param string|null $nonce * @return void */ - public static function removeNonce(string $target = null, string $nonce = null): void + public static function removeNonce(?string $target = null, ?string $nonce = null): void { if ($target === null) { self::$nonces['script'] = self::$nonces['style'] = []; From 6a78b0be7ec02bf2e5c5bd1295c862faa296741e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 28 Aug 2025 22:11:13 -0700 Subject: [PATCH 034/161] Changed to clear() method for unsetting session on logout. Signed-off-by: Joshua Parker --- Http/Middleware/Auth/ExpireUserSessionMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Middleware/Auth/ExpireUserSessionMiddleware.php b/Http/Middleware/Auth/ExpireUserSessionMiddleware.php index a3f1df5..d99bf82 100644 --- a/Http/Middleware/Auth/ExpireUserSessionMiddleware.php +++ b/Http/Middleware/Auth/ExpireUserSessionMiddleware.php @@ -38,7 +38,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** @var UserSession $user */ $user = $session->get(type: UserSession::class); $user - ->withToken(token: null); + ->clear(); $request = $request->withAttribute(self::SESSION_ATTRIBUTE, $session); From 6fcd053cb96973347cdd307339022bc59a13f214 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 30 Aug 2025 16:05:09 -0700 Subject: [PATCH 035/161] Small changes and package upgrades. Signed-off-by: Joshua Parker --- Console/Commands/MakeCommand.php | 4 ++-- Console/Commands/PasswordHashCommand.php | 4 ++++ Http/HttpClient.php | 23 ++++++++++------------- Http/Kernel.php | 4 ++-- Http/Middleware/ThrottleMiddleware.php | 2 +- Scheduler/FailedProcessor.php | 20 ++++++++------------ Scheduler/Stack.php | 4 ++-- Scheduler/Traits/ExpressionAware.php | 1 - composer.json | 5 ++--- 9 files changed, 31 insertions(+), 36 deletions(-) diff --git a/Console/Commands/MakeCommand.php b/Console/Commands/MakeCommand.php index 5acddad..a7a6a65 100644 --- a/Console/Commands/MakeCommand.php +++ b/Console/Commands/MakeCommand.php @@ -15,7 +15,7 @@ class MakeCommand extends ConsoleCommand { use MakeCommandAware; - protected const FILE_EXTENSION = '.php'; + protected const string FILE_EXTENSION = '.php'; private array $errors = []; @@ -30,7 +30,7 @@ class MakeCommand extends ConsoleCommand protected string $help = 'Command which can generate a class file from a set of predefined stub files'; /* @var array Stubs */ - private const STUBS = [ + private const array STUBS = [ 'controller' => 'App\Infrastructure\Http\Controllers', 'repository' => 'App\Infrastructure\Persistence\Repository', 'provider' => 'App\Infrastructure\Providers', diff --git a/Console/Commands/PasswordHashCommand.php b/Console/Commands/PasswordHashCommand.php index c051902..a834db1 100644 --- a/Console/Commands/PasswordHashCommand.php +++ b/Console/Commands/PasswordHashCommand.php @@ -7,6 +7,7 @@ use Codefy\Framework\Application; use Codefy\Framework\Console\ConsoleCommand; use Codefy\Framework\Support\Password; +use Qubus\Exception\Exception; class PasswordHashCommand extends ConsoleCommand { @@ -23,6 +24,9 @@ public function __construct(protected Application $codefy) parent::__construct(codefy: $codefy); } + /** + * @throws Exception + */ public function handle(): int { $password = $this->getArgument(key: 'password'); diff --git a/Http/HttpClient.php b/Http/HttpClient.php index 1c7c271..9972301 100644 --- a/Http/HttpClient.php +++ b/Http/HttpClient.php @@ -13,14 +13,12 @@ use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; use Psr\SimpleCache\InvalidArgumentException; -use Qubus\EventDispatcher\ActionFilter\Action; -use Qubus\EventDispatcher\ActionFilter\Filter; use Qubus\Exception\Exception; use Qubus\Http\Factories\JsonResponseFactory; -use ReflectionException; use function func_get_args; use function parse_url; +use function Qubus\Security\Helpers\__observer; use function Qubus\Support\Helpers\is_null__; class HttpClient extends GuzzleClient @@ -76,7 +74,6 @@ public static function factory(): self * * @throws InvalidArgumentException * @throws Exception - * @throws ReflectionException * @throws \Exception|GuzzleException */ #[\Override] public function request(string $method, $uri = '', array $options = []): ResponseInterface @@ -91,7 +88,7 @@ public static function factory(): self * Default: 10. * @param string|UriInterface $uri URI object or string. */ - 'timeout' => Filter::getInstance()->applyFilter('http.request.timeout', 10, $uri), + 'timeout' => __observer()->filter->applyFilter('http.request.timeout', 10, $uri), /** * Filters the number of seconds to wait while trying to connect to a server. * Use 0 to wait indefinitely. Default: 10. @@ -99,7 +96,7 @@ public static function factory(): self * @param float $connect_timeout Number of seconds. Default: 10. * @param string|UriInterface $uri URI object or string. */ - 'connect_timeout' => Filter::getInstance()->applyFilter('http.request.connect.timeout', 10, $uri), + 'connect_timeout' => __observer()->filter->applyFilter('http.request.connect.timeout', 10, $uri), /** * Filters the version of the HTTP protocol used in a request. * @@ -107,14 +104,14 @@ public static function factory(): self * Default: 1.1. * @param string|UriInterface $uri URI object or string. */ - 'version' => Filter::getInstance()->applyFilter('http.request.version', '1.1', $uri), + 'version' => __observer()->filter->applyFilter('http.request.version', '1.1', $uri), /** * Filters the redirect behavior of a request. * * @param bool|array $allow_redirects The redirect behavior of a request. Default: false. * @param string|UriInterface $uri URI object or string. */ - 'allow_redirects ' => Filter::getInstance()->applyFilter( + 'allow_redirects ' => __observer()->filter->applyFilter( 'http.request.allow.redirects', false, $uri @@ -142,7 +139,7 @@ public static function factory(): self * @param array $parsedArgs An array of HTTP request arguments. * @param string|UriInterface $uri URI object or string. */ - $parsedArgs = Filter::getInstance()->applyFilter('http.request.args', $parsedArgs, $uri); + $parsedArgs = __observer()->filter->applyFilter('http.request.args', $parsedArgs, $uri); /** * Filters the preemptive return value of an HTTP request. * @@ -159,7 +156,7 @@ public static function factory(): self * @param array $parsedArgs HTTP request arguments. * @param string|UriInterface $uri URI object or string. */ - $preempt = Filter::getInstance()->applyFilter('http.request.preempt', false, $parsedArgs, $uri); + $preempt = __observer()->filter->applyFilter('http.request.preempt', false, $parsedArgs, $uri); if ($preempt !== false) { return $preempt; } @@ -167,7 +164,7 @@ public static function factory(): self $parsedUrl = parse_url($uri); if (empty($parsedUrl) || !isset($parsedUrl['scheme'])) { $response = new HttpRequestError('A valid URL was not provided.', 405); - Action::getInstance()->doAction( + __observer()->action->doAction( 'http_api_debug', $response, 'response', @@ -193,7 +190,7 @@ public static function factory(): self * @param array $parsedArgs HTTP request arguments. * @param string|UriInterface $uri URI object or string. */ - Action::getInstance()->doAction( + __observer()->action->doAction( 'http_api_debug', $response, 'response', @@ -209,6 +206,6 @@ public static function factory(): self * @param array $parsedArgs HTTP request arguments. * @param string|UriInterface $uri URI object or string. */ - return Filter::getInstance()->applyFilter('http.request.response', $response, $parsedArgs, $uri); + return __observer()->filter->applyFilter('http.request.response', $response, $parsedArgs, $uri); } } diff --git a/Http/Kernel.php b/Http/Kernel.php index 8a70ee0..da15a9d 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -40,7 +40,7 @@ public function __construct(Application $codefy, Router $router) { $this->codefy = $codefy; $this->router = $router; - $this->router->setBaseMiddleware(middleware: $this->codefy->getBaseMiddlewares()); + $this->router->baseMiddleware = $this->codefy->getBaseMiddlewares(); $this->router->setBasePath(basePath: router_basepath(path: public_path())); $this->router->setDefaultNamespace(namespace: $this->codefy->controllerNamespace); @@ -92,7 +92,7 @@ public function boot(): bool ); } - __observer()->action->doAction('kernel.preboot'); + __observer()->action->doAction('kernel_preboot'); if (! $this->codefy->hasBeenBootstrapped()) { $this->codefy->bootstrapWith(bootstrappers: $this->bootstrappers()); diff --git a/Http/Middleware/ThrottleMiddleware.php b/Http/Middleware/ThrottleMiddleware.php index d05e590..fd4fe9d 100644 --- a/Http/Middleware/ThrottleMiddleware.php +++ b/Http/Middleware/ThrottleMiddleware.php @@ -7,12 +7,12 @@ use Codefy\Framework\Http\Throttle\Condition; use Codefy\Framework\Http\Throttle\RateException; use Codefy\Framework\Http\Throttle\RateLimiter; +use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Qubus\Config\ConfigContainer; -use Qubus\Exception\Exception; use Qubus\Http\Factories\JsonResponseFactory; use function sprintf; diff --git a/Scheduler/FailedProcessor.php b/Scheduler/FailedProcessor.php index 7ae4734..2484ea7 100644 --- a/Scheduler/FailedProcessor.php +++ b/Scheduler/FailedProcessor.php @@ -13,17 +13,13 @@ final class FailedProcessor * @param Processor $processor * @param Exception $exception */ - public function __construct(private Processor $processor, private Exception $exception) - { - } - - public function getProcessor(): Processor - { - return $this->processor; - } - - public function getException(): Exception - { - return $this->exception; + public function __construct( + private Processor $processor { + get => $this->processor; + }, + private Exception $exception { + get => $this->exception; + } + ) { } } diff --git a/Scheduler/Stack.php b/Scheduler/Stack.php index f7c47e2..a4fb047 100644 --- a/Scheduler/Stack.php +++ b/Scheduler/Stack.php @@ -26,9 +26,9 @@ class Stack protected array $options = []; - protected EventDispatcher $dispatcher; + protected ?EventDispatcher $dispatcher = null; - protected Locker $mutex; + protected ?Locker $mutex = null; protected DateTimeZone|string|null $timezone; diff --git a/Scheduler/Traits/ExpressionAware.php b/Scheduler/Traits/ExpressionAware.php index 41972c8..5617246 100644 --- a/Scheduler/Traits/ExpressionAware.php +++ b/Scheduler/Traits/ExpressionAware.php @@ -21,7 +21,6 @@ use function func_get_args; use function implode; use function is_array; -use function is_object; use function is_string; use function Qubus\Support\Helpers\is_false__; use function Qubus\Support\Helpers\is_null__; diff --git a/composer.json b/composer.json index 392d02a..84783a3 100644 --- a/composer.json +++ b/composer.json @@ -15,14 +15,13 @@ "ext-pdo": "*", "codefyphp/domain-driven-core": "^3.0.x-dev", "dragonmantank/cron-expression": "^3", - "guzzlehttp/guzzle": "^7.10", "melbahja/seo": "^2.1", "middlewares/cache": "^3.1", "middlewares/minifier": "^2.1", "middlewares/referrer-spam": "^2.1", "paragonie/csp-builder": "^3", "php-debugbar/php-debugbar": "^2.2", - "qubus/cache": "^3", + "qubus/cache": "^4", "qubus/error": "^2", "qubus/event-dispatcher": "^3", "qubus/exception": "^3", @@ -31,7 +30,7 @@ "qubus/inheritance": "^3", "qubus/injector": "^3", "qubus/mail": "^4", - "qubus/router": "^3", + "qubus/router": "^4", "qubus/security": "^3", "qubus/support": "^3", "qubus/view": "^2", From 364f521bc40e6069062a1b0aabd8c45151ff2149 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 30 Aug 2025 16:06:11 -0700 Subject: [PATCH 036/161] Updated readme min PHP. Signed-off-by: Joshua Parker --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index feb4743..ab83863 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ none of the features and instead use the interfaces to build your own implementa ## 📍 Requirement -- PHP 8.2+ +- PHP 8.4+ - Additional constraints based on which components are used. ## 🏆 Highlighted Features From d94a77e82768e586d22afd07b1019988ed406052 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 30 Aug 2025 16:06:52 -0700 Subject: [PATCH 037/161] Bumped version value. Signed-off-by: Joshua Parker --- Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application.php b/Application.php index 54d3477..cab61bd 100644 --- a/Application.php +++ b/Application.php @@ -54,7 +54,7 @@ final class Application extends Container { use InvokerAware; - public const string APP_VERSION = '3.0.0-beta.2'; + public const string APP_VERSION = '3.0.0-beta.3'; public const string MIN_PHP_VERSION = '8.4'; From 52e79afcf7bf01b57a3778339a454baa5f3dde0e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 30 Aug 2025 22:43:31 -0700 Subject: [PATCH 038/161] Updated composer packages. Signed-off-by: Joshua Parker --- composer.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 84783a3..ff828a4 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "paragonie/csp-builder": "^3", "php-debugbar/php-debugbar": "^2.2", "qubus/cache": "^4", - "qubus/error": "^2", + "qubus/error": "^3", "qubus/event-dispatcher": "^3", "qubus/exception": "^3", "qubus/expressive": "^2", @@ -32,8 +32,8 @@ "qubus/mail": "^4", "qubus/router": "^4", "qubus/security": "^3", - "qubus/support": "^3", - "qubus/view": "^2", + "qubus/support": "^4", + "qubus/view": "^3", "symfony/console": "^7", "symfony/options-resolver": "^7" }, @@ -52,8 +52,7 @@ "fenom/providers-collection": "^1.0", "foil/foil": "^0.6.7", "mockery/mockery": "^1", - "pestphp/pest": "^3", - "qubus/qubus-coding-standard": "^1" + "qubus/qubus-coding-standard": "^2" }, "scripts": { "test": "vendor/bin/pest", From fc20570526a8db1859f766b56a3cc199608c7fde Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 31 Aug 2025 07:45:48 -0700 Subject: [PATCH 039/161] Added new RouteList command. Signed-off-by: Joshua Parker --- Console/Commands/RouteListCommand.php | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Console/Commands/RouteListCommand.php diff --git a/Console/Commands/RouteListCommand.php b/Console/Commands/RouteListCommand.php new file mode 100644 index 0000000..df2240c --- /dev/null +++ b/Console/Commands/RouteListCommand.php @@ -0,0 +1,59 @@ +output); + $table->setHeaders([ + "Domain", + "Method", + "Uri", + "Name", + "Middleware", + ]); + + $routes = $this->codefy->make(name: 'router'); + + foreach ($routes as $route) { + $table->addRow([ + $route->getDomain(), + implode('| ', $route->methods), + $route->uri, + $route->name, + implode(', ', $route->gatherMiddlewares()), + ]); + } + + $table->render(); + + // return value is important when using CI + // to fail the build when the command fails + // 0 = success, other values = fail + return ConsoleCommand::SUCCESS; + } +} From 59adad22af1d825110002a28bf82c31579d275bd Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 31 Aug 2025 08:33:17 -0700 Subject: [PATCH 040/161] Downgraded symfony console for bug testing. Signed-off-by: Joshua Parker --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ff828a4..8a33349 100644 --- a/composer.json +++ b/composer.json @@ -34,8 +34,8 @@ "qubus/security": "^3", "qubus/support": "^4", "qubus/view": "^3", - "symfony/console": "^7", - "symfony/options-resolver": "^7" + "symfony/console": "^6", + "symfony/options-resolver": "^6" }, "autoload": { "psr-4": { From 11f138c3794bbbb2ea4e0e4849fe15a599ace8c5 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 31 Aug 2025 13:54:30 -0700 Subject: [PATCH 041/161] Upgraded symfony/console. Signed-off-by: Joshua Parker --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 8a33349..ff828a4 100644 --- a/composer.json +++ b/composer.json @@ -34,8 +34,8 @@ "qubus/security": "^3", "qubus/support": "^4", "qubus/view": "^3", - "symfony/console": "^6", - "symfony/options-resolver": "^6" + "symfony/console": "^7", + "symfony/options-resolver": "^7" }, "autoload": { "psr-4": { From 2a788ccb0e3ea5df808debf826910c4e03e2d327 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 21:02:24 -0700 Subject: [PATCH 042/161] Removed RouteList command. Signed-off-by: Joshua Parker --- Console/Commands/RouteListCommand.php | 59 --------------------------- 1 file changed, 59 deletions(-) delete mode 100644 Console/Commands/RouteListCommand.php diff --git a/Console/Commands/RouteListCommand.php b/Console/Commands/RouteListCommand.php deleted file mode 100644 index df2240c..0000000 --- a/Console/Commands/RouteListCommand.php +++ /dev/null @@ -1,59 +0,0 @@ -output); - $table->setHeaders([ - "Domain", - "Method", - "Uri", - "Name", - "Middleware", - ]); - - $routes = $this->codefy->make(name: 'router'); - - foreach ($routes as $route) { - $table->addRow([ - $route->getDomain(), - implode('| ', $route->methods), - $route->uri, - $route->name, - implode(', ', $route->gatherMiddlewares()), - ]); - } - - $table->render(); - - // return value is important when using CI - // to fail the build when the command fails - // 0 = success, other values = fail - return ConsoleCommand::SUCCESS; - } -} From 9fba0b5f457bb18021c14c85c96b50af6e18f7e6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 21:03:24 -0700 Subject: [PATCH 043/161] Set PDO as a singleton. Signed-off-by: Joshua Parker --- Providers/PdoServiceProvider.php | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Providers/PdoServiceProvider.php b/Providers/PdoServiceProvider.php index 1ce5488..b07b8e6 100644 --- a/Providers/PdoServiceProvider.php +++ b/Providers/PdoServiceProvider.php @@ -29,21 +29,23 @@ public function register(): void $config->getConfigKey(key: "database.connections.{$default}.host") ); - $this->codefy->define(name: PDO::class, args: [ - ':dsn' => $dsn, - ':username' => $config->getConfigKey( - key: "database.connections.{$default}.username" - ), - ':password' => $config->getConfigKey( - key: "database.connections.{$default}.password" - ), - ':options' => [ - PDO::ATTR_EMULATE_PREPARES => false, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_PERSISTENT => false, - PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" - ], - ]); + $this->codefy->singleton(PDO::class, function () use ($dsn, $config, $default) { + return new PDO( + $dsn, + $config->getConfigKey( + key: "database.connections.{$default}.username" + ), + $config->getConfigKey( + key: "database.connections.{$default}.password" + ), + [ + PDO::ATTR_EMULATE_PREPARES => $config->getConfigKey(key: "database.pdo.emulate_prepares"), + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_PERSISTENT => $config->getConfigKey(key: "database.pdo.persistent"), + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" + ] + ); + }); $this->codefy->share(nameOrInstance: PDO::class); } From 0a2837bfa5ff18c38099b7ec512d41606a1d23c2 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 21:08:55 -0700 Subject: [PATCH 044/161] Property hooks on BaseController. Signed-off-by: Joshua Parker --- Http/BaseController.php | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Http/BaseController.php b/Http/BaseController.php index 2052b7f..1f4f705 100644 --- a/Http/BaseController.php +++ b/Http/BaseController.php @@ -15,22 +15,18 @@ class BaseController extends Controller implements RoutingController { public function __construct( - protected SessionService $sessionService, - protected Router $router, - protected Renderer $view, + protected SessionService $sessionService { + get => $this->sessionService; + }, + protected Router $router { + get => $this->router; + }, + protected Renderer $view { + get => $this->view; + }, ) { } - /** - * Gets the view instance. - * - * @return Renderer - */ - public function getView(): Renderer - { - return $this->view; - } - /** * Sets the view instance. * From e401c1ac76853205178c20a9da066ec30c119144 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 21:09:26 -0700 Subject: [PATCH 045/161] Removed route function and small changes. Signed-off-by: Joshua Parker --- Helpers/core.php | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/Helpers/core.php b/Helpers/core.php index 386577d..43eff50 100644 --- a/Helpers/core.php +++ b/Helpers/core.php @@ -13,9 +13,6 @@ use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; -use Qubus\Routing\Exceptions\NamedRouteNotFoundException; -use Qubus\Routing\Exceptions\RouteParamFailedConstraintException; -use Qubus\Routing\Router; use ReflectionException; use function dirname; @@ -185,7 +182,7 @@ function mail(string|array $to, string $subject, string $message, array $headers // Set X-Mailer header $xMailer = __observer()->filter->applyFilter( 'mail.xmailer', - sprintf('CodefyPHP Framework %s', Application::APP_VERSION) + sprintf('CodefyPHP Framework %s', Codefy::$PHP::APP_VERSION) ); $instance = $instance->withXMailer(xmailer: $xMailer); @@ -210,19 +207,3 @@ function mail(string|array $to, string $subject, string $message, array $headers return false; } } - -/** - * Generate url's from named routes. - * - * @param string $name Name of the route. - * @param array $params Data parameters. - * @return string The url. - * @throws NamedRouteNotFoundException - * @throws RouteParamFailedConstraintException - */ -function route(string $name, array $params = []): string -{ - /** @var Router $route */ - $route = app('router'); - return $route->url($name, $params); -} From 518a9ae7eb59fea7aa0e2db921c37a760f10c82f Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 21:18:28 -0700 Subject: [PATCH 046/161] Added Emitter and Emitter middleware. Signed-off-by: Joshua Parker --- Application.php | 22 ++ Contracts/Http/Kernel.php | 15 +- Http/Emitter/BaseEmitter.php | 18 ++ Http/Emitter/ContentRange.php | 135 ++++++++++++ Http/Emitter/Emitter.php | 30 +++ Http/Emitter/Exceptions/EmitterException.php | 11 + .../HeadersAlreadySentException.php | 32 +++ .../Exceptions/PreviousOutputException.php | 9 + Http/Emitter/HttpUtil.php | 75 +++++++ Http/Emitter/SapiEmitter.php | 32 +++ Http/Emitter/SapiStreamEmitter.php | 192 ++++++++++++++++++ Http/Emitter/Traits/EmitterTraitAware.php | 129 ++++++++++++ Http/Kernel.php | 33 ++- Http/Middleware/EmitterMiddleware.php | 29 +++ 14 files changed, 743 insertions(+), 19 deletions(-) create mode 100644 Http/Emitter/BaseEmitter.php create mode 100644 Http/Emitter/ContentRange.php create mode 100644 Http/Emitter/Emitter.php create mode 100644 Http/Emitter/Exceptions/EmitterException.php create mode 100644 Http/Emitter/Exceptions/HeadersAlreadySentException.php create mode 100644 Http/Emitter/Exceptions/PreviousOutputException.php create mode 100644 Http/Emitter/HttpUtil.php create mode 100644 Http/Emitter/SapiEmitter.php create mode 100644 Http/Emitter/SapiStreamEmitter.php create mode 100644 Http/Emitter/Traits/EmitterTraitAware.php create mode 100644 Http/Middleware/EmitterMiddleware.php diff --git a/Application.php b/Application.php index cab61bd..26ed139 100644 --- a/Application.php +++ b/Application.php @@ -5,6 +5,7 @@ namespace Codefy\Framework; use Codefy\Framework\Configuration\ApplicationBuilder; +use Codefy\Framework\Contracts\Http\Kernel; use Codefy\Framework\Factory\FileLoggerFactory; use Codefy\Framework\Factory\FileLoggerSmtpFactory; use Codefy\Framework\Pipeline\PipelineBuilder; @@ -25,6 +26,8 @@ use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; use Qubus\Http\Cookies\Factory\HttpCookieFactory; +use Qubus\Http\ServerRequestFactory; +use Qubus\Http\ServerRequestFactory as ServerRequest; use Qubus\Http\Session\Flash; use Qubus\Http\Session\PhpSession; use Qubus\Inheritance\InvokerAware; @@ -803,6 +806,25 @@ protected function coreAliases(): array ]; } + /** + * @throws \Exception + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + /** @var Kernel $kernel */ + $kernel = $this->make(name: Kernel::class); + + return $kernel->handle($request); + } + + public function handleRequest(ServerRequestInterface $request): void + { + /** @var Kernel $kernel */ + $kernel = $this->make(name: Kernel::class); + + $kernel->boot($request); + } + /** * Load environment file(s). * diff --git a/Contracts/Http/Kernel.php b/Contracts/Http/Kernel.php index 0d29cac..f156c45 100644 --- a/Contracts/Http/Kernel.php +++ b/Contracts/Http/Kernel.php @@ -5,6 +5,8 @@ namespace Codefy\Framework\Contracts\Http; use Codefy\Framework\Application; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; interface Kernel { @@ -13,10 +15,19 @@ interface Kernel */ public function codefy(): Application; + /** + * Handle a server request. + * + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function handle(ServerRequestInterface $request): ResponseInterface; + /** * Kernel boots the application. * - * @return bool + * @param ServerRequestInterface $request + * @return void */ - public function boot(): bool; + public function boot(ServerRequestInterface $request): void; } diff --git a/Http/Emitter/BaseEmitter.php b/Http/Emitter/BaseEmitter.php new file mode 100644 index 0000000..82a7320 --- /dev/null +++ b/Http/Emitter/BaseEmitter.php @@ -0,0 +1,18 @@ +setStart($start) + ->setEnd($end); + } + + /** + * Get the unit in which ranges are specified. This is usually bytes. + * + * @return string + */ + public function getUnit(): string + { + return $this->unit; + } + + /** + * Set the unit in which ranges are specified. This is usually bytes. + * + * @param string $unit The unit in which ranges are specified. This is + * usually bytes. + * @return ContentRange + */ + public function setUnit(string $unit): ContentRange + { + $this->unit = $unit; + return $this; + } + + /** + * Get the beginning of the request range. + * + * @return int + */ + public function getStart(): int + { + return $this->start; + } + + /** + * Set the beginning of the request range. + * + * @param int $start the beginning of the request range. + * @return ContentRange + * @throws EmitterException + */ + public function setStart(int $start): ContentRange + { + if ($start < 0) { + throw new EmitterException("Range start value must be positive integer"); + } + + $this->start = $start; + + return $this; + } + + /** + * Get an integer in the given unit indicating + * the end of the requested range. + * + * @return int + */ + public function getEnd(): int + { + return $this->end; + } + + /** + * Set an integer in the given unit indicating + * the end of the requested range. + * + * @param int $end An integer in the given unit indicating the + * end of the requested range. + * @return ContentRange + */ + public function setEnd(int $end): ContentRange + { + if ($end < 0) { + throw new EmitterException( + "Range end value must be positive integer" + ); + } + + $this->end = $end; + return $this; + } + + /** + * Get the total size of the document. + * + * @return int|null + */ + public function getSize(): ?int + { + return $this->size; + } + + /** + * Set the total size of the document. + * + * @param int|null $size The total size of the document. + * @return self + */ + public function setSize(?int $size): self + { + $this->size = $size; + return $this; + } +} diff --git a/Http/Emitter/Emitter.php b/Http/Emitter/Emitter.php new file mode 100644 index 0000000..26247b2 --- /dev/null +++ b/Http/Emitter/Emitter.php @@ -0,0 +1,30 @@ + $this->headersSentFile; + }, + public private(set) int $headersSentLine { + get => $this->headersSentLine; + }, + int $code = 0, + ?Throwable $previous = null + ) { + $msg = sprintf('Headers already sent in file %s on line %s.', $headersSentFile, $headersSentLine); + parent::__construct($msg, $code, $previous); + } +} diff --git a/Http/Emitter/Exceptions/PreviousOutputException.php b/Http/Emitter/Exceptions/PreviousOutputException.php new file mode 100644 index 0000000..257e2b8 --- /dev/null +++ b/Http/Emitter/Exceptions/PreviousOutputException.php @@ -0,0 +1,9 @@ +hasHeader('Content-Length')) { + return $response; + } + + $responseBody = $response->getBody(); + + // PSR-7 indicates int OR null for the stream size; for null values, + // we will not auto-inject the Content-Length. + if ($responseBody->getSize() !== null) { + $response = $response->withHeader('Content-Length', (string) $responseBody->getSize()); + } + + return $response; + } + + /** + * Cleans or flushes output buffers up to target level. + * + * Resulting level can be greater than target level if + * a non-removable buffer has been encountered. + * + * @param int $maxBufferLevel The target output buffering level + * @param bool $flush Whether to flush or clean the buffers + */ + public static function closeOutputBuffers(int $maxBufferLevel, bool $flush): void + { + $status = ob_get_status(full_status: true); + $level = count($status); + $flags = PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE); + + while ( + $level-- > $maxBufferLevel + && isset($status[$level]) + && ($status[$level]['del'] ?? ! isset($status[$level]['flags']) + || $flags === ($status[$level]['flags'] & $flags)) + ) { + if ($flush) { + ob_end_flush(); + } else { + ob_end_clean(); + } + } + } +} diff --git a/Http/Emitter/SapiEmitter.php b/Http/Emitter/SapiEmitter.php new file mode 100644 index 0000000..d7c179c --- /dev/null +++ b/Http/Emitter/SapiEmitter.php @@ -0,0 +1,32 @@ +assertNoPreviousOutput(); + $this->emitHeaders($response); + $this->emitStatusLine($response); + $this->emitBody($response); + $this->closeConnection(); + } + + /** + * Emit the response body + * + * @param ResponseInterface $response + */ + private function emitBody(ResponseInterface $response): void + { + echo $response->getBody(); + } +} diff --git a/Http/Emitter/SapiStreamEmitter.php b/Http/Emitter/SapiStreamEmitter.php new file mode 100644 index 0000000..2074fa1 --- /dev/null +++ b/Http/Emitter/SapiStreamEmitter.php @@ -0,0 +1,192 @@ +[\w]+)\s+(?P\d+)-(?P\d+)\/(?P\d+|\*)/'; + + /** + * Maximum output buffering size for each iteration. + */ + protected int $maxBufferSize = 8192; + + + /** + * Get the value of max buffer size + * + * @return integer + */ + public function getMaxBufferSize(): int + { + return $this->maxBufferSize; + } + + /** + * Set the value of max buffer size + * + * @param int $maxBufferSize + * @return SapiStreamEmitter + * @throws EmitterException + */ + public function setMaxBufferSize(int $maxBufferSize): SapiStreamEmitter + { + if (!$maxBufferSize < 1) { + throw new EmitterException('Buffer size must be a positive integer'); + } + + $this->maxBufferSize = $maxBufferSize; + return $this; + } + + /** + * @inheritDoc + */ + public function emit(ResponseInterface $response): void + { + $this->assertNoPreviousOutput(); + $this->emitHeaders($response); + $this->emitStatusLine($response); + flush(); + $this->emitStream($response); + $this->closeConnection(); + } + + /** + * Emit response body as a stream + * + * @param ResponseInterface $response + * @return void + */ + private function emitStream(ResponseInterface $response): void + { + $range = $this->getContentRange($response); + + if ($range && $range->getUnit() == 'bytes') { + $this->emitBodyRange($response, $range); + return; + } + + $this->emitBody($response); + } + + /** + * Emit the response body by max buffer size + * + * @param ResponseInterface $response + */ + private function emitBody(ResponseInterface $response): void + { + $body = $response->getBody(); + + if ($body->isSeekable()) { + $body->rewind(); + } + + if (!$body->isReadable()) { + echo $body; + return; + } + + while (!$body->eof()) { + echo $body->read($this->getMaxBufferSize()); + + if (connection_status() != CONNECTION_NORMAL) { + // Connection is broken + // Stop emitting the rest of the stream + break; + } + } + } + + /** + * Emit the range of the response body by max buffer size. + * + * @param ResponseInterface $response + * @param ContentRange $range + * @return void + */ + private function emitBodyRange( + ResponseInterface $response, + ContentRange $range + ): void { + $start = $range->getStart(); + $end = $range->getEnd(); + + $body = $response->getBody(); + $length = $end - $start + 1; + + if ($body->isSeekable()) { + $body->seek($start); + $start = 0; + } + + if (!$body->isReadable()) { + echo substr($body->getContents(), $start, $length); + return; + } + + $remaining = $length; + + while ($remaining > 0 && !$body->eof()) { + $contents = $body->read( + $remaining >= $this->getMaxBufferSize() + ? $this->getMaxBufferSize() + : $remaining + ); + + echo $contents; + + if (connection_status() != CONNECTION_NORMAL) { + // Connection is broken + // Stop emitting the rest of the stream + break; + } + + $remaining -= strlen($contents); + } + } + + /** + * Get ContentRange + * + * Parses the Content-Range header line from the response and generates + * ContentRange instance. + * + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 + * + * @param ResponseInterface $response + * @return ContentRange|null + */ + private function getContentRange(ResponseInterface $response): ?ContentRange + { + $headerLine = $response->getHeaderLine('Content-Range'); + + if ( + !$headerLine + || !preg_match(self::CONTENT_PATTERN_REGEX, $headerLine, $matches) + ) { + return null; + } + + return new ContentRange( + (int) $matches['start'], + (int) $matches['end'], + $matches['size'] === '*' ? null : (int) $matches['size'], + $matches['unit'] + ); + } +} diff --git a/Http/Emitter/Traits/EmitterTraitAware.php b/Http/Emitter/Traits/EmitterTraitAware.php new file mode 100644 index 0000000..8324ecd --- /dev/null +++ b/Http/Emitter/Traits/EmitterTraitAware.php @@ -0,0 +1,129 @@ + 0 && ob_get_length() > 0) { + throw new PreviousOutputException(); + } + } + + /** + * Emit the status line. + * + * Emits the status line using the protocol version and status code from + * the response; if a reason phrase is available, it, too, is emitted. + * + * @param ResponseInterface $response + * @return void + */ + protected function emitStatusLine(ResponseInterface $response): void + { + $reasonPhrase = $response->getReasonPhrase(); + $statusCode = $response->getStatusCode(); + + $this->header(sprintf( + 'HTTP/%s %d%s', + $response->getProtocolVersion(), + $statusCode, + $reasonPhrase ? ' ' . $reasonPhrase : '' + ), true, $statusCode); + } + + /** + * Emit response headers. + * + * Loops through each header, emitting each; if the header value + * is an array with multiple values, ensures that each is sent + * in such a way as to create aggregate headers (instead of replace + * the previous). + * + * @param ResponseInterface $response + * @return void + */ + protected function emitHeaders(ResponseInterface $response): void + { + $statusCode = $response->getStatusCode(); + + foreach ($response->getHeaders() as $header => $values) { + assert(is_string($header)); + $name = $this->normalizeHeaderName(headerName: $header); + $first = $name !== 'Set-Cookie'; + + foreach ($values as $value) { + $this->header(sprintf( + '%s: %s', + $name, + $value + ), $first, $statusCode); + $first = false; + } + } + } + + /** + * Normalize a header name + * + * Normalized header will be in the following format: Example-Header-Name + * + * @param string $headerName + * @return string + */ + private function normalizeHeaderName(string $headerName): string + { + return ucwords(string: $headerName, separators: '-'); + } + + private function header(string $headerName, bool $replace, int $statusCode): void + { + header(header: $headerName, replace: $replace, response_code: $statusCode); + } + + protected function closeConnection(): void + { + if (! in_array(needle: PHP_SAPI, haystack: ['cli', 'phpdbg'], strict: true)) { + HttpUtil::closeOutputBuffers(maxBufferLevel: 0, flush: true); + } + + if (function_exists(function: 'fastcgi_finish_request')) { + fastcgi_finish_request(); + } + } +} diff --git a/Http/Kernel.php b/Http/Kernel.php index da15a9d..3f616a5 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -8,11 +8,11 @@ use Codefy\Framework\Contracts\Http\Kernel as HttpKernel; use Exception; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use Qubus\Error\Handlers\DebugErrorHandler; use Qubus\Error\Handlers\ErrorHandler; use Qubus\Error\Handlers\ProductionErrorHandler; -use Qubus\Http\HttpPublisher; -use Qubus\Http\ServerRequestFactory as ServerRequest; use Qubus\Routing\Router; use function Codefy\Framework\Helpers\public_path; @@ -55,26 +55,25 @@ public function codefy(): Application /** * @throws Exception */ - protected function dispatchRouter(): bool + protected function dispatchRouter(ServerRequestInterface $request): void { - return new HttpPublisher()->publish( - content: $this->router->match( - serverRequest: ServerRequest::fromGlobals( - server: $_SERVER, - query: $_GET, - body: $_POST, - cookies: $_COOKIE, - files: $_FILES - ) - ), - emitter: new SapiEmitter() - ); + $response = $this->handle($request); + $responseEmitter = new SapiEmitter(); + $responseEmitter->emit($response); + } + + /** + * @throws Exception + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->router->match(serverRequest: $request); } /** * @throws Exception */ - public function boot(): bool + public function boot(ServerRequestInterface $request): void { if ( version_compare( @@ -98,7 +97,7 @@ public function boot(): bool $this->codefy->bootstrapWith(bootstrappers: $this->bootstrappers()); } - return $this->dispatchRouter(); + $this->dispatchRouter($request); } /** diff --git a/Http/Middleware/EmitterMiddleware.php b/Http/Middleware/EmitterMiddleware.php new file mode 100644 index 0000000..02ccebe --- /dev/null +++ b/Http/Middleware/EmitterMiddleware.php @@ -0,0 +1,29 @@ +handle($request); + $this->emitter->emit($response); + return $response; + } +} \ No newline at end of file From cd387035c7500ae0261aaff735182acb77c1b728 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 21:24:08 -0700 Subject: [PATCH 047/161] Switched out different SapiEmitter. Signed-off-by: Joshua Parker --- Http/Kernel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Kernel.php b/Http/Kernel.php index 3f616a5..0aaf8f5 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -6,8 +6,8 @@ use Codefy\Framework\Application; use Codefy\Framework\Contracts\Http\Kernel as HttpKernel; +use Codefy\Framework\Http\Emitter\SapiEmitter; use Exception; -use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Error\Handlers\DebugErrorHandler; From 8496ebeec28f4158d73dc3193166b21610df7045 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 22:15:55 -0700 Subject: [PATCH 048/161] Moved Emitter to Http package. Signed-off-by: Joshua Parker --- Http/Emitter/BaseEmitter.php | 18 -- Http/Emitter/ContentRange.php | 135 ------------ Http/Emitter/Emitter.php | 30 --- Http/Emitter/Exceptions/EmitterException.php | 11 - .../HeadersAlreadySentException.php | 32 --- .../Exceptions/PreviousOutputException.php | 9 - Http/Emitter/HttpUtil.php | 75 ------- Http/Emitter/SapiEmitter.php | 32 --- Http/Emitter/SapiStreamEmitter.php | 192 ------------------ Http/Emitter/Traits/EmitterTraitAware.php | 129 ------------ Http/Middleware/EmitterMiddleware.php | 29 --- 11 files changed, 692 deletions(-) delete mode 100644 Http/Emitter/BaseEmitter.php delete mode 100644 Http/Emitter/ContentRange.php delete mode 100644 Http/Emitter/Emitter.php delete mode 100644 Http/Emitter/Exceptions/EmitterException.php delete mode 100644 Http/Emitter/Exceptions/HeadersAlreadySentException.php delete mode 100644 Http/Emitter/Exceptions/PreviousOutputException.php delete mode 100644 Http/Emitter/HttpUtil.php delete mode 100644 Http/Emitter/SapiEmitter.php delete mode 100644 Http/Emitter/SapiStreamEmitter.php delete mode 100644 Http/Emitter/Traits/EmitterTraitAware.php delete mode 100644 Http/Middleware/EmitterMiddleware.php diff --git a/Http/Emitter/BaseEmitter.php b/Http/Emitter/BaseEmitter.php deleted file mode 100644 index 82a7320..0000000 --- a/Http/Emitter/BaseEmitter.php +++ /dev/null @@ -1,18 +0,0 @@ -setStart($start) - ->setEnd($end); - } - - /** - * Get the unit in which ranges are specified. This is usually bytes. - * - * @return string - */ - public function getUnit(): string - { - return $this->unit; - } - - /** - * Set the unit in which ranges are specified. This is usually bytes. - * - * @param string $unit The unit in which ranges are specified. This is - * usually bytes. - * @return ContentRange - */ - public function setUnit(string $unit): ContentRange - { - $this->unit = $unit; - return $this; - } - - /** - * Get the beginning of the request range. - * - * @return int - */ - public function getStart(): int - { - return $this->start; - } - - /** - * Set the beginning of the request range. - * - * @param int $start the beginning of the request range. - * @return ContentRange - * @throws EmitterException - */ - public function setStart(int $start): ContentRange - { - if ($start < 0) { - throw new EmitterException("Range start value must be positive integer"); - } - - $this->start = $start; - - return $this; - } - - /** - * Get an integer in the given unit indicating - * the end of the requested range. - * - * @return int - */ - public function getEnd(): int - { - return $this->end; - } - - /** - * Set an integer in the given unit indicating - * the end of the requested range. - * - * @param int $end An integer in the given unit indicating the - * end of the requested range. - * @return ContentRange - */ - public function setEnd(int $end): ContentRange - { - if ($end < 0) { - throw new EmitterException( - "Range end value must be positive integer" - ); - } - - $this->end = $end; - return $this; - } - - /** - * Get the total size of the document. - * - * @return int|null - */ - public function getSize(): ?int - { - return $this->size; - } - - /** - * Set the total size of the document. - * - * @param int|null $size The total size of the document. - * @return self - */ - public function setSize(?int $size): self - { - $this->size = $size; - return $this; - } -} diff --git a/Http/Emitter/Emitter.php b/Http/Emitter/Emitter.php deleted file mode 100644 index 26247b2..0000000 --- a/Http/Emitter/Emitter.php +++ /dev/null @@ -1,30 +0,0 @@ - $this->headersSentFile; - }, - public private(set) int $headersSentLine { - get => $this->headersSentLine; - }, - int $code = 0, - ?Throwable $previous = null - ) { - $msg = sprintf('Headers already sent in file %s on line %s.', $headersSentFile, $headersSentLine); - parent::__construct($msg, $code, $previous); - } -} diff --git a/Http/Emitter/Exceptions/PreviousOutputException.php b/Http/Emitter/Exceptions/PreviousOutputException.php deleted file mode 100644 index 257e2b8..0000000 --- a/Http/Emitter/Exceptions/PreviousOutputException.php +++ /dev/null @@ -1,9 +0,0 @@ -hasHeader('Content-Length')) { - return $response; - } - - $responseBody = $response->getBody(); - - // PSR-7 indicates int OR null for the stream size; for null values, - // we will not auto-inject the Content-Length. - if ($responseBody->getSize() !== null) { - $response = $response->withHeader('Content-Length', (string) $responseBody->getSize()); - } - - return $response; - } - - /** - * Cleans or flushes output buffers up to target level. - * - * Resulting level can be greater than target level if - * a non-removable buffer has been encountered. - * - * @param int $maxBufferLevel The target output buffering level - * @param bool $flush Whether to flush or clean the buffers - */ - public static function closeOutputBuffers(int $maxBufferLevel, bool $flush): void - { - $status = ob_get_status(full_status: true); - $level = count($status); - $flags = PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE); - - while ( - $level-- > $maxBufferLevel - && isset($status[$level]) - && ($status[$level]['del'] ?? ! isset($status[$level]['flags']) - || $flags === ($status[$level]['flags'] & $flags)) - ) { - if ($flush) { - ob_end_flush(); - } else { - ob_end_clean(); - } - } - } -} diff --git a/Http/Emitter/SapiEmitter.php b/Http/Emitter/SapiEmitter.php deleted file mode 100644 index d7c179c..0000000 --- a/Http/Emitter/SapiEmitter.php +++ /dev/null @@ -1,32 +0,0 @@ -assertNoPreviousOutput(); - $this->emitHeaders($response); - $this->emitStatusLine($response); - $this->emitBody($response); - $this->closeConnection(); - } - - /** - * Emit the response body - * - * @param ResponseInterface $response - */ - private function emitBody(ResponseInterface $response): void - { - echo $response->getBody(); - } -} diff --git a/Http/Emitter/SapiStreamEmitter.php b/Http/Emitter/SapiStreamEmitter.php deleted file mode 100644 index 2074fa1..0000000 --- a/Http/Emitter/SapiStreamEmitter.php +++ /dev/null @@ -1,192 +0,0 @@ -[\w]+)\s+(?P\d+)-(?P\d+)\/(?P\d+|\*)/'; - - /** - * Maximum output buffering size for each iteration. - */ - protected int $maxBufferSize = 8192; - - - /** - * Get the value of max buffer size - * - * @return integer - */ - public function getMaxBufferSize(): int - { - return $this->maxBufferSize; - } - - /** - * Set the value of max buffer size - * - * @param int $maxBufferSize - * @return SapiStreamEmitter - * @throws EmitterException - */ - public function setMaxBufferSize(int $maxBufferSize): SapiStreamEmitter - { - if (!$maxBufferSize < 1) { - throw new EmitterException('Buffer size must be a positive integer'); - } - - $this->maxBufferSize = $maxBufferSize; - return $this; - } - - /** - * @inheritDoc - */ - public function emit(ResponseInterface $response): void - { - $this->assertNoPreviousOutput(); - $this->emitHeaders($response); - $this->emitStatusLine($response); - flush(); - $this->emitStream($response); - $this->closeConnection(); - } - - /** - * Emit response body as a stream - * - * @param ResponseInterface $response - * @return void - */ - private function emitStream(ResponseInterface $response): void - { - $range = $this->getContentRange($response); - - if ($range && $range->getUnit() == 'bytes') { - $this->emitBodyRange($response, $range); - return; - } - - $this->emitBody($response); - } - - /** - * Emit the response body by max buffer size - * - * @param ResponseInterface $response - */ - private function emitBody(ResponseInterface $response): void - { - $body = $response->getBody(); - - if ($body->isSeekable()) { - $body->rewind(); - } - - if (!$body->isReadable()) { - echo $body; - return; - } - - while (!$body->eof()) { - echo $body->read($this->getMaxBufferSize()); - - if (connection_status() != CONNECTION_NORMAL) { - // Connection is broken - // Stop emitting the rest of the stream - break; - } - } - } - - /** - * Emit the range of the response body by max buffer size. - * - * @param ResponseInterface $response - * @param ContentRange $range - * @return void - */ - private function emitBodyRange( - ResponseInterface $response, - ContentRange $range - ): void { - $start = $range->getStart(); - $end = $range->getEnd(); - - $body = $response->getBody(); - $length = $end - $start + 1; - - if ($body->isSeekable()) { - $body->seek($start); - $start = 0; - } - - if (!$body->isReadable()) { - echo substr($body->getContents(), $start, $length); - return; - } - - $remaining = $length; - - while ($remaining > 0 && !$body->eof()) { - $contents = $body->read( - $remaining >= $this->getMaxBufferSize() - ? $this->getMaxBufferSize() - : $remaining - ); - - echo $contents; - - if (connection_status() != CONNECTION_NORMAL) { - // Connection is broken - // Stop emitting the rest of the stream - break; - } - - $remaining -= strlen($contents); - } - } - - /** - * Get ContentRange - * - * Parses the Content-Range header line from the response and generates - * ContentRange instance. - * - * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16 - * - * @param ResponseInterface $response - * @return ContentRange|null - */ - private function getContentRange(ResponseInterface $response): ?ContentRange - { - $headerLine = $response->getHeaderLine('Content-Range'); - - if ( - !$headerLine - || !preg_match(self::CONTENT_PATTERN_REGEX, $headerLine, $matches) - ) { - return null; - } - - return new ContentRange( - (int) $matches['start'], - (int) $matches['end'], - $matches['size'] === '*' ? null : (int) $matches['size'], - $matches['unit'] - ); - } -} diff --git a/Http/Emitter/Traits/EmitterTraitAware.php b/Http/Emitter/Traits/EmitterTraitAware.php deleted file mode 100644 index 8324ecd..0000000 --- a/Http/Emitter/Traits/EmitterTraitAware.php +++ /dev/null @@ -1,129 +0,0 @@ - 0 && ob_get_length() > 0) { - throw new PreviousOutputException(); - } - } - - /** - * Emit the status line. - * - * Emits the status line using the protocol version and status code from - * the response; if a reason phrase is available, it, too, is emitted. - * - * @param ResponseInterface $response - * @return void - */ - protected function emitStatusLine(ResponseInterface $response): void - { - $reasonPhrase = $response->getReasonPhrase(); - $statusCode = $response->getStatusCode(); - - $this->header(sprintf( - 'HTTP/%s %d%s', - $response->getProtocolVersion(), - $statusCode, - $reasonPhrase ? ' ' . $reasonPhrase : '' - ), true, $statusCode); - } - - /** - * Emit response headers. - * - * Loops through each header, emitting each; if the header value - * is an array with multiple values, ensures that each is sent - * in such a way as to create aggregate headers (instead of replace - * the previous). - * - * @param ResponseInterface $response - * @return void - */ - protected function emitHeaders(ResponseInterface $response): void - { - $statusCode = $response->getStatusCode(); - - foreach ($response->getHeaders() as $header => $values) { - assert(is_string($header)); - $name = $this->normalizeHeaderName(headerName: $header); - $first = $name !== 'Set-Cookie'; - - foreach ($values as $value) { - $this->header(sprintf( - '%s: %s', - $name, - $value - ), $first, $statusCode); - $first = false; - } - } - } - - /** - * Normalize a header name - * - * Normalized header will be in the following format: Example-Header-Name - * - * @param string $headerName - * @return string - */ - private function normalizeHeaderName(string $headerName): string - { - return ucwords(string: $headerName, separators: '-'); - } - - private function header(string $headerName, bool $replace, int $statusCode): void - { - header(header: $headerName, replace: $replace, response_code: $statusCode); - } - - protected function closeConnection(): void - { - if (! in_array(needle: PHP_SAPI, haystack: ['cli', 'phpdbg'], strict: true)) { - HttpUtil::closeOutputBuffers(maxBufferLevel: 0, flush: true); - } - - if (function_exists(function: 'fastcgi_finish_request')) { - fastcgi_finish_request(); - } - } -} diff --git a/Http/Middleware/EmitterMiddleware.php b/Http/Middleware/EmitterMiddleware.php deleted file mode 100644 index 02ccebe..0000000 --- a/Http/Middleware/EmitterMiddleware.php +++ /dev/null @@ -1,29 +0,0 @@ -handle($request); - $this->emitter->emit($response); - return $response; - } -} \ No newline at end of file From aaf87a8cafcea42b4344da1ad6cc1217afd6d07b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 2 Sep 2025 22:32:29 -0700 Subject: [PATCH 049/161] Updated SapiEmitter import. Signed-off-by: Joshua Parker --- Http/Kernel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Kernel.php b/Http/Kernel.php index 0aaf8f5..437f66b 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -6,13 +6,13 @@ use Codefy\Framework\Application; use Codefy\Framework\Contracts\Http\Kernel as HttpKernel; -use Codefy\Framework\Http\Emitter\SapiEmitter; use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Error\Handlers\DebugErrorHandler; use Qubus\Error\Handlers\ErrorHandler; use Qubus\Error\Handlers\ProductionErrorHandler; +use Qubus\Http\Emitter\SapiEmitter; use Qubus\Routing\Router; use function Codefy\Framework\Helpers\public_path; From 81f9a4858a7ad411c8f3e1cb80f94c84b30770c6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 3 Sep 2025 19:42:51 -0700 Subject: [PATCH 050/161] Updated Http Kernel for alternative PHP servers. Signed-off-by: Joshua Parker --- Http/Kernel.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/Http/Kernel.php b/Http/Kernel.php index 437f66b..091a149 100644 --- a/Http/Kernel.php +++ b/Http/Kernel.php @@ -7,18 +7,21 @@ use Codefy\Framework\Application; use Codefy\Framework\Contracts\Http\Kernel as HttpKernel; use Exception; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Error\Handlers\DebugErrorHandler; use Qubus\Error\Handlers\ErrorHandler; use Qubus\Error\Handlers\ProductionErrorHandler; use Qubus\Http\Emitter\SapiEmitter; +use Qubus\Http\ServerRequestFactory; use Qubus\Routing\Router; use function Codefy\Framework\Helpers\public_path; use function Codefy\Framework\Helpers\router_basepath; use function Qubus\Config\Helpers\env; use function Qubus\Security\Helpers\__observer; +use function Qubus\Support\Helpers\is_null__; use function sprintf; use function version_compare; @@ -55,11 +58,21 @@ public function codefy(): Application /** * @throws Exception */ - protected function dispatchRouter(ServerRequestInterface $request): void + protected function dispatchRouter(?ServerRequestInterface $request = null): void { - $response = $this->handle($request); + if (is_null__(var: $request)) { + $request = ServerRequestFactory::fromGlobals( + server: $_SERVER, + query: $_GET, + body: $_POST, + cookies: $_COOKIE, + files: $_FILES + ); + } + + $response = $this->handle(request: $request); $responseEmitter = new SapiEmitter(); - $responseEmitter->emit($response); + $responseEmitter->emit(response: $response); } /** @@ -67,13 +80,21 @@ protected function dispatchRouter(ServerRequestInterface $request): void */ public function handle(ServerRequestInterface $request): ResponseInterface { - return $this->router->match(serverRequest: $request); + $response = $this->router->match(serverRequest: $request); + + $method = strtoupper($request->getMethod()); + if ($method === 'HEAD') { + $emptyBody = $this->codefy->make(name: ResponseFactoryInterface::class)->createResponse()->getBody(); + return $response->withBody($emptyBody); + } + + return $response; } /** * @throws Exception */ - public function boot(ServerRequestInterface $request): void + public function boot(?ServerRequestInterface $request = null): void { if ( version_compare( From 3e816519406769ef2d339a23ca0183c8cd004dba Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 4 Sep 2025 08:48:28 -0700 Subject: [PATCH 051/161] Router property hook and factories. Signed-off-by: Joshua Parker --- Application.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Application.php b/Application.php index 26ed139..d5c969e 100644 --- a/Application.php +++ b/Application.php @@ -26,8 +26,6 @@ use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; use Qubus\Http\Cookies\Factory\HttpCookieFactory; -use Qubus\Http\ServerRequestFactory; -use Qubus\Http\ServerRequestFactory as ServerRequest; use Qubus\Http\Session\Flash; use Qubus\Http\Session\PhpSession; use Qubus\Inheritance\InvokerAware; @@ -38,6 +36,7 @@ use Qubus\Injector\ServiceProvider\Bootable; use Qubus\Injector\ServiceProvider\Serviceable; use Qubus\Mail\Mailer; +use Qubus\Routing\Router; use Qubus\Support\ArrayHelper; use Qubus\Support\Assets; use Qubus\Support\StringHelper; @@ -765,11 +764,12 @@ protected function coreAliases(): array \Psr\Container\ContainerInterface::class => self::class, \Qubus\Injector\ServiceContainer::class => self::class, \Psr\Http\Message\ServerRequestInterface::class => \Qubus\Http\ServerRequest::class, - \Psr\Http\Message\ServerRequestFactoryInterface::class => \Qubus\Http\ServerRequestFactory::class, + \Psr\Http\Message\ServerRequestFactoryInterface::class => \Qubus\Http\Factories\Psr17Factory::class, \Psr\Http\Message\RequestInterface::class => \Qubus\Http\Request::class, + \Psr\Http\Message\RequestFactoryInterface::class => \Qubus\Http\Factories\Psr17Factory::class, \Psr\Http\Server\RequestHandlerInterface::class => \Relay\Runner::class, \Psr\Http\Message\ResponseInterface::class => \Qubus\Http\Response::class, - \Psr\Http\Message\ResponseFactoryInterface::class => \Laminas\Diactoros\ResponseFactory::class, + \Psr\Http\Message\ResponseFactoryInterface::class => \Qubus\Http\Factories\Psr17Factory::class, \Psr\Cache\CacheItemInterface::class => \Qubus\Cache\Psr6\Item::class, \Psr\Cache\CacheItemPoolInterface::class => \Qubus\Cache\Psr6\ItemPool::class, \Qubus\Cache\Psr6\TaggableCacheItem::class => \Qubus\Cache\Psr6\TaggablePsr6ItemAdapter::class, @@ -817,7 +817,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $kernel->handle($request); } - public function handleRequest(ServerRequestInterface $request): void + public function handleRequest(?ServerRequestInterface $request = null): void { /** @var Kernel $kernel */ $kernel = $this->make(name: Kernel::class); @@ -982,5 +982,9 @@ public static function getInstance(?string $path = null): self public private(set) ArrayHelper $array { get => $this->array ?? $this->make(name: ArrayHelper::class); } + + public private(set) Router $router { + get => $this->router ?? $this->make(name: 'router'); + } //phpcs:disable } From d29eebd3b509f54336a70297b0fa0236a5c36327 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 4 Sep 2025 15:24:11 -0700 Subject: [PATCH 052/161] Added Swoole Bridge to framework. Signed-off-by: Joshua Parker --- Application.php | 16 +++++++++++ Http/Swoole/BridgeManager.php | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Http/Swoole/BridgeManager.php diff --git a/Application.php b/Application.php index d5c969e..d54a42a 100644 --- a/Application.php +++ b/Application.php @@ -825,6 +825,22 @@ public function handleRequest(?ServerRequestInterface $request = null): void $kernel->boot($request); } + /** + * @throws \Exception + */ + public function process(ServerRequestInterface $request): ResponseInterface + { + /** @var Router $router */ + $router = $this->make(name: 'router'); + + // Ensure basePath is set + if (is_callable([$request->getUri(), 'getPath']) && is_callable([$router, 'setBasePath'])) { + $router->setBasePath($request->getUri()->getPath()); + } + + return $this->handle($request); + } + /** * Load environment file(s). * diff --git a/Http/Swoole/BridgeManager.php b/Http/Swoole/BridgeManager.php new file mode 100644 index 0000000..e310127 --- /dev/null +++ b/Http/Swoole/BridgeManager.php @@ -0,0 +1,52 @@ +app = $app; + $this->responseMerger = $responseMerger; + $this->requestFactory = $requestFactory; + } + + /** + * @param Request $swooleRequest + * @param Response $swooleResponse + * + * @return Response + * @throws Exception + */ + public function process( + Request $swooleRequest, + Response $swooleResponse, + ): Response { + $response = $this->app->process($this->requestFactory->createServerRequest($swooleRequest)); + + return $this->responseMerger->toSwoole($response, $swooleResponse); + } +} From 0bbbd2942e810974af35dfae2c1051277362ff42 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 4 Sep 2025 17:38:40 -0700 Subject: [PATCH 053/161] Added RbacLoader. Signed-off-by: Joshua Parker --- Auth/Rbac/RbacLoader.php | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 Auth/Rbac/RbacLoader.php diff --git a/Auth/Rbac/RbacLoader.php b/Auth/Rbac/RbacLoader.php new file mode 100644 index 0000000..10a8801 --- /dev/null +++ b/Auth/Rbac/RbacLoader.php @@ -0,0 +1,82 @@ +configContainer->getConfigKey(key: 'rbac.roles'))) { + $rolesConfig = (array) $this->configContainer->getConfigKey(key: 'rbac.roles', default: []); + $this->addRoles($rolesConfig); + } + } + + /** + * @throws Exception + */ + public function initRbacPermissions(): void + { + if (!is_null__($this->configContainer->getConfigKey(key: 'rbac.permissions'))) { + $permissionsConfig = (array) $this->configContainer->getConfigKey(key: 'rbac.permissions', default: []); + $this->addPermissions($permissionsConfig); + } + } + + private function addRoles(array $rolesConfig, ?Role $parent = null): void + { + foreach ($rolesConfig as $name => $config) { + $role = $this->rbac->addRole(name: $name, description: $config['description'] ?? ''); + if (!empty($config['permissions'])) { + foreach ((array) $config['permissions'] as $permissionName) { + if ($permission = $this->rbac->getPermission(name: $permissionName)) { + $role->addPermission($permission); + } else { + throw new RuntimeException(message: sprintf('Permission not found: %s', $permissionName)); + } + } + } + } + + $parent?->addChild($role); + + if (!empty($config['roles'])) { + $this->addRoles(rolesConfig: $config['roles'], parent: $role); + } + } + + /** + * @throws SentinelException + */ + private function addPermissions(array $permissionsConfig, ?Permission $parent = null): void + { + foreach ($permissionsConfig as $name => $config) { + $permission = $this->rbac->addPermission(name: $name, description: $config['description'] ?? ''); + + $parent?->addChild($permission); + + if (!empty($config['permissions'])) { + $this->addPermissions(permissionsConfig: $config['permissions'], parent: $permission); + } + } + } +} From 100c9d588fd621b8334f7d894312c0e6e9dec47e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 4 Sep 2025 22:37:00 -0700 Subject: [PATCH 054/161] Added union type to fix return type. Signed-off-by: Joshua Parker --- Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application.php b/Application.php index d54a42a..590606b 100644 --- a/Application.php +++ b/Application.php @@ -208,7 +208,7 @@ public static function getSmtpLogger(): LoggerInterface /** * @throws Exception */ - public function getDbConnection(): Connection + public function getDbConnection(): Connection\DbalPdo|Connection { /** @var ConfigContainer $config */ $config = $this->make(name: 'codefy.config'); From 3671c10773168a1fa8e355e44d2e00a228d48f01 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 6 Sep 2025 11:45:44 -0700 Subject: [PATCH 055/161] Added RouterServiceProvider. Signed-off-by: Joshua Parker --- Application.php | 8 +++---- Http/BaseController.php | 4 ++-- Providers/RouterServiceProvider.php | 36 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 Providers/RouterServiceProvider.php diff --git a/Application.php b/Application.php index 590606b..e2ef72d 100644 --- a/Application.php +++ b/Application.php @@ -36,6 +36,7 @@ use Qubus\Injector\ServiceProvider\Bootable; use Qubus\Injector\ServiceProvider\Serviceable; use Qubus\Mail\Mailer; +use Qubus\Routing\Psr7Router; use Qubus\Routing\Router; use Qubus\Support\ArrayHelper; use Qubus\Support\Assets; @@ -281,6 +282,7 @@ protected function registerDefaultServiceProviders(): void Providers\ConfigServiceProvider::class, Providers\PdoServiceProvider::class, Providers\FlysystemServiceProvider::class, + Providers\RouterServiceProvider::class, ] as $serviceProvider ) { $this->registerServiceProvider(serviceProvider: $serviceProvider); @@ -782,8 +784,6 @@ protected function coreAliases(): array 'dir.path' => \Codefy\Framework\Support\Paths::class, 'container' => self::class, 'codefy' => self::class, - \Qubus\Routing\Interfaces\Collector::class => \Qubus\Routing\Route\RouteCollector::class, - 'router' => \Qubus\Routing\Router::class, \Codefy\Framework\Contracts\RoutingController::class => \Codefy\Framework\Http\BaseController::class, \League\Flysystem\FilesystemOperator::class => \Qubus\FileSystem\FileSystem::class, \League\Flysystem\FilesystemAdapter::class => \Qubus\FileSystem\Adapter\LocalFlysystemAdapter::class, @@ -999,8 +999,8 @@ public static function getInstance(?string $path = null): self get => $this->array ?? $this->make(name: ArrayHelper::class); } - public private(set) Router $router { - get => $this->router ?? $this->make(name: 'router'); + public private(set) Psr7Router $router { + get => $this->router ?? $this->make(name: Psr7Router::class); } //phpcs:disable } diff --git a/Http/BaseController.php b/Http/BaseController.php index 1f4f705..ac12ac4 100644 --- a/Http/BaseController.php +++ b/Http/BaseController.php @@ -9,7 +9,7 @@ use Qubus\Http\Factories\RedirectResponseFactory; use Qubus\Http\Session\SessionService; use Qubus\Routing\Controller\Controller; -use Qubus\Routing\Router; +use Qubus\Routing\Psr7Router; use Qubus\View\Renderer; class BaseController extends Controller implements RoutingController @@ -18,7 +18,7 @@ public function __construct( protected SessionService $sessionService { get => $this->sessionService; }, - protected Router $router { + protected Psr7Router $router { get => $this->router; }, protected Renderer $view { diff --git a/Providers/RouterServiceProvider.php b/Providers/RouterServiceProvider.php new file mode 100644 index 0000000..397be72 --- /dev/null +++ b/Providers/RouterServiceProvider.php @@ -0,0 +1,36 @@ +codefy->singleton(Psr7Router::class, function () { + $router = new Router( + routeCollector: new RouteCollector( + routes: [], + basePath: $this->codefy->basePath(), + matchTypes: [] + ), + container: $this->codefy, + responseFactory: new ResponseFactory(), + resolver: new InjectorMiddlewareResolver($this->codefy), + ); + $router->setDefaultNamespace($this->codefy->controllerNamespace); + + return $router; + }); + + $this->codefy->share(nameOrInstance: Psr7Router::class); + } +} From a821ea522e1a83a1d8c3b8ea0810b75e976bdc59 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 6 Sep 2025 11:50:58 -0700 Subject: [PATCH 056/161] Backwards Compatibility. Signed-off-by: Joshua Parker --- Application.php | 5 ++--- Http/BaseController.php | 4 ++-- Providers/RouterServiceProvider.php | 6 +++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Application.php b/Application.php index e2ef72d..38f9d87 100644 --- a/Application.php +++ b/Application.php @@ -36,7 +36,6 @@ use Qubus\Injector\ServiceProvider\Bootable; use Qubus\Injector\ServiceProvider\Serviceable; use Qubus\Mail\Mailer; -use Qubus\Routing\Psr7Router; use Qubus\Routing\Router; use Qubus\Support\ArrayHelper; use Qubus\Support\Assets; @@ -999,8 +998,8 @@ public static function getInstance(?string $path = null): self get => $this->array ?? $this->make(name: ArrayHelper::class); } - public private(set) Psr7Router $router { - get => $this->router ?? $this->make(name: Psr7Router::class); + public private(set) Router $router { + get => $this->router ?? $this->make(name: Router::class); } //phpcs:disable } diff --git a/Http/BaseController.php b/Http/BaseController.php index ac12ac4..1f4f705 100644 --- a/Http/BaseController.php +++ b/Http/BaseController.php @@ -9,7 +9,7 @@ use Qubus\Http\Factories\RedirectResponseFactory; use Qubus\Http\Session\SessionService; use Qubus\Routing\Controller\Controller; -use Qubus\Routing\Psr7Router; +use Qubus\Routing\Router; use Qubus\View\Renderer; class BaseController extends Controller implements RoutingController @@ -18,7 +18,7 @@ public function __construct( protected SessionService $sessionService { get => $this->sessionService; }, - protected Psr7Router $router { + protected Router $router { get => $this->router; }, protected Renderer $view { diff --git a/Providers/RouterServiceProvider.php b/Providers/RouterServiceProvider.php index 397be72..8051912 100644 --- a/Providers/RouterServiceProvider.php +++ b/Providers/RouterServiceProvider.php @@ -15,7 +15,7 @@ final class RouterServiceProvider extends CodefyServiceProvider { public function register(): void { - $this->codefy->singleton(Psr7Router::class, function () { + $this->codefy->singleton(Router::class, function () { $router = new Router( routeCollector: new RouteCollector( routes: [], @@ -31,6 +31,10 @@ public function register(): void return $router; }); + $this->codefy->alias(Psr7Router::class, Router::class); + $this->codefy->alias(Router::class, 'router'); $this->codefy->share(nameOrInstance: Psr7Router::class); + $this->codefy->share(nameOrInstance: Router::class); + $this->codefy->share(nameOrInstance: 'router'); } } From a93f48640ddb3555a1a0b4ad0c18a59fcef2f3a5 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 6 Sep 2025 14:49:20 -0700 Subject: [PATCH 057/161] Fixed bug with router alias. Signed-off-by: Joshua Parker --- Providers/RouterServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Providers/RouterServiceProvider.php b/Providers/RouterServiceProvider.php index 8051912..d34ad4f 100644 --- a/Providers/RouterServiceProvider.php +++ b/Providers/RouterServiceProvider.php @@ -32,7 +32,7 @@ public function register(): void }); $this->codefy->alias(Psr7Router::class, Router::class); - $this->codefy->alias(Router::class, 'router'); + $this->codefy->alias('router', Router::class); $this->codefy->share(nameOrInstance: Psr7Router::class); $this->codefy->share(nameOrInstance: Router::class); $this->codefy->share(nameOrInstance: 'router'); From 8eeb48ee6f0145d81f33e81e2236d5b4fb0cf4b5 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 6 Sep 2025 14:50:25 -0700 Subject: [PATCH 058/161] Removed unnecessary method. Signed-off-by: Joshua Parker --- Application.php | 16 ---------------- Http/Swoole/BridgeManager.php | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/Application.php b/Application.php index 38f9d87..736c506 100644 --- a/Application.php +++ b/Application.php @@ -824,22 +824,6 @@ public function handleRequest(?ServerRequestInterface $request = null): void $kernel->boot($request); } - /** - * @throws \Exception - */ - public function process(ServerRequestInterface $request): ResponseInterface - { - /** @var Router $router */ - $router = $this->make(name: 'router'); - - // Ensure basePath is set - if (is_callable([$request->getUri(), 'getPath']) && is_callable([$router, 'setBasePath'])) { - $router->setBasePath($request->getUri()->getPath()); - } - - return $this->handle($request); - } - /** * Load environment file(s). * diff --git a/Http/Swoole/BridgeManager.php b/Http/Swoole/BridgeManager.php index e310127..e5da49e 100644 --- a/Http/Swoole/BridgeManager.php +++ b/Http/Swoole/BridgeManager.php @@ -45,7 +45,7 @@ public function process( Request $swooleRequest, Response $swooleResponse, ): Response { - $response = $this->app->process($this->requestFactory->createServerRequest($swooleRequest)); + $response = $this->app->handle($this->requestFactory->createServerRequest($swooleRequest)); return $this->responseMerger->toSwoole($response, $swooleResponse); } From 424dfd616fe13ae03541ab82c2a5eb80370a4963 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 6 Sep 2025 14:51:11 -0700 Subject: [PATCH 059/161] Bumped version value. Signed-off-by: Joshua Parker --- Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application.php b/Application.php index 736c506..95006c9 100644 --- a/Application.php +++ b/Application.php @@ -56,7 +56,7 @@ final class Application extends Container { use InvokerAware; - public const string APP_VERSION = '3.0.0-beta.3'; + public const string APP_VERSION = '3.0.0-beta.4'; public const string MIN_PHP_VERSION = '8.4'; From ffbf5dfe25a64561d4926238e84602d8fdc956f2 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 7 Sep 2025 18:32:09 -0700 Subject: [PATCH 060/161] Swoole callback. Signed-off-by: Joshua Parker --- Http/Swoole/App.php | 90 +++++++++++++++++++++++++++++++++++ Http/Swoole/BridgeManager.php | 18 ++++++- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 Http/Swoole/App.php diff --git a/Http/Swoole/App.php b/Http/Swoole/App.php new file mode 100644 index 0000000..6c76d9e --- /dev/null +++ b/Http/Swoole/App.php @@ -0,0 +1,90 @@ +app; + } + }, + public private(set) ?Server $server = null { + get { + return $this->server ?? $this->app->make(name: Server::class); + } + }, + public int $serverStartTimestamp = 0 { + get { + if ($this->serverStartTimestamp === 0) { + return 0; + } + return time() - $this->serverStartTimestamp; + } + } + ) { + $this->app->alias(original: App::class, alias: self::class); + } + //phpcs:enable + + public function init(callable $routesCallable): void + { + $this->initRoutes(callable: $routesCallable); + + $psr17 = new Psr17Factory(); + $bridge = new BridgeManager( + app: $this->app, + responseMerger: new ResponseMerger(), + requestFactory: new RequestFactory(uriFactory: $psr17, streamFactory: $psr17, uploadedFileFactory: $psr17) + ); + + $this->server->on( + event_name: 'request', + callback: function (SwooleRequest $request, SwooleResponse $response) use ($bridge) { + + try { + $response->header(key: 'X-Powered-By', value: 'Swoole + CodefyPHP'); + + // Boot fresh per request, ensures correct routing + $this->app->boot(); + + $bridge->process(swooleRequest: $request, swooleResponse: $response)->end(); + + flush(); + } catch (\Throwable $e) { + $response->status(http_code: 500); + $response->end(content: "Internal Server Error: {$e->getMessage()}"); + } + } + ); + } + + /** + * Starts Swoole Server + */ + public function start(): void + { + $this->serverStartTimestamp = time(); + $this->server->start(); + } + + private function initRoutes(callable $callable): void + { + $callable($this->app); + } +} diff --git a/Http/Swoole/BridgeManager.php b/Http/Swoole/BridgeManager.php index e5da49e..b150d75 100644 --- a/Http/Swoole/BridgeManager.php +++ b/Http/Swoole/BridgeManager.php @@ -45,8 +45,22 @@ public function process( Request $swooleRequest, Response $swooleResponse, ): Response { - $response = $this->app->handle($this->requestFactory->createServerRequest($swooleRequest)); - return $this->responseMerger->toSwoole($response, $swooleResponse); + $psrRequest = $this->requestFactory->createServerRequest($swooleRequest); + + if (!$psrRequest->getUri()->getHost() && isset($swooleRequest->header['host'])) { + $uri = $psrRequest->getUri()->withHost($swooleRequest->header['host']); + $psrRequest = $psrRequest->withUri($uri); + } + + try { + $response = $this->app->handle($psrRequest); + + return $this->responseMerger->toSwoole($response, $swooleResponse); + } catch (Exception $e) { + $swooleResponse->status(500); + $swooleResponse->end("BridgeManager error: {$e->getMessage()}"); + return $swooleResponse; + } } } From d8f95f918b3e8707ef220eb5dfe53915f50875a3 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 7 Sep 2025 19:23:38 -0700 Subject: [PATCH 061/161] Added correct Swoole server. Signed-off-by: Joshua Parker --- Http/Swoole/App.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Http/Swoole/App.php b/Http/Swoole/App.php index 6c76d9e..eb4d65e 100644 --- a/Http/Swoole/App.php +++ b/Http/Swoole/App.php @@ -10,7 +10,7 @@ use Qubus\Http\Swoole\ResponseMerger; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; -use Swoole\Server; +use Swoole\Http\Server; use function flush; use function time; From dbb7268f5c0814aeba00a91cae45d62dbe80c460 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 7 Sep 2025 21:57:47 -0700 Subject: [PATCH 062/161] Better basepath set. Signed-off-by: Joshua Parker --- Application.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Application.php b/Application.php index 95006c9..1b82edd 100644 --- a/Application.php +++ b/Application.php @@ -147,11 +147,11 @@ final class Application extends Container /** * @throws TypeException */ - public function __construct(array $params) + public function __construct(array $params = []) { - if (isset($params['basePath'])) { - $this->withBasePath(basePath: $params['basePath']); - } + $this->withBasePath( + basePath: $params['basePath'] ?? env(key: 'APP_BASE_PATH') + ); parent::__construct(InjectorFactory::create(config: $this->coreAliases())); $this->registerBaseBindings(); @@ -889,11 +889,11 @@ public function isDevelopment(): bool } /** - * Configure a new CodefyPHP application instance. + * Create a new CodefyPHP application instance. * * @throws TypeException */ - public static function configure(array $config): ApplicationBuilder + public static function create(array $config = []): ApplicationBuilder { return new ApplicationBuilder(new self($config)); } From d3ee57a10108d6f82f5b64caebd7019ed8d5e6a8 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 8 Sep 2025 13:55:03 -0700 Subject: [PATCH 063/161] Parsed code into traits. Signed-off-by: Joshua Parker --- Application.php | 39 ++++-------------- Traits/LoggerAware.php | 35 ++++++++++++++++ Traits/RouterAware.php | 94 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 Traits/LoggerAware.php create mode 100644 Traits/RouterAware.php diff --git a/Application.php b/Application.php index 1b82edd..c262198 100644 --- a/Application.php +++ b/Application.php @@ -6,17 +6,16 @@ use Codefy\Framework\Configuration\ApplicationBuilder; use Codefy\Framework\Contracts\Http\Kernel; -use Codefy\Framework\Factory\FileLoggerFactory; -use Codefy\Framework\Factory\FileLoggerSmtpFactory; use Codefy\Framework\Pipeline\PipelineBuilder; use Codefy\Framework\Support\BasePathDetector; use Codefy\Framework\Support\LocalStorage; use Codefy\Framework\Support\Paths; +use Codefy\Framework\Traits\LoggerAware; +use Codefy\Framework\Traits\RouterAware; use Dotenv\Dotenv; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Psr\Log\LoggerInterface; use Qubus\Config\ConfigContainer; use Qubus\Dbal\Connection; use Qubus\Dbal\DB; @@ -40,7 +39,6 @@ use Qubus\Support\ArrayHelper; use Qubus\Support\Assets; use Qubus\Support\StringHelper; -use ReflectionException; use function dirname; use function get_class; @@ -55,6 +53,8 @@ final class Application extends Container { use InvokerAware; + use RouterAware; + use LoggerAware; public const string APP_VERSION = '3.0.0-beta.4'; @@ -147,11 +147,11 @@ final class Application extends Container /** * @throws TypeException */ - public function __construct(array $params = []) + public function __construct(array $params) { - $this->withBasePath( - basePath: $params['basePath'] ?? env(key: 'APP_BASE_PATH') - ); + if (isset($params['basePath'])) { + $this->withBasePath(basePath: $params['basePath']); + } parent::__construct(InjectorFactory::create(config: $this->coreAliases())); $this->registerBaseBindings(); @@ -184,27 +184,6 @@ protected static function inferBasePath(): ?string }; } - /** - * FileLogger - * - * @throws ReflectionException|TypeException - */ - public static function getLogger(): LoggerInterface - { - return FileLoggerFactory::getLogger(); - } - - /** - * FileLogger with SMTP support. - * - * @throws ReflectionException - * @throws TypeException - */ - public static function getSmtpLogger(): LoggerInterface - { - return FileLoggerSmtpFactory::getLogger(); - } - /** * @throws Exception */ @@ -893,7 +872,7 @@ public function isDevelopment(): bool * * @throws TypeException */ - public static function create(array $config = []): ApplicationBuilder + public static function create(array $config): ApplicationBuilder { return new ApplicationBuilder(new self($config)); } diff --git a/Traits/LoggerAware.php b/Traits/LoggerAware.php new file mode 100644 index 0000000..0dd0b38 --- /dev/null +++ b/Traits/LoggerAware.php @@ -0,0 +1,35 @@ +make(name: Router::class); + } + + public function group(array|string $params, callable $callback): Router + { + return $this->getRouter()->group($params, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function get(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->get($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function post(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->post($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function head(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->head($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function delete(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->delete($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function connect(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->connect($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function options(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->options($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function patch(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->patch($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function trace(string $uri, callable|string $callback): Routable + { + return $this->getRouter()->trace($uri, $callback); + } + + /** + * @throws TooLateToAddNewRouteException + */ + public function map(array $verbs, string $uri, callable|string $callback): Routable + { + return $this->getRouter()->map($verbs, $uri, $callback); + } +} From ec3097ca61584a663e5267d33fee31b19e0df7c2 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 8 Sep 2025 17:45:48 -0700 Subject: [PATCH 064/161] Fixed issue with method names. Signed-off-by: Joshua Parker --- Traits/RouterAware.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Traits/RouterAware.php b/Traits/RouterAware.php index 7122568..859752e 100644 --- a/Traits/RouterAware.php +++ b/Traits/RouterAware.php @@ -23,7 +23,7 @@ public function group(array|string $params, callable $callback): Router /** * @throws TooLateToAddNewRouteException */ - public function get(string $uri, callable|string $callback): Routable + public function httpGet(string $uri, callable|string $callback): Routable { return $this->getRouter()->get($uri, $callback); } @@ -31,7 +31,7 @@ public function get(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function post(string $uri, callable|string $callback): Routable + public function httpPost(string $uri, callable|string $callback): Routable { return $this->getRouter()->post($uri, $callback); } @@ -39,7 +39,7 @@ public function post(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function head(string $uri, callable|string $callback): Routable + public function httpHead(string $uri, callable|string $callback): Routable { return $this->getRouter()->head($uri, $callback); } @@ -47,7 +47,7 @@ public function head(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function delete(string $uri, callable|string $callback): Routable + public function httpDelete(string $uri, callable|string $callback): Routable { return $this->getRouter()->delete($uri, $callback); } @@ -55,7 +55,7 @@ public function delete(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function connect(string $uri, callable|string $callback): Routable + public function httpConnect(string $uri, callable|string $callback): Routable { return $this->getRouter()->connect($uri, $callback); } @@ -63,7 +63,7 @@ public function connect(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function options(string $uri, callable|string $callback): Routable + public function httpOptions(string $uri, callable|string $callback): Routable { return $this->getRouter()->options($uri, $callback); } @@ -71,7 +71,7 @@ public function options(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function patch(string $uri, callable|string $callback): Routable + public function httpPatch(string $uri, callable|string $callback): Routable { return $this->getRouter()->patch($uri, $callback); } @@ -79,7 +79,7 @@ public function patch(string $uri, callable|string $callback): Routable /** * @throws TooLateToAddNewRouteException */ - public function trace(string $uri, callable|string $callback): Routable + public function httpTrace(string $uri, callable|string $callback): Routable { return $this->getRouter()->trace($uri, $callback); } From 48856d180d99ff1921857d5618b5aebcc6277284 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 12 Sep 2025 19:52:05 -0700 Subject: [PATCH 065/161] Removed RouterAware trait. Signed-off-by: Joshua Parker --- Traits/RouterAware.php | 94 ------------------------------------------ 1 file changed, 94 deletions(-) delete mode 100644 Traits/RouterAware.php diff --git a/Traits/RouterAware.php b/Traits/RouterAware.php deleted file mode 100644 index 859752e..0000000 --- a/Traits/RouterAware.php +++ /dev/null @@ -1,94 +0,0 @@ -make(name: Router::class); - } - - public function group(array|string $params, callable $callback): Router - { - return $this->getRouter()->group($params, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpGet(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->get($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpPost(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->post($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpHead(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->head($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpDelete(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->delete($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpConnect(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->connect($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpOptions(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->options($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpPatch(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->patch($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function httpTrace(string $uri, callable|string $callback): Routable - { - return $this->getRouter()->trace($uri, $callback); - } - - /** - * @throws TooLateToAddNewRouteException - */ - public function map(array $verbs, string $uri, callable|string $callback): Routable - { - return $this->getRouter()->map($verbs, $uri, $callback); - } -} From d855a9ab6d9ee3ba868267ba0cf56a38d74c2c56 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 12 Sep 2025 19:52:29 -0700 Subject: [PATCH 066/161] Fixed Router definition. Signed-off-by: Joshua Parker --- Providers/RouterServiceProvider.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Providers/RouterServiceProvider.php b/Providers/RouterServiceProvider.php index d34ad4f..0431cae 100644 --- a/Providers/RouterServiceProvider.php +++ b/Providers/RouterServiceProvider.php @@ -11,11 +11,13 @@ use Qubus\Routing\Route\RouteCollector; use Qubus\Routing\Router; -final class RouterServiceProvider extends CodefyServiceProvider +class RouterServiceProvider extends CodefyServiceProvider { + protected ?string $namespace = null; + public function register(): void { - $this->codefy->singleton(Router::class, function () { + $this->codefy->singleton(key: Router::class, value: function () { $router = new Router( routeCollector: new RouteCollector( routes: [], @@ -24,15 +26,15 @@ public function register(): void ), container: $this->codefy, responseFactory: new ResponseFactory(), - resolver: new InjectorMiddlewareResolver($this->codefy), + resolver: new InjectorMiddlewareResolver(container: $this->codefy), ); - $router->setDefaultNamespace($this->codefy->controllerNamespace); + $router->setDefaultNamespace(namespace: $this->namespace ?? $this->codefy->controllerNamespace); return $router; }); - $this->codefy->alias(Psr7Router::class, Router::class); - $this->codefy->alias('router', Router::class); + $this->codefy->alias(original: Psr7Router::class, alias: Router::class); + $this->codefy->alias(original: 'router', alias: Router::class); $this->codefy->share(nameOrInstance: Psr7Router::class); $this->codefy->share(nameOrInstance: Router::class); $this->codefy->share(nameOrInstance: 'router'); From b3afbb601b3ee6158f2652dceac3909065db315d Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 12 Sep 2025 19:52:51 -0700 Subject: [PATCH 067/161] Added withRouting method. Signed-off-by: Joshua Parker --- Configuration/ApplicationBuilder.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Configuration/ApplicationBuilder.php b/Configuration/ApplicationBuilder.php index 9764cd7..f53f99a 100644 --- a/Configuration/ApplicationBuilder.php +++ b/Configuration/ApplicationBuilder.php @@ -7,9 +7,12 @@ use Codefy\Framework\Application; use Codefy\Framework\Bootstrap\RegisterProviders; use Qubus\Exception\Data\TypeException; +use Qubus\Routing\Psr7Router; final class ApplicationBuilder { + private mixed $routing = null; + public function __construct(protected Application $app) { } @@ -72,6 +75,13 @@ public function withSingletons(array $singletons = []): self return $this; } + public function withRouting(callable|Psr7Router $routing): self + { + $this->routing = $routing; + + return $this; + } + /** * Register a callback to be invoked when the application's * service providers are registered. From 1dcd1d08675c2e64494b6c90c0a56058657e17ee Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 13 Sep 2025 17:54:25 -0700 Subject: [PATCH 068/161] Added easier way to register and/or boot routes. Signed-off-by: Joshua Parker --- Application.php | 8 +- Configuration/ApplicationBuilder.php | 122 +++++++++++++++++++++++++-- Providers/RoutingServiceProvider.php | 87 +++++++++++++++++++ Support/CodefyServiceProvider.php | 68 +++++++++++++++ 4 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 Providers/RoutingServiceProvider.php diff --git a/Application.php b/Application.php index c262198..e18e203 100644 --- a/Application.php +++ b/Application.php @@ -11,7 +11,6 @@ use Codefy\Framework\Support\LocalStorage; use Codefy\Framework\Support\Paths; use Codefy\Framework\Traits\LoggerAware; -use Codefy\Framework\Traits\RouterAware; use Dotenv\Dotenv; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; @@ -53,7 +52,6 @@ final class Application extends Container { use InvokerAware; - use RouterAware; use LoggerAware; public const string APP_VERSION = '3.0.0-beta.4'; @@ -147,7 +145,7 @@ final class Application extends Container /** * @throws TypeException */ - public function __construct(array $params) + public function __construct(array $params = []) { if (isset($params['basePath'])) { $this->withBasePath(basePath: $params['basePath']); @@ -446,9 +444,13 @@ public function providerIsRegistered(string $provider): bool */ protected function bootServiceProvider(Serviceable|Bootable $provider): void { + $provider->callBootingCallbacks(); + if (method_exists(object_or_class: $provider, method: 'boot')) { $this->execute(callableOrMethodStr: [$provider, 'boot']); } + + $provider->callBootedCallbacks(); } /** diff --git a/Configuration/ApplicationBuilder.php b/Configuration/ApplicationBuilder.php index f53f99a..560e706 100644 --- a/Configuration/ApplicationBuilder.php +++ b/Configuration/ApplicationBuilder.php @@ -4,15 +4,21 @@ namespace Codefy\Framework\Configuration; +use Closure; use Codefy\Framework\Application; use Codefy\Framework\Bootstrap\RegisterProviders; +use Codefy\Framework\Providers\RoutingServiceProvider; use Qubus\Exception\Data\TypeException; -use Qubus\Routing\Psr7Router; +use Qubus\Routing\Route\RouteFileRegistrar; + +use function is_array; +use function is_callable; +use function is_string; +use function Qubus\Support\Helpers\is_null__; +use function realpath; final class ApplicationBuilder { - private mixed $routing = null; - public function __construct(protected Application $app) { } @@ -75,13 +81,91 @@ public function withSingletons(array $singletons = []): self return $this; } - public function withRouting(callable|Psr7Router $routing): self - { - $this->routing = $routing; + /** + * Register the routing services for the application. + * + * @param callable|Closure|null $using + * @param array|string|null $web + * @param array|null $class + * @param array|string|null $api + * @param callable|null $then + * @return $this + * @throws TypeException + */ + public function withRouting( + callable|Closure|null $using = null, + array|string|null $web = null, + ?array $class = null, + array|string|null $api = null, + ?callable $then = null, + ): self { + if ( + is_null__($using) && + (is_string($web) || is_array($web) || is_string($api) || is_array($api)) || + is_callable($then) + ) { + $using = $this->buildRoutingCallback($web, $api, $then); + } + + RoutingServiceProvider::loadRoutesUsing($using); + + $this->app->booting(function () { + $this->app->registerServiceProvider(serviceProvider: RoutingServiceProvider::class, force: true); + }); + + if (!is_null__($class) && is_array($class)) { + foreach ($class as $route) { + $this->app->execute([$route, 'handle']); + } + } return $this; } + /** + * Create the routing callback for the application. + * + * @param array|string|null $web + * @param array|string|null $api + * @param callable|null $then + * @return Closure + */ + protected function buildRoutingCallback( + array|string|null $web = null, + array|string|null $api = null, + ?callable $then = null + ): Closure { + return function () use ($web, $api, $then) { + if (is_string($api) || is_array($api)) { + if (is_array($api)) { + foreach ($api as $apiRoute) { + if (realpath($apiRoute) !== false) { + new RouteFileRegistrar($this->app->router)->register($apiRoute); + } + } + } else { + require $api; + } + } + + if (is_string($web) || is_array($web)) { + if (is_array($web)) { + foreach ($web as $webRoute) { + if (realpath($webRoute) !== false) { + new RouteFileRegistrar($this->app->router)->register($webRoute); + } + } + } else { + new RouteFileRegistrar($this->app->router)->register($web); + } + } + + if (is_callable($then)) { + $then($this->app); + } + }; + } + /** * Register a callback to be invoked when the application's * service providers are registered. @@ -96,6 +180,32 @@ public function registered(callable $callback): self return $this; } + /** + * Register a callback to be invoked when the application is "booting". + * + * @param callable $callback + * @return $this + */ + public function booting(callable $callback): self + { + $this->app->booting($callback); + + return $this; + } + + /** + * Register a callback to be invoked when the application is "booted". + * + * @param callable $callback + * @return $this + */ + public function booted(callable $callback): self + { + $this->app->booted($callback); + + return $this; + } + /** * Return the application instance. * diff --git a/Providers/RoutingServiceProvider.php b/Providers/RoutingServiceProvider.php new file mode 100644 index 0000000..6ed5bc4 --- /dev/null +++ b/Providers/RoutingServiceProvider.php @@ -0,0 +1,87 @@ +booting(fn () => $this->loadRoutes()); + } + + /** + * Register the callback that will be used to load the application's routes. + * + * @param Closure $routesCallback + * @return $this + */ + protected function routes(Closure $routesCallback): static + { + $this->loadRoutesUsing = $routesCallback; + + return $this; + } + + /** + * Register the callback that will be used to load the application's routes. + * + * @param Closure|null $routesCallback + * @return void + */ + public static function loadRoutesUsing(?Closure $routesCallback = null): void + { + self::$alwaysLoadRoutesUsing = $routesCallback; + } + + /** + * Load the application routes. + * + * @return void + * @throws TypeException + */ + protected function loadRoutes(): void + { + if (! is_null__(self::$alwaysLoadRoutesUsing)) { + $this->codefy->execute(self::$alwaysLoadRoutesUsing, [$this->codefy->router]); + } + + if (! is_null__($this->loadRoutesUsing)) { + $this->codefy->execute($this->loadRoutesUsing, [$this->codefy->router]); + } + } + + /** + * Pass dynamic methods onto the router instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call(string $method, array $parameters) + { + return $this->forwardCallTo( + $this->codefy->make(name: Psr7Router::class), + $method, + $parameters + ); + } +} diff --git a/Support/CodefyServiceProvider.php b/Support/CodefyServiceProvider.php index 5331eaf..5e15ba6 100644 --- a/Support/CodefyServiceProvider.php +++ b/Support/CodefyServiceProvider.php @@ -4,13 +4,81 @@ namespace Codefy\Framework\Support; +use Closure; use Codefy\Framework\Application; +use Qubus\Inheritance\ForwardCallAware; use Qubus\Injector\ServiceProvider\BaseServiceProvider; abstract class CodefyServiceProvider extends BaseServiceProvider { + use ForwardCallAware; + + /** + * All the registered booting callbacks. + */ + protected array $bootingCallbacks = []; + + /** + * All the registered booted callbacks. + */ + protected array $bootedCallbacks = []; + public function __construct(protected Application $codefy) { parent::__construct($codefy); } + + /** + * Register a booting callback to be run before the "boot" method is called. + * + * @param Closure $callback + * @return void + */ + public function booting(Closure $callback): void + { + $this->bootingCallbacks[] = $callback; + } + + /** + * Register a booted callback to be run after the "boot" method is called. + * + * @param Closure $callback + * @return void + */ + public function booted(Closure $callback): void + { + $this->bootedCallbacks[] = $callback; + } + + /** + * Call the registered booting callbacks. + * + * @return void + */ + public function callBootingCallbacks(): void + { + $index = 0; + + while ($index < count($this->bootingCallbacks)) { + $this->codefy->call($this->bootingCallbacks[$index]); + + $index++; + } + } + + /** + * Call the registered booted callbacks. + * + * @return void + */ + public function callBootedCallbacks(): void + { + $index = 0; + + while ($index < count($this->bootedCallbacks)) { + $this->codefy->call($this->bootedCallbacks[$index]); + + $index++; + } + } } From b4468bd48e2a0782cbf4a0269f5b10cc5f830c11 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 14 Sep 2025 20:00:03 -0700 Subject: [PATCH 069/161] Fixed route loading during instantiation. Signed-off-by: Joshua Parker --- Configuration/ApplicationBuilder.php | 67 ++++++++++++------------ Providers/RoutingServiceProvider.php | 76 ++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 44 deletions(-) diff --git a/Configuration/ApplicationBuilder.php b/Configuration/ApplicationBuilder.php index 560e706..d23b204 100644 --- a/Configuration/ApplicationBuilder.php +++ b/Configuration/ApplicationBuilder.php @@ -9,13 +9,14 @@ use Codefy\Framework\Bootstrap\RegisterProviders; use Codefy\Framework\Providers\RoutingServiceProvider; use Qubus\Exception\Data\TypeException; -use Qubus\Routing\Route\RouteFileRegistrar; +use Qubus\Routing\Route\RoutingRegistrar; +use Qubus\Routing\Router; use function is_array; use function is_callable; use function is_string; +use function Qubus\Security\Helpers\__observer; use function Qubus\Support\Helpers\is_null__; -use function realpath; final class ApplicationBuilder { @@ -85,10 +86,11 @@ public function withSingletons(array $singletons = []): self * Register the routing services for the application. * * @param callable|Closure|null $using - * @param array|string|null $web - * @param array|null $class - * @param array|string|null $api - * @param callable|null $then + * @param array|string|null $web + * @param array|null $class + * @param array|string|null $api + * @param string $apiPrefix + * @param callable|null $then * @return $this * @throws TypeException */ @@ -97,14 +99,15 @@ public function withRouting( array|string|null $web = null, ?array $class = null, array|string|null $api = null, + string $apiPrefix = 'api', ?callable $then = null, ): self { if ( - is_null__($using) && + is_null__($using) && (is_string($web) || is_array($web) || is_string($api) || is_array($api)) || - is_callable($then) + is_callable($then) ) { - $using = $this->buildRoutingCallback($web, $api, $then); + $using = $this->buildRoutingCallback($web, $api, $apiPrefix, $then); } RoutingServiceProvider::loadRoutesUsing($using); @@ -113,6 +116,7 @@ public function withRouting( $this->app->registerServiceProvider(serviceProvider: RoutingServiceProvider::class, force: true); }); + // Class-based routes if (!is_null__($class) && is_array($class)) { foreach ($class as $route) { $this->app->execute([$route, 'handle']); @@ -125,43 +129,38 @@ public function withRouting( /** * Create the routing callback for the application. * - * @param array|string|null $web - * @param array|string|null $api - * @param callable|null $then + * @param array|string|null $web + * @param array|string|null $api + * @param string $apiPrefix + * @param callable|null $then * @return Closure */ protected function buildRoutingCallback( array|string|null $web = null, array|string|null $api = null, + string $apiPrefix = 'api', ?callable $then = null ): Closure { - return function () use ($web, $api, $then) { - if (is_string($api) || is_array($api)) { - if (is_array($api)) { - foreach ($api as $apiRoute) { - if (realpath($apiRoute) !== false) { - new RouteFileRegistrar($this->app->router)->register($apiRoute); - } - } - } else { - require $api; - } + return function (Router $router) use ($web, $api, $apiPrefix, $then) { + $registrar = new RoutingRegistrar($router); + + // Web routes + if ($web) { + $registrar->group($web); } - if (is_string($web) || is_array($web)) { - if (is_array($web)) { - foreach ($web as $webRoute) { - if (realpath($webRoute) !== false) { - new RouteFileRegistrar($this->app->router)->register($webRoute); - } - } - } else { - new RouteFileRegistrar($this->app->router)->register($web); - } + // API routes + if ($api) { + /** + * API middleware filter. + */ + $apiMiddleware = __observer()->filter->applyFilter('rest.api', ['api']); + $registrar->group($api, middleware: $apiMiddleware, prefix: $apiPrefix); } + // Final callback if (is_callable($then)) { - $then($this->app); + $then($router); } }; } diff --git a/Providers/RoutingServiceProvider.php b/Providers/RoutingServiceProvider.php index 6ed5bc4..35b7be5 100644 --- a/Providers/RoutingServiceProvider.php +++ b/Providers/RoutingServiceProvider.php @@ -8,8 +8,18 @@ use Codefy\Framework\Support\CodefyServiceProvider; use Qubus\Exception\Data\TypeException; use Qubus\Routing\Psr7Router; +use Qubus\Routing\Route\RouteFileRegistrar; +use Qubus\Routing\Router; +use RuntimeException; -use function Qubus\Support\Helpers\is_null__; +use function file_exists; +use function is_array; +use function is_callable; +use function is_string; +use function pathinfo; +use function sprintf; + +use const PATHINFO_EXTENSION; class RoutingServiceProvider extends CodefyServiceProvider { @@ -31,12 +41,12 @@ public function register(): void /** * Register the callback that will be used to load the application's routes. * - * @param Closure $routesCallback + * @param Closure|callable|string|array|null $routes * @return $this */ - protected function routes(Closure $routesCallback): static + protected function routes(Closure|callable|string|array|null $routes): static { - $this->loadRoutesUsing = $routesCallback; + $this->loadRoutesUsing = $this->normalizeRoutes($routes); return $this; } @@ -44,12 +54,14 @@ protected function routes(Closure $routesCallback): static /** * Register the callback that will be used to load the application's routes. * - * @param Closure|null $routesCallback + * @param Closure|callable|string|array|null $routes * @return void */ - public static function loadRoutesUsing(?Closure $routesCallback = null): void + public static function loadRoutesUsing(Closure|callable|string|array|null $routes): void { - self::$alwaysLoadRoutesUsing = $routesCallback; + static::$alwaysLoadRoutesUsing = $routes !== null + ? static::normalizeRoutes($routes) + : null; } /** @@ -60,15 +72,59 @@ public static function loadRoutesUsing(?Closure $routesCallback = null): void */ protected function loadRoutes(): void { - if (! is_null__(self::$alwaysLoadRoutesUsing)) { - $this->codefy->execute(self::$alwaysLoadRoutesUsing, [$this->codefy->router]); + if (static::$alwaysLoadRoutesUsing !== null) { + $this->codefy->execute(static::$alwaysLoadRoutesUsing, [$this->codefy->router]); } - if (! is_null__($this->loadRoutesUsing)) { + if ($this->loadRoutesUsing !== null) { $this->codefy->execute($this->loadRoutesUsing, [$this->codefy->router]); } } + protected static function normalizeRoutes(Closure|callable|string|array $routes): Closure + { + return function (Router $router) use ($routes): void { + // Handle arrays recursively + if (is_array($routes)) { + foreach ($routes as $route) { + $callback = $this->normalizeRoutes($route); + $callback($router); + } + return; + } + + // Handle closures and callables + if ($routes instanceof Closure || is_callable($routes)) { + $routes($router); + return; + } + + // Handle string (file path) + if (is_string($routes)) { + $ext = pathinfo($routes, PATHINFO_EXTENSION); + + if ($ext === 'php') { + // PHP route file + if (file_exists($routes)) { + $result = new RouteFileRegistrar()->register($routes); + $result($this->codefy->router); + } + return; + } + + if ($ext === 'json') { + // JSON routes + if (file_exists($routes)) { + $this->codefy->router->loadRoutesFromJson($routes); + } + return; + } + } + + throw new RuntimeException(sprintf("Unsupported routes definition: %s", get_debug_type($routes))); + }; + } + /** * Pass dynamic methods onto the router instance. * From 5bf4a37f9c2b7035ab37bff5431817bb947f037a Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 14 Sep 2025 22:58:10 -0700 Subject: [PATCH 070/161] Added call for withKernels. Signed-off-by: Joshua Parker --- Application.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Application.php b/Application.php index e18e203..3c1723e 100644 --- a/Application.php +++ b/Application.php @@ -876,7 +876,8 @@ public function isDevelopment(): bool */ public static function create(array $config): ApplicationBuilder { - return new ApplicationBuilder(new self($config)); + return new ApplicationBuilder(new self($config)) + ->withKernels(); } /** From afef7eb8d48bc09b099a5c98b8831cc0493c1552 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 11:54:51 -0700 Subject: [PATCH 071/161] Updated Password class to follow native PHP documentation and api. Signed-off-by: Joshua Parker --- Support/Password.php | 50 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/Support/Password.php b/Support/Password.php index 47e4eec..3960afb 100644 --- a/Support/Password.php +++ b/Support/Password.php @@ -8,6 +8,7 @@ use function defined; use function password_hash; +use function password_needs_rehash; use function password_verify; use function Qubus\Security\Helpers\__observer; @@ -24,11 +25,12 @@ final class Password */ private static function algorithm(): string { - if (!defined(constant_name: 'PASSWORD_ARGON2ID')) { - $algo = PASSWORD_BCRYPT; - } else { + $algo = PASSWORD_BCRYPT; + + if (defined(constant_name: 'PASSWORD_ARGON2ID')) { $algo = PASSWORD_ARGON2ID; } + /** * Filters the password_hash() hashing algorithm. * @@ -45,6 +47,12 @@ private static function algorithm(): string */ private static function options(): array { + $options = ['memory_cost' => 1 << 12, 'time_cost' => 2, 'threads' => 2]; + + if (self::algorithm() === '2y') { + $options = ['cost' => 12]; + } + /** * Filters the password_hash() options parameter. * @@ -52,7 +60,7 @@ private static function options(): array */ return __observer()->filter->applyFilter( 'password.hash.options', - (array) ['memory_cost' => 1 << 12, 'time_cost' => 2, 'threads' => 2] + (array) $options ); } @@ -79,4 +87,38 @@ public static function verify(string $password, string $hash): bool { return password_verify($password, $hash); } + + /** + * Checks if the given hash matches the given algorithm and options provider. + * If not, it is assumed that the hash needs to be rehashed. + * + * @param string $hash + * @return bool + * @throws Exception + */ + public static function rehash(string $hash): bool + { + return password_needs_rehash(hash: $hash, algo: self::algorithm(), options: self::options()); + } + + /** + * Get available password hashing algorithm IDs. + * + * @return array + */ + public static function algos(): array + { + return password_algos(); + } + + /** + * Returns information about the given hash. + * + * @param string $password + * @return array + */ + public static function getInfo(string $password): array + { + return password_get_info(hash: $password); + } } From a0c009679591036ab20148a5efee48a6ad3e8e7b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 22:55:25 -0700 Subject: [PATCH 072/161] Added encrypted env method and relocated Codefy class. Signed-off-by: Joshua Parker --- Configuration/ApplicationBuilder.php | 14 ++++++++++++++ Codefy.php => Proxy/Codefy.php | 0 2 files changed, 14 insertions(+) rename Codefy.php => Proxy/Codefy.php (100%) diff --git a/Configuration/ApplicationBuilder.php b/Configuration/ApplicationBuilder.php index d23b204..590c893 100644 --- a/Configuration/ApplicationBuilder.php +++ b/Configuration/ApplicationBuilder.php @@ -126,6 +126,20 @@ public function withRouting( return $this; } + /** + * Set whether environment variables + * should be encrypted. + * + * @param bool $bool Default: false. + * @return $this + */ + public function withEncryptedEnv(bool $bool = false): self + { + $this->app::$encryptedEnv = $bool; + + return $this; + } + /** * Create the routing callback for the application. * diff --git a/Codefy.php b/Proxy/Codefy.php similarity index 100% rename from Codefy.php rename to Proxy/Codefy.php From 26151f9d6d7f9058fadec178ff063df96cc129cc Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:23:20 -0700 Subject: [PATCH 073/161] Added ability to load encrypted .env file. Signed-off-by: Joshua Parker --- Application.php | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/Application.php b/Application.php index 3c1723e..4f736b5 100644 --- a/Application.php +++ b/Application.php @@ -6,11 +6,16 @@ use Codefy\Framework\Configuration\ApplicationBuilder; use Codefy\Framework\Contracts\Http\Kernel; +use Codefy\Framework\Factory\FileLoggerFactory; use Codefy\Framework\Pipeline\PipelineBuilder; +use Codefy\Framework\Proxy\Codefy; use Codefy\Framework\Support\BasePathDetector; use Codefy\Framework\Support\LocalStorage; use Codefy\Framework\Support\Paths; use Codefy\Framework\Traits\LoggerAware; +use Defuse\Crypto\Exception\BadFormatException; +use Defuse\Crypto\Exception\EnvironmentIsBrokenException; +use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Dotenv\Dotenv; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; @@ -24,6 +29,7 @@ use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; use Qubus\Http\Cookies\Factory\HttpCookieFactory; +use Qubus\Http\Encryption\Env\SecureEnv; use Qubus\Http\Session\Flash; use Qubus\Http\Session\PhpSession; use Qubus\Inheritance\InvokerAware; @@ -38,7 +44,9 @@ use Qubus\Support\ArrayHelper; use Qubus\Support\Assets; use Qubus\Support\StringHelper; +use ReflectionException; +use function Codefy\Framework\Helpers\base_path; use function dirname; use function get_class; use function is_string; @@ -79,7 +87,7 @@ final class Application extends Container /** * @var string Language of the application. */ - public string $language = 'en-US' { + public string $language = 'en' { get => $this->language; set(string $language) => $this->language = strtoupper($language); } @@ -99,6 +107,8 @@ final class Application extends Container public static string $ROOT_PATH = ''; + public static bool $encryptedEnv = false; + private string $basePath = '' { get => $this->basePath; } @@ -810,15 +820,25 @@ public function handleRequest(?ServerRequestInterface $request = null): void * * @param string $basePath * @return void + * @throws TypeException + * @throws ReflectionException */ private static function loadEnvironment(string $basePath): void { - $dotenv = Dotenv::createImmutable( - paths: $basePath, - names: ['.env','.env.local','.env.staging','.env.development','.env.production'], - shortCircuit: false - ); - $dotenv->safeLoad(); + if (self::$encryptedEnv) { + try { + SecureEnv::parse(base_path(path: '.env.enc'), base_path(path: '.enc.key')); + } catch (BadFormatException | EnvironmentIsBrokenException | WrongKeyOrModifiedCiphertextException $e) { + FileLoggerFactory::getLogger()->error($e->getMessage()); + } + } else { + $dotenv = Dotenv::createImmutable( + paths: $basePath, + names: ['.env','.env.local','.env.staging','.env.development','.env.production'], + shortCircuit: false + ); + $dotenv->safeLoad(); + } } public function __get(mixed $name) @@ -885,6 +905,7 @@ public static function create(array $config): ApplicationBuilder * * @return static * @throws TypeException + * @throws ReflectionException */ public static function getInstance(?string $path = null): self { From 5dc14244b14ea564390c57b500e365418c4f8aab Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:23:47 -0700 Subject: [PATCH 074/161] Added PipelineFactory class. Signed-off-by: Joshua Parker --- Pipeline/PipelineFactory.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Pipeline/PipelineFactory.php diff --git a/Pipeline/PipelineFactory.php b/Pipeline/PipelineFactory.php new file mode 100644 index 0000000..1329c4a --- /dev/null +++ b/Pipeline/PipelineFactory.php @@ -0,0 +1,22 @@ + $pipes + * @return Chainable + */ + public function create(iterable $pipes): Chainable + { + $builder = new PipelineBuilder(); + foreach ($pipes as $pipe) { + $builder->pipe($pipe); + } + + return $builder->build(); + } +} From 1c8f5be76259363ab54b9b358b8bb16b76dc340b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:24:22 -0700 Subject: [PATCH 075/161] Full wrapper of native PHP password_* functions. Signed-off-by: Joshua Parker --- Support/Password.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Support/Password.php b/Support/Password.php index 3960afb..41ae521 100644 --- a/Support/Password.php +++ b/Support/Password.php @@ -7,6 +7,8 @@ use Qubus\Exception\Exception; use function defined; +use function password_algos; +use function password_get_info; use function password_hash; use function password_needs_rehash; use function password_verify; @@ -96,7 +98,7 @@ public static function verify(string $password, string $hash): bool * @return bool * @throws Exception */ - public static function rehash(string $hash): bool + public static function needsRehash(string $hash): bool { return password_needs_rehash(hash: $hash, algo: self::algorithm(), options: self::options()); } From 0d60dbfa64758dee8be52976367e9a217eff4116 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:27:36 -0700 Subject: [PATCH 076/161] No functions like GuzzleClient native api. Signed-off-by: Joshua Parker --- Http/HttpClient.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Http/HttpClient.php b/Http/HttpClient.php index 9972301..279e84d 100644 --- a/Http/HttpClient.php +++ b/Http/HttpClient.php @@ -14,10 +14,8 @@ use Psr\Http\Message\UriInterface; use Psr\SimpleCache\InvalidArgumentException; use Qubus\Exception\Exception; -use Qubus\Http\Factories\JsonResponseFactory; use function func_get_args; -use function parse_url; use function Qubus\Security\Helpers\__observer; use function Qubus\Support\Helpers\is_null__; @@ -37,7 +35,7 @@ public static function factory(): self * {@inheritDoc} * * @param string $method HTTP method. - * @param string|UriInterface $uri URI object or string. + * @param string|UriInterface $uri URL, URI object or string. * @param array $options { * Optional. Array of Request options to apply. * See \GuzzleHttp\RequestOptions. @@ -161,20 +159,6 @@ public static function factory(): self return $preempt; } - $parsedUrl = parse_url($uri); - if (empty($parsedUrl) || !isset($parsedUrl['scheme'])) { - $response = new HttpRequestError('A valid URL was not provided.', 405); - __observer()->action->doAction( - 'http_api_debug', - $response, - 'response', - \Qubus\Http\Request::class, - $parsedArgs, - $uri - ); - return JsonResponseFactory::create($response->getMessage(), (int) $response->getCode()); - } - if (is_null__($parsedArgs['headers'])) { $parsedArgs['headers'] = []; } From 7f6419bbd2d8e1bf27a649eaea6fb2346b798511 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:29:41 -0700 Subject: [PATCH 077/161] Added command and query functions. Signed-off-by: Joshua Parker --- Helpers/core.php | 52 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/Helpers/core.php b/Helpers/core.php index 43eff50..d7caae9 100644 --- a/Helpers/core.php +++ b/Helpers/core.php @@ -4,10 +4,22 @@ namespace Codefy\Framework\Helpers; +use Codefy\CommandBus\Busses\SynchronousCommandBus; +use Codefy\CommandBus\Command; +use Codefy\CommandBus\Containers\ContainerFactory; +use Codefy\CommandBus\Exceptions\CommandCouldNotBeHandledException; +use Codefy\CommandBus\Exceptions\UnresolvableCommandHandlerException; +use Codefy\CommandBus\Odin; +use Codefy\CommandBus\Resolvers\NativeCommandHandlerResolver; use Codefy\Framework\Application; -use Codefy\Framework\Codefy; +use Codefy\Framework\Proxy\Codefy; use Codefy\Framework\Factory\FileLoggerFactory; use Codefy\Framework\Support\CodefyMailer; +use Codefy\QueryBus\Busses\SynchronousQueryBus; +use Codefy\QueryBus\Enquire; +use Codefy\QueryBus\Query; +use Codefy\QueryBus\Resolvers\NativeQueryHandlerResolver; +use Codefy\QueryBus\UnresolvableQueryHandlerException; use Qubus\Config\Collection; use Qubus\Dbal\Connection; use Qubus\Exception\Data\TypeException; @@ -207,3 +219,41 @@ function mail(string|array $to, string $subject, string $message, array $headers return false; } } + +/** + * Dispatches the given `$command` through + * the CommandBus. + * + * @param Command $command + * @throws ReflectionException + * @throws TypeException + * @throws CommandCouldNotBeHandledException + * @throws UnresolvableCommandHandlerException + */ +function command(Command $command): void +{ + $resolver = new NativeCommandHandlerResolver( + container: ContainerFactory::make(config: config(key: 'commandbus.container')) + ); + $odin = new Odin(bus: new SynchronousCommandBus($resolver)); + + $odin->execute($command); +} + +/** + * Queries the given query and returns + * a result if any. + * + * @throws ReflectionException + * @throws TypeException + * @throws UnresolvableQueryHandlerException + */ +function ask(Query $query): mixed +{ + $resolver = new NativeQueryHandlerResolver( + container: ContainerFactory::make(config: config(key: 'querybus.aliases')) + ); + $enquirer = new Enquire(bus: new SynchronousQueryBus($resolver)); + + return $enquirer->execute($query); +} From 907055428729a8cd052415e9a4a41cd160d21c3b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:30:20 -0700 Subject: [PATCH 078/161] Relocated Codefy class. Signed-off-by: Joshua Parker --- Pipeline/PipelineBuilder.php | 2 +- Proxy/Codefy.php | 3 ++- Support/Traits/ContainerAware.php | 2 +- Support/Traits/DbTransactionsAware.php | 2 +- tests/Pipes/PipelineTest.php | 3 +-- tests/vendor/bootstrap.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Pipeline/PipelineBuilder.php b/Pipeline/PipelineBuilder.php index 2006e7d..32017a8 100644 --- a/Pipeline/PipelineBuilder.php +++ b/Pipeline/PipelineBuilder.php @@ -4,7 +4,7 @@ namespace Codefy\Framework\Pipeline; -use Codefy\Framework\Codefy; +use Codefy\Framework\Proxy\Codefy; final class PipelineBuilder { diff --git a/Proxy/Codefy.php b/Proxy/Codefy.php index 26bf9a5..d026b7e 100644 --- a/Proxy/Codefy.php +++ b/Proxy/Codefy.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace Codefy\Framework; +namespace Codefy\Framework\Proxy; +use Codefy\Framework\Application; use stdClass; class Codefy extends stdClass diff --git a/Support/Traits/ContainerAware.php b/Support/Traits/ContainerAware.php index c8a59bb..27cb268 100644 --- a/Support/Traits/ContainerAware.php +++ b/Support/Traits/ContainerAware.php @@ -4,7 +4,7 @@ namespace Codefy\Framework\Support\Traits; -use Codefy\Framework\Codefy; +use Codefy\Framework\Proxy\Codefy; trait ContainerAware { diff --git a/Support/Traits/DbTransactionsAware.php b/Support/Traits/DbTransactionsAware.php index 50c75c7..0b655e5 100644 --- a/Support/Traits/DbTransactionsAware.php +++ b/Support/Traits/DbTransactionsAware.php @@ -4,7 +4,7 @@ namespace Codefy\Framework\Support\Traits; -use Codefy\Framework\Codefy; +use Codefy\Framework\Proxy\Codefy; use Qubus\Exception\Exception; trait DbTransactionsAware diff --git a/tests/Pipes/PipelineTest.php b/tests/Pipes/PipelineTest.php index f4527e4..3fddfbf 100644 --- a/tests/Pipes/PipelineTest.php +++ b/tests/Pipes/PipelineTest.php @@ -3,9 +3,8 @@ declare(strict_types=1); use Codefy\Framework\Application; -use Codefy\Framework\Codefy; +use Codefy\Framework\Proxy\Codefy; use Codefy\Framework\Pipeline\Pipeline; -use Codefy\Framework\Pipeline\PipelineBuilder; use Codefy\Framework\tests\Pipes\PipeFour; use Codefy\Framework\tests\Pipes\PipeOne; use Codefy\Framework\tests\Pipes\PipeThree; diff --git a/tests/vendor/bootstrap.php b/tests/vendor/bootstrap.php index adf1144..3d103d9 100644 --- a/tests/vendor/bootstrap.php +++ b/tests/vendor/bootstrap.php @@ -4,7 +4,7 @@ use Qubus\Exception\Data\TypeException; try { - return Application::configure(['basePath' => dirname(path: __DIR__, levels: 2)]) + return Application::create(['basePath' => dirname(path: __DIR__, levels: 2)]) ->withKernels() ->withProviders([ // fill in custom providers From 5429cbae42cfd46c872fe1ba52db89bf1a3aad28 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 27 Sep 2025 23:41:16 -0700 Subject: [PATCH 079/161] Relocated into src directory. Signed-off-by: Joshua Parker --- .gitignore | 7 +++---- composer.json | 13 +++++++++---- Application.php => src/Application.php | 0 {Auth => src/Auth}/Auth.php | 0 {Auth => src/Auth}/Rbac/Entity/AssertionRule.php | 0 {Auth => src/Auth}/Rbac/Entity/Permission.php | 0 {Auth => src/Auth}/Rbac/Entity/RbacPermission.php | 0 {Auth => src/Auth}/Rbac/Entity/RbacRole.php | 0 {Auth => src/Auth}/Rbac/Entity/Role.php | 0 .../Auth}/Rbac/Exception/SentinelException.php | 0 {Auth => src/Auth}/Rbac/Guard.php | 0 {Auth => src/Auth}/Rbac/Rbac.php | 0 {Auth => src/Auth}/Rbac/RbacLoader.php | 0 .../Auth}/Rbac/Resource/BaseStorageResource.php | 0 {Auth => src/Auth}/Rbac/Resource/FileResource.php | 0 .../Auth}/Rbac/Resource/StorageResource.php | 0 .../Auth}/Repository/AuthUserRepository.php | 0 {Auth => src/Auth}/Repository/PdoRepository.php | 0 {Auth => src/Auth}/Sentinel.php | 0 .../Auth}/Traits/BadPropertyCallException.php | 0 {Auth => src/Auth}/Traits/ImmutableAware.php | 0 {Auth => src/Auth}/UserSession.php | 0 {Bootstrap => src/Bootstrap}/BootProviders.php | 0 {Bootstrap => src/Bootstrap}/RegisterProviders.php | 0 .../Configuration}/ApplicationBuilder.php | 0 {Console => src/Console}/Commands/CheckCommand.php | 0 {Console => src/Console}/Commands/DownCommand.php | 0 .../Console}/Commands/GenerateCommand.php | 0 {Console => src/Console}/Commands/InitCommand.php | 0 {Console => src/Console}/Commands/ListCommand.php | 0 {Console => src/Console}/Commands/MakeCommand.php | 0 .../Console}/Commands/MigrateCommand.php | 0 .../Console}/Commands/PasswordHashCommand.php | 0 {Console => src/Console}/Commands/PhpMigCommand.php | 0 {Console => src/Console}/Commands/RedoCommand.php | 0 .../Console}/Commands/RollbackCommand.php | 0 .../Console}/Commands/ScheduleRunCommand.php | 0 {Console => src/Console}/Commands/ServeCommand.php | 0 {Console => src/Console}/Commands/StatusCommand.php | 0 .../Console}/Commands/Traits/MakeCommandAware.php | 0 {Console => src/Console}/Commands/UlidCommand.php | 0 {Console => src/Console}/Commands/UpCommand.php | 0 {Console => src/Console}/Commands/UuidCommand.php | 0 {Console => src/Console}/ConsoleApplication.php | 0 {Console => src/Console}/ConsoleCommand.php | 0 {Console => src/Console}/ConsoleKernel.php | 0 .../MakeCommandFileAlreadyExistsException.php | 0 {Contracts => src/Contracts}/Console/Kernel.php | 0 {Contracts => src/Contracts}/Http/Kernel.php | 0 {Contracts => src/Contracts}/LoggerFactory.php | 0 {Contracts => src/Contracts}/MailerFactory.php | 0 {Contracts => src/Contracts}/RoutingController.php | 0 {Factory => src/Factory}/FileLoggerFactory.php | 0 {Factory => src/Factory}/FileLoggerSmtpFactory.php | 0 {Factory => src/Factory}/PHPMailerSmtpFactory.php | 0 {Factory => src/Factory}/Traits/FileLoggerAware.php | 0 {Helpers => src/Helpers}/core.php | 0 {Helpers => src/Helpers}/path.php | 0 {Http => src/Http}/BaseController.php | 0 {Http => src/Http}/Errors/HttpRequestError.php | 0 {Http => src/Http}/HttpClient.php | 0 {Http => src/Http}/Kernel.php | 0 .../Middleware/Auth/AuthenticationMiddleware.php | 0 .../Middleware/Auth/ExpireUserSessionMiddleware.php | 0 .../Middleware/Auth/UserAuthorizationMiddleware.php | 0 .../Http}/Middleware/Auth/UserSessionMiddleware.php | 0 .../Middleware/Cache/CacheExpiresMiddleware.php | 0 .../Http}/Middleware/Cache/CacheMiddleware.php | 0 .../Middleware/Cache/CachePreventionMiddleware.php | 0 .../Middleware/Cache/ClearSiteDataMiddleware.php | 0 .../Http}/Middleware/ContentCacheMiddleware.php | 0 {Http => src/Http}/Middleware/CorsMiddleware.php | 0 .../Middleware/Csrf/CsrfProtectionMiddleware.php | 0 {Http => src/Http}/Middleware/Csrf/CsrfSession.php | 0 .../Http}/Middleware/Csrf/CsrfTokenMiddleware.php | 0 .../Http}/Middleware/Csrf/Traits/CsrfTokenAware.php | 0 {Http => src/Http}/Middleware/Csrf/helpers.php | 0 .../Http}/Middleware/CssMinifierMiddleware.php | 0 .../Http}/Middleware/DebugBarMiddleware.php | 0 .../Http}/Middleware/HtmlMinifierMiddleware.php | 0 .../Http}/Middleware/JsMinifierMiddleware.php | 0 .../ContentSecurityPolicyMiddleware.php | 0 .../Middleware/SecureHeaders/SecureHeaders.php | 0 .../Http}/Middleware/Spam/HoneyPotMiddleware.php | 0 .../Middleware/Spam/ReferrerSpamMiddleware.php | 0 .../Http}/Middleware/ThrottleMiddleware.php | 0 {Http => src/Http}/Swoole/App.php | 0 {Http => src/Http}/Swoole/BridgeManager.php | 0 {Http => src/Http}/Throttle/Condition.php | 0 {Http => src/Http}/Throttle/Interval.php | 0 {Http => src/Http}/Throttle/RateException.php | 0 {Http => src/Http}/Throttle/RateLimiter.php | 0 .../Migration}/Adapter/DbalMigrationAdapter.php | 0 .../Migration}/Adapter/FileMigrationAdapter.php | 0 .../Migration}/Adapter/MigrationAdapter.php | 0 {Migration => src/Migration}/Migration.php | 0 {Migration => src/Migration}/Migrator.php | 0 {Pipeline => src/Pipeline}/Chainable.php | 0 {Pipeline => src/Pipeline}/Pipeline.php | 0 {Pipeline => src/Pipeline}/PipelineBuilder.php | 0 {Pipeline => src/Pipeline}/PipelineFactory.php | 0 .../Providers}/ConfigServiceProvider.php | 0 .../Providers}/FlysystemServiceProvider.php | 0 {Providers => src/Providers}/PdoServiceProvider.php | 0 .../Providers}/RouterServiceProvider.php | 0 .../Providers}/RoutingServiceProvider.php | 0 {Proxy => src/Proxy}/Codefy.php | 0 {Scheduler => src/Scheduler}/BaseTask.php | 0 .../Scheduler}/Event/TaskCompleted.php | 0 {Scheduler => src/Scheduler}/Event/TaskFailed.php | 0 {Scheduler => src/Scheduler}/Event/TaskSkipped.php | 0 {Scheduler => src/Scheduler}/Event/TaskStarted.php | 0 {Scheduler => src/Scheduler}/Expressions/At.php | 0 {Scheduler => src/Scheduler}/Expressions/Daily.php | 0 {Scheduler => src/Scheduler}/Expressions/Date.php | 0 .../Scheduler}/Expressions/DayOfWeek/Friday.php | 0 .../Scheduler}/Expressions/DayOfWeek/Monday.php | 0 .../Scheduler}/Expressions/DayOfWeek/Saturday.php | 0 .../Scheduler}/Expressions/DayOfWeek/Sunday.php | 0 .../Scheduler}/Expressions/DayOfWeek/Thursday.php | 0 .../Scheduler}/Expressions/DayOfWeek/Tuesday.php | 0 .../Scheduler}/Expressions/DayOfWeek/Wednesday.php | 0 .../Scheduler}/Expressions/EveryMinute.php | 0 .../Scheduler}/Expressions/Expressional.php | 0 {Scheduler => src/Scheduler}/Expressions/Hourly.php | 0 .../Scheduler}/Expressions/MonthOfYear/April.php | 0 .../Scheduler}/Expressions/MonthOfYear/August.php | 0 .../Scheduler}/Expressions/MonthOfYear/December.php | 0 .../Scheduler}/Expressions/MonthOfYear/February.php | 0 .../Scheduler}/Expressions/MonthOfYear/January.php | 0 .../Scheduler}/Expressions/MonthOfYear/July.php | 0 .../Scheduler}/Expressions/MonthOfYear/June.php | 0 .../Scheduler}/Expressions/MonthOfYear/March.php | 0 .../Scheduler}/Expressions/MonthOfYear/May.php | 0 .../Scheduler}/Expressions/MonthOfYear/November.php | 0 .../Scheduler}/Expressions/MonthOfYear/October.php | 0 .../Expressions/MonthOfYear/September.php | 0 .../Scheduler}/Expressions/Monthly.php | 0 .../Scheduler}/Expressions/Quarterly.php | 0 .../Scheduler}/Expressions/WeekDays.php | 0 .../Scheduler}/Expressions/WeekEnds.php | 0 {Scheduler => src/Scheduler}/Expressions/Weekly.php | 0 {Scheduler => src/Scheduler}/FailedProcessor.php | 0 {Scheduler => src/Scheduler}/Mutex/CacheLocker.php | 0 {Scheduler => src/Scheduler}/Mutex/Locker.php | 0 .../Scheduler}/Processor/BaseProcessor.php | 0 {Scheduler => src/Scheduler}/Processor/Callback.php | 0 .../Scheduler}/Processor/Dispatcher.php | 0 .../Scheduler}/Processor/Processor.php | 0 {Scheduler => src/Scheduler}/Processor/Shell.php | 0 {Scheduler => src/Scheduler}/Schedule.php | 0 {Scheduler => src/Scheduler}/Stack.php | 0 {Scheduler => src/Scheduler}/Task.php | 0 .../Scheduler}/Traits/ExpressionAware.php | 0 .../Scheduler}/Traits/LiteralAware.php | 0 {Scheduler => src/Scheduler}/Traits/MailerAware.php | 0 .../Scheduler}/Traits/ScheduleValidateAware.php | 0 {Scheduler => src/Scheduler}/ValueObject/TaskId.php | 0 {Stubs => src/Stubs}/ExampleController.stub | 0 {Stubs => src/Stubs}/ExampleError.stub | 0 {Stubs => src/Stubs}/ExampleMiddleware.stub | 0 {Stubs => src/Stubs}/ExampleProvider.stub | 0 {Stubs => src/Stubs}/ExampleRepository.stub | 0 {Support => src/Support}/ArgsParser.php | 0 {Support => src/Support}/BasePathDetector.php | 0 {Support => src/Support}/CodefyMailer.php | 0 {Support => src/Support}/CodefyServiceProvider.php | 0 {Support => src/Support}/LocalStorage.php | 0 {Support => src/Support}/Password.php | 0 {Support => src/Support}/Paths.php | 0 {Support => src/Support}/RequestMethod.php | 0 {Support => src/Support}/SeoFactory.php | 0 {Support => src/Support}/StringParser.php | 0 {Support => src/Support}/Traits/ContainerAware.php | 0 .../Support}/Traits/DbTransactionsAware.php | 0 {Traits => src/Traits}/LoggerAware.php | 0 {View => src/View}/FenomView.php | 0 {View => src/View}/FoilView.php | 0 178 files changed, 12 insertions(+), 8 deletions(-) rename Application.php => src/Application.php (100%) rename {Auth => src/Auth}/Auth.php (100%) rename {Auth => src/Auth}/Rbac/Entity/AssertionRule.php (100%) rename {Auth => src/Auth}/Rbac/Entity/Permission.php (100%) rename {Auth => src/Auth}/Rbac/Entity/RbacPermission.php (100%) rename {Auth => src/Auth}/Rbac/Entity/RbacRole.php (100%) rename {Auth => src/Auth}/Rbac/Entity/Role.php (100%) rename {Auth => src/Auth}/Rbac/Exception/SentinelException.php (100%) rename {Auth => src/Auth}/Rbac/Guard.php (100%) rename {Auth => src/Auth}/Rbac/Rbac.php (100%) rename {Auth => src/Auth}/Rbac/RbacLoader.php (100%) rename {Auth => src/Auth}/Rbac/Resource/BaseStorageResource.php (100%) rename {Auth => src/Auth}/Rbac/Resource/FileResource.php (100%) rename {Auth => src/Auth}/Rbac/Resource/StorageResource.php (100%) rename {Auth => src/Auth}/Repository/AuthUserRepository.php (100%) rename {Auth => src/Auth}/Repository/PdoRepository.php (100%) rename {Auth => src/Auth}/Sentinel.php (100%) rename {Auth => src/Auth}/Traits/BadPropertyCallException.php (100%) rename {Auth => src/Auth}/Traits/ImmutableAware.php (100%) rename {Auth => src/Auth}/UserSession.php (100%) rename {Bootstrap => src/Bootstrap}/BootProviders.php (100%) rename {Bootstrap => src/Bootstrap}/RegisterProviders.php (100%) rename {Configuration => src/Configuration}/ApplicationBuilder.php (100%) rename {Console => src/Console}/Commands/CheckCommand.php (100%) rename {Console => src/Console}/Commands/DownCommand.php (100%) rename {Console => src/Console}/Commands/GenerateCommand.php (100%) rename {Console => src/Console}/Commands/InitCommand.php (100%) rename {Console => src/Console}/Commands/ListCommand.php (100%) rename {Console => src/Console}/Commands/MakeCommand.php (100%) rename {Console => src/Console}/Commands/MigrateCommand.php (100%) rename {Console => src/Console}/Commands/PasswordHashCommand.php (100%) rename {Console => src/Console}/Commands/PhpMigCommand.php (100%) rename {Console => src/Console}/Commands/RedoCommand.php (100%) rename {Console => src/Console}/Commands/RollbackCommand.php (100%) rename {Console => src/Console}/Commands/ScheduleRunCommand.php (100%) rename {Console => src/Console}/Commands/ServeCommand.php (100%) rename {Console => src/Console}/Commands/StatusCommand.php (100%) rename {Console => src/Console}/Commands/Traits/MakeCommandAware.php (100%) rename {Console => src/Console}/Commands/UlidCommand.php (100%) rename {Console => src/Console}/Commands/UpCommand.php (100%) rename {Console => src/Console}/Commands/UuidCommand.php (100%) rename {Console => src/Console}/ConsoleApplication.php (100%) rename {Console => src/Console}/ConsoleCommand.php (100%) rename {Console => src/Console}/ConsoleKernel.php (100%) rename {Console => src/Console}/Exceptions/MakeCommandFileAlreadyExistsException.php (100%) rename {Contracts => src/Contracts}/Console/Kernel.php (100%) rename {Contracts => src/Contracts}/Http/Kernel.php (100%) rename {Contracts => src/Contracts}/LoggerFactory.php (100%) rename {Contracts => src/Contracts}/MailerFactory.php (100%) rename {Contracts => src/Contracts}/RoutingController.php (100%) rename {Factory => src/Factory}/FileLoggerFactory.php (100%) rename {Factory => src/Factory}/FileLoggerSmtpFactory.php (100%) rename {Factory => src/Factory}/PHPMailerSmtpFactory.php (100%) rename {Factory => src/Factory}/Traits/FileLoggerAware.php (100%) rename {Helpers => src/Helpers}/core.php (100%) rename {Helpers => src/Helpers}/path.php (100%) rename {Http => src/Http}/BaseController.php (100%) rename {Http => src/Http}/Errors/HttpRequestError.php (100%) rename {Http => src/Http}/HttpClient.php (100%) rename {Http => src/Http}/Kernel.php (100%) rename {Http => src/Http}/Middleware/Auth/AuthenticationMiddleware.php (100%) rename {Http => src/Http}/Middleware/Auth/ExpireUserSessionMiddleware.php (100%) rename {Http => src/Http}/Middleware/Auth/UserAuthorizationMiddleware.php (100%) rename {Http => src/Http}/Middleware/Auth/UserSessionMiddleware.php (100%) rename {Http => src/Http}/Middleware/Cache/CacheExpiresMiddleware.php (100%) rename {Http => src/Http}/Middleware/Cache/CacheMiddleware.php (100%) rename {Http => src/Http}/Middleware/Cache/CachePreventionMiddleware.php (100%) rename {Http => src/Http}/Middleware/Cache/ClearSiteDataMiddleware.php (100%) rename {Http => src/Http}/Middleware/ContentCacheMiddleware.php (100%) rename {Http => src/Http}/Middleware/CorsMiddleware.php (100%) rename {Http => src/Http}/Middleware/Csrf/CsrfProtectionMiddleware.php (100%) rename {Http => src/Http}/Middleware/Csrf/CsrfSession.php (100%) rename {Http => src/Http}/Middleware/Csrf/CsrfTokenMiddleware.php (100%) rename {Http => src/Http}/Middleware/Csrf/Traits/CsrfTokenAware.php (100%) rename {Http => src/Http}/Middleware/Csrf/helpers.php (100%) rename {Http => src/Http}/Middleware/CssMinifierMiddleware.php (100%) rename {Http => src/Http}/Middleware/DebugBarMiddleware.php (100%) rename {Http => src/Http}/Middleware/HtmlMinifierMiddleware.php (100%) rename {Http => src/Http}/Middleware/JsMinifierMiddleware.php (100%) rename {Http => src/Http}/Middleware/SecureHeaders/ContentSecurityPolicyMiddleware.php (100%) rename {Http => src/Http}/Middleware/SecureHeaders/SecureHeaders.php (100%) rename {Http => src/Http}/Middleware/Spam/HoneyPotMiddleware.php (100%) rename {Http => src/Http}/Middleware/Spam/ReferrerSpamMiddleware.php (100%) rename {Http => src/Http}/Middleware/ThrottleMiddleware.php (100%) rename {Http => src/Http}/Swoole/App.php (100%) rename {Http => src/Http}/Swoole/BridgeManager.php (100%) rename {Http => src/Http}/Throttle/Condition.php (100%) rename {Http => src/Http}/Throttle/Interval.php (100%) rename {Http => src/Http}/Throttle/RateException.php (100%) rename {Http => src/Http}/Throttle/RateLimiter.php (100%) rename {Migration => src/Migration}/Adapter/DbalMigrationAdapter.php (100%) rename {Migration => src/Migration}/Adapter/FileMigrationAdapter.php (100%) rename {Migration => src/Migration}/Adapter/MigrationAdapter.php (100%) rename {Migration => src/Migration}/Migration.php (100%) rename {Migration => src/Migration}/Migrator.php (100%) rename {Pipeline => src/Pipeline}/Chainable.php (100%) rename {Pipeline => src/Pipeline}/Pipeline.php (100%) rename {Pipeline => src/Pipeline}/PipelineBuilder.php (100%) rename {Pipeline => src/Pipeline}/PipelineFactory.php (100%) rename {Providers => src/Providers}/ConfigServiceProvider.php (100%) rename {Providers => src/Providers}/FlysystemServiceProvider.php (100%) rename {Providers => src/Providers}/PdoServiceProvider.php (100%) rename {Providers => src/Providers}/RouterServiceProvider.php (100%) rename {Providers => src/Providers}/RoutingServiceProvider.php (100%) rename {Proxy => src/Proxy}/Codefy.php (100%) rename {Scheduler => src/Scheduler}/BaseTask.php (100%) rename {Scheduler => src/Scheduler}/Event/TaskCompleted.php (100%) rename {Scheduler => src/Scheduler}/Event/TaskFailed.php (100%) rename {Scheduler => src/Scheduler}/Event/TaskSkipped.php (100%) rename {Scheduler => src/Scheduler}/Event/TaskStarted.php (100%) rename {Scheduler => src/Scheduler}/Expressions/At.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Daily.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Date.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Friday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Monday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Saturday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Sunday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Thursday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Tuesday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/DayOfWeek/Wednesday.php (100%) rename {Scheduler => src/Scheduler}/Expressions/EveryMinute.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Expressional.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Hourly.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/April.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/August.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/December.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/February.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/January.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/July.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/June.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/March.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/May.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/November.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/October.php (100%) rename {Scheduler => src/Scheduler}/Expressions/MonthOfYear/September.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Monthly.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Quarterly.php (100%) rename {Scheduler => src/Scheduler}/Expressions/WeekDays.php (100%) rename {Scheduler => src/Scheduler}/Expressions/WeekEnds.php (100%) rename {Scheduler => src/Scheduler}/Expressions/Weekly.php (100%) rename {Scheduler => src/Scheduler}/FailedProcessor.php (100%) rename {Scheduler => src/Scheduler}/Mutex/CacheLocker.php (100%) rename {Scheduler => src/Scheduler}/Mutex/Locker.php (100%) rename {Scheduler => src/Scheduler}/Processor/BaseProcessor.php (100%) rename {Scheduler => src/Scheduler}/Processor/Callback.php (100%) rename {Scheduler => src/Scheduler}/Processor/Dispatcher.php (100%) rename {Scheduler => src/Scheduler}/Processor/Processor.php (100%) rename {Scheduler => src/Scheduler}/Processor/Shell.php (100%) rename {Scheduler => src/Scheduler}/Schedule.php (100%) rename {Scheduler => src/Scheduler}/Stack.php (100%) rename {Scheduler => src/Scheduler}/Task.php (100%) rename {Scheduler => src/Scheduler}/Traits/ExpressionAware.php (100%) rename {Scheduler => src/Scheduler}/Traits/LiteralAware.php (100%) rename {Scheduler => src/Scheduler}/Traits/MailerAware.php (100%) rename {Scheduler => src/Scheduler}/Traits/ScheduleValidateAware.php (100%) rename {Scheduler => src/Scheduler}/ValueObject/TaskId.php (100%) rename {Stubs => src/Stubs}/ExampleController.stub (100%) rename {Stubs => src/Stubs}/ExampleError.stub (100%) rename {Stubs => src/Stubs}/ExampleMiddleware.stub (100%) rename {Stubs => src/Stubs}/ExampleProvider.stub (100%) rename {Stubs => src/Stubs}/ExampleRepository.stub (100%) rename {Support => src/Support}/ArgsParser.php (100%) rename {Support => src/Support}/BasePathDetector.php (100%) rename {Support => src/Support}/CodefyMailer.php (100%) rename {Support => src/Support}/CodefyServiceProvider.php (100%) rename {Support => src/Support}/LocalStorage.php (100%) rename {Support => src/Support}/Password.php (100%) rename {Support => src/Support}/Paths.php (100%) rename {Support => src/Support}/RequestMethod.php (100%) rename {Support => src/Support}/SeoFactory.php (100%) rename {Support => src/Support}/StringParser.php (100%) rename {Support => src/Support}/Traits/ContainerAware.php (100%) rename {Support => src/Support}/Traits/DbTransactionsAware.php (100%) rename {Traits => src/Traits}/LoggerAware.php (100%) rename {View => src/View}/FenomView.php (100%) rename {View => src/View}/FoilView.php (100%) diff --git a/.gitignore b/.gitignore index e50a0bc..6780dee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ .idea -/config/ -files -Scheduler/Tests -storage +src/config +src/files +src/storage /vendor/ .env .env.development diff --git a/composer.json b/composer.json index ff828a4..1c51aa6 100644 --- a/composer.json +++ b/composer.json @@ -39,12 +39,12 @@ }, "autoload": { "psr-4": { - "Codefy\\Framework\\": "" + "Codefy\\Framework\\": "src/" }, "files": [ - "Helpers/core.php", - "Helpers/path.php", - "Http/Middleware/Csrf/helpers.php" + "src/Helpers/core.php", + "src/Helpers/path.php", + "src/Http/Middleware/Csrf/helpers.php" ] }, "require-dev": { @@ -54,6 +54,11 @@ "mockery/mockery": "^1", "qubus/qubus-coding-standard": "^2" }, + "autoload-dev": { + "psr-4": { + "Codefy\\Framework\\Tests\\": "tests/" + } + }, "scripts": { "test": "vendor/bin/pest", "cs-check": "phpcs", diff --git a/Application.php b/src/Application.php similarity index 100% rename from Application.php rename to src/Application.php diff --git a/Auth/Auth.php b/src/Auth/Auth.php similarity index 100% rename from Auth/Auth.php rename to src/Auth/Auth.php diff --git a/Auth/Rbac/Entity/AssertionRule.php b/src/Auth/Rbac/Entity/AssertionRule.php similarity index 100% rename from Auth/Rbac/Entity/AssertionRule.php rename to src/Auth/Rbac/Entity/AssertionRule.php diff --git a/Auth/Rbac/Entity/Permission.php b/src/Auth/Rbac/Entity/Permission.php similarity index 100% rename from Auth/Rbac/Entity/Permission.php rename to src/Auth/Rbac/Entity/Permission.php diff --git a/Auth/Rbac/Entity/RbacPermission.php b/src/Auth/Rbac/Entity/RbacPermission.php similarity index 100% rename from Auth/Rbac/Entity/RbacPermission.php rename to src/Auth/Rbac/Entity/RbacPermission.php diff --git a/Auth/Rbac/Entity/RbacRole.php b/src/Auth/Rbac/Entity/RbacRole.php similarity index 100% rename from Auth/Rbac/Entity/RbacRole.php rename to src/Auth/Rbac/Entity/RbacRole.php diff --git a/Auth/Rbac/Entity/Role.php b/src/Auth/Rbac/Entity/Role.php similarity index 100% rename from Auth/Rbac/Entity/Role.php rename to src/Auth/Rbac/Entity/Role.php diff --git a/Auth/Rbac/Exception/SentinelException.php b/src/Auth/Rbac/Exception/SentinelException.php similarity index 100% rename from Auth/Rbac/Exception/SentinelException.php rename to src/Auth/Rbac/Exception/SentinelException.php diff --git a/Auth/Rbac/Guard.php b/src/Auth/Rbac/Guard.php similarity index 100% rename from Auth/Rbac/Guard.php rename to src/Auth/Rbac/Guard.php diff --git a/Auth/Rbac/Rbac.php b/src/Auth/Rbac/Rbac.php similarity index 100% rename from Auth/Rbac/Rbac.php rename to src/Auth/Rbac/Rbac.php diff --git a/Auth/Rbac/RbacLoader.php b/src/Auth/Rbac/RbacLoader.php similarity index 100% rename from Auth/Rbac/RbacLoader.php rename to src/Auth/Rbac/RbacLoader.php diff --git a/Auth/Rbac/Resource/BaseStorageResource.php b/src/Auth/Rbac/Resource/BaseStorageResource.php similarity index 100% rename from Auth/Rbac/Resource/BaseStorageResource.php rename to src/Auth/Rbac/Resource/BaseStorageResource.php diff --git a/Auth/Rbac/Resource/FileResource.php b/src/Auth/Rbac/Resource/FileResource.php similarity index 100% rename from Auth/Rbac/Resource/FileResource.php rename to src/Auth/Rbac/Resource/FileResource.php diff --git a/Auth/Rbac/Resource/StorageResource.php b/src/Auth/Rbac/Resource/StorageResource.php similarity index 100% rename from Auth/Rbac/Resource/StorageResource.php rename to src/Auth/Rbac/Resource/StorageResource.php diff --git a/Auth/Repository/AuthUserRepository.php b/src/Auth/Repository/AuthUserRepository.php similarity index 100% rename from Auth/Repository/AuthUserRepository.php rename to src/Auth/Repository/AuthUserRepository.php diff --git a/Auth/Repository/PdoRepository.php b/src/Auth/Repository/PdoRepository.php similarity index 100% rename from Auth/Repository/PdoRepository.php rename to src/Auth/Repository/PdoRepository.php diff --git a/Auth/Sentinel.php b/src/Auth/Sentinel.php similarity index 100% rename from Auth/Sentinel.php rename to src/Auth/Sentinel.php diff --git a/Auth/Traits/BadPropertyCallException.php b/src/Auth/Traits/BadPropertyCallException.php similarity index 100% rename from Auth/Traits/BadPropertyCallException.php rename to src/Auth/Traits/BadPropertyCallException.php diff --git a/Auth/Traits/ImmutableAware.php b/src/Auth/Traits/ImmutableAware.php similarity index 100% rename from Auth/Traits/ImmutableAware.php rename to src/Auth/Traits/ImmutableAware.php diff --git a/Auth/UserSession.php b/src/Auth/UserSession.php similarity index 100% rename from Auth/UserSession.php rename to src/Auth/UserSession.php diff --git a/Bootstrap/BootProviders.php b/src/Bootstrap/BootProviders.php similarity index 100% rename from Bootstrap/BootProviders.php rename to src/Bootstrap/BootProviders.php diff --git a/Bootstrap/RegisterProviders.php b/src/Bootstrap/RegisterProviders.php similarity index 100% rename from Bootstrap/RegisterProviders.php rename to src/Bootstrap/RegisterProviders.php diff --git a/Configuration/ApplicationBuilder.php b/src/Configuration/ApplicationBuilder.php similarity index 100% rename from Configuration/ApplicationBuilder.php rename to src/Configuration/ApplicationBuilder.php diff --git a/Console/Commands/CheckCommand.php b/src/Console/Commands/CheckCommand.php similarity index 100% rename from Console/Commands/CheckCommand.php rename to src/Console/Commands/CheckCommand.php diff --git a/Console/Commands/DownCommand.php b/src/Console/Commands/DownCommand.php similarity index 100% rename from Console/Commands/DownCommand.php rename to src/Console/Commands/DownCommand.php diff --git a/Console/Commands/GenerateCommand.php b/src/Console/Commands/GenerateCommand.php similarity index 100% rename from Console/Commands/GenerateCommand.php rename to src/Console/Commands/GenerateCommand.php diff --git a/Console/Commands/InitCommand.php b/src/Console/Commands/InitCommand.php similarity index 100% rename from Console/Commands/InitCommand.php rename to src/Console/Commands/InitCommand.php diff --git a/Console/Commands/ListCommand.php b/src/Console/Commands/ListCommand.php similarity index 100% rename from Console/Commands/ListCommand.php rename to src/Console/Commands/ListCommand.php diff --git a/Console/Commands/MakeCommand.php b/src/Console/Commands/MakeCommand.php similarity index 100% rename from Console/Commands/MakeCommand.php rename to src/Console/Commands/MakeCommand.php diff --git a/Console/Commands/MigrateCommand.php b/src/Console/Commands/MigrateCommand.php similarity index 100% rename from Console/Commands/MigrateCommand.php rename to src/Console/Commands/MigrateCommand.php diff --git a/Console/Commands/PasswordHashCommand.php b/src/Console/Commands/PasswordHashCommand.php similarity index 100% rename from Console/Commands/PasswordHashCommand.php rename to src/Console/Commands/PasswordHashCommand.php diff --git a/Console/Commands/PhpMigCommand.php b/src/Console/Commands/PhpMigCommand.php similarity index 100% rename from Console/Commands/PhpMigCommand.php rename to src/Console/Commands/PhpMigCommand.php diff --git a/Console/Commands/RedoCommand.php b/src/Console/Commands/RedoCommand.php similarity index 100% rename from Console/Commands/RedoCommand.php rename to src/Console/Commands/RedoCommand.php diff --git a/Console/Commands/RollbackCommand.php b/src/Console/Commands/RollbackCommand.php similarity index 100% rename from Console/Commands/RollbackCommand.php rename to src/Console/Commands/RollbackCommand.php diff --git a/Console/Commands/ScheduleRunCommand.php b/src/Console/Commands/ScheduleRunCommand.php similarity index 100% rename from Console/Commands/ScheduleRunCommand.php rename to src/Console/Commands/ScheduleRunCommand.php diff --git a/Console/Commands/ServeCommand.php b/src/Console/Commands/ServeCommand.php similarity index 100% rename from Console/Commands/ServeCommand.php rename to src/Console/Commands/ServeCommand.php diff --git a/Console/Commands/StatusCommand.php b/src/Console/Commands/StatusCommand.php similarity index 100% rename from Console/Commands/StatusCommand.php rename to src/Console/Commands/StatusCommand.php diff --git a/Console/Commands/Traits/MakeCommandAware.php b/src/Console/Commands/Traits/MakeCommandAware.php similarity index 100% rename from Console/Commands/Traits/MakeCommandAware.php rename to src/Console/Commands/Traits/MakeCommandAware.php diff --git a/Console/Commands/UlidCommand.php b/src/Console/Commands/UlidCommand.php similarity index 100% rename from Console/Commands/UlidCommand.php rename to src/Console/Commands/UlidCommand.php diff --git a/Console/Commands/UpCommand.php b/src/Console/Commands/UpCommand.php similarity index 100% rename from Console/Commands/UpCommand.php rename to src/Console/Commands/UpCommand.php diff --git a/Console/Commands/UuidCommand.php b/src/Console/Commands/UuidCommand.php similarity index 100% rename from Console/Commands/UuidCommand.php rename to src/Console/Commands/UuidCommand.php diff --git a/Console/ConsoleApplication.php b/src/Console/ConsoleApplication.php similarity index 100% rename from Console/ConsoleApplication.php rename to src/Console/ConsoleApplication.php diff --git a/Console/ConsoleCommand.php b/src/Console/ConsoleCommand.php similarity index 100% rename from Console/ConsoleCommand.php rename to src/Console/ConsoleCommand.php diff --git a/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php similarity index 100% rename from Console/ConsoleKernel.php rename to src/Console/ConsoleKernel.php diff --git a/Console/Exceptions/MakeCommandFileAlreadyExistsException.php b/src/Console/Exceptions/MakeCommandFileAlreadyExistsException.php similarity index 100% rename from Console/Exceptions/MakeCommandFileAlreadyExistsException.php rename to src/Console/Exceptions/MakeCommandFileAlreadyExistsException.php diff --git a/Contracts/Console/Kernel.php b/src/Contracts/Console/Kernel.php similarity index 100% rename from Contracts/Console/Kernel.php rename to src/Contracts/Console/Kernel.php diff --git a/Contracts/Http/Kernel.php b/src/Contracts/Http/Kernel.php similarity index 100% rename from Contracts/Http/Kernel.php rename to src/Contracts/Http/Kernel.php diff --git a/Contracts/LoggerFactory.php b/src/Contracts/LoggerFactory.php similarity index 100% rename from Contracts/LoggerFactory.php rename to src/Contracts/LoggerFactory.php diff --git a/Contracts/MailerFactory.php b/src/Contracts/MailerFactory.php similarity index 100% rename from Contracts/MailerFactory.php rename to src/Contracts/MailerFactory.php diff --git a/Contracts/RoutingController.php b/src/Contracts/RoutingController.php similarity index 100% rename from Contracts/RoutingController.php rename to src/Contracts/RoutingController.php diff --git a/Factory/FileLoggerFactory.php b/src/Factory/FileLoggerFactory.php similarity index 100% rename from Factory/FileLoggerFactory.php rename to src/Factory/FileLoggerFactory.php diff --git a/Factory/FileLoggerSmtpFactory.php b/src/Factory/FileLoggerSmtpFactory.php similarity index 100% rename from Factory/FileLoggerSmtpFactory.php rename to src/Factory/FileLoggerSmtpFactory.php diff --git a/Factory/PHPMailerSmtpFactory.php b/src/Factory/PHPMailerSmtpFactory.php similarity index 100% rename from Factory/PHPMailerSmtpFactory.php rename to src/Factory/PHPMailerSmtpFactory.php diff --git a/Factory/Traits/FileLoggerAware.php b/src/Factory/Traits/FileLoggerAware.php similarity index 100% rename from Factory/Traits/FileLoggerAware.php rename to src/Factory/Traits/FileLoggerAware.php diff --git a/Helpers/core.php b/src/Helpers/core.php similarity index 100% rename from Helpers/core.php rename to src/Helpers/core.php diff --git a/Helpers/path.php b/src/Helpers/path.php similarity index 100% rename from Helpers/path.php rename to src/Helpers/path.php diff --git a/Http/BaseController.php b/src/Http/BaseController.php similarity index 100% rename from Http/BaseController.php rename to src/Http/BaseController.php diff --git a/Http/Errors/HttpRequestError.php b/src/Http/Errors/HttpRequestError.php similarity index 100% rename from Http/Errors/HttpRequestError.php rename to src/Http/Errors/HttpRequestError.php diff --git a/Http/HttpClient.php b/src/Http/HttpClient.php similarity index 100% rename from Http/HttpClient.php rename to src/Http/HttpClient.php diff --git a/Http/Kernel.php b/src/Http/Kernel.php similarity index 100% rename from Http/Kernel.php rename to src/Http/Kernel.php diff --git a/Http/Middleware/Auth/AuthenticationMiddleware.php b/src/Http/Middleware/Auth/AuthenticationMiddleware.php similarity index 100% rename from Http/Middleware/Auth/AuthenticationMiddleware.php rename to src/Http/Middleware/Auth/AuthenticationMiddleware.php diff --git a/Http/Middleware/Auth/ExpireUserSessionMiddleware.php b/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php similarity index 100% rename from Http/Middleware/Auth/ExpireUserSessionMiddleware.php rename to src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php diff --git a/Http/Middleware/Auth/UserAuthorizationMiddleware.php b/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php similarity index 100% rename from Http/Middleware/Auth/UserAuthorizationMiddleware.php rename to src/Http/Middleware/Auth/UserAuthorizationMiddleware.php diff --git a/Http/Middleware/Auth/UserSessionMiddleware.php b/src/Http/Middleware/Auth/UserSessionMiddleware.php similarity index 100% rename from Http/Middleware/Auth/UserSessionMiddleware.php rename to src/Http/Middleware/Auth/UserSessionMiddleware.php diff --git a/Http/Middleware/Cache/CacheExpiresMiddleware.php b/src/Http/Middleware/Cache/CacheExpiresMiddleware.php similarity index 100% rename from Http/Middleware/Cache/CacheExpiresMiddleware.php rename to src/Http/Middleware/Cache/CacheExpiresMiddleware.php diff --git a/Http/Middleware/Cache/CacheMiddleware.php b/src/Http/Middleware/Cache/CacheMiddleware.php similarity index 100% rename from Http/Middleware/Cache/CacheMiddleware.php rename to src/Http/Middleware/Cache/CacheMiddleware.php diff --git a/Http/Middleware/Cache/CachePreventionMiddleware.php b/src/Http/Middleware/Cache/CachePreventionMiddleware.php similarity index 100% rename from Http/Middleware/Cache/CachePreventionMiddleware.php rename to src/Http/Middleware/Cache/CachePreventionMiddleware.php diff --git a/Http/Middleware/Cache/ClearSiteDataMiddleware.php b/src/Http/Middleware/Cache/ClearSiteDataMiddleware.php similarity index 100% rename from Http/Middleware/Cache/ClearSiteDataMiddleware.php rename to src/Http/Middleware/Cache/ClearSiteDataMiddleware.php diff --git a/Http/Middleware/ContentCacheMiddleware.php b/src/Http/Middleware/ContentCacheMiddleware.php similarity index 100% rename from Http/Middleware/ContentCacheMiddleware.php rename to src/Http/Middleware/ContentCacheMiddleware.php diff --git a/Http/Middleware/CorsMiddleware.php b/src/Http/Middleware/CorsMiddleware.php similarity index 100% rename from Http/Middleware/CorsMiddleware.php rename to src/Http/Middleware/CorsMiddleware.php diff --git a/Http/Middleware/Csrf/CsrfProtectionMiddleware.php b/src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php similarity index 100% rename from Http/Middleware/Csrf/CsrfProtectionMiddleware.php rename to src/Http/Middleware/Csrf/CsrfProtectionMiddleware.php diff --git a/Http/Middleware/Csrf/CsrfSession.php b/src/Http/Middleware/Csrf/CsrfSession.php similarity index 100% rename from Http/Middleware/Csrf/CsrfSession.php rename to src/Http/Middleware/Csrf/CsrfSession.php diff --git a/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php similarity index 100% rename from Http/Middleware/Csrf/CsrfTokenMiddleware.php rename to src/Http/Middleware/Csrf/CsrfTokenMiddleware.php diff --git a/Http/Middleware/Csrf/Traits/CsrfTokenAware.php b/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php similarity index 100% rename from Http/Middleware/Csrf/Traits/CsrfTokenAware.php rename to src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php diff --git a/Http/Middleware/Csrf/helpers.php b/src/Http/Middleware/Csrf/helpers.php similarity index 100% rename from Http/Middleware/Csrf/helpers.php rename to src/Http/Middleware/Csrf/helpers.php diff --git a/Http/Middleware/CssMinifierMiddleware.php b/src/Http/Middleware/CssMinifierMiddleware.php similarity index 100% rename from Http/Middleware/CssMinifierMiddleware.php rename to src/Http/Middleware/CssMinifierMiddleware.php diff --git a/Http/Middleware/DebugBarMiddleware.php b/src/Http/Middleware/DebugBarMiddleware.php similarity index 100% rename from Http/Middleware/DebugBarMiddleware.php rename to src/Http/Middleware/DebugBarMiddleware.php diff --git a/Http/Middleware/HtmlMinifierMiddleware.php b/src/Http/Middleware/HtmlMinifierMiddleware.php similarity index 100% rename from Http/Middleware/HtmlMinifierMiddleware.php rename to src/Http/Middleware/HtmlMinifierMiddleware.php diff --git a/Http/Middleware/JsMinifierMiddleware.php b/src/Http/Middleware/JsMinifierMiddleware.php similarity index 100% rename from Http/Middleware/JsMinifierMiddleware.php rename to src/Http/Middleware/JsMinifierMiddleware.php diff --git a/Http/Middleware/SecureHeaders/ContentSecurityPolicyMiddleware.php b/src/Http/Middleware/SecureHeaders/ContentSecurityPolicyMiddleware.php similarity index 100% rename from Http/Middleware/SecureHeaders/ContentSecurityPolicyMiddleware.php rename to src/Http/Middleware/SecureHeaders/ContentSecurityPolicyMiddleware.php diff --git a/Http/Middleware/SecureHeaders/SecureHeaders.php b/src/Http/Middleware/SecureHeaders/SecureHeaders.php similarity index 100% rename from Http/Middleware/SecureHeaders/SecureHeaders.php rename to src/Http/Middleware/SecureHeaders/SecureHeaders.php diff --git a/Http/Middleware/Spam/HoneyPotMiddleware.php b/src/Http/Middleware/Spam/HoneyPotMiddleware.php similarity index 100% rename from Http/Middleware/Spam/HoneyPotMiddleware.php rename to src/Http/Middleware/Spam/HoneyPotMiddleware.php diff --git a/Http/Middleware/Spam/ReferrerSpamMiddleware.php b/src/Http/Middleware/Spam/ReferrerSpamMiddleware.php similarity index 100% rename from Http/Middleware/Spam/ReferrerSpamMiddleware.php rename to src/Http/Middleware/Spam/ReferrerSpamMiddleware.php diff --git a/Http/Middleware/ThrottleMiddleware.php b/src/Http/Middleware/ThrottleMiddleware.php similarity index 100% rename from Http/Middleware/ThrottleMiddleware.php rename to src/Http/Middleware/ThrottleMiddleware.php diff --git a/Http/Swoole/App.php b/src/Http/Swoole/App.php similarity index 100% rename from Http/Swoole/App.php rename to src/Http/Swoole/App.php diff --git a/Http/Swoole/BridgeManager.php b/src/Http/Swoole/BridgeManager.php similarity index 100% rename from Http/Swoole/BridgeManager.php rename to src/Http/Swoole/BridgeManager.php diff --git a/Http/Throttle/Condition.php b/src/Http/Throttle/Condition.php similarity index 100% rename from Http/Throttle/Condition.php rename to src/Http/Throttle/Condition.php diff --git a/Http/Throttle/Interval.php b/src/Http/Throttle/Interval.php similarity index 100% rename from Http/Throttle/Interval.php rename to src/Http/Throttle/Interval.php diff --git a/Http/Throttle/RateException.php b/src/Http/Throttle/RateException.php similarity index 100% rename from Http/Throttle/RateException.php rename to src/Http/Throttle/RateException.php diff --git a/Http/Throttle/RateLimiter.php b/src/Http/Throttle/RateLimiter.php similarity index 100% rename from Http/Throttle/RateLimiter.php rename to src/Http/Throttle/RateLimiter.php diff --git a/Migration/Adapter/DbalMigrationAdapter.php b/src/Migration/Adapter/DbalMigrationAdapter.php similarity index 100% rename from Migration/Adapter/DbalMigrationAdapter.php rename to src/Migration/Adapter/DbalMigrationAdapter.php diff --git a/Migration/Adapter/FileMigrationAdapter.php b/src/Migration/Adapter/FileMigrationAdapter.php similarity index 100% rename from Migration/Adapter/FileMigrationAdapter.php rename to src/Migration/Adapter/FileMigrationAdapter.php diff --git a/Migration/Adapter/MigrationAdapter.php b/src/Migration/Adapter/MigrationAdapter.php similarity index 100% rename from Migration/Adapter/MigrationAdapter.php rename to src/Migration/Adapter/MigrationAdapter.php diff --git a/Migration/Migration.php b/src/Migration/Migration.php similarity index 100% rename from Migration/Migration.php rename to src/Migration/Migration.php diff --git a/Migration/Migrator.php b/src/Migration/Migrator.php similarity index 100% rename from Migration/Migrator.php rename to src/Migration/Migrator.php diff --git a/Pipeline/Chainable.php b/src/Pipeline/Chainable.php similarity index 100% rename from Pipeline/Chainable.php rename to src/Pipeline/Chainable.php diff --git a/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php similarity index 100% rename from Pipeline/Pipeline.php rename to src/Pipeline/Pipeline.php diff --git a/Pipeline/PipelineBuilder.php b/src/Pipeline/PipelineBuilder.php similarity index 100% rename from Pipeline/PipelineBuilder.php rename to src/Pipeline/PipelineBuilder.php diff --git a/Pipeline/PipelineFactory.php b/src/Pipeline/PipelineFactory.php similarity index 100% rename from Pipeline/PipelineFactory.php rename to src/Pipeline/PipelineFactory.php diff --git a/Providers/ConfigServiceProvider.php b/src/Providers/ConfigServiceProvider.php similarity index 100% rename from Providers/ConfigServiceProvider.php rename to src/Providers/ConfigServiceProvider.php diff --git a/Providers/FlysystemServiceProvider.php b/src/Providers/FlysystemServiceProvider.php similarity index 100% rename from Providers/FlysystemServiceProvider.php rename to src/Providers/FlysystemServiceProvider.php diff --git a/Providers/PdoServiceProvider.php b/src/Providers/PdoServiceProvider.php similarity index 100% rename from Providers/PdoServiceProvider.php rename to src/Providers/PdoServiceProvider.php diff --git a/Providers/RouterServiceProvider.php b/src/Providers/RouterServiceProvider.php similarity index 100% rename from Providers/RouterServiceProvider.php rename to src/Providers/RouterServiceProvider.php diff --git a/Providers/RoutingServiceProvider.php b/src/Providers/RoutingServiceProvider.php similarity index 100% rename from Providers/RoutingServiceProvider.php rename to src/Providers/RoutingServiceProvider.php diff --git a/Proxy/Codefy.php b/src/Proxy/Codefy.php similarity index 100% rename from Proxy/Codefy.php rename to src/Proxy/Codefy.php diff --git a/Scheduler/BaseTask.php b/src/Scheduler/BaseTask.php similarity index 100% rename from Scheduler/BaseTask.php rename to src/Scheduler/BaseTask.php diff --git a/Scheduler/Event/TaskCompleted.php b/src/Scheduler/Event/TaskCompleted.php similarity index 100% rename from Scheduler/Event/TaskCompleted.php rename to src/Scheduler/Event/TaskCompleted.php diff --git a/Scheduler/Event/TaskFailed.php b/src/Scheduler/Event/TaskFailed.php similarity index 100% rename from Scheduler/Event/TaskFailed.php rename to src/Scheduler/Event/TaskFailed.php diff --git a/Scheduler/Event/TaskSkipped.php b/src/Scheduler/Event/TaskSkipped.php similarity index 100% rename from Scheduler/Event/TaskSkipped.php rename to src/Scheduler/Event/TaskSkipped.php diff --git a/Scheduler/Event/TaskStarted.php b/src/Scheduler/Event/TaskStarted.php similarity index 100% rename from Scheduler/Event/TaskStarted.php rename to src/Scheduler/Event/TaskStarted.php diff --git a/Scheduler/Expressions/At.php b/src/Scheduler/Expressions/At.php similarity index 100% rename from Scheduler/Expressions/At.php rename to src/Scheduler/Expressions/At.php diff --git a/Scheduler/Expressions/Daily.php b/src/Scheduler/Expressions/Daily.php similarity index 100% rename from Scheduler/Expressions/Daily.php rename to src/Scheduler/Expressions/Daily.php diff --git a/Scheduler/Expressions/Date.php b/src/Scheduler/Expressions/Date.php similarity index 100% rename from Scheduler/Expressions/Date.php rename to src/Scheduler/Expressions/Date.php diff --git a/Scheduler/Expressions/DayOfWeek/Friday.php b/src/Scheduler/Expressions/DayOfWeek/Friday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Friday.php rename to src/Scheduler/Expressions/DayOfWeek/Friday.php diff --git a/Scheduler/Expressions/DayOfWeek/Monday.php b/src/Scheduler/Expressions/DayOfWeek/Monday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Monday.php rename to src/Scheduler/Expressions/DayOfWeek/Monday.php diff --git a/Scheduler/Expressions/DayOfWeek/Saturday.php b/src/Scheduler/Expressions/DayOfWeek/Saturday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Saturday.php rename to src/Scheduler/Expressions/DayOfWeek/Saturday.php diff --git a/Scheduler/Expressions/DayOfWeek/Sunday.php b/src/Scheduler/Expressions/DayOfWeek/Sunday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Sunday.php rename to src/Scheduler/Expressions/DayOfWeek/Sunday.php diff --git a/Scheduler/Expressions/DayOfWeek/Thursday.php b/src/Scheduler/Expressions/DayOfWeek/Thursday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Thursday.php rename to src/Scheduler/Expressions/DayOfWeek/Thursday.php diff --git a/Scheduler/Expressions/DayOfWeek/Tuesday.php b/src/Scheduler/Expressions/DayOfWeek/Tuesday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Tuesday.php rename to src/Scheduler/Expressions/DayOfWeek/Tuesday.php diff --git a/Scheduler/Expressions/DayOfWeek/Wednesday.php b/src/Scheduler/Expressions/DayOfWeek/Wednesday.php similarity index 100% rename from Scheduler/Expressions/DayOfWeek/Wednesday.php rename to src/Scheduler/Expressions/DayOfWeek/Wednesday.php diff --git a/Scheduler/Expressions/EveryMinute.php b/src/Scheduler/Expressions/EveryMinute.php similarity index 100% rename from Scheduler/Expressions/EveryMinute.php rename to src/Scheduler/Expressions/EveryMinute.php diff --git a/Scheduler/Expressions/Expressional.php b/src/Scheduler/Expressions/Expressional.php similarity index 100% rename from Scheduler/Expressions/Expressional.php rename to src/Scheduler/Expressions/Expressional.php diff --git a/Scheduler/Expressions/Hourly.php b/src/Scheduler/Expressions/Hourly.php similarity index 100% rename from Scheduler/Expressions/Hourly.php rename to src/Scheduler/Expressions/Hourly.php diff --git a/Scheduler/Expressions/MonthOfYear/April.php b/src/Scheduler/Expressions/MonthOfYear/April.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/April.php rename to src/Scheduler/Expressions/MonthOfYear/April.php diff --git a/Scheduler/Expressions/MonthOfYear/August.php b/src/Scheduler/Expressions/MonthOfYear/August.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/August.php rename to src/Scheduler/Expressions/MonthOfYear/August.php diff --git a/Scheduler/Expressions/MonthOfYear/December.php b/src/Scheduler/Expressions/MonthOfYear/December.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/December.php rename to src/Scheduler/Expressions/MonthOfYear/December.php diff --git a/Scheduler/Expressions/MonthOfYear/February.php b/src/Scheduler/Expressions/MonthOfYear/February.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/February.php rename to src/Scheduler/Expressions/MonthOfYear/February.php diff --git a/Scheduler/Expressions/MonthOfYear/January.php b/src/Scheduler/Expressions/MonthOfYear/January.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/January.php rename to src/Scheduler/Expressions/MonthOfYear/January.php diff --git a/Scheduler/Expressions/MonthOfYear/July.php b/src/Scheduler/Expressions/MonthOfYear/July.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/July.php rename to src/Scheduler/Expressions/MonthOfYear/July.php diff --git a/Scheduler/Expressions/MonthOfYear/June.php b/src/Scheduler/Expressions/MonthOfYear/June.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/June.php rename to src/Scheduler/Expressions/MonthOfYear/June.php diff --git a/Scheduler/Expressions/MonthOfYear/March.php b/src/Scheduler/Expressions/MonthOfYear/March.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/March.php rename to src/Scheduler/Expressions/MonthOfYear/March.php diff --git a/Scheduler/Expressions/MonthOfYear/May.php b/src/Scheduler/Expressions/MonthOfYear/May.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/May.php rename to src/Scheduler/Expressions/MonthOfYear/May.php diff --git a/Scheduler/Expressions/MonthOfYear/November.php b/src/Scheduler/Expressions/MonthOfYear/November.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/November.php rename to src/Scheduler/Expressions/MonthOfYear/November.php diff --git a/Scheduler/Expressions/MonthOfYear/October.php b/src/Scheduler/Expressions/MonthOfYear/October.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/October.php rename to src/Scheduler/Expressions/MonthOfYear/October.php diff --git a/Scheduler/Expressions/MonthOfYear/September.php b/src/Scheduler/Expressions/MonthOfYear/September.php similarity index 100% rename from Scheduler/Expressions/MonthOfYear/September.php rename to src/Scheduler/Expressions/MonthOfYear/September.php diff --git a/Scheduler/Expressions/Monthly.php b/src/Scheduler/Expressions/Monthly.php similarity index 100% rename from Scheduler/Expressions/Monthly.php rename to src/Scheduler/Expressions/Monthly.php diff --git a/Scheduler/Expressions/Quarterly.php b/src/Scheduler/Expressions/Quarterly.php similarity index 100% rename from Scheduler/Expressions/Quarterly.php rename to src/Scheduler/Expressions/Quarterly.php diff --git a/Scheduler/Expressions/WeekDays.php b/src/Scheduler/Expressions/WeekDays.php similarity index 100% rename from Scheduler/Expressions/WeekDays.php rename to src/Scheduler/Expressions/WeekDays.php diff --git a/Scheduler/Expressions/WeekEnds.php b/src/Scheduler/Expressions/WeekEnds.php similarity index 100% rename from Scheduler/Expressions/WeekEnds.php rename to src/Scheduler/Expressions/WeekEnds.php diff --git a/Scheduler/Expressions/Weekly.php b/src/Scheduler/Expressions/Weekly.php similarity index 100% rename from Scheduler/Expressions/Weekly.php rename to src/Scheduler/Expressions/Weekly.php diff --git a/Scheduler/FailedProcessor.php b/src/Scheduler/FailedProcessor.php similarity index 100% rename from Scheduler/FailedProcessor.php rename to src/Scheduler/FailedProcessor.php diff --git a/Scheduler/Mutex/CacheLocker.php b/src/Scheduler/Mutex/CacheLocker.php similarity index 100% rename from Scheduler/Mutex/CacheLocker.php rename to src/Scheduler/Mutex/CacheLocker.php diff --git a/Scheduler/Mutex/Locker.php b/src/Scheduler/Mutex/Locker.php similarity index 100% rename from Scheduler/Mutex/Locker.php rename to src/Scheduler/Mutex/Locker.php diff --git a/Scheduler/Processor/BaseProcessor.php b/src/Scheduler/Processor/BaseProcessor.php similarity index 100% rename from Scheduler/Processor/BaseProcessor.php rename to src/Scheduler/Processor/BaseProcessor.php diff --git a/Scheduler/Processor/Callback.php b/src/Scheduler/Processor/Callback.php similarity index 100% rename from Scheduler/Processor/Callback.php rename to src/Scheduler/Processor/Callback.php diff --git a/Scheduler/Processor/Dispatcher.php b/src/Scheduler/Processor/Dispatcher.php similarity index 100% rename from Scheduler/Processor/Dispatcher.php rename to src/Scheduler/Processor/Dispatcher.php diff --git a/Scheduler/Processor/Processor.php b/src/Scheduler/Processor/Processor.php similarity index 100% rename from Scheduler/Processor/Processor.php rename to src/Scheduler/Processor/Processor.php diff --git a/Scheduler/Processor/Shell.php b/src/Scheduler/Processor/Shell.php similarity index 100% rename from Scheduler/Processor/Shell.php rename to src/Scheduler/Processor/Shell.php diff --git a/Scheduler/Schedule.php b/src/Scheduler/Schedule.php similarity index 100% rename from Scheduler/Schedule.php rename to src/Scheduler/Schedule.php diff --git a/Scheduler/Stack.php b/src/Scheduler/Stack.php similarity index 100% rename from Scheduler/Stack.php rename to src/Scheduler/Stack.php diff --git a/Scheduler/Task.php b/src/Scheduler/Task.php similarity index 100% rename from Scheduler/Task.php rename to src/Scheduler/Task.php diff --git a/Scheduler/Traits/ExpressionAware.php b/src/Scheduler/Traits/ExpressionAware.php similarity index 100% rename from Scheduler/Traits/ExpressionAware.php rename to src/Scheduler/Traits/ExpressionAware.php diff --git a/Scheduler/Traits/LiteralAware.php b/src/Scheduler/Traits/LiteralAware.php similarity index 100% rename from Scheduler/Traits/LiteralAware.php rename to src/Scheduler/Traits/LiteralAware.php diff --git a/Scheduler/Traits/MailerAware.php b/src/Scheduler/Traits/MailerAware.php similarity index 100% rename from Scheduler/Traits/MailerAware.php rename to src/Scheduler/Traits/MailerAware.php diff --git a/Scheduler/Traits/ScheduleValidateAware.php b/src/Scheduler/Traits/ScheduleValidateAware.php similarity index 100% rename from Scheduler/Traits/ScheduleValidateAware.php rename to src/Scheduler/Traits/ScheduleValidateAware.php diff --git a/Scheduler/ValueObject/TaskId.php b/src/Scheduler/ValueObject/TaskId.php similarity index 100% rename from Scheduler/ValueObject/TaskId.php rename to src/Scheduler/ValueObject/TaskId.php diff --git a/Stubs/ExampleController.stub b/src/Stubs/ExampleController.stub similarity index 100% rename from Stubs/ExampleController.stub rename to src/Stubs/ExampleController.stub diff --git a/Stubs/ExampleError.stub b/src/Stubs/ExampleError.stub similarity index 100% rename from Stubs/ExampleError.stub rename to src/Stubs/ExampleError.stub diff --git a/Stubs/ExampleMiddleware.stub b/src/Stubs/ExampleMiddleware.stub similarity index 100% rename from Stubs/ExampleMiddleware.stub rename to src/Stubs/ExampleMiddleware.stub diff --git a/Stubs/ExampleProvider.stub b/src/Stubs/ExampleProvider.stub similarity index 100% rename from Stubs/ExampleProvider.stub rename to src/Stubs/ExampleProvider.stub diff --git a/Stubs/ExampleRepository.stub b/src/Stubs/ExampleRepository.stub similarity index 100% rename from Stubs/ExampleRepository.stub rename to src/Stubs/ExampleRepository.stub diff --git a/Support/ArgsParser.php b/src/Support/ArgsParser.php similarity index 100% rename from Support/ArgsParser.php rename to src/Support/ArgsParser.php diff --git a/Support/BasePathDetector.php b/src/Support/BasePathDetector.php similarity index 100% rename from Support/BasePathDetector.php rename to src/Support/BasePathDetector.php diff --git a/Support/CodefyMailer.php b/src/Support/CodefyMailer.php similarity index 100% rename from Support/CodefyMailer.php rename to src/Support/CodefyMailer.php diff --git a/Support/CodefyServiceProvider.php b/src/Support/CodefyServiceProvider.php similarity index 100% rename from Support/CodefyServiceProvider.php rename to src/Support/CodefyServiceProvider.php diff --git a/Support/LocalStorage.php b/src/Support/LocalStorage.php similarity index 100% rename from Support/LocalStorage.php rename to src/Support/LocalStorage.php diff --git a/Support/Password.php b/src/Support/Password.php similarity index 100% rename from Support/Password.php rename to src/Support/Password.php diff --git a/Support/Paths.php b/src/Support/Paths.php similarity index 100% rename from Support/Paths.php rename to src/Support/Paths.php diff --git a/Support/RequestMethod.php b/src/Support/RequestMethod.php similarity index 100% rename from Support/RequestMethod.php rename to src/Support/RequestMethod.php diff --git a/Support/SeoFactory.php b/src/Support/SeoFactory.php similarity index 100% rename from Support/SeoFactory.php rename to src/Support/SeoFactory.php diff --git a/Support/StringParser.php b/src/Support/StringParser.php similarity index 100% rename from Support/StringParser.php rename to src/Support/StringParser.php diff --git a/Support/Traits/ContainerAware.php b/src/Support/Traits/ContainerAware.php similarity index 100% rename from Support/Traits/ContainerAware.php rename to src/Support/Traits/ContainerAware.php diff --git a/Support/Traits/DbTransactionsAware.php b/src/Support/Traits/DbTransactionsAware.php similarity index 100% rename from Support/Traits/DbTransactionsAware.php rename to src/Support/Traits/DbTransactionsAware.php diff --git a/Traits/LoggerAware.php b/src/Traits/LoggerAware.php similarity index 100% rename from Traits/LoggerAware.php rename to src/Traits/LoggerAware.php diff --git a/View/FenomView.php b/src/View/FenomView.php similarity index 100% rename from View/FenomView.php rename to src/View/FenomView.php diff --git a/View/FoilView.php b/src/View/FoilView.php similarity index 100% rename from View/FoilView.php rename to src/View/FoilView.php From 344c9dd38cf59094a3839f5681be6787a976c070 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 28 Sep 2025 09:18:17 -0700 Subject: [PATCH 080/161] Fixed issue with basePath. Signed-off-by: Joshua Parker --- src/Application.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Application.php b/src/Application.php index 4f736b5..933ace7 100644 --- a/src/Application.php +++ b/src/Application.php @@ -46,7 +46,6 @@ use Qubus\Support\StringHelper; use ReflectionException; -use function Codefy\Framework\Helpers\base_path; use function dirname; use function get_class; use function is_string; @@ -153,6 +152,8 @@ final class Application extends Container // phpcs:enable /** + * @param array $params + * @throws ReflectionException * @throws TypeException */ public function __construct(array $params = []) @@ -827,7 +828,7 @@ private static function loadEnvironment(string $basePath): void { if (self::$encryptedEnv) { try { - SecureEnv::parse(base_path(path: '.env.enc'), base_path(path: '.enc.key')); + SecureEnv::parse(inputFile: $basePath . '/.env.enc', keyFile: $basePath . '/.enc.key'); } catch (BadFormatException | EnvironmentIsBrokenException | WrongKeyOrModifiedCiphertextException $e) { FileLoggerFactory::getLogger()->error($e->getMessage()); } @@ -893,6 +894,7 @@ public function isDevelopment(): bool * Create a new CodefyPHP application instance. * * @throws TypeException + * @throws ReflectionException */ public static function create(array $config): ApplicationBuilder { From 995eafce751135ebc669340fba25a710943c4a89 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 28 Sep 2025 09:18:35 -0700 Subject: [PATCH 081/161] Fixed bug in env function. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index d7caae9..044fe2b 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -98,11 +98,11 @@ function get_fresh_bootstrap(): mixed * * @param string $key * @param mixed|null $default - * @return mixed|null + * @return mixed */ function env(string $key, mixed $default = null): mixed { - return $_ENV[$key] ?? $default; + return \Qubus\Config\Helpers\env($key, $default); } /** From c6f9da5e92c8c4058f649e0d97b4c627b27cb7ee Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 28 Sep 2025 14:48:51 -0700 Subject: [PATCH 082/161] Updated php version. Signed-off-by: Joshua Parker --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab83863..16dbe75 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Latest Stable Version - PHP 8.2 + PHP 8.4 License Total Downloads CodefyPHP Support Forum From af67a96b6f47854069ed10e7e6f93ae33f338b29 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 28 Sep 2025 14:49:10 -0700 Subject: [PATCH 083/161] Ignored files and exports. Signed-off-by: Joshua Parker --- .gitattributes | 11 +++++++++++ .gitignore | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..22c2b04 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# .gitattributes + +.github export-ignore +.gitattributes export-ignore +.gitignore export-ignore +tests export-ignore +CHANGELOG.md export-ignore +LICENSE.md export-ignore +phpcs.xml export-ignore +phpunit.xml export-ignore +README.md export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6780dee..445ac00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea -src/config -src/files -src/storage +config +files +storage /vendor/ .env .env.development From 4e55a178df368540f0189b15d1c659b91fc8d201 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 28 Sep 2025 14:54:22 -0700 Subject: [PATCH 084/161] Updated phpcs and phpunit config. Signed-off-by: Joshua Parker --- phpcs.xml | 31 +++++++++++++++++-------------- phpunit.xml | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index 1b99f57..9a0285f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -12,20 +12,23 @@ - Auth - Bootstrap - Console - Contracts - Factory - Helpers - Http - Migration - Providers - Scheduler - Support - View - Application.php - Codefy.php + src/Auth + src/Bootstrap + src/Configuration + src/Console + src/Contracts + src/Factory + src/Helpers + src/Http + src/Migration + src/Pipeline + src/Providers + src/Proxy + src/Scheduler + src/Support + src/Traits + src/View + src/Application.php diff --git a/phpunit.xml b/phpunit.xml index 08a27fd..b1bedaf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ - ./ + ./src ./vendor From e4bde817510c590fc6943663d1b7c8cc65852f8f Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 08:39:45 -0700 Subject: [PATCH 085/161] Moved migrations to qubus/expressive. Signed-off-by: Joshua Parker --- .../Adapter/DbalMigrationAdapter.php | 109 -------- .../Adapter/FileMigrationAdapter.php | 103 -------- src/Migration/Adapter/MigrationAdapter.php | 47 ---- src/Migration/Migration.php | 233 ------------------ src/Migration/Migrator.php | 159 ------------ 5 files changed, 651 deletions(-) delete mode 100644 src/Migration/Adapter/DbalMigrationAdapter.php delete mode 100644 src/Migration/Adapter/FileMigrationAdapter.php delete mode 100644 src/Migration/Adapter/MigrationAdapter.php delete mode 100644 src/Migration/Migration.php delete mode 100644 src/Migration/Migrator.php diff --git a/src/Migration/Adapter/DbalMigrationAdapter.php b/src/Migration/Adapter/DbalMigrationAdapter.php deleted file mode 100644 index 3afd123..0000000 --- a/src/Migration/Adapter/DbalMigrationAdapter.php +++ /dev/null @@ -1,109 +0,0 @@ -connection; - } - - /** - * Get all migrated version numbers - * - * @return array - * @throws Exception - */ - public function fetchAll(): array - { - $tableName = $this->connection->quoteIdentifier($this->tableName); - $sql = DB::query(query: "SELECT version FROM $tableName ORDER BY version ASC", type: DB::SELECT)->asAssoc(); - $all = $sql->execute($this->connection); - - return array_map(fn ($v) => $v['version'], $all); - } - - /** - * Up - * - * @param Migration $migration - * @return MigrationAdapter - */ - public function up(Migration $migration): MigrationAdapter - { - $this->connection->insert($this->tableName) - ->values( - [ - 'version' => $migration->getVersion(), - 'recorded_on' => new QubusDateTimeImmutable(time: 'now')->format(format: 'Y-m-d h:i:s') - ] - )->execute(); - - return $this; - } - - /** - * Down - * - * @param Migration $migration - * @return MigrationAdapter - */ - public function down(Migration $migration): MigrationAdapter - { - $this->connection->delete($this->tableName) - ->where('version', $migration->getVersion()) - ->execute(); - - return $this; - } - - /** - * Is the schema ready? - * - * @return bool - * @throws Exception - */ - public function hasSchema(): bool - { - $tables = $this->connection->listTables(); - - if (in_array(needle: $this->tableName, haystack: $tables)) { - return true; - } - - return false; - } - - /** - * Create Schema - * - * @return MigrationAdapter - */ - public function createSchema(): MigrationAdapter - { - $this->connection->schema()->create($this->tableName, function (CreateTable $table) { - $table->integer(name: 'id')->size(value: 'big')->autoincrement(); - $table->string(name: 'version', length: 191)->notNull(); - $table->dateTime(name: 'recorded_on'); - }); - - return $this; - } -} diff --git a/src/Migration/Adapter/FileMigrationAdapter.php b/src/Migration/Adapter/FileMigrationAdapter.php deleted file mode 100644 index 2a7527c..0000000 --- a/src/Migration/Adapter/FileMigrationAdapter.php +++ /dev/null @@ -1,103 +0,0 @@ -filename = $filename; - } - - /** - * {@inheritdoc} - */ - public function fetchAll(): array - { - $versions = file(filename: $this->filename, flags: FILE_IGNORE_NEW_LINES); - sort($versions); - return $versions; - } - - /** - * {@inheritdoc} - * @throws TypeException - */ - public function up(Migration $migration): MigrationAdapter - { - $versions = $this->fetchAll(); - if (in_array(needle: $migration->getVersion(), haystack: $versions)) { - return $this; - } - - $versions[] = $migration->getVersion(); - $this->write(versions: $versions); - return $this; - } - - /** - * {@inheritdoc} - * @throws TypeException - */ - public function down(Migration $migration): MigrationAdapter - { - $versions = $this->fetchAll(); - if (!in_array(needle: $migration->getVersion(), haystack: $versions)) { - return $this; - } - - unset($versions[array_search(needle: $migration->getVersion(), haystack: $versions)]); - $this->write(versions: $versions); - return $this; - } - - /** - * {@inheritdoc} - */ - public function hasSchema(): bool - { - return file_exists(filename: $this->filename); - } - - /** - * {@inheritdoc} - * @throws TypeException - */ - public function createSchema(): MigrationAdapter - { - if (!is_writable(filename: dirname(path: $this->filename))) { - throw new TypeException(message: sprintf('The file "%s" is not writeable', $this->filename)); - } - - if (false === touch(filename: $this->filename)) { - throw new TypeException(message: sprintf('The file "%s" could not be written to', $this->filename)); - } - - return $this; - } - - /** - * Write to file - * - * @param array $versions - * @throws TypeException - */ - protected function write(array $versions): void - { - if (false === file_put_contents(filename: $this->filename, data: implode(separator: "\n", array: $versions))) { - throw new TypeException(message: sprintf('The file "%s" could not be written to', $this->filename)); - } - } -} diff --git a/src/Migration/Adapter/MigrationAdapter.php b/src/Migration/Adapter/MigrationAdapter.php deleted file mode 100644 index 5a71743..0000000 --- a/src/Migration/Adapter/MigrationAdapter.php +++ /dev/null @@ -1,47 +0,0 @@ -version = $version; - } - - /** - * Init. - * - * @return void - */ - public function init(): void - { - return; - } - - /** - * Do the migration. - * - * @return void - */ - public function up(): void - { - return; - } - - /** - * Undo the migration. - * - * @return void - */ - public function down(): void - { - return; - } - - /** - * Get adapter. - * - * @return MigrationAdapter - */ - public function getAdapter(): MigrationAdapter - { - return $this->get('phpmig.adapter'); - } - - /** - * Get Version. - * - * @return int|string|null - */ - public function getVersion(): int|string|null - { - return $this->version; - } - - /** - * Set version. - * - * @param int|string $version - * @return Migration - */ - public function setVersion(int|string $version): static - { - $this->version = $version; - return $this; - } - - /** - * Get name. - * - * @return string - */ - public function getName(): string - { - return get_class(object: $this); - } - - /** - * Get ObjectMap. - * - * @return ArrayAccess - */ - public function getObjectMap(): ArrayAccess - { - return $this->objectmap; - } - - /** - * Set ObjectMap. - * - * @param ArrayAccess $objectmap - * @return Migration - */ - public function setObjectMap(ArrayAccess $objectmap): static - { - $this->objectmap = $objectmap; - return $this; - } - - - /** - * Get Output. - * - * @return OutputInterface|null - */ - public function getOutput(): ?OutputInterface - { - return $this->output; - } - - /** - * Set Output. - * - * @param OutputInterface $output - * @return Migration - */ - public function setOutput(OutputInterface $output): static - { - $this->output = $output; - return $this; - } - - /** - * Get Input. - * - * @return InputInterface|null - */ - public function getInput(): ?InputInterface - { - return $this->input; - } - - /** - * Set Input. - * - * @param InputInterface $input - * @return Migration - */ - public function setInput(InputInterface $input): static - { - $this->input = $input; - return $this; - } - - /** - * Ask for input. - * - * @param Question $question - * @return mixed - */ - public function ask(Question $question): string - { - return $this->getDialogHelper()->ask(input: $this->getInput(), output: $this->getOutput(), question: $question); - } - - /** - * Get something from the objectmap - * - * @param string $key - * @return mixed - */ - public function get(string $key): mixed - { - $c = $this->getObjectMap(); - return $c[$key]; - } - - /** - * Get Dialog Helper. - * - * @return QuestionHelper|null - */ - public function getDialogHelper(): ?QuestionHelper - { - if ($this->dialogHelper) { - return $this->dialogHelper; - } - - return $this->dialogHelper = new QuestionHelper(); - } - - /** - * Set Dialog Helper. - * - * @param QuestionHelper $dialogHelper - * @return Migration - */ - public function setDialogHelper(QuestionHelper $dialogHelper): static - { - $this->dialogHelper = $dialogHelper; - return $this; - } - - public function schema(): Schema - { - return $this->getAdapter()->connection()->schema(); - } -} diff --git a/src/Migration/Migrator.php b/src/Migration/Migrator.php deleted file mode 100644 index ed582cd..0000000 --- a/src/Migration/Migrator.php +++ /dev/null @@ -1,159 +0,0 @@ -objectmap = $objectmap; - $this->adapter = $adapter; - $this->output = $output; - } - - /** - * Run the up method on a migration - * - * @param Migration $migration - * @return void - */ - public function up(Migration $migration): void - { - $this->run(migration: $migration, direction: 'up'); - } - - /** - * Run the down method on a migration - * - * @param Migration $migration - * @return void - */ - public function down(Migration $migration): void - { - $this->run(migration: $migration, direction: 'down'); - } - - /** - * Run a migration in a particular direction - * - * @param Migration $migration - * @param string $direction - * @return void - */ - protected function run(Migration $migration, string $direction = 'up'): void - { - $direction = ($direction == 'down' ? 'down' : 'up'); - $this->getOutput()?->writeln( - messages: sprintf( - ' == ' . - $migration->getVersion() . ' ' . - $migration->getName() . ' ' . - '' . - ($direction == 'up' ? 'migrating' : 'reverting') . - '' - ) - ); - $start = microtime(as_float: true); - $migration->setObjectMap($this->getObjectMap()); - $migration->init(); - $migration->{$direction}(); - $this->getAdapter()->{$direction}($migration); - $end = microtime(as_float: true); - $this->getOutput()?->writeln( - messages: sprintf( - ' == ' . - $migration->getVersion() . ' ' . - $migration->getName() . ' ' . - '' . - ($direction == 'up' ? 'migrated ' : 'reverted ') . - sprintf("%.4fs", $end - $start) . - '' - ) - ); - } - - /** - * Get ObjectMap. - * - * @return ArrayAccess - */ - public function getObjectMap(): ArrayAccess - { - return $this->objectmap; - } - - /** - * Set ObjectMap. - * - * @param ArrayAccess $objectmap - * @return Migrator - */ - public function setObjectMap(ArrayAccess $objectmap): static - { - $this->objectmap = $objectmap; - return $this; - } - - /** - * Get Adapter - * - * @return MigrationAdapter|null - */ - public function getAdapter(): ?MigrationAdapter - { - return $this->adapter; - } - - /** - * Set Adapter - * - * @param MigrationAdapter $adapter - * @return Migrator - */ - public function setAdapter(MigrationAdapter $adapter): static - { - $this->adapter = $adapter; - return $this; - } - - /** - * Get Output - * - * @return OutputInterface|null - */ - public function getOutput(): ?OutputInterface - { - return $this->output; - } - - /** - * Set Output - * - * @param OutputInterface $output - * @return Migrator - */ - public function setOutput(OutputInterface $output): static - { - $this->output = $output; - return $this; - } -} From d994954af8483b6ed8bc7de32d4126fbefa2a041 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 08:40:06 -0700 Subject: [PATCH 086/161] Upgraded to a different dbal. Signed-off-by: Joshua Parker --- src/Application.php | 39 ++++++++++--------- src/Auth/Repository/PdoRepository.php | 6 +-- src/Helpers/core.php | 12 +++--- .../DatabaseConnectionServiceProvider.php | 25 ++++++++++++ src/Providers/PdoServiceProvider.php | 34 +--------------- 5 files changed, 56 insertions(+), 60 deletions(-) create mode 100644 src/Providers/DatabaseConnectionServiceProvider.php diff --git a/src/Application.php b/src/Application.php index 933ace7..052abfa 100644 --- a/src/Application.php +++ b/src/Application.php @@ -17,12 +17,11 @@ use Defuse\Crypto\Exception\EnvironmentIsBrokenException; use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Dotenv\Dotenv; +use Opis\Database\Connection; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Config\ConfigContainer; -use Qubus\Dbal\Connection; -use Qubus\Dbal\DB; use Qubus\EventDispatcher\ActionFilter\Observer; use Qubus\EventDispatcher\EventDispatcher; use Qubus\Exception\Data\TypeException; @@ -196,26 +195,24 @@ protected static function inferBasePath(): ?string /** * @throws Exception */ - public function getDbConnection(): Connection\DbalPdo|Connection + public function getDbConnection(): Connection { /** @var ConfigContainer $config */ $config = $this->make(name: 'codefy.config'); + $default = $config->getConfigKey(key: 'database.default'); - $connection = $config->getConfigKey(key: 'database.default'); - - return DB::connection([ - 'driver' => $config->getConfigKey(key: "database.connections.{$connection}.driver"), - 'host' => $config->getConfigKey(key: "database.connections.{$connection}.host", default: 'localhost'), - 'port' => $config->getConfigKey(key: "database.connections.{$connection}.port", default: 3306), - 'charset' => $config->getConfigKey(key: "database.connections.{$connection}.charset", default: 'utf8mb4'), - 'collation' => $config->getConfigKey( - key: "database.connections.{$connection}.collation", - default: 'utf8mb4_unicode_ci' - ), - 'username' => $config->getConfigKey(key: "database.connections.{$connection}.username"), - 'password' => $config->getConfigKey(key: "database.connections.{$connection}.password"), - 'dbname' => $config->getConfigKey(key: "database.connections.{$connection}.dbname"), - ]); + $connection = new Connection( + dsn: $config->getConfigKey(key: "database.connections.{$default}.dsn"), + username: $config->getConfigKey(key: "database.connections.{$default}.username", default: null), + password: $config->getConfigKey(key: "database.connections.{$default}.password", default: null), + driver: $config->getConfigKey(key: "database.connections.{$default}.driver"), + ); + + if (!empty($config->getConfigKey(key: "database.connections.{$default}.options"))) { + $connection->options($config->getConfigKey(key: "database.connections.{$default}.options")); + } + + return $connection; } /** @@ -267,7 +264,7 @@ protected function registerDefaultServiceProviders(): void foreach ( [ Providers\ConfigServiceProvider::class, - Providers\PdoServiceProvider::class, + Providers\DatabaseConnectionServiceProvider::class, Providers\FlysystemServiceProvider::class, Providers\RouterServiceProvider::class, ] as $serviceProvider @@ -990,5 +987,9 @@ public static function getInstance(?string $path = null): self public private(set) Router $router { get => $this->router ?? $this->make(name: Router::class); } + + public private(set) Paths $path { + get => $this->path ?? $this->make(name: Paths::class); + } //phpcs:disable } diff --git a/src/Auth/Repository/PdoRepository.php b/src/Auth/Repository/PdoRepository.php index 61ca969..2531f49 100644 --- a/src/Auth/Repository/PdoRepository.php +++ b/src/Auth/Repository/PdoRepository.php @@ -5,7 +5,7 @@ namespace Codefy\Framework\Auth\Repository; use Codefy\Framework\Support\Password; -use PDO; +use Opis\Database\Connection; use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; use Qubus\Http\Session\SessionEntity; @@ -15,7 +15,7 @@ class PdoRepository implements AuthUserRepository { - public function __construct(private PDO $pdo, protected ConfigContainer $config) + public function __construct(private Connection $connection, protected ConfigContainer $config) { } @@ -33,7 +33,7 @@ public function authenticate(string $credential, #[SensitiveParameter] ?string $ $fields['identity'] ); - $stmt = $this->pdo->prepare(query: $sql); + $stmt = $this->connection->getPDO()->prepare(query: $sql); if (false === $stmt) { return null; } diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 044fe2b..8fc5f4e 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -20,8 +20,8 @@ use Codefy\QueryBus\Query; use Codefy\QueryBus\Resolvers\NativeQueryHandlerResolver; use Codefy\QueryBus\UnresolvableQueryHandlerException; +use Opis\Database\Database; use Qubus\Config\Collection; -use Qubus\Dbal\Connection; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; @@ -111,20 +111,20 @@ function env(string $key, mixed $default = null): mixed * @return QueryBuilder|null * @throws Exception */ -function qb(): ?QueryBuilder +function queryBuilder(): ?QueryBuilder { return Codefy::$PHP->getDb(); } /** - * Dbal database instance. + * Database abstraction layer (dbal) instance. * - * @return Connection + * @return Database * @throws Exception */ -function dbal(): Connection +function dbal(): Database { - return Codefy::$PHP->getDbConnection(); + return new Database(Codefy::$PHP->getDbConnection()); } /** diff --git a/src/Providers/DatabaseConnectionServiceProvider.php b/src/Providers/DatabaseConnectionServiceProvider.php new file mode 100644 index 0000000..bf37648 --- /dev/null +++ b/src/Providers/DatabaseConnectionServiceProvider.php @@ -0,0 +1,25 @@ +codefy->singleton(Connection::class, function () { + return $this->codefy->getDbConnection(); + }); + + $this->codefy->share(nameOrInstance: Connection::class); + } +} diff --git a/src/Providers/PdoServiceProvider.php b/src/Providers/PdoServiceProvider.php index b07b8e6..d76a06f 100644 --- a/src/Providers/PdoServiceProvider.php +++ b/src/Providers/PdoServiceProvider.php @@ -6,45 +6,15 @@ use Codefy\Framework\Support\CodefyServiceProvider; use PDO; -use Qubus\Config\ConfigContainer; -use Qubus\Exception\Exception; use function sprintf; final class PdoServiceProvider extends CodefyServiceProvider { - /** - * @throws Exception - */ public function register(): void { - /** @var ConfigContainer $config */ - $config = $this->codefy->make('codefy.config'); - - $default = $config->getConfigKey(key: 'database.default'); - $dsn = sprintf( - '%s:dbname=%s;host=%s;charset=utf8mb4', - $config->getConfigKey(key: "database.connections.{$default}.driver"), - $config->getConfigKey(key: "database.connections.{$default}.dbname"), - $config->getConfigKey(key: "database.connections.{$default}.host") - ); - - $this->codefy->singleton(PDO::class, function () use ($dsn, $config, $default) { - return new PDO( - $dsn, - $config->getConfigKey( - key: "database.connections.{$default}.username" - ), - $config->getConfigKey( - key: "database.connections.{$default}.password" - ), - [ - PDO::ATTR_EMULATE_PREPARES => $config->getConfigKey(key: "database.pdo.emulate_prepares"), - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_PERSISTENT => $config->getConfigKey(key: "database.pdo.persistent"), - PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" - ] - ); + $this->codefy->singleton(PDO::class, function () { + return $this->codefy->getDbConnection()->getPDO(); }); $this->codefy->share(nameOrInstance: PDO::class); From 2b72c2c5f8a5b57dbb920fc54b21ab115e31d3aa Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 09:17:37 -0700 Subject: [PATCH 087/161] Removed mockery package. Signed-off-by: Joshua Parker --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 1c51aa6..a141342 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,6 @@ "fenom/fenom": "^3.0", "fenom/providers-collection": "^1.0", "foil/foil": "^0.6.7", - "mockery/mockery": "^1", "qubus/qubus-coding-standard": "^2" }, "autoload-dev": { From eb39538825c148440470134fe48b74bac1fde488 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 14:40:32 -0700 Subject: [PATCH 088/161] Updated composer packages. Signed-off-by: Joshua Parker --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a141342..0e39362 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "php-debugbar/php-debugbar": "^2.2", "qubus/cache": "^4", "qubus/error": "^3", - "qubus/event-dispatcher": "^3", + "qubus/event-dispatcher": "^4", "qubus/exception": "^3", "qubus/expressive": "^2", "qubus/filesystem": "^3", @@ -31,7 +31,7 @@ "qubus/injector": "^3", "qubus/mail": "^4", "qubus/router": "^4", - "qubus/security": "^3", + "qubus/security": "^4", "qubus/support": "^4", "qubus/view": "^3", "symfony/console": "^7", From e26064a0f7a63d38c97d7d847f41bcab57fb8bdc Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 16:38:49 -0700 Subject: [PATCH 089/161] Updated composer packages. Signed-off-by: Joshua Parker --- composer.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 0e39362..5430535 100644 --- a/composer.json +++ b/composer.json @@ -24,12 +24,11 @@ "qubus/cache": "^4", "qubus/error": "^3", "qubus/event-dispatcher": "^4", - "qubus/exception": "^3", + "qubus/exception": "^4", "qubus/expressive": "^2", - "qubus/filesystem": "^3", - "qubus/inheritance": "^3", - "qubus/injector": "^3", - "qubus/mail": "^4", + "qubus/filesystem": "^4", + "qubus/inheritance": "^4", + "qubus/mail": "^5", "qubus/router": "^4", "qubus/security": "^4", "qubus/support": "^4", From 26477f4a7b9efc52c3e94a2fc21ea4d1897a9531 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 16:46:06 -0700 Subject: [PATCH 090/161] Fixed event dispatcher update. Signed-off-by: Joshua Parker --- src/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 052abfa..04087e9 100644 --- a/src/Application.php +++ b/src/Application.php @@ -766,7 +766,7 @@ protected function coreAliases(): array \Qubus\Cache\Psr6\TaggableCacheItemPool::class => \Qubus\Cache\Psr6\TaggablePsr6PoolAdapter::class, \Qubus\Config\Path\Path::class => \Qubus\Config\Path\ConfigPath::class, \Qubus\Config\ConfigContainer::class => \Qubus\Config\Collection::class, - \Qubus\EventDispatcher\EventDispatcher::class => \Qubus\EventDispatcher\Dispatcher::class, + \Psr\EventDispatcher\EventDispatcherInterface::class => \Qubus\EventDispatcher\EventDispatcher::class, \Qubus\Mail\Mailer::class => \Codefy\Framework\Support\CodefyMailer::class, 'mailer' => Mailer::class, 'dir.path' => \Codefy\Framework\Support\Paths::class, From e1a5eabdf89267ec02dc96abdcf2ff42a2c892d3 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 21:36:43 -0700 Subject: [PATCH 091/161] Removed EventDispatcher definition. Signed-off-by: Joshua Parker --- src/Application.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Application.php b/src/Application.php index 04087e9..ec5cc99 100644 --- a/src/Application.php +++ b/src/Application.php @@ -23,7 +23,7 @@ use Psr\Http\Message\ServerRequestInterface; use Qubus\Config\ConfigContainer; use Qubus\EventDispatcher\ActionFilter\Observer; -use Qubus\EventDispatcher\EventDispatcher; +use Qubus\EventDispatcher\Legacy\EventDispatcher; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; @@ -766,7 +766,6 @@ protected function coreAliases(): array \Qubus\Cache\Psr6\TaggableCacheItemPool::class => \Qubus\Cache\Psr6\TaggablePsr6PoolAdapter::class, \Qubus\Config\Path\Path::class => \Qubus\Config\Path\ConfigPath::class, \Qubus\Config\ConfigContainer::class => \Qubus\Config\Collection::class, - \Psr\EventDispatcher\EventDispatcherInterface::class => \Qubus\EventDispatcher\EventDispatcher::class, \Qubus\Mail\Mailer::class => \Codefy\Framework\Support\CodefyMailer::class, 'mailer' => Mailer::class, 'dir.path' => \Codefy\Framework\Support\Paths::class, @@ -952,10 +951,6 @@ public static function getInstance(?string $path = null): self get => $this->flash ?? $this->make(name: Flash::class); } - public private(set) EventDispatcher $event { - get => $this->event ?? $this->make(name: EventDispatcher::class); - } - public private(set) HttpCookieFactory $httpCookie { get => $this->httpCookie ?? $this->make(name: HttpCookieFactory::class); } From 551e2c5b8db7031fd1e48b2b39131a9b2f1d34ca Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 21:39:39 -0700 Subject: [PATCH 092/161] Re-added the legacy event dispatcher. Signed-off-by: Joshua Parker --- src/Application.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Application.php b/src/Application.php index ec5cc99..af9b113 100644 --- a/src/Application.php +++ b/src/Application.php @@ -766,6 +766,7 @@ protected function coreAliases(): array \Qubus\Cache\Psr6\TaggableCacheItemPool::class => \Qubus\Cache\Psr6\TaggablePsr6PoolAdapter::class, \Qubus\Config\Path\Path::class => \Qubus\Config\Path\ConfigPath::class, \Qubus\Config\ConfigContainer::class => \Qubus\Config\Collection::class, + \Qubus\EventDispatcher\EventDispatcher::class => \Qubus\EventDispatcher\Legacy\Dispatcher::class, \Qubus\Mail\Mailer::class => \Codefy\Framework\Support\CodefyMailer::class, 'mailer' => Mailer::class, 'dir.path' => \Codefy\Framework\Support\Paths::class, @@ -951,6 +952,10 @@ public static function getInstance(?string $path = null): self get => $this->flash ?? $this->make(name: Flash::class); } + public private(set) EventDispatcher $event { + get => $this->event ?? $this->make(name: EventDispatcher::class); + } + public private(set) HttpCookieFactory $httpCookie { get => $this->httpCookie ?? $this->make(name: HttpCookieFactory::class); } From 22ca6439b8e6720bc1b0c2d07596afc1b02ddc31 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 21:46:26 -0700 Subject: [PATCH 093/161] Fixed legacy event dispatcher imports. Signed-off-by: Joshua Parker --- src/Scheduler/Processor/Dispatcher.php | 2 +- src/Scheduler/Stack.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Scheduler/Processor/Dispatcher.php b/src/Scheduler/Processor/Dispatcher.php index 6aab8b5..486cf75 100644 --- a/src/Scheduler/Processor/Dispatcher.php +++ b/src/Scheduler/Processor/Dispatcher.php @@ -4,7 +4,7 @@ namespace Codefy\Framework\Scheduler\Processor; -use Qubus\EventDispatcher\EventDispatcher; +use Qubus\EventDispatcher\Legacy\EventDispatcher; use Stringable; class Dispatcher extends BaseProcessor implements Stringable, Processor diff --git a/src/Scheduler/Stack.php b/src/Scheduler/Stack.php index a4fb047..9bb1425 100644 --- a/src/Scheduler/Stack.php +++ b/src/Scheduler/Stack.php @@ -10,7 +10,7 @@ use Codefy\Framework\Scheduler\Mutex\Locker; use Codefy\Framework\Scheduler\Traits\MailerAware; use DateTimeZone; -use Qubus\EventDispatcher\EventDispatcher; +use Qubus\EventDispatcher\Legacy\EventDispatcher; use Qubus\Exception\Exception; use Symfony\Component\OptionsResolver\OptionsResolver; From ee1041ba36863758ea4878d633c4fa5046d7c274 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 22:00:12 -0700 Subject: [PATCH 094/161] Fixed alias. Signed-off-by: Joshua Parker --- src/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index af9b113..5707925 100644 --- a/src/Application.php +++ b/src/Application.php @@ -766,7 +766,7 @@ protected function coreAliases(): array \Qubus\Cache\Psr6\TaggableCacheItemPool::class => \Qubus\Cache\Psr6\TaggablePsr6PoolAdapter::class, \Qubus\Config\Path\Path::class => \Qubus\Config\Path\ConfigPath::class, \Qubus\Config\ConfigContainer::class => \Qubus\Config\Collection::class, - \Qubus\EventDispatcher\EventDispatcher::class => \Qubus\EventDispatcher\Legacy\Dispatcher::class, + \Qubus\EventDispatcher\Legacy\EventDispatcher::class => \Qubus\EventDispatcher\Legacy\Dispatcher::class, \Qubus\Mail\Mailer::class => \Codefy\Framework\Support\CodefyMailer::class, 'mailer' => Mailer::class, 'dir.path' => \Codefy\Framework\Support\Paths::class, From d58282bbba01f2da62e3b2e27082063415ff52db Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 22:14:18 -0700 Subject: [PATCH 095/161] Fixed migration imports. Signed-off-by: Joshua Parker --- src/Console/Commands/PhpMigCommand.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Console/Commands/PhpMigCommand.php b/src/Console/Commands/PhpMigCommand.php index 6f77ad1..10218b7 100644 --- a/src/Console/Commands/PhpMigCommand.php +++ b/src/Console/Commands/PhpMigCommand.php @@ -7,11 +7,11 @@ use ArrayAccess; use Codefy\Framework\Application; use Codefy\Framework\Console\ConsoleCommand; -use Codefy\Framework\Migration\Adapter\MigrationAdapter; -use Codefy\Framework\Migration\Migration; -use Codefy\Framework\Migration\Migrator; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; +use Qubus\Expressive\Migration\Adapter\MigrationAdapter; +use Qubus\Expressive\Migration\Migration; +use Qubus\Expressive\Migration\Migrator; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -226,7 +226,7 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o } $migrationName = preg_replace(pattern: '/^[0-9]+_/', replacement: '', subject: basename(path: $path)); - if (false !== strpos(haystack: $migrationName, needle: '.')) { + if (str_contains($migrationName, '.')) { $migrationName = substr( string: $migrationName, offset: 0, From 0221661b08939c7fca490d866c5d54622aabc727 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 30 Sep 2025 23:53:37 -0700 Subject: [PATCH 096/161] Bumped version value. Signed-off-by: Joshua Parker --- src/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 5707925..b4be7bb 100644 --- a/src/Application.php +++ b/src/Application.php @@ -60,7 +60,7 @@ final class Application extends Container use InvokerAware; use LoggerAware; - public const string APP_VERSION = '3.0.0-beta.4'; + public const string APP_VERSION = '3.0.0-beta.5'; public const string MIN_PHP_VERSION = '8.4'; From 916f1193346a852a3484ebb03687e5b0cac378ab Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 3 Oct 2025 10:48:19 -0700 Subject: [PATCH 097/161] Added config for hash_hmac algo parameter. Signed-off-by: Joshua Parker --- src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php b/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php index b19a89b..3968da3 100644 --- a/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php +++ b/src/Http/Middleware/Csrf/Traits/CsrfTokenAware.php @@ -36,7 +36,11 @@ protected function prepareToken(HttpSession $session): string if ($token === '') { $token = $this->generateToken(); } - return hash_hmac(algo: 'sha256', data: $token, key: $this->configContainer->getConfigKey(key: 'csrf.salt')); + return hash_hmac( + algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'), + data: $token, + key: $this->configContainer->getConfigKey(key: 'csrf.salt') + ); } /** @@ -47,7 +51,7 @@ protected function hashEquals(string $knownString, string $userString): bool return hash_equals( $knownString, hash_hmac( - algo: 'sha256', + algo: $this->configContainer->getConfigKey(key: 'csrf.hash_algo', default: 'sha256'), data: $userString, key: $this->configContainer->getConfigKey(key: 'csrf.salt') ) From c759ef30cb9a49c49522ced60add7027e2b718f4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 3 Oct 2025 10:49:04 -0700 Subject: [PATCH 098/161] Added config for cookie name. Signed-off-by: Joshua Parker --- src/Http/Middleware/Csrf/CsrfTokenMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php index 8c9c6e1..f01df9b 100644 --- a/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php +++ b/src/Http/Middleware/Csrf/CsrfTokenMiddleware.php @@ -57,7 +57,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { try { $this->sessionService::$options = [ - 'cookie-name' => 'CSRFSESSID', + 'cookie-name' => $this->configContainer->getConfigKey(key: 'csrf.cookie_name', default: 'CSRFSESSID'), 'cookie-lifetime' => (int) $this->configContainer->getConfigKey(key: 'csrf.lifetime', default: 86400), ]; From e74cac7413db8ecc857b822b1efa4f5728e8d431 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 3 Oct 2025 10:49:49 -0700 Subject: [PATCH 099/161] Relocated ApiMiddleware to Codefy library. Signed-off-by: Joshua Parker --- src/Http/Middleware/ApiMiddleware.php | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Http/Middleware/ApiMiddleware.php diff --git a/src/Http/Middleware/ApiMiddleware.php b/src/Http/Middleware/ApiMiddleware.php new file mode 100644 index 0000000..8031e29 --- /dev/null +++ b/src/Http/Middleware/ApiMiddleware.php @@ -0,0 +1,49 @@ +getHeaderLine('authorization'), 'Bearer ') && + $request->getHeaderLine('authorization') === + sprintf('Bearer %s', $this->configContainer->getConfigKey(key: 'app.api_key')) + ) { + return $handler->handle($request); + } + + return JsonResponseFactory::create( + data: t__(msgid: 'Unauthorized.', domain: 'codefy'), + status: 401 + ); + } +} From a7081b2337310c413b3ddd8389003f6b73f50713 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 3 Oct 2025 10:50:29 -0700 Subject: [PATCH 100/161] Added localization service provider for translating strings. Signed-off-by: Joshua Parker --- src/Providers/LocalizationServiceProvider.php | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/Providers/LocalizationServiceProvider.php diff --git a/src/Providers/LocalizationServiceProvider.php b/src/Providers/LocalizationServiceProvider.php new file mode 100644 index 0000000..c41f94d --- /dev/null +++ b/src/Providers/LocalizationServiceProvider.php @@ -0,0 +1,48 @@ +codefy->isRunningInConsole()) { + return; + } + + // Instantiate MoLoader for loading .mo files. + $loader = new MoLoader(); + // Retrieve the current locale. + $locale = $this->codefy->configContainer->getConfigKey(key: 'app.locale'); + // Retrieve the current locale domain. + $domain = $this->codefy->configContainer->getConfigKey(key: 'app.locale_domain'); + // Set translation array for push. + $translations = []; + // Relative path to domain file. + $domainFilename = sprintf('%s/%s-%s.mo', $locale, $domain, $locale); + // Absolute path to the .mo file. + $mofile = $this->codefy->localePath() . $this->codefy::DS . $domainFilename; + + if (file_exists($mofile)) { + $translations[] = $loader->loadFile(filename: $mofile)->setDomain(domain: $domain); + } + + $gettext = Translator::createFromTranslations(...$translations); + + TranslatorFunctions::register($gettext); + } +} From 74e26ca3cd66e747f86615936400718176951716 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 3 Oct 2025 10:51:41 -0700 Subject: [PATCH 101/161] Configurable namespace. Signed-off-by: Joshua Parker --- src/Providers/RouterServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Providers/RouterServiceProvider.php b/src/Providers/RouterServiceProvider.php index 0431cae..e789a93 100644 --- a/src/Providers/RouterServiceProvider.php +++ b/src/Providers/RouterServiceProvider.php @@ -13,7 +13,7 @@ class RouterServiceProvider extends CodefyServiceProvider { - protected ?string $namespace = null; + public const ?string NAMESPACE = null; public function register(): void { @@ -29,7 +29,7 @@ public function register(): void resolver: new InjectorMiddlewareResolver(container: $this->codefy), ); - $router->setDefaultNamespace(namespace: $this->namespace ?? $this->codefy->controllerNamespace); + $router->setDefaultNamespace(namespace: self::NAMESPACE ?? $this->codefy->controllerNamespace); return $router; }); From fadb91e7024e0ed3bdcb71ff2730e228040053d4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 4 Oct 2025 17:12:28 -0700 Subject: [PATCH 102/161] Added minimum stability. Signed-off-by: Joshua Parker --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5430535..754e9e9 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "qubus/error": "^3", "qubus/event-dispatcher": "^4", "qubus/exception": "^4", - "qubus/expressive": "^2", + "qubus/expressive": "^3.x-dev", "qubus/filesystem": "^4", "qubus/inheritance": "^4", "qubus/mail": "^5", @@ -62,6 +62,7 @@ "cs-check": "phpcs", "cs-fix": "phpcbf" }, + "minimum-stability": "dev", "config": { "optimize-autoloader": true, "sort-packages": true, From 3e7bc13d2ef12cd4b4a4bddc0f8e9d2db3eb93c8 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 4 Oct 2025 22:17:19 -0700 Subject: [PATCH 103/161] Updated database connection. Signed-off-by: Joshua Parker --- src/Application.php | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Application.php b/src/Application.php index b4be7bb..a8f6eda 100644 --- a/src/Application.php +++ b/src/Application.php @@ -17,16 +17,17 @@ use Defuse\Crypto\Exception\EnvironmentIsBrokenException; use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Dotenv\Dotenv; -use Opis\Database\Connection; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Config\ConfigContainer; +use Qubus\Dbal\Connection; +use Qubus\Dbal\Connection\DriverConnection; +use Qubus\Dbal\QueryBuilder; use Qubus\EventDispatcher\ActionFilter\Observer; use Qubus\EventDispatcher\Legacy\EventDispatcher; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; -use Qubus\Expressive\QueryBuilder; use Qubus\Http\Cookies\Factory\HttpCookieFactory; use Qubus\Http\Encryption\Env\SecureEnv; use Qubus\Http\Session\Flash; @@ -201,18 +202,7 @@ public function getDbConnection(): Connection $config = $this->make(name: 'codefy.config'); $default = $config->getConfigKey(key: 'database.default'); - $connection = new Connection( - dsn: $config->getConfigKey(key: "database.connections.{$default}.dsn"), - username: $config->getConfigKey(key: "database.connections.{$default}.username", default: null), - password: $config->getConfigKey(key: "database.connections.{$default}.password", default: null), - driver: $config->getConfigKey(key: "database.connections.{$default}.driver"), - ); - - if (!empty($config->getConfigKey(key: "database.connections.{$default}.options"))) { - $connection->options($config->getConfigKey(key: "database.connections.{$default}.options")); - } - - return $connection; + return DriverConnection::make($config->getConfigKey(key: "database.connections.{$default}")); } /** From d00e2f0972fdadade3ffb5fb82b012083f20d672 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 5 Oct 2025 21:04:11 -0700 Subject: [PATCH 104/161] Cleanup and database connection changes. Signed-off-by: Joshua Parker --- src/Application.php | 6 +++--- src/Console/Commands/ListCommand.php | 2 +- src/Console/Commands/PhpMigCommand.php | 8 ++++---- src/Console/Commands/Traits/MakeCommandAware.php | 3 +++ src/Helpers/core.php | 12 ------------ src/Pipeline/Pipeline.php | 12 ++++++------ src/Providers/ConfigServiceProvider.php | 4 ++++ src/Providers/DatabaseConnectionServiceProvider.php | 5 +---- src/Providers/PdoServiceProvider.php | 4 +--- 9 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/Application.php b/src/Application.php index a8f6eda..5741c64 100644 --- a/src/Application.php +++ b/src/Application.php @@ -21,13 +21,13 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Config\ConfigContainer; -use Qubus\Dbal\Connection; -use Qubus\Dbal\Connection\DriverConnection; -use Qubus\Dbal\QueryBuilder; use Qubus\EventDispatcher\ActionFilter\Observer; use Qubus\EventDispatcher\Legacy\EventDispatcher; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; +use Qubus\Expressive\Connection; +use Qubus\Expressive\Connection\DriverConnection; +use Qubus\Expressive\QueryBuilder; use Qubus\Http\Cookies\Factory\HttpCookieFactory; use Qubus\Http\Encryption\Env\SecureEnv; use Qubus\Http\Session\Flash; diff --git a/src/Console/Commands/ListCommand.php b/src/Console/Commands/ListCommand.php index b8ec3e0..e513d14 100644 --- a/src/Console/Commands/ListCommand.php +++ b/src/Console/Commands/ListCommand.php @@ -45,7 +45,7 @@ public function handle(): int $nextRun = new CronExpression($job->getExpression()); $fullCommand = $job->getCommand(); - if($job instanceof Callback) { + if ($job instanceof Callback) { $fullCommand = $job->__toString(); } diff --git a/src/Console/Commands/PhpMigCommand.php b/src/Console/Commands/PhpMigCommand.php index 10218b7..1bdcb37 100644 --- a/src/Console/Commands/PhpMigCommand.php +++ b/src/Console/Commands/PhpMigCommand.php @@ -135,7 +135,7 @@ protected function bootstrapAdapter(InputInterface $input): MigrationAdapter if (!($adapter instanceof MigrationAdapter)) { throw new RuntimeException( message: "phpmig.adapter or phpmig.sets must be an - instance of \\Codefy\\Framework\\Migration\\Adapter\\MigrationAdapter" + instance of \Qubus\Expressive\Migration\Adapter\MigrationAdapter" ); } @@ -276,7 +276,7 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o if (!($migration instanceof Migration)) { throw new TypeException( message: sprintf( - 'The class "%s" in file "%s" must extend \Codefy\Framework\Migration\Migration', + 'The class "%s" in file "%s" must extend \Qubus\Expressive\Migration\Migration', $class, $path ) @@ -299,9 +299,9 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o /** * @param OutputInterface $output - * @return mixed + * @return Migrator */ - protected function bootstrapMigrator(OutputInterface $output): mixed + protected function bootstrapMigrator(OutputInterface $output): Migrator { return new Migrator(adapter: $this->getAdapter(), objectmap: $this->getObjectMap(), output: $output); } diff --git a/src/Console/Commands/Traits/MakeCommandAware.php b/src/Console/Commands/Traits/MakeCommandAware.php index 60792ca..7f349f3 100644 --- a/src/Console/Commands/Traits/MakeCommandAware.php +++ b/src/Console/Commands/Traits/MakeCommandAware.php @@ -12,6 +12,7 @@ use League\Flysystem\FilesystemException; use Qubus\Exception\Data\TypeException; use Qubus\Support\Inflector; +use ReflectionException; use RuntimeException; use function str_contains; @@ -103,6 +104,8 @@ classNameSuffix: $classNameSuffix, * @param string|null $qualifiedNamespaces - will return the namespace for the stub command * @return void * @throws MakeCommandFileAlreadyExistsException + * @throws \Qubus\Exception\Exception + * @throws ReflectionException */ public function createClassFromStub( string $qualifiedClass, diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 8fc5f4e..79ef67c 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -20,7 +20,6 @@ use Codefy\QueryBus\Query; use Codefy\QueryBus\Resolvers\NativeQueryHandlerResolver; use Codefy\QueryBus\UnresolvableQueryHandlerException; -use Opis\Database\Database; use Qubus\Config\Collection; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; @@ -116,17 +115,6 @@ function queryBuilder(): ?QueryBuilder return Codefy::$PHP->getDb(); } -/** - * Database abstraction layer (dbal) instance. - * - * @return Database - * @throws Exception - */ -function dbal(): Database -{ - return new Database(Codefy::$PHP->getDbConnection()); -} - /** * Alternative to PHP's native mail function with SMTP support. * diff --git a/src/Pipeline/Pipeline.php b/src/Pipeline/Pipeline.php index d72b254..5405f4a 100644 --- a/src/Pipeline/Pipeline.php +++ b/src/Pipeline/Pipeline.php @@ -7,7 +7,7 @@ use Closure; use Codefy\Framework\Support\Traits\DbTransactionsAware; use Exception; -use Qubus\Inheritance\ActionAware; +use Qubus\EventDispatcher\ActionFilter\Traits\ActionAware; use Qubus\Injector\ServiceContainer; use RuntimeException; use Throwable; @@ -109,7 +109,7 @@ public function then(Closure $destination): mixed { try { $this->doAction( - 'pipeline.started', + 'pipeline_started', $destination, $this->passable, $this->pipes(), @@ -129,7 +129,7 @@ public function then(Closure $destination): mixed $this->commitTransaction(); $this->doAction( - 'pipeline.finished', + 'pipeline_finished', $destination, $this->passable, $this->pipes(), @@ -197,7 +197,7 @@ protected function carry(): Closure { return function ($stack, $pipe) { return function ($passable) use ($stack, $pipe) { - $this->doAction('pipeline.execution.started', $pipe, $passable); + $this->doAction('pipeline_execution_started', $pipe, $passable); if (is_callable($pipe)) { // If the pipe is a callable, then we will call it directly, but otherwise we @@ -205,7 +205,7 @@ protected function carry(): Closure // the appropriate method and arguments, returning the results back out. $result = $pipe($passable, $stack); - $this->doAction('pipeline.execution.finished', $pipe, $passable); + $this->doAction('pipeline_execution_finished', $pipe, $passable); return $result; } elseif (! is_object($pipe)) { @@ -228,7 +228,7 @@ protected function carry(): Closure ? $pipe->{$this->method}(...$parameters) : $pipe(...$parameters); - $this->doAction('pipeline.execution.finished', $pipe, $passable); + $this->doAction('pipeline_execution_finished', $pipe, $passable); return $this->handleCarry($carry); }; diff --git a/src/Providers/ConfigServiceProvider.php b/src/Providers/ConfigServiceProvider.php index 3cb1834..6535b09 100644 --- a/src/Providers/ConfigServiceProvider.php +++ b/src/Providers/ConfigServiceProvider.php @@ -7,11 +7,15 @@ use Codefy\Framework\Support\CodefyServiceProvider; use Qubus\Config\Collection; use Qubus\Config\Configuration; +use Qubus\Config\Path\PathNotFoundException; use function Codefy\Framework\Helpers\env; final class ConfigServiceProvider extends CodefyServiceProvider { + /** + * @throws PathNotFoundException + */ public function register(): void { $this->codefy->defineParam( diff --git a/src/Providers/DatabaseConnectionServiceProvider.php b/src/Providers/DatabaseConnectionServiceProvider.php index bf37648..a1a056a 100644 --- a/src/Providers/DatabaseConnectionServiceProvider.php +++ b/src/Providers/DatabaseConnectionServiceProvider.php @@ -5,10 +5,7 @@ namespace Codefy\Framework\Providers; use Codefy\Framework\Support\CodefyServiceProvider; -use Opis\Database\Connection; -use PDO; -use Qubus\Config\ConfigContainer; -use Qubus\Exception\Exception; +use Qubus\Expressive\Connection; use function sprintf; diff --git a/src/Providers/PdoServiceProvider.php b/src/Providers/PdoServiceProvider.php index d76a06f..c78728b 100644 --- a/src/Providers/PdoServiceProvider.php +++ b/src/Providers/PdoServiceProvider.php @@ -7,14 +7,12 @@ use Codefy\Framework\Support\CodefyServiceProvider; use PDO; -use function sprintf; - final class PdoServiceProvider extends CodefyServiceProvider { public function register(): void { $this->codefy->singleton(PDO::class, function () { - return $this->codefy->getDbConnection()->getPDO(); + return $this->codefy->getDbConnection()->pdo; }); $this->codefy->share(nameOrInstance: PDO::class); From 6aee82e88e58342eedf9a0ea3c13d2e1670fedf4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 6 Oct 2025 12:57:46 -0700 Subject: [PATCH 105/161] Added assets config to container. Signed-off-by: Joshua Parker --- src/Application.php | 5 +- src/Console/Commands/FlushPipelineCommand.php | 170 ++++++++++++++++++ src/Console/ConsoleCommand.php | 16 ++ src/Console/ConsoleKernel.php | 6 +- src/Providers/AssetsServiceProvider.php | 54 ++++++ src/Support/Assets.php | 41 +++++ 6 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 src/Console/Commands/FlushPipelineCommand.php create mode 100644 src/Providers/AssetsServiceProvider.php create mode 100644 src/Support/Assets.php diff --git a/src/Application.php b/src/Application.php index 5741c64..20fc3ac 100644 --- a/src/Application.php +++ b/src/Application.php @@ -9,6 +9,7 @@ use Codefy\Framework\Factory\FileLoggerFactory; use Codefy\Framework\Pipeline\PipelineBuilder; use Codefy\Framework\Proxy\Codefy; +use Codefy\Framework\Support\Assets; use Codefy\Framework\Support\BasePathDetector; use Codefy\Framework\Support\LocalStorage; use Codefy\Framework\Support\Paths; @@ -42,7 +43,6 @@ use Qubus\Mail\Mailer; use Qubus\Routing\Router; use Qubus\Support\ArrayHelper; -use Qubus\Support\Assets; use Qubus\Support\StringHelper; use ReflectionException; @@ -257,6 +257,7 @@ protected function registerDefaultServiceProviders(): void Providers\DatabaseConnectionServiceProvider::class, Providers\FlysystemServiceProvider::class, Providers\RouterServiceProvider::class, + Providers\AssetsServiceProvider::class, ] as $serviceProvider ) { $this->registerServiceProvider(serviceProvider: $serviceProvider); @@ -927,7 +928,7 @@ public static function getInstance(?string $path = null): self } public private(set) Assets $assets { - get => $this->assets ?? $this->make(name: Assets::class); + get => $this->assets ?? $this->make(name: 'assets.group.default'); } public private(set) Mailer $mailer { diff --git a/src/Console/Commands/FlushPipelineCommand.php b/src/Console/Commands/FlushPipelineCommand.php new file mode 100644 index 0000000..7ddc249 --- /dev/null +++ b/src/Console/Commands/FlushPipelineCommand.php @@ -0,0 +1,170 @@ +addOption( + name: 'group', + shortcut: '-g', + mode: InputOption::VALUE_REQUIRED, + description: 'Only flush the provided group' + ) + ->addOption( + name: 'force', + shortcut: '-f', + mode: InputOption::VALUE_NONE, + description: 'Do not prompt for confirmation' + ) + ->setDescription(description: 'Flush assets pipeline..') + ->setHelp( + help: <<asset:flush flushes the assets pipeline. +php codex asset:flush +EOT + ); + } + + /** + * @throws Exception + * @throws FilesystemException + */ + public function handle(): int + { + // Get directories to purge + if (! $directories = $this->getPipelineDirectories()) { + $this->terminalError(string: 'The provided group does not exist'); + + return ConsoleCommand::FAILURE; + } + + // Ask for confirmation + if (! $this->getOptions(key: 'force')) { + $this->terminalInfo(string: 'All content of the following directories will be deleted:'); + + foreach ($directories as $dir) { + $this->terminalComment($dir); + } + + if (! $this->confirm(question: 'Do you want to continue?')) { + return ConsoleCommand::SUCCESS; + } + } + + // Purge directories + $this->terminalComment(string: 'Flushing pipeline directories...'); + + foreach ($directories as $dir) { + $this->output->write(messages: "$dir "); + + if ($this->purgeDir($dir)) { + $this->terminalInfo(string: 'OK'); + } else { + $this->terminalError(string: 'ERROR'); + } + } + + $this->terminalComment(string: 'Done!'); + + return ConsoleCommand::SUCCESS; + } + + /** + * Get the pipeline directories of the groups. + * + * @return array + * @throws Exception + */ + protected function getPipelineDirectories(): array + { + // Parse configured groups + $config = $this->codefy->configContainer->getConfigKey(key: 'assets'); + + $groups = (isset($config['default'])) ? $config : ['default' => $config]; + + if (! is_null__($group = $this->getOptions(key: 'group'))) { + $groups = $this->codefy->array->subset(array: $groups, keys: [$group]); + } + + // Parse pipeline directories of each group + $directories = []; + + foreach ($groups as $group => $config) { + $pipelineDir = (isset($config['pipeline_dir'])) ? $config['pipeline_dir'] : 'min'; + $publicDir = (isset($config['public_dir'])) ? $config['public_dir'] : public_path(); + $publicDir = rtrim($publicDir, $this->codefy::DS); + + $cssDir = (isset($config['css_dir'])) ? $config['css_dir'] : 'css'; + $directories[] = implode($this->codefy::DS, [$publicDir, $cssDir, $pipelineDir]); + + $jsDir = (isset($config['js_dir'])) ? $config['js_dir'] : 'js'; + $directories[] = implode($this->codefy::DS, [$publicDir, $jsDir, $pipelineDir]); + } + + // Clean results + $directories = array_unique($directories); + sort($directories); + + return $directories; + } + + /** + * Remove the contents of a given directory. + * + * @param string $directory + * @return bool + * @throws FilesystemException + */ + protected function purgeDir(string $directory): bool + { + /** @var FileSystem $filesystem */ + $filesystem = $this->codefy->make(name: 'filesystem.default'); + + if (! $filesystem->directoryExists($directory)) { + return true; + } + + if (\Qubus\Support\Helpers\is_writable($directory)) { + $contents = $filesystem->listContents(location: $directory, deep: true)->toArray(); + + foreach ($contents as $item) { + // Check if the item is a file or a directory + if ($item->isFile()) { + $filesystem->delete($item->path()); + } elseif ($item->isDir()) { + // Delete the directory and its contents (already handled by recursive listing) + // You might want to delete directories only after their contents are cleared + // Or, if you're deleting from the deepest level first, this will work. + // For a more controlled approach, you might want to sort the $contents + // to process deeper items first. + $filesystem->deleteDirectory($item->path()); + } + } + $filesystem->deleteDirectory($directory); + + return true; + } + + $this->terminalError(sprintf('Directory "%s" is not writable.', $directory)); + + return false; + } +} diff --git a/src/Console/ConsoleCommand.php b/src/Console/ConsoleCommand.php index 0dcc26f..c228cc7 100644 --- a/src/Console/ConsoleCommand.php +++ b/src/Console/ConsoleCommand.php @@ -9,11 +9,14 @@ use ReflectionException; use ReflectionMethod; use Symfony\Component\Console\Command\Command as SymfonyCommand; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\Question; + use function count; use function method_exists; @@ -177,6 +180,19 @@ private function setArguments(): ConsoleCommand|bool return true; } + /** + * @param string $question + * @return mixed + */ + protected function confirm(string $question): mixed + { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper(name: 'question'); + $question = new Question(question: $question); + + return $helper->ask($this->input, $this->output, $question); + } + /** * @return bool|ConsoleCommand * @throws TypeException diff --git a/src/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php index 0b2350f..b0c112c 100644 --- a/src/Console/ConsoleKernel.php +++ b/src/Console/ConsoleKernel.php @@ -90,12 +90,12 @@ protected function commands(): void /** * Registers a command. * - * @param Command $command + * @param callable|Command $command * @return void */ - public function registerCommand(Command $command): void + public function registerCommand(callable|Command $command): void { - $this->getCodex()->add(command: $command); + $this->getCodex()->addCommand(command: $command); } /** diff --git a/src/Providers/AssetsServiceProvider.php b/src/Providers/AssetsServiceProvider.php new file mode 100644 index 0000000..7815b1b --- /dev/null +++ b/src/Providers/AssetsServiceProvider.php @@ -0,0 +1,54 @@ +codefy->configContainer->getConfigKey(key: 'assets'); + + // No groups defined. Assume the config is for the default group. + if (!isset($config['default'])) { + $this->registerAssetsManagerInstance(name: 'default', config: $config); + + return; + } + + // Multiple groups + foreach ($config as $groupName => $groupConfig) { + $this->registerAssetsManagerInstance($groupName, (array) $groupConfig); + } + } + + /** + * Register an instance of the assets manager library. + * + * @param string $name Name of the group. + * @param array $config Config of the group. + * + * @return void + */ + protected function registerAssetsManagerInstance(string $name, array $config): void + { + $this->codefy->singleton("assets.group.$name", function () use ($config) { + + if (! isset($config['public_dir'])) { + $config['public_dir'] = public_path(); + } + + return new Assets($this->codefy, $config); + }); + } +} diff --git a/src/Support/Assets.php b/src/Support/Assets.php new file mode 100644 index 0000000..65f4b9c --- /dev/null +++ b/src/Support/Assets.php @@ -0,0 +1,41 @@ +codefy->make($binding); + + if (!$assets instanceof Assets) { + throw new RuntimeException( + message: sprintf("Assets group '%s' not found in the config file", $group) + ); + } + + return $assets; + } +} From ee4feb2836a863ccb8df18ca2118bf19db334f52 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 6 Oct 2025 14:21:16 -0700 Subject: [PATCH 106/161] Removed AssetsServiceProvider from base call. Signed-off-by: Joshua Parker --- src/Application.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 20fc3ac..fb8a945 100644 --- a/src/Application.php +++ b/src/Application.php @@ -257,7 +257,6 @@ protected function registerDefaultServiceProviders(): void Providers\DatabaseConnectionServiceProvider::class, Providers\FlysystemServiceProvider::class, Providers\RouterServiceProvider::class, - Providers\AssetsServiceProvider::class, ] as $serviceProvider ) { $this->registerServiceProvider(serviceProvider: $serviceProvider); From 3bca29cc0a63486b4cee2ca10a9498066eee916e Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 6 Oct 2025 14:33:43 -0700 Subject: [PATCH 107/161] Fixed option checking. Signed-off-by: Joshua Parker --- src/Console/Commands/FlushPipelineCommand.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Console/Commands/FlushPipelineCommand.php b/src/Console/Commands/FlushPipelineCommand.php index 7ddc249..a8a785f 100644 --- a/src/Console/Commands/FlushPipelineCommand.php +++ b/src/Console/Commands/FlushPipelineCommand.php @@ -11,7 +11,6 @@ use Symfony\Component\Console\Input\InputOption; use function Codefy\Framework\Helpers\public_path; -use function Qubus\Support\Helpers\is_null__; class FlushPipelineCommand extends ConsoleCommand { @@ -100,7 +99,7 @@ protected function getPipelineDirectories(): array $groups = (isset($config['default'])) ? $config : ['default' => $config]; - if (! is_null__($group = $this->getOptions(key: 'group'))) { + if (! empty($group = $this->getOptions(key: 'group'))) { $groups = $this->codefy->array->subset(array: $groups, keys: [$group]); } From 073f2c6e943a17b025ac2594893e79624d344639 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 6 Oct 2025 14:43:57 -0700 Subject: [PATCH 108/161] Re-added service provider to load before views. Signed-off-by: Joshua Parker --- src/Application.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Application.php b/src/Application.php index fb8a945..20fc3ac 100644 --- a/src/Application.php +++ b/src/Application.php @@ -257,6 +257,7 @@ protected function registerDefaultServiceProviders(): void Providers\DatabaseConnectionServiceProvider::class, Providers\FlysystemServiceProvider::class, Providers\RouterServiceProvider::class, + Providers\AssetsServiceProvider::class, ] as $serviceProvider ) { $this->registerServiceProvider(serviceProvider: $serviceProvider); From 61f47fef917b7ac5db1f8ca77a7bed5c3b208471 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Mon, 6 Oct 2025 14:48:16 -0700 Subject: [PATCH 109/161] Assets service provider should not be in Application. Signed-off-by: Joshua Parker --- src/Application.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 20fc3ac..fb8a945 100644 --- a/src/Application.php +++ b/src/Application.php @@ -257,7 +257,6 @@ protected function registerDefaultServiceProviders(): void Providers\DatabaseConnectionServiceProvider::class, Providers\FlysystemServiceProvider::class, Providers\RouterServiceProvider::class, - Providers\AssetsServiceProvider::class, ] as $serviceProvider ) { $this->registerServiceProvider(serviceProvider: $serviceProvider); From 6d8727537f9534bc12346f77e98505d1154eb2ae Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 7 Oct 2025 11:58:31 -0700 Subject: [PATCH 110/161] Updates to scheduler, events, and tasks. Signed-off-by: Joshua Parker --- src/Application.php | 12 +- src/Console/ConsoleKernel.php | 9 +- .../EventDispatcherServiceProvider.php | 33 +++ src/Scheduler/BaseTask.php | 193 ++++++------------ src/Scheduler/Event/TaskCompleted.php | 11 +- src/Scheduler/Event/TaskFailed.php | 11 +- src/Scheduler/Event/TaskSkipped.php | 8 +- src/Scheduler/Event/TaskStarted.php | 11 +- src/Scheduler/Processor/BaseProcessor.php | 2 + src/Scheduler/Processor/Callback.php | 4 +- src/Scheduler/Schedule.php | 29 ++- src/Scheduler/Stack.php | 133 ------------ src/Scheduler/Traits/MailerAware.php | 2 +- 13 files changed, 169 insertions(+), 289 deletions(-) create mode 100644 src/Providers/EventDispatcherServiceProvider.php delete mode 100644 src/Scheduler/Stack.php diff --git a/src/Application.php b/src/Application.php index fb8a945..0ba5c69 100644 --- a/src/Application.php +++ b/src/Application.php @@ -19,11 +19,11 @@ use Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException; use Dotenv\Dotenv; use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Qubus\Config\ConfigContainer; use Qubus\EventDispatcher\ActionFilter\Observer; -use Qubus\EventDispatcher\Legacy\EventDispatcher; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; use Qubus\Expressive\Connection; @@ -254,6 +254,7 @@ protected function registerDefaultServiceProviders(): void foreach ( [ Providers\ConfigServiceProvider::class, + Providers\EventDispatcherServiceProvider::class, Providers\DatabaseConnectionServiceProvider::class, Providers\FlysystemServiceProvider::class, Providers\RouterServiceProvider::class, @@ -269,11 +270,11 @@ public function bootstrapWith(array $bootstrappers): void $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { - $this->make(name: EventDispatcher::class)->dispatch("bootstrapping.{$bootstrapper}"); + $this->make(name: EventDispatcherInterface::class)->dispatch($bootstrapper); $this->make(name: $bootstrapper)->bootstrap($this); - $this->make(name: EventDispatcher::class)->dispatch("bootstrapped.{$bootstrapper}"); + $this->make(name: EventDispatcherInterface::class)->dispatch($bootstrapper); } } @@ -756,7 +757,6 @@ protected function coreAliases(): array \Qubus\Cache\Psr6\TaggableCacheItemPool::class => \Qubus\Cache\Psr6\TaggablePsr6PoolAdapter::class, \Qubus\Config\Path\Path::class => \Qubus\Config\Path\ConfigPath::class, \Qubus\Config\ConfigContainer::class => \Qubus\Config\Collection::class, - \Qubus\EventDispatcher\Legacy\EventDispatcher::class => \Qubus\EventDispatcher\Legacy\Dispatcher::class, \Qubus\Mail\Mailer::class => \Codefy\Framework\Support\CodefyMailer::class, 'mailer' => Mailer::class, 'dir.path' => \Codefy\Framework\Support\Paths::class, @@ -942,8 +942,8 @@ public static function getInstance(?string $path = null): self get => $this->flash ?? $this->make(name: Flash::class); } - public private(set) EventDispatcher $event { - get => $this->event ?? $this->make(name: EventDispatcher::class); + public private(set) EventDispatcherInterface $event { + get => $this->event ?? $this->make(name: EventDispatcherInterface::class); } public private(set) HttpCookieFactory $httpCookie { diff --git a/src/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php index b0c112c..8c1de20 100644 --- a/src/Console/ConsoleKernel.php +++ b/src/Console/ConsoleKernel.php @@ -11,6 +11,7 @@ use Codefy\Framework\Scheduler\Mutex\Locker; use Codefy\Framework\Scheduler\Schedule; use Exception; +use Psr\EventDispatcher\EventDispatcherInterface; use Qubus\Support\DateTime\QubusDateTimeZone; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; @@ -47,13 +48,19 @@ protected function defineConsoleSchedule(): void $this->codefy->share(nameOrInstance: Schedule::class); $this->codefy->prepare(name: Schedule::class, callableOrMethodStr: function () { + $dispatcher = $this->codefy->make(name: EventDispatcherInterface::class); + $timeZone = new QubusDateTimeZone(timezone: $this->codefy->make( name: 'codefy.config' )->getConfigKey('app.timezone')); $mutex = $this->codefy->make(name: Locker::class); - return tap(value: new Schedule(timeZone: $timeZone, mutex: $mutex), callback: function ($schedule) { + return tap(value: new Schedule( + dispatcher: $dispatcher, + timeZone: $timeZone, + mutex: $mutex + ), callback: function ($schedule) { $this->schedule(schedule: $schedule); }); }); diff --git a/src/Providers/EventDispatcherServiceProvider.php b/src/Providers/EventDispatcherServiceProvider.php new file mode 100644 index 0000000..b2daafa --- /dev/null +++ b/src/Providers/EventDispatcherServiceProvider.php @@ -0,0 +1,33 @@ +codefy->configContainer->getConfigKey( + key: 'app.event_listener', + default: SimpleProvider::class + ); + $dispatcher = $this->codefy->configContainer->getConfigKey( + key: 'app.event_dispatcher', + default: EventDispatcher::class + ); + + $this->codefy->alias(original: ListenerProviderInterface::class, alias: $provider); + $this->codefy->alias(original: EventDispatcherInterface::class, alias: $dispatcher); + } +} diff --git a/src/Scheduler/BaseTask.php b/src/Scheduler/BaseTask.php index 253ef6f..e0efe73 100644 --- a/src/Scheduler/BaseTask.php +++ b/src/Scheduler/BaseTask.php @@ -4,84 +4,92 @@ namespace Codefy\Framework\Scheduler; +use Codefy\Framework\Scheduler\Event\TaskCompleted; +use Codefy\Framework\Scheduler\Event\TaskFailed; +use Codefy\Framework\Scheduler\Event\TaskStarted; use Codefy\Framework\Scheduler\Expressions\Expressional; use Codefy\Framework\Scheduler\Processor\BaseProcessor; +use Codefy\Framework\Scheduler\Traits\MailerAware; use Codefy\Framework\Scheduler\ValueObject\TaskId; use Closure; -use DateTimeZone; -use Exception as NativeException; +use Exception; +use Psr\EventDispatcher\EventDispatcherInterface; use Qubus\Exception\Data\TypeException; -use Qubus\Exception\Exception; -use function call_user_func; -use function date_default_timezone_get; use function is_string; use function Qubus\Support\Helpers\is_false__; -use function Qubus\Support\Helpers\is_null__; -use function sprintf; use function strtotime; use function time; abstract class BaseTask extends BaseProcessor implements Task { - protected bool $acquireLock = false; + use MailerAware; - protected mixed $lockValue; - - protected TaskId $pid; + protected ?TaskId $pid = null; protected array $options = []; - protected bool $truthy = false; - protected string|null $timeZone = null; - /** @var callable $whenOverlapping */ - protected $whenOverlapping; - /** @var callable[]|Closure[] $filters */ protected array $filters = []; /** @var callable[]|Closure[] $rejects */ protected array $rejects = []; - protected Schedule $schedule; + protected ?Schedule $schedule = null; + + protected ?EventDispatcherInterface $dispatcher = null; /** * @throws TypeException */ public function withOptions(array $options): self { - $this->options = $options + [ - 'schedule' => null, - 'maxRuntime' => 120, - 'recipients' => null, - 'smtpSender' => null, - 'smtpSenderName' => null, - 'enabled' => false, + $new = clone $this; + $new->options = $options + [ + 'expression' => null, + 'maxRuntime' => 120, + 'recipients' => null, + 'smtpSender' => null, + 'smtpSenderName' => null, + 'enabled' => true, ]; - if ($this->options['schedule'] instanceof Expressional) { - $this->expression = $this->options['schedule']; + if ($new->options['expression'] instanceof Expressional) { + $new->expression = $new->options['expression']; } - if (is_string($this->options['schedule'])) { - $this->expression = $this->alias($this->options['schedule']); + if (is_string($new->options['expression'])) { + $new->expression = $new->alias($new->options['expression']); } - return $this; + return $new; } - public function getOption(?string $option): mixed + public function withScheduler(Schedule $schedule): self { - return $this->options[$option]; + $new = clone $this; + $new->schedule = $schedule; + + return $new; } - public function withTimezone(?string $timeZone): self + public function withDispatcher(EventDispatcherInterface $dispatcher): self { - $this->timeZone = $timeZone; + $new = clone $this; + $new->dispatcher = $dispatcher; - return $this; + return $new; + } + + public function getOption(?string $option): mixed + { + if ($option === null) { + return $this->options; + } + + return $this->options[$option] ?? null; } /** @@ -113,17 +121,6 @@ public function tearDown(): void { } - /** - * Check if the Task is overlapping. - * @throws TypeException - */ - public function isOverlapping(): bool - { - return $this->acquireLock && - $this->cache->hasItem($this->pid()) && - call_user_func($this->whenOverlapping, $this->lockValue) === false; - } - /** * Check if time hasn't arrived. * @@ -160,7 +157,7 @@ public function from(string $datetime): self } /** - * Check if event should be not run. + * Check if event should not run. * * @param string $datetime * @return self @@ -172,76 +169,8 @@ public function to(string $datetime): self }); } - /** - * Acquires the Task lock. - * @throws TypeException - */ - protected function acquireTaskLock(): void - { - if ($this->acquireLock) { - $item = $this->cache->getItem($this->pid()); - - if (!$item->isHit()) { - $item->set($this->options['maxRuntime'] + time()); - $item->expiresAfter($this->options['maxRuntime']); - $this->cache->save($item); - } - - $this->lockValue = $item->get(); - } - } - - /** - * Removes the Task lock. - * @throws TypeException - */ - protected function removeTaskLock(): bool - { - return $this->cache->deleteItem($this->pid()); - } - - /** - * @throws TypeException - * @throws Exception - */ - public function checkMaxRuntime(): void - { - $maxRuntime = $this->options['maxRuntime']; - if (is_null__($maxRuntime)) { - return; - } - - $runtime = $this->getLockLifetime($this->pid()); - if ($runtime < $maxRuntime) { - return; - } - - throw new Exception( - sprintf( - 'Max Runtime of %s secs exceeded! Current runtime for %s: %s secs.', - $maxRuntime, - static::class, - $runtime - ), - 8712 - ); - } - - /** - * Retrieve the Task lock lifetime value. - */ - private function getLockLifetime(TaskId $lockKey): int - { - if (!$this->cache->hasItem($lockKey)) { - return 0; - } - - return time() - $this->lockValue; - } - /** * Checks whether the task can and should run. - * @throws TypeException */ private function shouldRun(): bool { @@ -250,34 +179,38 @@ private function shouldRun(): bool } // If task overlaps don't execute. - if ($this->isOverlapping()) { + if ( + $this->canRunOnlyOneInstance() && + ! $this->mutex->tryLock($this) + ) { return false; } return true; } - /** - * Checks whether the task can and should run. - * @throws TypeException - * @throws NativeException - */ - public function isDue(string|DateTimeZone|null $timeZone = null): bool + public function run(): bool { - if (is_false__($this->shouldRun())) { + if (!$this->shouldRun()) { return false; } - if (is_null__($timeZone)) { - $timeZone = date_default_timezone_get(); + $this->dispatcher->dispatch(event: new TaskStarted($this)); + + try { + // Run code before executing task. + $this->setUp(); + // Execute task. + $this->execute($this->schedule); + // Run code after executing task. + $this->tearDown(); + } catch (Exception $ex) { + $this->dispatcher->dispatch(event: new TaskFailed($this)); + $this->sendEmail($ex); } - $isDue = $this->expression->isDue(currentTime: 'now', timeZone: $this->timeZone ?? $timeZone); + $this->dispatcher->dispatch(event: new TaskCompleted($this)); - if (parent::isDue($this->timeZone ?? $timeZone) && $isDue) { - return true; - } - - return false; + return true; } } diff --git a/src/Scheduler/Event/TaskCompleted.php b/src/Scheduler/Event/TaskCompleted.php index f08e14a..dcfcd13 100644 --- a/src/Scheduler/Event/TaskCompleted.php +++ b/src/Scheduler/Event/TaskCompleted.php @@ -6,20 +6,23 @@ use Codefy\Framework\Scheduler\Task; use Qubus\EventDispatcher\BaseEvent; +use Qubus\EventDispatcher\Legacy\Event; -final class TaskCompleted extends BaseEvent +final class TaskCompleted extends BaseEvent implements Event { public const string EVENT_NAME = 'task.completed'; - private Task $task; + public private(set) ?Task $task = null { + get => $this->task; + } public function __construct(Task $task) { $this->task = $task; } - public function getTask(): Task + public function getName(): string { - return $this->task; + return self::EVENT_NAME; } } diff --git a/src/Scheduler/Event/TaskFailed.php b/src/Scheduler/Event/TaskFailed.php index 56d1b88..d5e18fa 100644 --- a/src/Scheduler/Event/TaskFailed.php +++ b/src/Scheduler/Event/TaskFailed.php @@ -6,20 +6,23 @@ use Codefy\Framework\Scheduler\Task; use Qubus\EventDispatcher\BaseEvent; +use Qubus\EventDispatcher\Legacy\Event; -final class TaskFailed extends BaseEvent +final class TaskFailed extends BaseEvent implements Event { public const string EVENT_NAME = 'task.failed'; - private Task $task; + public private(set) ?Task $task = null { + get => $this->task; + } public function __construct(Task $task) { $this->task = $task; } - public function getTask(): Task + public function getName(): string { - return $this->task; + return self::EVENT_NAME; } } diff --git a/src/Scheduler/Event/TaskSkipped.php b/src/Scheduler/Event/TaskSkipped.php index 797d15e..eb9dd89 100644 --- a/src/Scheduler/Event/TaskSkipped.php +++ b/src/Scheduler/Event/TaskSkipped.php @@ -5,8 +5,14 @@ namespace Codefy\Framework\Scheduler\Event; use Qubus\EventDispatcher\BaseEvent; +use Qubus\EventDispatcher\Legacy\Event; -final class TaskSkipped extends BaseEvent +final class TaskSkipped extends BaseEvent implements Event { public const string EVENT_NAME = 'task.skipped'; + + public function getName(): string + { + return self::EVENT_NAME; + } } diff --git a/src/Scheduler/Event/TaskStarted.php b/src/Scheduler/Event/TaskStarted.php index 4eccfe1..1a76b63 100644 --- a/src/Scheduler/Event/TaskStarted.php +++ b/src/Scheduler/Event/TaskStarted.php @@ -6,20 +6,23 @@ use Codefy\Framework\Scheduler\Task; use Qubus\EventDispatcher\BaseEvent; +use Qubus\EventDispatcher\Legacy\Event; -final class TaskStarted extends BaseEvent +final class TaskStarted extends BaseEvent implements Event { public const string EVENT_NAME = 'task.started'; - private Task $task; + public private(set) ?Task $task = null { + get => $this->task; + } public function __construct(Task $task) { $this->task = $task; } - public function getTask(): Task + public function getName(): string { - return $this->task; + return self::EVENT_NAME; } } diff --git a/src/Scheduler/Processor/BaseProcessor.php b/src/Scheduler/Processor/BaseProcessor.php index c6a74e5..ebd0216 100644 --- a/src/Scheduler/Processor/BaseProcessor.php +++ b/src/Scheduler/Processor/BaseProcessor.php @@ -249,6 +249,8 @@ public function maxRuntime(): int */ public function getExpression(): ?string { + $expression = null; + if ($this->expression instanceof CronExpression) { $expression = $this->expression->getExpression(); } diff --git a/src/Scheduler/Processor/Callback.php b/src/Scheduler/Processor/Callback.php index 0945e2c..bd06bbe 100644 --- a/src/Scheduler/Processor/Callback.php +++ b/src/Scheduler/Processor/Callback.php @@ -10,7 +10,7 @@ class Callback extends BaseProcessor implements Stringable, Processor { - public function run(): mixed + public function run(): string|false { if ( $this->preventOverlapping && @@ -31,7 +31,7 @@ public function run(): mixed /** * Executes command. */ - private function exec(callable $fn): mixed + private function exec(callable $fn): string { $data = $this->call($fn, $this->args, true); diff --git a/src/Scheduler/Schedule.php b/src/Scheduler/Schedule.php index 7246431..64c0a2e 100644 --- a/src/Scheduler/Schedule.php +++ b/src/Scheduler/Schedule.php @@ -9,9 +9,12 @@ use Codefy\Framework\Scheduler\Processor\Processor; use Codefy\Framework\Scheduler\Processor\Shell; use Codefy\Framework\Scheduler\Traits\LiteralAware; +use Exception; +use Psr\EventDispatcher\EventDispatcherInterface; use Qubus\Exception\Data\TypeException; -use Qubus\Exception\Exception; use Qubus\Support\DateTime\QubusDateTimeZone; +use ReflectionClass; +use ReflectionException; use function array_filter; use function count; @@ -51,8 +54,11 @@ class Schedule //phpcs:enable - public function __construct(public readonly QubusDateTimeZone $timeZone, public readonly Locker $mutex) - { + public function __construct( + public readonly EventDispatcherInterface $dispatcher, + public readonly QubusDateTimeZone $timeZone, + public readonly Locker $mutex + ) { } /** @@ -106,6 +112,23 @@ public function php(string $script, ?string $bin = null, array $args = []): Shel return $command; } + /** + * @throws ReflectionException + * @throws \Qubus\Exception\Exception + */ + public function task(Task $task, array $options = []): BaseTask + { + $instance = new ReflectionClass($task); + $task = $instance->newInstanceArgs([$this->mutex, $this->timeZone]); + $task->withOptions($options); + $task->withScheduler($this); + $task->withDispatcher($this->dispatcher); + + $this->queueProcessor($task); + + return $task; + } + /** * @return Processor[] */ diff --git a/src/Scheduler/Stack.php b/src/Scheduler/Stack.php deleted file mode 100644 index 9bb1425..0000000 --- a/src/Scheduler/Stack.php +++ /dev/null @@ -1,133 +0,0 @@ -dispatcher = $dispatcher; - $this->mutex = $mutex; - $this->timezone = $timezone; - } - - /** - * Add tasks to the list. - * - * @param Task[] $tasks - */ - public function addTasks(array $tasks): void - { - foreach ($tasks as $task) { - $this->addTask($task); - } - } - - /** - * Add a task to the list. - */ - public function addTask(Task $task): Stack - { - $this->tasks[] = $task; - - return $this; - } - - /** - * Returns an array of tasks. - */ - public function tasks(): array - { - return $this->tasks; - } - - /** - * Gets tasks that are due and can run. - * - * @return array - */ - public function tasksDue(): array - { - $that = clone $this; - - return array_filter($this->tasks, function (Task $task) use ($that) { - return $task->isDue($that->timezone); - }); - } - - public function withOptions(array $options = []): self - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - - $this->options = $resolver->resolve($options); - - return $this; - } - - protected function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'dispatcher' => $this->dispatcher, - 'mutex' => $this->mutex, - 'timezone' => $this->timezone, - 'maxRuntime' => 120, - 'encryption' => null, - ]); - } - - public function run(): void - { - $schedule = new Schedule($this->options['timezone'], $this->options['mutex']); - - foreach ($this->tasks() as $task) { - if (is_false__($task->getOption('enabled'))) { - continue; - } - - $this->dispatcher->dispatch(TaskStarted::EVENT_NAME); - - try { - // Run code before executing task. - $task->setUp(); - // Execute task. - $task->execute($schedule); - // Run code after executing task. - $task->tearDown(); - } catch (Exception $ex) { - $this->dispatcher->dispatch(TaskFailed::EVENT_NAME); - $this->sendEmail($ex); - } - - $this->dispatcher->dispatch(TaskCompleted::EVENT_NAME); - } - } -} diff --git a/src/Scheduler/Traits/MailerAware.php b/src/Scheduler/Traits/MailerAware.php index 730fff0..3bc9e1c 100644 --- a/src/Scheduler/Traits/MailerAware.php +++ b/src/Scheduler/Traits/MailerAware.php @@ -4,7 +4,7 @@ namespace Codefy\Framework\Scheduler\Traits; -use Qubus\Exception\Exception; +use Exception; use function Codefy\Framework\Helpers\app; use function explode; From 7b0b6ce8ca2503fdbbba8014081574c61544304d Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 7 Oct 2025 12:05:12 -0700 Subject: [PATCH 111/161] Object fix for dispatcher. Signed-off-by: Joshua Parker --- src/Application.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application.php b/src/Application.php index 0ba5c69..37d28cd 100644 --- a/src/Application.php +++ b/src/Application.php @@ -270,11 +270,11 @@ public function bootstrapWith(array $bootstrappers): void $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { - $this->make(name: EventDispatcherInterface::class)->dispatch($bootstrapper); + $this->make(name: EventDispatcherInterface::class)->dispatch(new $bootstrapper()); $this->make(name: $bootstrapper)->bootstrap($this); - $this->make(name: EventDispatcherInterface::class)->dispatch($bootstrapper); + $this->make(name: EventDispatcherInterface::class)->dispatch(new $bootstrapper()); } } From 6ca059d97141f318d1371a86a371280d2fd961ab Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 7 Oct 2025 12:31:07 -0700 Subject: [PATCH 112/161] Translation function. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 79ef67c..ee78158 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -30,6 +30,7 @@ use function getcwd; use function is_int; use function Qubus\Security\Helpers\__observer; +use function Qubus\Security\Helpers\t__; use function Qubus\Support\Helpers\is_false__; use function Qubus\Support\Helpers\is_null__; use function file_exists; @@ -63,17 +64,17 @@ function app(?string $name = null, array $args = []): mixed * * @param string $key * @param array|bool $set + * @param mixed $default * @return mixed - * @throws TypeException */ -function config(string $key, array|bool $set = false): mixed +function config(string $key, array|bool $set = false, mixed $default = ''): mixed { if (!is_false__(var: $set)) { app(name: Collection::class)->setConfigKey($key, $set); - return app(name: Collection::class)->getConfigKey($key); + return app(name: Collection::class)->getConfigKey($key, $default); } - return app(name: Collection::class)->getConfigKey($key); + return app(name: Collection::class)->getConfigKey($key, $default); } /** @@ -245,3 +246,14 @@ function ask(Query $query): mixed return $enquirer->execute($query); } + +/** + * Displays the returned translated text. + * + * @param string $string + * @return string + */ +function trans(string $string): string +{ + return t__(msgid: $string, domain: config(key: 'app.locale_domain', default: 'codefy')); +} From d63b5009f3a422765090be645bc8c6b402e7dad7 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 7 Oct 2025 16:12:08 -0700 Subject: [PATCH 113/161] Task and console command changes. Signed-off-by: Joshua Parker --- src/Console/Commands/MakeCommand.php | 2 ++ ...Command.php => MigrateGenerateCommand.php} | 2 +- src/Console/Commands/PhpMigCommand.php | 2 +- .../Commands/Traits/MakeCommandAware.php | 2 +- src/Scheduler/Task.php | 21 ------------------- src/Stubs/ExampleCommand.stub | 15 +++++++++++++ 6 files changed, 20 insertions(+), 24 deletions(-) rename src/Console/Commands/{GenerateCommand.php => MigrateGenerateCommand.php} (99%) create mode 100644 src/Stubs/ExampleCommand.stub diff --git a/src/Console/Commands/MakeCommand.php b/src/Console/Commands/MakeCommand.php index a7a6a65..5aa2146 100644 --- a/src/Console/Commands/MakeCommand.php +++ b/src/Console/Commands/MakeCommand.php @@ -36,6 +36,8 @@ class MakeCommand extends ConsoleCommand 'provider' => 'App\Infrastructure\Providers', 'middleware' => 'App\Infrastructure\Http\Middleware', 'error' => 'App\Infrastructure\Errors', + 'command' => 'App\Application\Console\Commands', + 'route' => 'App\Infrastructure\Http\Routes', ]; protected array $args = [ diff --git a/src/Console/Commands/GenerateCommand.php b/src/Console/Commands/MigrateGenerateCommand.php similarity index 99% rename from src/Console/Commands/GenerateCommand.php rename to src/Console/Commands/MigrateGenerateCommand.php index 1a8efff..1db6765 100644 --- a/src/Console/Commands/GenerateCommand.php +++ b/src/Console/Commands/MigrateGenerateCommand.php @@ -13,7 +13,7 @@ use function Qubus\Support\Helpers\is_writable; -class GenerateCommand extends PhpMigCommand +class MigrateGenerateCommand extends PhpMigCommand { protected string $name = 'migrate:generate'; diff --git a/src/Console/Commands/PhpMigCommand.php b/src/Console/Commands/PhpMigCommand.php index 1bdcb37..b9e65d9 100644 --- a/src/Console/Commands/PhpMigCommand.php +++ b/src/Console/Commands/PhpMigCommand.php @@ -237,7 +237,7 @@ protected function bootstrapMigrations(InputInterface $input, OutputInterface $o $class = $this->migrationToClassName(migrationName: $migrationName); if ( - $this instanceof GenerateCommand + $this instanceof MigrateGenerateCommand && $class == $this->migrationToClassName(migrationName: $input->getArgument(name: 'name')) ) { throw new TypeException( diff --git a/src/Console/Commands/Traits/MakeCommandAware.php b/src/Console/Commands/Traits/MakeCommandAware.php index 7f349f3..57c2bc7 100644 --- a/src/Console/Commands/Traits/MakeCommandAware.php +++ b/src/Console/Commands/Traits/MakeCommandAware.php @@ -176,7 +176,7 @@ private function addOptionalDirFlag(mixed $options): string */ private function getStubFiles(string $classNameSuffix): string|false { - $files = glob(pattern: Application::$ROOT_PATH . '/vendor/codefyphp/framework/Stubs/*.stub'); + $files = glob(pattern: Application::$ROOT_PATH . '/vendor/codefyphp/codefy/src/Stubs/*.stub'); if (is_array(value: $files) && count($files)) { foreach ($files as $file) { if (is_file(filename: $file)) { diff --git a/src/Scheduler/Task.php b/src/Scheduler/Task.php index ddec023..d662eea 100644 --- a/src/Scheduler/Task.php +++ b/src/Scheduler/Task.php @@ -4,16 +4,8 @@ namespace Codefy\Framework\Scheduler; -use DateTimeZone; -use Codefy\Framework\Scheduler\ValueObject\TaskId; - interface Task { - /** - * Unique ID for the task. - */ - public function pid(): TaskId; - /** * Called before a task is executed. */ @@ -28,17 +20,4 @@ public function execute(Schedule $schedule): void; * Called after a task is executed. */ public function tearDown(): void; - - /** - * Get set options. - * - * @param string|null $option - * @return mixed - */ - public function getOption(?string $option): mixed; - - /** - * Checks whether the task is currently due. - */ - public function isDue(string|DateTimeZone|null $timeZone = null): bool; } diff --git a/src/Stubs/ExampleCommand.stub b/src/Stubs/ExampleCommand.stub new file mode 100644 index 0000000..0f7732b --- /dev/null +++ b/src/Stubs/ExampleCommand.stub @@ -0,0 +1,15 @@ + Date: Tue, 7 Oct 2025 16:32:44 -0700 Subject: [PATCH 114/161] Removed unnecessary finally block. Signed-off-by: Joshua Parker --- src/Console/Commands/MakeCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Console/Commands/MakeCommand.php b/src/Console/Commands/MakeCommand.php index 5aa2146..2184fda 100644 --- a/src/Console/Commands/MakeCommand.php +++ b/src/Console/Commands/MakeCommand.php @@ -69,7 +69,6 @@ public function handle(): int return ConsoleCommand::SUCCESS; } catch (MakeCommandFileAlreadyExistsException | TypeException | RuntimeException | FilesystemException $e) { $this->terminalError(string: sprintf('%s', $e->getMessage())); - } finally { return ConsoleCommand::FAILURE; } } From 659368abc947497ceb1326683f8c487090632dba Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 7 Oct 2025 23:03:41 -0700 Subject: [PATCH 115/161] Changes to how application and console loads. Signed-off-by: Joshua Parker --- composer.json | 1 + src/Application.php | 20 ++++++- src/Configuration/ApplicationBuilder.php | 74 ++++++++++++++++++++++++ src/Console/ConsoleCommand.php | 64 +++++++++++++++++++- src/Console/ConsoleKernel.php | 14 +++++ 5 files changed, 169 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 754e9e9..b4af781 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "php": ">=8.4", "ext-pdo": "*", "codefyphp/domain-driven-core": "^3.0.x-dev", + "composer/class-map-generator": "^1.0@dev", "dragonmantank/cron-expression": "^3", "melbahja/seo": "^2.1", "middlewares/cache": "^3.1", diff --git a/src/Application.php b/src/Application.php index 37d28cd..7d71929 100644 --- a/src/Application.php +++ b/src/Application.php @@ -45,6 +45,8 @@ use Qubus\Support\ArrayHelper; use Qubus\Support\StringHelper; use ReflectionException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; use function dirname; use function get_class; @@ -573,7 +575,7 @@ public function withAppPath(string $appPath): self */ public function path(): string { - return $this->appPath ?: $this->basePath . self::DS . 'app'; + return $this->appPath ?: $this->basePath . self::DS . 'App'; } /** @@ -803,6 +805,19 @@ public function handleRequest(?ServerRequestInterface $request = null): void $kernel->boot($request); } + /** + * Handle the incoming Artisan command. + * + * @param InputInterface $input + * @return int + */ + public function handleCommand(InputInterface $input): int + { + $kernel = $this->make(\Codefy\Framework\Contracts\Console\Kernel::class); + + return $kernel->handle($input, new ConsoleOutput()); + } + /** * Load environment file(s). * @@ -886,7 +901,8 @@ public function isDevelopment(): bool public static function create(array $config): ApplicationBuilder { return new ApplicationBuilder(new self($config)) - ->withKernels(); + ->withKernels() + ->withCommands(); } /** diff --git a/src/Configuration/ApplicationBuilder.php b/src/Configuration/ApplicationBuilder.php index 590c893..ec015da 100644 --- a/src/Configuration/ApplicationBuilder.php +++ b/src/Configuration/ApplicationBuilder.php @@ -7,14 +7,21 @@ use Closure; use Codefy\Framework\Application; use Codefy\Framework\Bootstrap\RegisterProviders; +use Codefy\Framework\Console\ConsoleCommand; +use Codefy\Framework\Contracts\Console\Kernel; use Codefy\Framework\Providers\RoutingServiceProvider; +use Codefy\Framework\Scheduler\Schedule; +use Composer\ClassMapGenerator\ClassMapGenerator; use Qubus\Exception\Data\TypeException; use Qubus\Routing\Route\RoutingRegistrar; use Qubus\Routing\Router; +use Qubus\Support\Collection\ArrayCollection; +use function array_merge; use function is_array; use function is_callable; use function is_string; +use function is_subclass_of; use function Qubus\Security\Helpers\__observer; use function Qubus\Support\Helpers\is_null__; @@ -140,6 +147,31 @@ public function withEncryptedEnv(bool $bool = false): self return $this; } + public function withCommands(array $commands = []): self + { + $commands = $this->registerConsoleCommands($commands); + + $this->app->prepare(Kernel::class, function ($kernel) use ($commands): void { + $commands = new ArrayCollection($commands) + ->filter(fn (string $item, string $key) => is_subclass_of($item, ConsoleCommand::class)); + + $this->app->booting(static function () use ($kernel, $commands): void { + $kernel->addCommands($commands->all()); + }); + }); + + return $this; + } + + public function withSchedule(callable $callback): self + { + $schedule = $this->app->make(name: Schedule::class); + + $callback($schedule); + + return $this; + } + /** * Create the routing callback for the application. * @@ -179,6 +211,35 @@ protected function buildRoutingCallback( }; } + /** + * Register all console commands in given directories/namespaces. + * + * @param array $directories keyed by namespace prefix + */ + public function registerConsoleCommands(array $directories): array + { + $commands = []; + + $directories = array_merge($directories, $this->defaultCommandDirectories()); + + foreach ($directories as $namespace => $directory) { + // Generate class map for the given directory + $classMap = ClassMapGenerator::createMap($directory); + + foreach ($classMap as $class => $path) { + // Ensure class belongs to the given namespace + if (str_starts_with($class, $namespace)) { + /*if (is_subclass_of($class, ConsoleCommand::class) && !new ReflectionClass($class)->isAbstract()) { + $this->app->make(name: $class); + }*/ + $commands[] = $class; + } + } + } + + return $commands; + } + /** * Register a callback to be invoked when the application's * service providers are registered. @@ -228,4 +289,17 @@ public function return(): Application { return $this->app; } + + protected function defaultCommandDirectories(): array + { + $ds = $this->app::DS; + + return [ + 'App\\Application\\Console\\Commands' => + $this->app->path() . $ds . 'Application' . $ds . 'Console' . $ds . 'Commands', + 'Codefy\\Framework\\Console\\Commands' => + $this->app->basePath() . $ds . 'vendor' . $ds . 'codefyphp' . $ds . 'codefy' . + $ds . 'src' . $ds . 'Console' . $ds . 'Commands', + ]; + } } diff --git a/src/Console/ConsoleCommand.php b/src/Console/ConsoleCommand.php index c228cc7..e36b9bd 100644 --- a/src/Console/ConsoleCommand.php +++ b/src/Console/ConsoleCommand.php @@ -14,7 +14,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; - +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use function count; @@ -188,7 +189,66 @@ protected function confirm(string $question): mixed { /** @var QuestionHelper $helper */ $helper = $this->getHelper(name: 'question'); - $question = new Question(question: $question); + $question = new ConfirmationQuestion(question: $question, default: false); + + return $helper->ask($this->input, $this->output, $question); + } + + /** + * @param string $question + * @param bool|float|int|string|null $default + * @return mixed + */ + protected function ask(string $question, bool|float|int|null|string $default = null): mixed + { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper(name: 'question'); + $question = new Question(question: $question, default: $default); + + return $helper->ask($this->input, $this->output, $question); + } + + /** + * @param string $question + * @param array $choices + * @param bool|float|int|string|null $default + * @param string|null $message + * @return mixed + */ + protected function choice( + string $question, + array $choices, + bool|float|int|null|string $default = null, + ?string $message = null + ): mixed { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper(name: 'question'); + $question = new ChoiceQuestion(question: $question, choices: $choices, default: $default); + + $question->setErrorMessage(errorMessage: $message ?? 'There is an error.'); + + return $helper->ask($this->input, $this->output, $question); + } + + /** + * @param string $question + * @param array $choices + * @param bool|float|int|string|null $default + * @param string|null $message + * @return mixed + */ + protected function multiChoice( + string $question, + array $choices, + bool|float|int|null|string $default = null, + ?string $message = null + ): mixed { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper(name: 'question'); + $question = new ChoiceQuestion(question: $question, choices: $choices, default: $default); + + $question->setMultiselect(multiselect: true); + $question->setErrorMessage(errorMessage: $message ?? 'There is an error.'); return $helper->ask($this->input, $this->output, $question); } diff --git a/src/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php index 8c1de20..48c96d4 100644 --- a/src/Console/ConsoleKernel.php +++ b/src/Console/ConsoleKernel.php @@ -105,6 +105,20 @@ public function registerCommand(callable|Command $command): void $this->getCodex()->addCommand(command: $command); } + /** + * Add an array of commands to the console. + * + * @param array $commands + * @return void + */ + public function addCommands(array $commands): void + { + foreach ($commands as $command) { + $command = $this->codefy->make(name: $command); + $this->registerCommand($command); + } + } + /** * {@inheritDoc} */ From 29289d45871442aa7eb1108d46a1cb7d47073f93 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 7 Oct 2025 23:48:41 -0700 Subject: [PATCH 116/161] Fix loading commands. Signed-off-by: Joshua Parker --- src/Configuration/ApplicationBuilder.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Configuration/ApplicationBuilder.php b/src/Configuration/ApplicationBuilder.php index ec015da..0e95f5c 100644 --- a/src/Configuration/ApplicationBuilder.php +++ b/src/Configuration/ApplicationBuilder.php @@ -16,6 +16,7 @@ use Qubus\Routing\Route\RoutingRegistrar; use Qubus\Routing\Router; use Qubus\Support\Collection\ArrayCollection; +use ReflectionClass; use function array_merge; use function is_array; @@ -147,13 +148,19 @@ public function withEncryptedEnv(bool $bool = false): self return $this; } + /** + * @param array $commands + * @return $this + */ public function withCommands(array $commands = []): self { $commands = $this->registerConsoleCommands($commands); $this->app->prepare(Kernel::class, function ($kernel) use ($commands): void { $commands = new ArrayCollection($commands) - ->filter(fn (string $item, string $key) => is_subclass_of($item, ConsoleCommand::class)); + ->filter(fn (string $item, int $key) => + is_subclass_of($item, ConsoleCommand::class) + && !new ReflectionClass($item)->isAbstract()); $this->app->booting(static function () use ($kernel, $commands): void { $kernel->addCommands($commands->all()); @@ -229,9 +236,6 @@ public function registerConsoleCommands(array $directories): array foreach ($classMap as $class => $path) { // Ensure class belongs to the given namespace if (str_starts_with($class, $namespace)) { - /*if (is_subclass_of($class, ConsoleCommand::class) && !new ReflectionClass($class)->isAbstract()) { - $this->app->make(name: $class); - }*/ $commands[] = $class; } } From 6dbfe571a7d303aefd1be06c6d818c6e919f5adf Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 8 Oct 2025 10:14:21 -0700 Subject: [PATCH 117/161] Automatic loading of commands and service providers with little human intervention. Signed-off-by: Joshua Parker --- src/Application.php | 27 +++---- src/Bootstrap/RegisterProviders.php | 34 +++++++- src/Configuration/ApplicationBuilder.php | 88 ++------------------- src/Console/ConsoleKernel.php | 25 +++++- src/Support/CodefyServiceProvider.php | 10 +++ src/Support/DefaultCommands.php | 35 ++++++++ src/Support/DefaultProviders.php | 23 ++++++ src/Support/Traits/CollectionStackAware.php | 74 +++++++++++++++++ 8 files changed, 217 insertions(+), 99 deletions(-) create mode 100644 src/Support/DefaultCommands.php create mode 100644 src/Support/DefaultProviders.php create mode 100644 src/Support/Traits/CollectionStackAware.php diff --git a/src/Application.php b/src/Application.php index 7d71929..0aaae9a 100644 --- a/src/Application.php +++ b/src/Application.php @@ -45,8 +45,6 @@ use Qubus\Support\ArrayHelper; use Qubus\Support\StringHelper; use ReflectionException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\ConsoleOutput; use function dirname; use function get_class; @@ -598,6 +596,16 @@ public function bootStrapPath(): string return $this->basePath . self::DS . 'bootstrap'; } + /** + * Get the path to the service provider list in the bootstrap directory. + * + * @return string + */ + public function getBootstrapProvidersPath(): string + { + return $this->bootstrapPath() . self::DS . 'providers.php'; + } + /** * Get the path to the application's "config" directory. * @@ -805,19 +813,6 @@ public function handleRequest(?ServerRequestInterface $request = null): void $kernel->boot($request); } - /** - * Handle the incoming Artisan command. - * - * @param InputInterface $input - * @return int - */ - public function handleCommand(InputInterface $input): int - { - $kernel = $this->make(\Codefy\Framework\Contracts\Console\Kernel::class); - - return $kernel->handle($input, new ConsoleOutput()); - } - /** * Load environment file(s). * @@ -902,7 +897,7 @@ public static function create(array $config): ApplicationBuilder { return new ApplicationBuilder(new self($config)) ->withKernels() - ->withCommands(); + ->withProviders(); } /** diff --git a/src/Bootstrap/RegisterProviders.php b/src/Bootstrap/RegisterProviders.php index b17f81c..0f2432f 100644 --- a/src/Bootstrap/RegisterProviders.php +++ b/src/Bootstrap/RegisterProviders.php @@ -5,6 +5,7 @@ namespace Codefy\Framework\Bootstrap; use Codefy\Framework\Application; +use Codefy\Framework\Support\CodefyServiceProvider; use Qubus\Config\ConfigContainer; use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; @@ -18,6 +19,13 @@ final class RegisterProviders { protected static array $merge = []; + /** + * The path to the bootstrap provider configuration file. + * + * @var string|null + */ + protected static ?string $bootstrapProviderPath = null; + /** * @throws TypeException * @throws Exception @@ -37,13 +45,30 @@ public function bootstrap(Application $app): void */ protected function mergeAdditionalProviders(Application $app): void { + if ( + self::$bootstrapProviderPath && + file_exists(self::$bootstrapProviderPath) + ) { + $packageProviders = require self::$bootstrapProviderPath; + + foreach ($packageProviders as $index => $provider) { + if (! class_exists($provider)) { + unset($packageProviders[$index]); + } + } + } + /** @var ConfigContainer $config */ $config = $app->make(name: 'codefy.config'); $arrayMerge = array_unique( array: array_merge( self::$merge, - $config->getConfigKey(key: 'app.providers'), + $config->getConfigKey( + key: 'app.providers', + default: CodefyServiceProvider::defaultProviders()->toArray() + ), + array_values($packageProviders ?? []), ) ); @@ -55,10 +80,13 @@ protected function mergeAdditionalProviders(Application $app): void * before registration. * * @param array $providers + * @param string|null $bootstrapProviderPath * @return void */ - public static function merge(array $providers): void + public static function merge(array $providers, ?string $bootstrapProviderPath = null): void { + self::$bootstrapProviderPath = $bootstrapProviderPath; + self::$merge = array_values( array: array_filter( array: array_unique( @@ -70,6 +98,8 @@ public static function merge(array $providers): void public static function flushState(): void { + self::$bootstrapProviderPath = null; + self::$merge = []; } } diff --git a/src/Configuration/ApplicationBuilder.php b/src/Configuration/ApplicationBuilder.php index 0e95f5c..e45f726 100644 --- a/src/Configuration/ApplicationBuilder.php +++ b/src/Configuration/ApplicationBuilder.php @@ -7,22 +7,14 @@ use Closure; use Codefy\Framework\Application; use Codefy\Framework\Bootstrap\RegisterProviders; -use Codefy\Framework\Console\ConsoleCommand; -use Codefy\Framework\Contracts\Console\Kernel; use Codefy\Framework\Providers\RoutingServiceProvider; -use Codefy\Framework\Scheduler\Schedule; -use Composer\ClassMapGenerator\ClassMapGenerator; use Qubus\Exception\Data\TypeException; use Qubus\Routing\Route\RoutingRegistrar; use Qubus\Routing\Router; -use Qubus\Support\Collection\ArrayCollection; -use ReflectionClass; -use function array_merge; use function is_array; use function is_callable; use function is_string; -use function is_subclass_of; use function Qubus\Security\Helpers\__observer; use function Qubus\Support\Helpers\is_null__; @@ -56,12 +48,18 @@ public function withKernels(): self * Register additional service providers. * * @param array $providers + * @param bool $withBootstrapProviders * @return $this * @throws TypeException */ - public function withProviders(array $providers = []): self + public function withProviders(array $providers = [], bool $withBootstrapProviders = true): self { - RegisterProviders::merge($providers); + RegisterProviders::merge( + providers: $providers, + bootstrapProviderPath: $withBootstrapProviders + ? $this->app->getBootstrapProvidersPath() + : null + ); foreach ($providers as $provider) { $this->app->registerServiceProvider($provider); @@ -148,37 +146,6 @@ public function withEncryptedEnv(bool $bool = false): self return $this; } - /** - * @param array $commands - * @return $this - */ - public function withCommands(array $commands = []): self - { - $commands = $this->registerConsoleCommands($commands); - - $this->app->prepare(Kernel::class, function ($kernel) use ($commands): void { - $commands = new ArrayCollection($commands) - ->filter(fn (string $item, int $key) => - is_subclass_of($item, ConsoleCommand::class) - && !new ReflectionClass($item)->isAbstract()); - - $this->app->booting(static function () use ($kernel, $commands): void { - $kernel->addCommands($commands->all()); - }); - }); - - return $this; - } - - public function withSchedule(callable $callback): self - { - $schedule = $this->app->make(name: Schedule::class); - - $callback($schedule); - - return $this; - } - /** * Create the routing callback for the application. * @@ -218,32 +185,6 @@ protected function buildRoutingCallback( }; } - /** - * Register all console commands in given directories/namespaces. - * - * @param array $directories keyed by namespace prefix - */ - public function registerConsoleCommands(array $directories): array - { - $commands = []; - - $directories = array_merge($directories, $this->defaultCommandDirectories()); - - foreach ($directories as $namespace => $directory) { - // Generate class map for the given directory - $classMap = ClassMapGenerator::createMap($directory); - - foreach ($classMap as $class => $path) { - // Ensure class belongs to the given namespace - if (str_starts_with($class, $namespace)) { - $commands[] = $class; - } - } - } - - return $commands; - } - /** * Register a callback to be invoked when the application's * service providers are registered. @@ -293,17 +234,4 @@ public function return(): Application { return $this->app; } - - protected function defaultCommandDirectories(): array - { - $ds = $this->app::DS; - - return [ - 'App\\Application\\Console\\Commands' => - $this->app->path() . $ds . 'Application' . $ds . 'Console' . $ds . 'Commands', - 'Codefy\\Framework\\Console\\Commands' => - $this->app->basePath() . $ds . 'vendor' . $ds . 'codefyphp' . $ds . 'codefy' . - $ds . 'src' . $ds . 'Console' . $ds . 'Commands', - ]; - } } diff --git a/src/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php index 48c96d4..65e1bc5 100644 --- a/src/Console/ConsoleKernel.php +++ b/src/Console/ConsoleKernel.php @@ -10,6 +10,7 @@ use Codefy\Framework\Factory\FileLoggerSmtpFactory; use Codefy\Framework\Scheduler\Mutex\Locker; use Codefy\Framework\Scheduler\Schedule; +use Codefy\Framework\Support\DefaultCommands; use Exception; use Psr\EventDispatcher\EventDispatcherInterface; use Qubus\Support\DateTime\QubusDateTimeZone; @@ -19,6 +20,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; +use function array_unique; use function Qubus\Inheritance\Helpers\tap; class ConsoleKernel implements Kernel @@ -114,7 +116,11 @@ public function registerCommand(callable|Command $command): void public function addCommands(array $commands): void { foreach ($commands as $command) { - $command = $this->codefy->make(name: $command); + if (is_callable($command)) { + $command = $command($this->codefy); + } else { + $command = $this->codefy->make(name: $command); + } $this->registerCommand($command); } } @@ -169,6 +175,23 @@ protected function getCodex(): Codex return $this->codex; } + /** + * @param array $commands + * @return void + */ + protected function load(array $commands = []): void + { + $commands = array_unique( + array_merge( + $this->commands, + $this->codefy->make(name: 'codefy.config')->getConfigKey('app.commands'), + new DefaultCommands($commands)->toArray() + ) + ); + + $this->addCommands($commands); + } + /** * {@inheritDoc} * diff --git a/src/Support/CodefyServiceProvider.php b/src/Support/CodefyServiceProvider.php index 5e15ba6..ac01806 100644 --- a/src/Support/CodefyServiceProvider.php +++ b/src/Support/CodefyServiceProvider.php @@ -81,4 +81,14 @@ public function callBootedCallbacks(): void $index++; } } + + /** + * Get the default providers for a CodefyPHP application. + * + * @return DefaultProviders + */ + public static function defaultProviders(): DefaultProviders + { + return new DefaultProviders(); + } } diff --git a/src/Support/DefaultCommands.php b/src/Support/DefaultCommands.php new file mode 100644 index 0000000..0f0b53e --- /dev/null +++ b/src/Support/DefaultCommands.php @@ -0,0 +1,35 @@ +collection = $collection ?: [ + \Codefy\Framework\Console\Commands\MakeCommand::class, + \Codefy\Framework\Console\Commands\ScheduleRunCommand::class, + \Codefy\Framework\Console\Commands\PasswordHashCommand::class, + \Codefy\Framework\Console\Commands\InitCommand::class, + \Codefy\Framework\Console\Commands\StatusCommand::class, + \Codefy\Framework\Console\Commands\CheckCommand::class, + \Codefy\Framework\Console\Commands\MigrateGenerateCommand::class, + \Codefy\Framework\Console\Commands\UpCommand::class, + \Codefy\Framework\Console\Commands\DownCommand::class, + \Codefy\Framework\Console\Commands\MigrateCommand::class, + \Codefy\Framework\Console\Commands\RollbackCommand::class, + \Codefy\Framework\Console\Commands\RedoCommand::class, + \Codefy\Framework\Console\Commands\ListCommand::class, + \Codefy\Framework\Console\Commands\ServeCommand::class, + \Codefy\Framework\Console\Commands\UuidCommand::class, + \Codefy\Framework\Console\Commands\UlidCommand::class, + \Codefy\Framework\Console\Commands\FlushPipelineCommand::class, + ]; + } +} diff --git a/src/Support/DefaultProviders.php b/src/Support/DefaultProviders.php new file mode 100644 index 0000000..a59c3fd --- /dev/null +++ b/src/Support/DefaultProviders.php @@ -0,0 +1,23 @@ +collection = $collection ?: [ + \Codefy\Framework\Providers\AssetsServiceProvider::class, + \Codefy\Framework\Providers\EventDispatcherServiceProvider::class, + \Codefy\Framework\Providers\DatabaseConnectionServiceProvider::class, + \Codefy\Framework\Providers\PdoServiceProvider::class, + \Codefy\Framework\Providers\LocalizationServiceProvider::class, + ]; + } +} diff --git a/src/Support/Traits/CollectionStackAware.php b/src/Support/Traits/CollectionStackAware.php new file mode 100644 index 0000000..61ee61d --- /dev/null +++ b/src/Support/Traits/CollectionStackAware.php @@ -0,0 +1,74 @@ +collection = array_merge($this->collection, $collection); + + return new static($this->collection); + } + + /** + * Replace the given collection with other collections. + * + * @param array $replacements + * @return static + */ + public function replace(array $replacements): static + { + $current = new Collection($this->collection); + + foreach ($replacements as $from => $to) { + $key = $current->search($from); + + $current = is_int($key) ? $current->replace([$key => $to]) : $current; + } + + return new static($current->values()->toArray()); + } + + /** + * Disable the given collection. + * + * @param array $collection + * @return static + */ + public function except(array $collection): static + { + return new static( + new Collection($this->collection) + ->reject(fn ($p) => in_array($p, $collection)) + ->values() + ->toArray() + ); + } + + /** + * Convert the collection to an array. + * + * @return array + */ + public function toArray(): array + { + return $this->collection; + } +} From f447267a4bf3c6445b2ae63aab757f818bb837e6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 8 Oct 2025 21:44:22 -0700 Subject: [PATCH 118/161] Added new translation functions. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index ee78158..ddbcdf2 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -21,7 +21,6 @@ use Codefy\QueryBus\Resolvers\NativeQueryHandlerResolver; use Codefy\QueryBus\UnresolvableQueryHandlerException; use Qubus\Config\Collection; -use Qubus\Exception\Data\TypeException; use Qubus\Exception\Exception; use Qubus\Expressive\QueryBuilder; use ReflectionException; @@ -30,6 +29,8 @@ use function getcwd; use function is_int; use function Qubus\Security\Helpers\__observer; +use function Qubus\Security\Helpers\esc_attr__; +use function Qubus\Security\Helpers\esc_html__; use function Qubus\Security\Helpers\t__; use function Qubus\Support\Helpers\is_false__; use function Qubus\Support\Helpers\is_null__; @@ -215,7 +216,6 @@ function mail(string|array $to, string $subject, string $message, array $headers * * @param Command $command * @throws ReflectionException - * @throws TypeException * @throws CommandCouldNotBeHandledException * @throws UnresolvableCommandHandlerException */ @@ -234,7 +234,6 @@ function command(Command $command): void * a result if any. * * @throws ReflectionException - * @throws TypeException * @throws UnresolvableQueryHandlerException */ function ask(Query $query): mixed @@ -257,3 +256,23 @@ function trans(string $string): string { return t__(msgid: $string, domain: config(key: 'app.locale_domain', default: 'codefy')); } + +/** + * Escapes a translated string to make it safe for HTML output. + * + * @throws Exception + */ +function trans_html(string $string): string +{ + return esc_html__(string: $string, domain: config(key: 'app.locale_domain', default: 'codefy')); +} + +/** + * Escapes a translated string to make it safe for HTML attribute. + * + * @throws Exception + */ +function trans_attr(string $string): string +{ + return esc_attr__(string: $string, domain: config(key: 'app.locale_domain', default: 'codefy')); +} From 2741426cc0912af3d712ee24e61c26ea8b25433f Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 8 Oct 2025 21:44:52 -0700 Subject: [PATCH 119/161] Added Whoops middleware. Signed-off-by: Joshua Parker --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index b4af781..e023111 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "middlewares/cache": "^3.1", "middlewares/minifier": "^2.1", "middlewares/referrer-spam": "^2.1", + "middlewares/whoops": "dev-master", "paragonie/csp-builder": "^3", "php-debugbar/php-debugbar": "^2.2", "qubus/cache": "^4", From 4a2b14fb339d03fef3e7aead991a6adc25075cf3 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 8 Oct 2025 21:46:00 -0700 Subject: [PATCH 120/161] Added new terminal methods. Signed-off-by: Joshua Parker --- src/Console/ConsoleCommand.php | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Console/ConsoleCommand.php b/src/Console/ConsoleCommand.php index e36b9bd..f3e661d 100644 --- a/src/Console/ConsoleCommand.php +++ b/src/Console/ConsoleCommand.php @@ -20,6 +20,9 @@ use function count; use function method_exists; +use function str_repeat; + +use const PHP_EOL; abstract class ConsoleCommand extends SymfonyCommand { @@ -106,55 +109,60 @@ protected function getOptions(?string $key = null): mixed * Outputs the string to the console without any tag. * * @param string $string - * @return mixed */ - protected function terminalRaw(string $string): mixed + protected function terminalRaw(string $string): void { - return $this->output->writeln(messages: $string); + $this->output->writeln(messages: $string); } /** * Output to the terminal wrap in info tags. * * @param string $string - * @return string */ - protected function terminalInfo(string $string): mixed + protected function terminalInfo(string $string): void { - return $this->output->writeln(messages: '' . $string . ''); + $this->output->writeln(messages: '' . $string . ''); } /** * Output to the terminal wrap in comment tags. * * @param string $string - * @return string */ - protected function terminalComment(string $string): mixed + protected function terminalComment(string $string): void { - return $this->output->writeln(messages: '' . $string . ''); + $this->output->writeln(messages: '' . $string . ''); } /** * Output to the terminal wrap in question tags. * * @param string $string - * @return string */ - protected function terminalQuestion(string $string): mixed + protected function terminalQuestion(string $string): void { - return $this->output->writeln(messages: '' . $string . ''); + $this->output->writeln(messages: '' . $string . ''); } /** * Output to the terminal wrap in error tags. * * @param string $string - * @return string */ - protected function terminalError(string $string): mixed + protected function terminalError(string $string): void + { + $this->output->writeln(messages: '' . $string . ''); + } + + /** + * Output to the terminal with a blank line. + * + * @param int $count + */ + protected function terminalNewLine(int $count = 1): void { - return $this->output->writeln(messages: '' . $string . ''); + $this->output->write(str_repeat(PHP_EOL, $count)); } /** From c40535bc8e04060ae1ddf383d96d976ba196490a Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 8 Oct 2025 21:46:40 -0700 Subject: [PATCH 121/161] Removed unused import. Signed-off-by: Joshua Parker --- src/Providers/DatabaseConnectionServiceProvider.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Providers/DatabaseConnectionServiceProvider.php b/src/Providers/DatabaseConnectionServiceProvider.php index a1a056a..991ec81 100644 --- a/src/Providers/DatabaseConnectionServiceProvider.php +++ b/src/Providers/DatabaseConnectionServiceProvider.php @@ -7,8 +7,6 @@ use Codefy\Framework\Support\CodefyServiceProvider; use Qubus\Expressive\Connection; -use function sprintf; - final class DatabaseConnectionServiceProvider extends CodefyServiceProvider { public function register(): void From 0b444293d7a667dd9444b6f302c9481dc2a01109 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Wed, 8 Oct 2025 21:46:59 -0700 Subject: [PATCH 122/161] Attempt to fix issue with login. Signed-off-by: Joshua Parker --- src/Auth/Rbac/Resource/FileResource.php | 26 ++++++++++++------------- src/Auth/Repository/PdoRepository.php | 4 ++-- src/Auth/UserSession.php | 6 ++++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Auth/Rbac/Resource/FileResource.php b/src/Auth/Rbac/Resource/FileResource.php index 2819f68..edfe93c 100644 --- a/src/Auth/Rbac/Resource/FileResource.php +++ b/src/Auth/Rbac/Resource/FileResource.php @@ -49,14 +49,14 @@ public function load(): void public function save(): void { $data = [ - 'roles' => [], - 'permissions' => [], + 'roles' => [], + 'permissions' => [], ]; foreach ($this->roles as $role) { - $data['roles'][$role->getName()] = $this->roleToRow($role); + $data['roles'][$role->name] = $this->roleToRow($role); } foreach ($this->permissions as $permission) { - $data['permissions'][$permission->getName()] = $this->permissionToRow($permission); + $data['permissions'][$permission->name] = $this->permissionToRow($permission); } LocalStorage::disk()->write($this->file, json_encode(value: $data, flags: JSON_PRETTY_PRINT)); @@ -65,16 +65,16 @@ public function save(): void protected function roleToRow(Role $role): array { $result = []; - $result['name'] = $role->getName(); - $result['description'] = $role->getDescription(); + $result['name'] = $role->name; + $result['description'] = $role->description; $childrenNames = []; foreach ($role->getChildren() as $child) { - $childrenNames[] = $child->getName(); + $childrenNames[] = $child->name; } $result['children'] = $childrenNames; $permissionNames = []; foreach ($role->getPermissions() as $permission) { - $permissionNames[] = $permission->getName(); + $permissionNames[] = $permission->name; } $result['permissions'] = $permissionNames; return $result; @@ -83,11 +83,11 @@ protected function roleToRow(Role $role): array protected function permissionToRow(Permission $permission): array { $result = []; - $result['name'] = $permission->getName(); - $result['description'] = $permission->getDescription(); + $result['name'] = $permission->name; + $result['description'] = $permission->description; $childrenNames = []; foreach ($permission->getChildren() as $child) { - $childrenNames[] = $child->getName(); + $childrenNames[] = $child->name; } $result['children'] = $childrenNames; $result['ruleClass'] = $permission->getRuleClass(); @@ -105,7 +105,7 @@ protected function restorePermissions(array $permissionsData): void foreach ($permissionsData as $pData) { $permission = $this->addPermission($pData['name'] ?? '', $pData['description'] ?? ''); $permission->setRuleClass($pData['ruleClass'] ?? ''); - $permChildrenNames[$permission->getName()] = $pData['children'] ?? []; + $permChildrenNames[$permission->name] = $pData['children'] ?? []; } foreach ($permChildrenNames as $permissionName => $childrenNames) { @@ -129,7 +129,7 @@ protected function restoreRoles($rolesData): void foreach ($rolesData as $rData) { $role = $this->addRole($rData['name'] ?? '', $rData['description'] ?? ''); - $rolesChildrenNames[$role->getName()] = $rData['children'] ?? []; + $rolesChildrenNames[$role->name] = $rData['children'] ?? []; $permissionNames = $rData['permissions'] ?? []; foreach ($permissionNames as $permissionName) { if ($permission = $this->getPermission($permissionName)) { diff --git a/src/Auth/Repository/PdoRepository.php b/src/Auth/Repository/PdoRepository.php index 2531f49..df8d0bd 100644 --- a/src/Auth/Repository/PdoRepository.php +++ b/src/Auth/Repository/PdoRepository.php @@ -5,9 +5,9 @@ namespace Codefy\Framework\Auth\Repository; use Codefy\Framework\Support\Password; -use Opis\Database\Connection; use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; +use Qubus\Expressive\Connection; use Qubus\Http\Session\SessionEntity; use SensitiveParameter; @@ -33,7 +33,7 @@ public function authenticate(string $credential, #[SensitiveParameter] ?string $ $fields['identity'] ); - $stmt = $this->connection->getPDO()->prepare(query: $sql); + $stmt = $this->connection->pdo->prepare($sql); if (false === $stmt) { return null; } diff --git a/src/Auth/UserSession.php b/src/Auth/UserSession.php index 8efdd8b..3ad6cc1 100644 --- a/src/Auth/UserSession.php +++ b/src/Auth/UserSession.php @@ -12,8 +12,10 @@ class UserSession implements SessionEntity public function withToken(?string $token = null): self { - $this->token = $token; - return $this; + $new = clone $this; + $new->token = $token; + + return $new; } public function clear(): void From 3a62d00cc5aa43c2d377510f385e07f13b6696e4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 06:16:40 -0700 Subject: [PATCH 123/161] Fix UserSession entity. Signed-off-by: Joshua Parker --- src/Auth/UserSession.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Auth/UserSession.php b/src/Auth/UserSession.php index 3ad6cc1..40246f6 100644 --- a/src/Auth/UserSession.php +++ b/src/Auth/UserSession.php @@ -12,10 +12,9 @@ class UserSession implements SessionEntity public function withToken(?string $token = null): self { - $new = clone $this; - $new->token = $token; + $this->token = $token; - return $new; + return $this; } public function clear(): void From c620e75b419871d097a0a5ededd86706eb907916 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 07:18:30 -0700 Subject: [PATCH 124/161] Added Server class. Signed-off-by: Joshua Parker --- src/Support/Server.php | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/Support/Server.php diff --git a/src/Support/Server.php b/src/Support/Server.php new file mode 100644 index 0000000..7d11ad0 --- /dev/null +++ b/src/Support/Server.php @@ -0,0 +1,71 @@ +getHost(); + $url = add_trailing_slash($url); + + $url = concat_ws(string1: $url, string2: $path, separator: ''); + + return esc_url(url: $url); + } +} From 527ac27477e9da2c46b9bf41ba6205b3906ddcc4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 07:20:56 -0700 Subject: [PATCH 125/161] Added site_url function. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index ddbcdf2..89b9d4d 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -15,6 +15,7 @@ use Codefy\Framework\Proxy\Codefy; use Codefy\Framework\Factory\FileLoggerFactory; use Codefy\Framework\Support\CodefyMailer; +use Codefy\Framework\Support\Server; use Codefy\QueryBus\Busses\SynchronousQueryBus; use Codefy\QueryBus\Enquire; use Codefy\QueryBus\Query; @@ -276,3 +277,13 @@ function trans_attr(string $string): string { return esc_attr__(string: $string, domain: config(key: 'app.locale_domain', default: 'codefy')); } + +/** + * Returns the url of the application. + * + * @throws Exception + */ +function site_url(string $path = ''): string +{ + return Server::siteUrl($path); +} From fbda96ca0719f46eeb389d5abd8abd1d4e5b0011 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 07:26:02 -0700 Subject: [PATCH 126/161] Catch exception and then log it. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 89b9d4d..d53bc9a 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -27,6 +27,7 @@ use ReflectionException; use function dirname; +use function error_log; use function getcwd; use function is_int; use function Qubus\Security\Helpers\__observer; @@ -280,10 +281,14 @@ function trans_attr(string $string): string /** * Returns the url of the application. - * - * @throws Exception */ function site_url(string $path = ''): string { - return Server::siteUrl($path); + try { + return Server::siteUrl($path); + } catch (Exception $e) { + error_log($e->getMessage()); + } + + return ''; } From 433b78dc91e684e55f325e9e4630a7dbd00dbfec Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 07:44:03 -0700 Subject: [PATCH 127/161] Added normalization of url. Signed-off-by: Joshua Parker --- src/Support/Server.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Support/Server.php b/src/Support/Server.php index 7d11ad0..e76b801 100644 --- a/src/Support/Server.php +++ b/src/Support/Server.php @@ -66,6 +66,9 @@ public static function siteUrl(string $path = ''): string $url = concat_ws(string1: $url, string2: $path, separator: ''); - return esc_url(url: $url); + // Replace multiple slashes with a single slash, but not after the protocol (http:// or https://) + $normalizedUrl = preg_replace(pattern: '#(? Date: Thu, 9 Oct 2025 07:54:24 -0700 Subject: [PATCH 128/161] Added normalize_url function. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 113 ++++++++++++++++++++++++++++++++++++++++- src/Support/Server.php | 5 +- 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index d53bc9a..27e127d 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -28,8 +28,12 @@ use function dirname; use function error_log; +use function filter_var; use function getcwd; use function is_int; +use function parse_url; +use function preg_match; +use function preg_replace; use function Qubus\Security\Helpers\__observer; use function Qubus\Security\Helpers\esc_attr__; use function Qubus\Security\Helpers\esc_html__; @@ -41,9 +45,16 @@ use function is_string; use function realpath; use function sprintf; +use function str_contains; +use function str_starts_with; +use function strlen; +use function substr; use function substr_count; use function ucfirst; +use const FILTER_FLAG_IPV6; +use const FILTER_VALIDATE_IP; + /** * Get the available container instance. * @@ -248,6 +259,106 @@ function ask(Query $query): mixed return $enquirer->execute($query); } +/** + * Normalize a URL by collapsing multiple consecutive slashes into one, + * but preserve the scheme's "://" (for http/https) and do not touch query or fragment. + * + * Examples: + * normalize_url('http://example.com//foo///bar') => 'http://example.com/foo/bar' + * normalize_url('//example.com//a') => '//example.com/a' + * normalize_url('/some//relative//path') => '/some/relative/path' + * + * @param string $url + * @return string + */ +function normalize_url(string $url): string +{ + $original = $url; + $parts = parse_url($url); + + // If parse_url fails, fall back to simple regex while protecting scheme. + if ($parts === false) { + if (preg_match('#^(https?://)#i', $url, $m)) { + $prefix = $m[1]; + $rest = substr($url, strlen($prefix)); + $rest = preg_replace(pattern: '#/+#', replacement: '/', subject: $rest); + return $prefix . $rest; + } + + if (str_starts_with($url, '//')) { + return '//' . preg_replace(pattern: '#/+#', replacement: '/', subject: substr(string: $url, offset: 2)); + } + + return preg_replace(pattern: '#/+#', replacement: '/', subject: $url); + } + + $scheme = $parts['scheme'] ?? null; + $user = $parts['user'] ?? null; + $pass = $parts['pass'] ?? null; + $host = $parts['host'] ?? null; + $port = $parts['port'] ?? null; + $path = $parts['path'] ?? ''; + $query = $parts['query'] ?? null; + $fragment = $parts['fragment'] ?? null; + + // Collapse multiple slashes in the path only. + // This preserves a single leading slash (if any) and turns '///a//b' => '/a/b'. + $path = preg_replace(pattern: '#/+#', replacement: '/', subject: $path); + + // Rebuild authority (user[:pass]@host[:port]) + $authority = ''; + if ($host !== null) { + if ($user !== null) { + $authority .= $user; + if ($pass !== null) { + $authority .= ':' . $pass; + } + $authority .= '@'; + } + + // For IPv6 host strings we ensure brackets when reconstructing authority. + $hostOut = $host; + if ( + str_contains($hostOut, ':') + && $hostOut[0] !== '[' + && filter_var($hostOut, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) + ) { + $hostOut = '[' . $hostOut . ']'; + } + + $authority .= $hostOut; + if ($port !== null) { + $authority .= ':' . $port; + } + } + + $result = ''; + + if ($scheme !== null) { + // Preserve scheme and "://" + $result .= $scheme . '://'; + $result .= $authority; + } elseif ($host !== null) { + // protocol-relative or host-without-scheme: preserve leading // + $result .= '//' . $authority; + } elseif (str_starts_with($original, '//')) { + // preserve protocol-relative leading // + $result .= '//'; + } + + $result .= $path; + + if ($query !== null) { + $result .= '?' . $query; + } + + if ($fragment !== null) { + $result .= '#' . $fragment; + } + + return $result; +} + /** * Displays the returned translated text. * @@ -285,7 +396,7 @@ function trans_attr(string $string): string function site_url(string $path = ''): string { try { - return Server::siteUrl($path); + return normalize_url(Server::siteUrl($path)); } catch (Exception $e) { error_log($e->getMessage()); } diff --git a/src/Support/Server.php b/src/Support/Server.php index e76b801..7d11ad0 100644 --- a/src/Support/Server.php +++ b/src/Support/Server.php @@ -66,9 +66,6 @@ public static function siteUrl(string $path = ''): string $url = concat_ws(string1: $url, string2: $path, separator: ''); - // Replace multiple slashes with a single slash, but not after the protocol (http:// or https://) - $normalizedUrl = preg_replace(pattern: '#(? Date: Thu, 9 Oct 2025 10:31:02 -0700 Subject: [PATCH 129/161] Fix issue with unauthorized method not returning correct ResponseInterface. Signed-off-by: Joshua Parker --- src/Auth/Auth.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Auth/Auth.php b/src/Auth/Auth.php index 0e89211..17b4a08 100644 --- a/src/Auth/Auth.php +++ b/src/Auth/Auth.php @@ -11,6 +11,8 @@ use Psr\Http\Message\ServerRequestInterface; use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; +use Qubus\Http\Factories\JsonResponseFactory; +use Qubus\Http\Factories\RedirectResponseFactory; use Qubus\Http\Session\SessionEntity; class Auth implements Sentinel @@ -49,18 +51,15 @@ public function authenticate(ServerRequestInterface $request): ?SessionEntity } /** - * @throws Exception + * @throws \Exception */ public function unauthorized(ServerRequestInterface $request): ResponseInterface { - return $this->responseFactory - ->createResponse(code: 302) - ->withHeader( - name: 'Location', - value: $this->configContainer->getConfigKey( - key: 'auth.http_redirect', - default: $this->configContainer->getConfigKey(key: 'auth.login_url') - ) - ); + $location = $this->configContainer->getConfigKey(key: 'auth.http_redirect'); + if (!empty($location)) { + return RedirectResponseFactory::create($location); + } + + return JsonResponseFactory::create(data: 'Invalid credentials.', status: 403); } } From 6c59863a3df0482f738d1e4606216d9407c353f1 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 11:49:17 -0700 Subject: [PATCH 130/161] Few auth fixes. Signed-off-by: Joshua Parker --- src/Auth/Repository/PdoRepository.php | 17 ++--------------- src/Auth/UserSession.php | 2 +- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Auth/Repository/PdoRepository.php b/src/Auth/Repository/PdoRepository.php index df8d0bd..15a5ccb 100644 --- a/src/Auth/Repository/PdoRepository.php +++ b/src/Auth/Repository/PdoRepository.php @@ -4,6 +4,7 @@ namespace Codefy\Framework\Auth\Repository; +use Codefy\Framework\Auth\UserSession; use Codefy\Framework\Support\Password; use Qubus\Config\ConfigContainer; use Qubus\Exception\Exception; @@ -49,21 +50,7 @@ public function authenticate(string $credential, #[SensitiveParameter] ?string $ $passwordHash = (string) ($result->{$fields['password']} ?? ''); if (Password::verify(password: $password ?? '', hash: $passwordHash)) { - $user = new class () implements SessionEntity { - public ?string $token = null; - - public function withToken(?string $token = null): self - { - $this->token = $token; - return $this; - } - - public function isEmpty(): bool - { - return !empty($this->token); - } - }; - + $user = new UserSession(); $user ->withToken($result->token); diff --git a/src/Auth/UserSession.php b/src/Auth/UserSession.php index 40246f6..3c94c8e 100644 --- a/src/Auth/UserSession.php +++ b/src/Auth/UserSession.php @@ -8,7 +8,7 @@ class UserSession implements SessionEntity { - public private(set) ?string $token = null; + public protected(set) ?string $token = null; public function withToken(?string $token = null): self { From ddf6d8bbfa7058054b1603cf2f65f4433ca19a6f Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 11:50:05 -0700 Subject: [PATCH 131/161] Bumped version value. Signed-off-by: Joshua Parker --- src/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 0aaae9a..5eab592 100644 --- a/src/Application.php +++ b/src/Application.php @@ -61,7 +61,7 @@ final class Application extends Container use InvokerAware; use LoggerAware; - public const string APP_VERSION = '3.0.0-beta.5'; + public const string APP_VERSION = '3.0.0-rc.1'; public const string MIN_PHP_VERSION = '8.4'; From 7728ddc7e2f07488e715ff3c6cbe9e5fa830f00a Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Thu, 9 Oct 2025 14:16:02 -0700 Subject: [PATCH 132/161] Updated http message. Signed-off-by: Joshua Parker --- src/Auth/Auth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth/Auth.php b/src/Auth/Auth.php index 17b4a08..1e560e9 100644 --- a/src/Auth/Auth.php +++ b/src/Auth/Auth.php @@ -60,6 +60,6 @@ public function unauthorized(ServerRequestInterface $request): ResponseInterface return RedirectResponseFactory::create($location); } - return JsonResponseFactory::create(data: 'Invalid credentials.', status: 403); + return JsonResponseFactory::create(data: 'Forbidden.', status: 403); } } From ed7e6372df740f960b78566669550be1788278e0 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 12:15:34 -0700 Subject: [PATCH 133/161] Removed translation function. Signed-off-by: Joshua Parker --- src/Http/Middleware/ApiMiddleware.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Http/Middleware/ApiMiddleware.php b/src/Http/Middleware/ApiMiddleware.php index 8031e29..6fbad3e 100644 --- a/src/Http/Middleware/ApiMiddleware.php +++ b/src/Http/Middleware/ApiMiddleware.php @@ -13,7 +13,6 @@ use Qubus\Exception\Exception; use Qubus\Http\Factories\JsonResponseFactory; -use function Qubus\Security\Helpers\t__; use function sprintf; class ApiMiddleware implements MiddlewareInterface @@ -41,9 +40,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } - return JsonResponseFactory::create( - data: t__(msgid: 'Unauthorized.', domain: 'codefy'), - status: 401 - ); + return JsonResponseFactory::create(data: 'Unauthorized.', status: 401); } } From eb4d74be08d4f839c8b255ac2f4cde7eed4ff721 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 12:16:11 -0700 Subject: [PATCH 134/161] Updated stability. Signed-off-by: Joshua Parker --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e023111..18cc22e 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "qubus/error": "^3", "qubus/event-dispatcher": "^4", "qubus/exception": "^4", - "qubus/expressive": "^3.x-dev", + "qubus/expressive": "^3", "qubus/filesystem": "^4", "qubus/inheritance": "^4", "qubus/mail": "^5", From cfa5032f9536d7a57c21404cdee4d00425d7ee62 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 12:16:46 -0700 Subject: [PATCH 135/161] Middleware more configurable. Signed-off-by: Joshua Parker --- .../Auth/ExpireUserSessionMiddleware.php | 9 +++++---- .../Auth/UserAuthorizationMiddleware.php | 15 ++++++++++----- .../Middleware/Auth/UserSessionMiddleware.php | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php b/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php index d99bf82..ecc0fac 100644 --- a/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php +++ b/src/Http/Middleware/Auth/ExpireUserSessionMiddleware.php @@ -10,6 +10,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Qubus\Config\ConfigContainer; use Qubus\Exception\Data\TypeException; use Qubus\Http\Cookies\CookiesResponse; use Qubus\Http\Session\SessionService; @@ -18,7 +19,7 @@ class ExpireUserSessionMiddleware implements MiddlewareInterface { public const string SESSION_ATTRIBUTE = 'EXPIRE_USERSESSION'; - public function __construct(protected SessionService $sessionService) + public function __construct(protected ConfigContainer $configContainer, protected SessionService $sessionService) { } @@ -30,8 +31,8 @@ public function __construct(protected SessionService $sessionService) public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $this->sessionService::$options = [ - 'cookie-name' => 'USERSESSID', - 'cookie-lifetime' => 0, + 'cookie-name' => $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'), + 'cookie-lifetime' => 0, ]; $session = $this->sessionService->makeSession($request); @@ -47,7 +48,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $response = CookiesResponse::set( response: $response, setCookieCollection: $this->sessionService->cookie->make( - name: 'USERSESSID', + name: $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'), value: '', maxAge: 0 ) diff --git a/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php b/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php index 5bbc72e..9f1eddd 100644 --- a/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php +++ b/src/Http/Middleware/Auth/UserAuthorizationMiddleware.php @@ -11,16 +11,19 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Qubus\Config\ConfigContainer; use Qubus\Exception\Data\TypeException; +use Qubus\Http\Factories\RedirectResponseFactory; use Qubus\Http\Session\SessionService; -use function Qubus\Support\Helpers\is_true__; +use function Qubus\Support\Helpers\is_false__; class UserAuthorizationMiddleware implements MiddlewareInterface { public const string HEADER_HTTP_STATUS_CODE = 'AUTH_STATUS_CODE'; public function __construct( + protected ConfigContainer $configContainer, protected SessionService $sessionService, protected ResponseFactoryInterface $responseFactory ) { @@ -33,11 +36,13 @@ public function __construct( */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (is_true__($this->isLoggedIn($request))) { - return $handler->handle($request->withHeader(self::HEADER_HTTP_STATUS_CODE, 'ok')); + if (is_false__($this->isLoggedIn($request))) { + return RedirectResponseFactory::create( + $this->configContainer->getConfigKey(key: 'auth.redirect_guests_to') + )->withAddedHeader(self::HEADER_HTTP_STATUS_CODE, 'not_authorized'); } - return $handler->handle($request->withHeader(self::HEADER_HTTP_STATUS_CODE, 'not_authorized')); + return $handler->handle($request); } /** @@ -47,7 +52,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface private function isLoggedIn(ServerRequestInterface $request): bool { $this->sessionService::$options = [ - 'cookie-name' => 'USERSESSID', + 'cookie-name' => $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'), ]; $session = $this->sessionService->makeSession($request); diff --git a/src/Http/Middleware/Auth/UserSessionMiddleware.php b/src/Http/Middleware/Auth/UserSessionMiddleware.php index 2b70e57..4e5de24 100644 --- a/src/Http/Middleware/Auth/UserSessionMiddleware.php +++ b/src/Http/Middleware/Auth/UserSessionMiddleware.php @@ -34,7 +34,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface : $this->configContainer->getConfigKey(key: 'cookies.lifetime'); $this->sessionService::$options = [ - 'cookie-name' => 'USERSESSID', + 'cookie-name' => $this->configContainer->getConfigKey(key: 'auth.cookie_name', default: 'USERSESSID'), 'cookie-lifetime' => (int) $expire, ]; $session = $this->sessionService->makeSession($request); From 889dbb65c958aedd1386c32fe40ee86609467d55 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 12:17:49 -0700 Subject: [PATCH 136/161] Fixed issue with marking providers as registered. Signed-off-by: Joshua Parker --- src/Application.php | 40 +++++++++++++++------------ src/Bootstrap/RegisterProviders.php | 4 +++ src/Support/CodefyServiceProvider.php | 6 ++++ 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/Application.php b/src/Application.php index 5eab592..0f61b7e 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,6 +4,7 @@ namespace Codefy\Framework; +use Codefy\Framework\Bootstrap\RegisterProviders; use Codefy\Framework\Configuration\ApplicationBuilder; use Codefy\Framework\Contracts\Http\Kernel; use Codefy\Framework\Factory\FileLoggerFactory; @@ -46,6 +47,8 @@ use Qubus\Support\StringHelper; use ReflectionException; +use function array_merge; +use function array_unique; use function dirname; use function get_class; use function is_string; @@ -336,7 +339,8 @@ public function registerConfiguredServiceProviders(): void /** @var ConfigContainer $config */ $config = $this->make(name: 'codefy.config'); - $providers = $config->getConfigKey(key: 'app.providers'); + $appProviders = $config->getConfigKey(key: 'app.providers'); + $providers = array_unique(array_merge($appProviders, RegisterProviders::$providers)); foreach ($providers as $serviceProvider) { $this->registerServiceProvider(serviceProvider: $serviceProvider); @@ -368,7 +372,7 @@ public function registerServiceProvider( $serviceProvider->register(); - $this->markServiceProviderAsRegistered(registered: $registered, serviceProvider: $serviceProvider); + $this->markServiceProviderAsRegistered(serviceProvider: $serviceProvider); // If application is booted, call the boot method on the service provider // if it exists. if ($this->booted) { @@ -381,12 +385,14 @@ public function registerServiceProvider( /** * Get the registered service provider instance if it exists. * - * @param Serviceable|Bootable|string $provider - * @return string|null + * @param Serviceable|Bootable|string $provider + * @return Serviceable|Bootable|null */ - public function getRegisteredServiceProvider(Serviceable|Bootable|string $provider): string|null + public function getRegisteredServiceProvider(Serviceable|Bootable|string $provider): Serviceable|Bootable|null { - return $this->serviceProviders[$provider] ?? null; + $registered = is_string(value: $provider) ? $provider : get_class(object: $provider); + + return $this->serviceProviders[$registered] ?? null; } /** @@ -416,7 +422,7 @@ public function resolveServiceProvider(string $provider): BaseServiceProvider /** * Get the service providers that have been registered. * - * @return array + * @return array */ public function getRegisteredProviders(): array { @@ -426,12 +432,14 @@ public function getRegisteredProviders(): array /** * Determine if the given service provider is registered. * - * @param string $provider + * @param string|Serviceable|Bootable $provider * @return bool */ - public function providerIsRegistered(string $provider): bool + public function providerIsRegistered(string|Serviceable|Bootable $provider): bool { - return isset($this->serviceProvidersRegistered[$provider]); + $registered = is_string(value: $provider) ? $provider : get_class(object: $provider); + + return isset($this->serviceProvidersRegistered[$registered]); } /** @@ -498,16 +506,14 @@ protected function fireAppCallbacks(array &$callbacks): void /** * Mark the particular ServiceProvider as having been registered. * - * @param string|null $registered * @param Serviceable|Bootable $serviceProvider * @return void */ - protected function markServiceProviderAsRegistered( - string|null $registered, - Serviceable|Bootable $serviceProvider - ): void { - $this->serviceProviders[$registered] = $serviceProvider; - $this->serviceProvidersRegistered[$registered] = true; + protected function markServiceProviderAsRegistered(Serviceable|Bootable $serviceProvider): void + { + $class = get_class($serviceProvider); + $this->serviceProviders[$class] = $serviceProvider; + $this->serviceProvidersRegistered[$class] = true; } /** diff --git a/src/Bootstrap/RegisterProviders.php b/src/Bootstrap/RegisterProviders.php index 0f2432f..becfb40 100644 --- a/src/Bootstrap/RegisterProviders.php +++ b/src/Bootstrap/RegisterProviders.php @@ -26,6 +26,8 @@ final class RegisterProviders */ protected static ?string $bootstrapProviderPath = null; + public static array $providers = []; + /** * @throws TypeException * @throws Exception @@ -72,6 +74,8 @@ protected function mergeAdditionalProviders(Application $app): void ) ); + self::$providers = array_merge(self::$providers, $arrayMerge); + $config->setConfigKey(key: 'app', value: ['providers' => $arrayMerge,]); } diff --git a/src/Support/CodefyServiceProvider.php b/src/Support/CodefyServiceProvider.php index ac01806..5a5e141 100644 --- a/src/Support/CodefyServiceProvider.php +++ b/src/Support/CodefyServiceProvider.php @@ -23,6 +23,12 @@ abstract class CodefyServiceProvider extends BaseServiceProvider */ protected array $bootedCallbacks = []; + /** @var array> */ + protected array $publishes = []; + + /** @var array> */ + protected array $publishGroups = []; + public function __construct(protected Application $codefy) { parent::__construct($codefy); From 7bb5b6775687632682fbde6461ca45317ccab8b9 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 13:12:12 -0700 Subject: [PATCH 137/161] New command to publish assets declared in service providers. Signed-off-by: Joshua Parker --- src/Console/Commands/VendorPublishCommand.php | 192 ++++++++++++++++++ src/Support/CodefyServiceProvider.php | 39 ++++ 2 files changed, 231 insertions(+) create mode 100644 src/Console/Commands/VendorPublishCommand.php diff --git a/src/Console/Commands/VendorPublishCommand.php b/src/Console/Commands/VendorPublishCommand.php new file mode 100644 index 0000000..434c13c --- /dev/null +++ b/src/Console/Commands/VendorPublishCommand.php @@ -0,0 +1,192 @@ +addOption( + name: 'tag', + shortcut: null, + mode: InputOption::VALUE_REQUIRED, + description: 'Publish group (config, migrations, etc.).' + ) + ->addOption( + name: 'force', + shortcut: '--f', + mode: InputOption::VALUE_NONE, + description: 'Overwrite existing files.' + ) + ->addOption( + name: 'list', + shortcut: '--l', + mode: InputOption::VALUE_NONE, + description: 'List publishable providers and tags.' + ) + ->setDescription(description: 'Publish any publishable assets from vendor packages') + ->setHelp( + help: <<vendor:publish publishes assets from vendor packages. +php codex vendor:publish +EOT + ); + } + + public function handle(): int + { + // If user wants to list publishable tags instead of publishing + if ($this->input->getOption('list')) { + return $this->listPublishables(); + } + + $tag = $this->input->getOption('tag'); + $force = $this->input->getOption('force'); + + $providers = $this->codefy->getRegisteredProviders(); + + foreach ($providers as $instance => $value) { + + $paths = $instance->pathsToPublish($tag); + + foreach ($paths as $from => $type) { + $destination = $this->resolveDestination($from, $type); + + if (is_dir($from)) { + $this->copyDirectory($from, $destination, $force); + } else { + $this->copyFile($from, $destination, $force); + } + } + } + + return ConsoleCommand::SUCCESS; + } + + private function resolveDestination(string $from, string $type): string + { + // Normalize trailing separators + $from = rtrim($from, $this->codefy::DS); + + return match ($type) { + 'config' => is_dir($from) ? 'config' : 'config/' . basename($from), + 'migrations' => is_dir($from) ? 'database/migrations' : 'database/migrations/' . basename($from), + default => $type, // allow explicit path fallback + }; + } + + /** + * @throws FilesystemException + */ + private function copyFile(string $from, string $to, bool $force): void + { + /** @var FileSystem $filesystem */ + $filesystem = $this->codefy->make(name: 'filesystem.default'); + + // Ensure parent directory exists in Flysystem + $dir = pathinfo($to, PATHINFO_DIRNAME); + if ($dir !== '' && ! $filesystem->directoryExists($dir)) { + $filesystem->createDirectory($dir); + } + + if (! $force && $filesystem->fileExists($to)) { + $this->output->writeln("Skipped {$to}, file exists."); + return; + } + + // Prefer streaming for large files, fallback to write() + $stream = fopen($from, 'rb'); + if ($stream !== false && method_exists($filesystem, 'writeStream')) { + $filesystem->writeStream($to, $stream); + if (is_resource($stream)) { + fclose($stream); + } + } else { + $contents = file_get_contents($from); + $filesystem->write($to, $contents === false ? '' : $contents); + } + + $this->output->writeln("Published: {$from} -> {$to}"); + } + + /** + * @throws FilesystemException + */ + private function copyDirectory(string $from, string $to, bool $force): void + { + $from = rtrim($from, $this->codefy::DS); + $to = rtrim($to, $this->codefy::DS); + + $dirIterator = new RecursiveDirectoryIterator($from, FilesystemIterator::SKIP_DOTS); + $iterator = new RecursiveIteratorIterator($dirIterator, RecursiveIteratorIterator::LEAVES_ONLY); + + foreach ($iterator as $fileInfo) { + /** @var SplFileInfo $fileInfo */ + if ($fileInfo->isDir()) { + continue; + } + + // Compute relative path robustly (works on Windows and *nix) + $absolutePath = $fileInfo->getPathname(); + $relative = substr($absolutePath, strlen($from) + 1); + $relative = str_replace(['\\', '/'], $this->codefy::DS, $relative); + + $target = $to . $this->codefy::DS . $relative; + + $this->copyFile($absolutePath, $target, $force); + } + } + + private function listPublishables(): int + { + $providers = $this->codefy->getRegisteredProviders(); + + $this->output->writeln("Available publishable resources:"); + + foreach ($providers as $instance => $value) { + + foreach (['config', 'migrations'] as $tag) { + $paths = $instance->pathsToPublish($tag); + if (!empty($paths)) { + $this->output->writeln(" - tag: {$tag}"); + foreach ($paths as $from => $type) { + $this->output->writeln(" {$from} -> {$this->resolveDestination($from, $type)}"); + } + } + } + } + + return ConsoleCommand::SUCCESS; + } +} diff --git a/src/Support/CodefyServiceProvider.php b/src/Support/CodefyServiceProvider.php index 5a5e141..9c637e5 100644 --- a/src/Support/CodefyServiceProvider.php +++ b/src/Support/CodefyServiceProvider.php @@ -97,4 +97,43 @@ public static function defaultProviders(): DefaultProviders { return new DefaultProviders(); } + + /** + * Register publishable paths for this provider. + * + * @param array $paths [from => tag] + * @param string|null $group Optional tag/group name ("config", "migrations", etc.) + */ + public function publishes(array $paths, ?string $group = null): void + { + foreach ($paths as $from => $tag) { + $tag = $group ?? $tag; // if group given, override + $this->publishes[$tag][$from] = $tag; + } + } + + /** + * Get all publishable paths for this provider. + * + * @param string|null $tag Restrict to a tag (e.g. "config", "migrations") + * @return array [from => tag] + */ + public function pathsToPublish(?string $tag = null): array + { + if ($tag !== null) { + return $this->publishes[$tag] ?? []; + } + + return array_merge(...array_values($this->publishes ?: [[]])); + } + + /** + * List all tags defined by this provider. + * + * @return array + */ + public function publishTags(): array + { + return array_keys($this->publishes); + } } From 596a416bb0e8ff313fd0d4175dbe945699d52949 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 13:16:47 -0700 Subject: [PATCH 138/161] Added new command to default commands. Signed-off-by: Joshua Parker --- src/Support/DefaultCommands.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Support/DefaultCommands.php b/src/Support/DefaultCommands.php index 0f0b53e..bb6bc88 100644 --- a/src/Support/DefaultCommands.php +++ b/src/Support/DefaultCommands.php @@ -30,6 +30,7 @@ public function __construct(array $collection = []) \Codefy\Framework\Console\Commands\UuidCommand::class, \Codefy\Framework\Console\Commands\UlidCommand::class, \Codefy\Framework\Console\Commands\FlushPipelineCommand::class, + \Codefy\Framework\Console\Commands\VendorPublishCommand::class, ]; } } From b3c67922492642e0650dd88e33ad551695b6aff0 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 15:01:02 -0700 Subject: [PATCH 139/161] Added ability to publish from a specific service provider. Signed-off-by: Joshua Parker --- src/Console/Commands/VendorPublishCommand.php | 82 ++++++++++++++++++- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/src/Console/Commands/VendorPublishCommand.php b/src/Console/Commands/VendorPublishCommand.php index 434c13c..0139ad9 100644 --- a/src/Console/Commands/VendorPublishCommand.php +++ b/src/Console/Commands/VendorPublishCommand.php @@ -37,6 +37,12 @@ protected function configure(): void parent::configure(); $this + ->addOption( + name: 'provider', + shortcut: null, + mode: InputOption::VALUE_REQUIRED, + description: 'ServiceProvider class' + ) ->addOption( name: 'tag', shortcut: null, @@ -45,13 +51,13 @@ protected function configure(): void ) ->addOption( name: 'force', - shortcut: '--f', + shortcut: 'f', mode: InputOption::VALUE_NONE, description: 'Overwrite existing files.' ) ->addOption( name: 'list', - shortcut: '--l', + shortcut: 'l', mode: InputOption::VALUE_NONE, description: 'List publishable providers and tags.' ) @@ -64,6 +70,9 @@ protected function configure(): void ); } + /** + * @throws FilesystemException + */ public function handle(): int { // If user wants to list publishable tags instead of publishing @@ -71,12 +80,17 @@ public function handle(): int return $this->listPublishables(); } + // If user wants to publish from a specific provider + if ($this->input->getOption('provider')) { + return $this->publishDefinedProvider(); + } + $tag = $this->input->getOption('tag'); $force = $this->input->getOption('force'); - $providers = $this->codefy->getRegisteredProviders(); + $registeredProviders = $this->codefy->getRegisteredProviders(); - foreach ($providers as $instance => $value) { + foreach ($registeredProviders as $instance => $value) { $paths = $instance->pathsToPublish($tag); @@ -189,4 +203,64 @@ private function listPublishables(): int return ConsoleCommand::SUCCESS; } + + /** + * @throws FilesystemException + */ + private function publishDefinedProvider(): int + { + $registeredProviders = $this->codefy->getRegisteredProviders(); + + $provider = $this->input->getOption('provider'); + $tag = $this->input->getOption('tag'); + $force = $this->input->getOption('force'); + + $providers = $this->findKeysLike($provider, $registeredProviders); + + foreach ($providers as $int => $p) { + + $paths = $p->pathsToPublish($tag); + + foreach ($paths as $from => $type) { + $destination = $this->resolveDestination($from, $type); + + if (is_dir($from)) { + $this->copyDirectory($from, $destination, $force); + } else { + $this->copyFile($from, $destination, $force); + } + } + } + + return ConsoleCommand::SUCCESS; + } + + /** + * Search array keys for a substring and return all matching keys. + * + * @param string $needle + * @param array $haystack + * @param bool $caseSensitive + * + * @return array List of matching keys (empty if none found). + */ + protected function findKeysLike(string $needle, array $haystack, bool $caseSensitive = true): array + { + $matches = []; + + foreach ($haystack as $key => $_) { + if (!is_string($key)) { + continue; + } + + $hay = $caseSensitive ? $key : strtolower($key); + $needleCmp = $caseSensitive ? $needle : strtolower($needle); + + if (str_contains($hay, $needleCmp)) { + $matches[] = $key; + } + } + + return $matches; + } } From 4ebb03251475e5b52e1e57f2aca54fef0b765de6 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Fri, 10 Oct 2025 15:33:39 -0700 Subject: [PATCH 140/161] Moved commands from skeleton to main framework. Signed-off-by: Joshua Parker --- src/Console/Commands/EncryptEnvCommand.php | 43 +++++++++++++++++++ .../Commands/GenerateEncryptionKeyCommand.php | 36 ++++++++++++++++ .../GenerateEncryptionKeyFileCommand.php | 40 +++++++++++++++++ src/Support/DefaultCommands.php | 3 ++ 4 files changed, 122 insertions(+) create mode 100644 src/Console/Commands/EncryptEnvCommand.php create mode 100644 src/Console/Commands/GenerateEncryptionKeyCommand.php create mode 100644 src/Console/Commands/GenerateEncryptionKeyFileCommand.php diff --git a/src/Console/Commands/EncryptEnvCommand.php b/src/Console/Commands/EncryptEnvCommand.php new file mode 100644 index 0000000..44402b4 --- /dev/null +++ b/src/Console/Commands/EncryptEnvCommand.php @@ -0,0 +1,43 @@ +terminalRaw(string: 'Encrypting data and creating file . . .'); + + try { + $file = file_get_contents(filename: base_path(path: '.enc.key')); + + $key = Key::loadFromAsciiSafeString(saved_key_string: $file); + + File::encrypt(base_path(path: '.env'), base_path(path: '.env.enc'), $key); + } catch (BadFormatException | EnvironmentIsBrokenException $e) { + return ConsoleCommand::FAILURE; + } + + $this->terminalRaw(string: '.env.enc file created.'); + + // return value is important when using CI + // to fail the build when the command fails + // 0 = success, other values = fail + return ConsoleCommand::SUCCESS; + } +} diff --git a/src/Console/Commands/GenerateEncryptionKeyCommand.php b/src/Console/Commands/GenerateEncryptionKeyCommand.php new file mode 100644 index 0000000..e91c2e0 --- /dev/null +++ b/src/Console/Commands/GenerateEncryptionKeyCommand.php @@ -0,0 +1,36 @@ +saveToAsciiSafeString(); + + $this->terminalRaw(string: sprintf( + 'Encryption Key: %s', + $key + )); + + // return value is important when using CI + // to fail the build when the command fails + // 0 = success, other values = fail + return ConsoleCommand::SUCCESS; + } +} diff --git a/src/Console/Commands/GenerateEncryptionKeyFileCommand.php b/src/Console/Commands/GenerateEncryptionKeyFileCommand.php new file mode 100644 index 0000000..6393ba8 --- /dev/null +++ b/src/Console/Commands/GenerateEncryptionKeyFileCommand.php @@ -0,0 +1,40 @@ +terminalRaw(string: 'Generating encryption key . . .'); + + $key = Key::createNewRandomKey()->saveToAsciiSafeString(); + + $this->terminalRaw(string: 'Generating encryption key file . . .'); + + file_put_contents(base_path(path: '.enc.key'), $key); + + $this->terminalRaw(string: '.enc.key file created.'); + + // return value is important when using CI + // to fail the build when the command fails + // 0 = success, other values = fail + return ConsoleCommand::SUCCESS; + } +} diff --git a/src/Support/DefaultCommands.php b/src/Support/DefaultCommands.php index bb6bc88..4640f98 100644 --- a/src/Support/DefaultCommands.php +++ b/src/Support/DefaultCommands.php @@ -31,6 +31,9 @@ public function __construct(array $collection = []) \Codefy\Framework\Console\Commands\UlidCommand::class, \Codefy\Framework\Console\Commands\FlushPipelineCommand::class, \Codefy\Framework\Console\Commands\VendorPublishCommand::class, + \Codefy\Framework\Console\Commands\GenerateEncryptionKeyCommand::class, + \Codefy\Framework\Console\Commands\GenerateEncryptionKeyFileCommand::class, + \Codefy\Framework\Console\Commands\EncryptEnvCommand::class, ]; } } From 1edba3d5d811a9f2f86f61b5b8c3ee54e9ddc36b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 11 Oct 2025 21:58:36 -0700 Subject: [PATCH 141/161] Changes and updated functions. Signed-off-by: Joshua Parker --- src/Helpers/core.php | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 27e127d..36056e2 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -21,8 +21,8 @@ use Codefy\QueryBus\Query; use Codefy\QueryBus\Resolvers\NativeQueryHandlerResolver; use Codefy\QueryBus\UnresolvableQueryHandlerException; -use Qubus\Config\Collection; use Qubus\Exception\Exception; +use Qubus\Expressive\Connection; use Qubus\Expressive\QueryBuilder; use ReflectionException; @@ -38,7 +38,6 @@ use function Qubus\Security\Helpers\esc_attr__; use function Qubus\Security\Helpers\esc_html__; use function Qubus\Security\Helpers\t__; -use function Qubus\Support\Helpers\is_false__; use function Qubus\Support\Helpers\is_null__; use function file_exists; use function in_array; @@ -76,19 +75,21 @@ function app(?string $name = null, array $args = []): mixed /** * Get the available config instance. * - * @param string $key - * @param array|bool $set + * @param array|string|null $key * @param mixed $default * @return mixed */ -function config(string $key, array|bool $set = false, mixed $default = ''): mixed +function config(string|array|null $key, mixed $default = ''): mixed { - if (!is_false__(var: $set)) { - app(name: Collection::class)->setConfigKey($key, $set); - return app(name: Collection::class)->getConfigKey($key, $default); + if (is_null__($key)) { + return app(name: 'codefy.config'); + } + + if (is_array($key)) { + app(name: 'codefy.config')->setConfigKey($key[0], $key[1]); } - return app(name: Collection::class)->getConfigKey($key, $default); + return app(name: 'codefy.config')->getConfigKey($key, $default); } /** @@ -120,14 +121,24 @@ function env(string $key, mixed $default = null): mixed } /** - * QueryBuilder database instance. + * Database abstraction layer global function. + * + * @throws Exception + */ +function dbal(): Connection +{ + return Codefy::$PHP->getDbConnection(); +} + +/** + * QueryBuilder global function. * * @return QueryBuilder|null * @throws Exception */ function queryBuilder(): ?QueryBuilder { - return Codefy::$PHP->getDb(); + return dbal()->queryBuilder(); } /** From cdb046f05ed32af09686d25d4dc169dc038cb8cd Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 11 Oct 2025 22:00:05 -0700 Subject: [PATCH 142/161] Fixed migration import and terminal comments. Signed-off-by: Joshua Parker --- src/Console/Commands/MigrateGenerateCommand.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Console/Commands/MigrateGenerateCommand.php b/src/Console/Commands/MigrateGenerateCommand.php index 1db6765..81d2dbd 100644 --- a/src/Console/Commands/MigrateGenerateCommand.php +++ b/src/Console/Commands/MigrateGenerateCommand.php @@ -11,7 +11,10 @@ use RuntimeException; use Symfony\Component\Console\Input\InputArgument; +use function getcwd; use function Qubus\Support\Helpers\is_writable; +use function sprintf; +use function str_replace; class MigrateGenerateCommand extends PhpMigCommand { @@ -122,7 +125,7 @@ public function handle(): int declare(strict_types=1); -use Codefy\Framework\Migration\Migration; +use Qubus\Expressive\Migration\Migration; class $className extends Migration { @@ -144,17 +147,13 @@ public function down(): void } if (false === file_put_contents(filename: $path, data: $contents)) { - throw new RuntimeException( - message: sprintf( - 'The file "%s" could not be written to', - $path - ) - ); + $this->terminalRaw(sprintf('The file "%s" could not be generated.', $path)); + return ConsoleCommand::FAILURE; } $this->terminalRaw( - string: '+f ' . - '.' . str_replace(search: getcwd(), replace: '', subject: $path) + string: '+f ' . + '.' . str_replace(search: getcwd(), replace: '', subject: $path) . '' ); return ConsoleCommand::SUCCESS; From f8523bbb91434ca67ba6f997ccdc5027c5ba2971 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 11 Oct 2025 22:00:37 -0700 Subject: [PATCH 143/161] Added console option. Signed-off-by: Joshua Parker --- src/Console/Commands/EncryptEnvCommand.php | 32 ++++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Console/Commands/EncryptEnvCommand.php b/src/Console/Commands/EncryptEnvCommand.php index 44402b4..abd4c32 100644 --- a/src/Console/Commands/EncryptEnvCommand.php +++ b/src/Console/Commands/EncryptEnvCommand.php @@ -9,9 +9,12 @@ use Defuse\Crypto\Exception\EnvironmentIsBrokenException; use Defuse\Crypto\Key; use Qubus\Http\Encryption\Env\File; +use Symfony\Component\Console\Input\InputOption; use function Codefy\Framework\Helpers\base_path; +use function file_exists; use function file_get_contents; +use function sprintf; class EncryptEnvCommand extends ConsoleCommand { @@ -19,16 +22,39 @@ class EncryptEnvCommand extends ConsoleCommand protected string $description = 'Encrypts .env data and saves the encrypted data to a new file.'; + protected function configure(): void + { + parent::configure(); + + $this + ->addOption( + name: 'env', + shortcut: null, + mode: InputOption::VALUE_REQUIRED, + description: 'Set the environment file to encrypt.' + ); + } + public function handle(): int { $this->terminalRaw(string: 'Encrypting data and creating file . . .'); try { - $file = file_get_contents(filename: base_path(path: '.enc.key')); + $savedKeyString = file_get_contents(filename: base_path(path: '.enc.key')); + $key = Key::loadFromAsciiSafeString(saved_key_string: $savedKeyString); + + if ($this->getOptions('env')) { + $file = base_path(path: sprintf('.env.%s', $this->getOptions('env'))); + } else { + $file = base_path(path: '.env'); + } - $key = Key::loadFromAsciiSafeString(saved_key_string: $file); + if (!file_exists($file)) { + $this->output->writeln(sprintf('File %s does not exist.', $file)); + return ConsoleCommand::FAILURE; + } - File::encrypt(base_path(path: '.env'), base_path(path: '.env.enc'), $key); + File::encrypt($file, base_path(path: '.env.enc'), $key); } catch (BadFormatException | EnvironmentIsBrokenException $e) { return ConsoleCommand::FAILURE; } From 26e816a22365691976e19c0db9719c20060b4c73 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 11 Oct 2025 22:01:23 -0700 Subject: [PATCH 144/161] Fixed line break. Signed-off-by: Joshua Parker --- src/Http/Errors/HttpRequestError.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Errors/HttpRequestError.php b/src/Http/Errors/HttpRequestError.php index afa34f1..c0cf7e5 100644 --- a/src/Http/Errors/HttpRequestError.php +++ b/src/Http/Errors/HttpRequestError.php @@ -12,4 +12,4 @@ public function __construct(string $message = '', int|string $code = '', $contex { parent::__construct($message, $code, $context); } -} \ No newline at end of file +} From 6339e5409394ee79f5b2dce3c3d5d45eab1c4fd4 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sat, 11 Oct 2025 22:02:09 -0700 Subject: [PATCH 145/161] Updated environment parameter. Signed-off-by: Joshua Parker --- src/Providers/ConfigServiceProvider.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Providers/ConfigServiceProvider.php b/src/Providers/ConfigServiceProvider.php index 6535b09..fecf867 100644 --- a/src/Providers/ConfigServiceProvider.php +++ b/src/Providers/ConfigServiceProvider.php @@ -8,13 +8,13 @@ use Qubus\Config\Collection; use Qubus\Config\Configuration; use Qubus\Config\Path\PathNotFoundException; - -use function Codefy\Framework\Helpers\env; +use Qubus\Exception\Exception; final class ConfigServiceProvider extends CodefyServiceProvider { /** * @throws PathNotFoundException + * @throws Exception */ public function register(): void { @@ -24,7 +24,7 @@ public function register(): void [ 'path' => $this->codefy->configPath(), 'dotenv' => $this->codefy->basePath(), - 'environment' => env(key: 'APP_ENV', default: 'local'), + 'environment' => $this->codefy->configContainer->getConfigKey(key: 'app.env', default: 'local'), ] ) ); From 1d3921be937c871a13c4ff9d4230481db1361327 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 10:56:08 -0700 Subject: [PATCH 146/161] Removed unused import. Signed-off-by: Joshua Parker --- tests/ApplicationTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ApplicationTest.php b/tests/ApplicationTest.php index 6f6aaa5..463de0d 100644 --- a/tests/ApplicationTest.php +++ b/tests/ApplicationTest.php @@ -2,7 +2,6 @@ use Codefy\Framework\Application; use PHPUnit\Framework\Assert; -use Psr\Http\Message\ServerRequestInterface; it(description: 'gets default charset value.', closure: function () { $charset = Application::getInstance()->charset; From 1365c08782a4064816ae0b52c6597a88d92c2697 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 10:56:26 -0700 Subject: [PATCH 147/161] Added new queue feature. Signed-off-by: Joshua Parker --- composer.json | 1 + src/Helpers/core.php | 13 + src/Queue/NodeQueue.php | 355 +++++++++++++++++++++++++++ src/Queue/Queue.php | 99 ++++++++ src/Queue/QueueGarbageCollection.php | 19 ++ src/Queue/ReliableQueue.php | 15 ++ src/Queue/ShouldQueue.php | 17 ++ src/Queue/SimpleQueue.php | 19 ++ src/Queue/Traits/QueueAware.php | 25 ++ 9 files changed, 563 insertions(+) create mode 100644 src/Queue/NodeQueue.php create mode 100644 src/Queue/Queue.php create mode 100644 src/Queue/QueueGarbageCollection.php create mode 100644 src/Queue/ReliableQueue.php create mode 100644 src/Queue/ShouldQueue.php create mode 100644 src/Queue/SimpleQueue.php create mode 100644 src/Queue/Traits/QueueAware.php diff --git a/composer.json b/composer.json index 18cc22e..c65f055 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "qubus/filesystem": "^4", "qubus/inheritance": "^4", "qubus/mail": "^5", + "qubus/nosql": "dev-master", "qubus/router": "^4", "qubus/security": "^4", "qubus/support": "^4", diff --git a/src/Helpers/core.php b/src/Helpers/core.php index 36056e2..13ddfb8 100644 --- a/src/Helpers/core.php +++ b/src/Helpers/core.php @@ -14,6 +14,8 @@ use Codefy\Framework\Application; use Codefy\Framework\Proxy\Codefy; use Codefy\Framework\Factory\FileLoggerFactory; +use Codefy\Framework\Queue\NodeQueue; +use Codefy\Framework\Queue\ShouldQueue; use Codefy\Framework\Support\CodefyMailer; use Codefy\Framework\Support\Server; use Codefy\QueryBus\Busses\SynchronousQueryBus; @@ -414,3 +416,14 @@ function site_url(string $path = ''): string return ''; } + +/** + * Queues an item. + * + * @param ShouldQueue $queue + * @return NodeQueue + */ +function queue(ShouldQueue $queue): NodeQueue +{ + return new NodeQueue($queue); +} diff --git a/src/Queue/NodeQueue.php b/src/Queue/NodeQueue.php new file mode 100644 index 0000000..86b86cb --- /dev/null +++ b/src/Queue/NodeQueue.php @@ -0,0 +1,355 @@ +queue = $queue; + $this->db = Node::open(file: $node ?? $this->queue->node()); + } + + /** + * @param string|callable $schedule + * @return bool + */ + public function isDue(string|callable $schedule): bool + { + if (is_callable($schedule)) { + return call_user_func($schedule); + } + + $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $schedule); + if ($dateTime !== false) { + return $dateTime->format('Y-m-d H:i') == (date('Y-m-d H:i')); + } + + return new CronExpression((string) $schedule)->isDue(); + } + + /** + * @inheritDoc + */ + public function createItem(): string + { + $id = ''; + + try { + $id = $this->doCreateItem(); + } catch (Exception $e) { + FileLoggerFactory::getLogger()->error( + sprintf('NODEQSTATE: %s', $e->getMessage()), + ['Queue' => 'NodeQueue::createItem'] + ); + } + + return $id; + } + + /** + * Adds a queue item and store it directly to the queue. + * + * @return string A unique ID if the item was successfully created and was (best effort) + * added to the queue, otherwise false. We don't guarantee the item was + * committed to disk etc, but as far as we know, the item is now in the + * queue. + * @throws TypeException + * @throws ReflectionException + */ + protected function doCreateItem(): string + { + $lastId = ''; + + /** + * Check if queue is due or not due. + */ + if (!$this->isDue($this->queue->schedule)) { + return ''; + } + + $query = $this->db; + $query->begin(); + try { + $query->insert([ + 'name' => $this->queue->name, + 'object' => new JsonSerializer()->serialize($this->queue), + 'created' => time(), + 'expire' => (int) 0 + ]); + $query->commit(); + $lastId = $query->lastInsertId(); + } catch (Exception $e) { + $query->rollback(); + FileLoggerFactory::getLogger()->error( + sprintf('NODEQSTATE: %s', $e->getMessage()), + ['Queue' => 'NodeQueue::doCreateItem'] + ); + } + /** + * Return the new serial ID, or false on failure. + */ + return $lastId; + } + + /** + * @inheritDoc + */ + public function numberOfItems(): int + { + try { + return count($this->db->where('name', $this->queue->name)->get()); + } catch (Exception $e) { + $this->catchException($e); + /** + * If there is no node there cannot be any items. + */ + return 0; + } + } + + /** + * @inheritDoc + */ + public function claimItem(int $leaseTime = 3600): array|object|bool + { + /** + * Claim an item by updating its expiry fields. If claim is not + * successful another thread may have claimed the item in the meantime. + * Therefore, loop until an item is successfully claimed, or we are + * reasonably sure there are no unclaimed items left. + */ + try { + $item = $this->db + ->where('expire', (int) 0) + ->where('name', $this->queue->name) + ->sortBy('created') + ->sortBy('_id') + ->first(); + } catch (Exception $e) { + $this->catchException($e); + /** + * If the node does not exist there are no items currently + * available to claim. + */ + return false; + } + if ($item) { + $update = $this->db; + $update->begin(); + try { + /** + * Try to update the item. Only one thread can succeed in + * UPDATEing the same row. We cannot rely on REQUEST_TIME + * because items might be claimed by a single consumer which + * runs longer than 1 second. If we continue to use REQUEST_TIME + * instead of the current time(), we steal time from the lease, + * and will tend to reset items before the lease should really + * expire. + */ + $update->where('expire', (int) 0)->where('_id', $item['_id']) + ->update([ + 'expire' => (int) time() + ( + $this->queue->leaseTime <= (int) 0 + ? (int) $leaseTime + : (int) $this->queue->leaseTime + ) + ]); + $update->commit(); + return $item; + } catch (Exception $e) { + $update->rollback(); + $this->catchException($e); + /** + * If the node does not exist there are no items currently + * available to claim. + */ + return false; + } + } else { + /** + * No items currently available to claim. + */ + return false; + } + } + + /** + * @inheritDoc + */ + public function deleteItem(mixed $item): void + { + $delete = $this->db; + $delete->begin(); + try { + $delete->where('_id', $item['_id']) + ->delete(); + $delete->commit(); + } catch (Exception $e) { + $delete->rollback(); + $this->catchException($e); + } + } + + /** + * @inheritDoc + */ + public function releaseItem(mixed $item): bool + { + $update = $this->db; + $update->begin(); + try { + $update->where('_id', $item['_id']) + ->update([ + 'expire' => (int) 0 + ]); + $update->commit(); + + return true; + } catch (Exception $e) { + $update->rollback(); + $this->catchException($e); + } + + return false; + } + + /** + * @inheritDoc + */ + public function createQueue() + { + /** + * All tasks are stored in a single node (which is created on + * demand) so there is nothing we need to do to create a new queue. + */ + } + + /** + * @inheritDoc + */ + public function deleteQueue(): void + { + $delete = $this->db; + $delete->begin(); + try { + $delete->where('name', $this->queue->name) + ->delete(); + $delete->commit(); + } catch (Exception $e) { + $delete->rollback(); + $this->catchException($e); + } + } + + /** + * @throws ReflectionException + * @throws TypeException + */ + public function garbageCollection(): void + { + $delete = $this->db; + $delete->begin(); + try { + /** + * Clean up the queue for failed batches. + */ + $delete->where('created', '<', $_SERVER['REQUEST_TIME'] - 864000) + ->where('name', $this->queue->name) + ->delete(); + $delete->commit(); + } catch (Exception $e) { + $delete->rollback(); + $this->catchException($e); + } + + $update = $this->db; + $update->begin(); + + try { + /** + * Reset expired items in the default queue implementation node. If + * that's not used, this will simply be a no-op. + */ + $update->where('expire', 'not in', (int) 0) + ->where('expire', '<', $_SERVER['REQUEST_TIME']) + ->update([ + 'expire' => (int) 0 + ]); + $update->commit(); + } catch (Exception $e) { + $update->rollback(); + $this->catchException($e); + } + } + + /** + * Act on an exception when queue might be stale. + * + * If the node does not yet exist, that's fine, but if the node exists and + * yet the query failed, then the queue is stale and the exception needs to + * propagate. + * + * @param Exception $e The exception. + * @throws ReflectionException + * @throws TypeException + */ + protected function catchException(Exception $e): void + { + FileLoggerSmtpFactory::getLogger()->error( + sprintf('QUEUESTATE: %s', $e->getMessage()), + ['Queue' => 'catchException'] + ); + } + + public function dispatch(): bool + { + $this->createItem(); + + try { + if (false !== $item = $this->claimItem()) { + $object = new JsonSerializer()->unserialize($item['object']); + if (!$object instanceof ShouldQueue) { + return false; + }; + + if (!method_exists(object_or_class: $object, method: 'handle')) { + return false; + } + + if (false === call_user_func([$object, 'handle'])) { + $this->releaseItem($item); + return false; + } + + $this->deleteItem($item); + return true; + } + } catch (Exception $e) { + $this->catchException($e); + } + + return false; + } +} diff --git a/src/Queue/Queue.php b/src/Queue/Queue.php new file mode 100644 index 0000000..e67f8bc --- /dev/null +++ b/src/Queue/Queue.php @@ -0,0 +1,99 @@ + Date: Sun, 12 Oct 2025 11:05:46 -0700 Subject: [PATCH 148/161] Fixed issue with config. Signed-off-by: Joshua Parker --- src/Providers/ConfigServiceProvider.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Providers/ConfigServiceProvider.php b/src/Providers/ConfigServiceProvider.php index fecf867..e53836c 100644 --- a/src/Providers/ConfigServiceProvider.php +++ b/src/Providers/ConfigServiceProvider.php @@ -10,6 +10,8 @@ use Qubus\Config\Path\PathNotFoundException; use Qubus\Exception\Exception; +use function Codefy\Framework\Helpers\env; + final class ConfigServiceProvider extends CodefyServiceProvider { /** @@ -24,7 +26,7 @@ public function register(): void [ 'path' => $this->codefy->configPath(), 'dotenv' => $this->codefy->basePath(), - 'environment' => $this->codefy->configContainer->getConfigKey(key: 'app.env', default: 'local'), + 'environment' => env(key: 'APP_ENV', default: 'local'), ] ) ); From 7764c3b69e9e4a86b34120c6bde68b3268b0c10b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 14:00:09 -0700 Subject: [PATCH 149/161] Renamed commands where appropriate. Signed-off-by: Joshua Parker --- .../Commands/{CheckCommand.php => MigrateCheckCommand.php} | 2 +- .../Commands/{DownCommand.php => MigrateDownCommand.php} | 2 +- .../Commands/{RedoCommand.php => MigrateRedoCommand.php} | 2 +- .../{RollbackCommand.php => MigrateRollbackCommand.php} | 2 +- .../Commands/{StatusCommand.php => MigrateStatusCommand.php} | 2 +- src/Console/Commands/{UpCommand.php => MigrateUpCommand.php} | 2 +- .../Commands/{ListCommand.php => ScheduleListCommand.php} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename src/Console/Commands/{CheckCommand.php => MigrateCheckCommand.php} (97%) rename src/Console/Commands/{DownCommand.php => MigrateDownCommand.php} (97%) rename src/Console/Commands/{RedoCommand.php => MigrateRedoCommand.php} (97%) rename src/Console/Commands/{RollbackCommand.php => MigrateRollbackCommand.php} (98%) rename src/Console/Commands/{StatusCommand.php => MigrateStatusCommand.php} (97%) rename src/Console/Commands/{UpCommand.php => MigrateUpCommand.php} (97%) rename src/Console/Commands/{ListCommand.php => ScheduleListCommand.php} (100%) diff --git a/src/Console/Commands/CheckCommand.php b/src/Console/Commands/MigrateCheckCommand.php similarity index 97% rename from src/Console/Commands/CheckCommand.php rename to src/Console/Commands/MigrateCheckCommand.php index 2693101..decef05 100644 --- a/src/Console/Commands/CheckCommand.php +++ b/src/Console/Commands/MigrateCheckCommand.php @@ -7,7 +7,7 @@ use Qubus\Exception\Exception; use Symfony\Component\Console\Helper\Table; -class CheckCommand extends PhpMigCommand +class MigrateCheckCommand extends PhpMigCommand { protected string $name = 'migrate:check'; diff --git a/src/Console/Commands/DownCommand.php b/src/Console/Commands/MigrateDownCommand.php similarity index 97% rename from src/Console/Commands/DownCommand.php rename to src/Console/Commands/MigrateDownCommand.php index 8208416..df67b85 100644 --- a/src/Console/Commands/DownCommand.php +++ b/src/Console/Commands/MigrateDownCommand.php @@ -8,7 +8,7 @@ use Qubus\Exception\Exception; use Symfony\Component\Console\Input\InputArgument; -class DownCommand extends PhpMigCommand +class MigrateDownCommand extends PhpMigCommand { protected string $name = 'migrate:down'; diff --git a/src/Console/Commands/RedoCommand.php b/src/Console/Commands/MigrateRedoCommand.php similarity index 97% rename from src/Console/Commands/RedoCommand.php rename to src/Console/Commands/MigrateRedoCommand.php index 6ac7fa9..dc4353e 100644 --- a/src/Console/Commands/RedoCommand.php +++ b/src/Console/Commands/MigrateRedoCommand.php @@ -8,7 +8,7 @@ use Qubus\Exception\Exception; use Symfony\Component\Console\Input\InputArgument; -class RedoCommand extends PhpMigCommand +class MigrateRedoCommand extends PhpMigCommand { protected string $name = 'migrate:redo'; diff --git a/src/Console/Commands/RollbackCommand.php b/src/Console/Commands/MigrateRollbackCommand.php similarity index 98% rename from src/Console/Commands/RollbackCommand.php rename to src/Console/Commands/MigrateRollbackCommand.php index 098755a..ff1d13e 100644 --- a/src/Console/Commands/RollbackCommand.php +++ b/src/Console/Commands/MigrateRollbackCommand.php @@ -8,7 +8,7 @@ use Qubus\Exception\Exception; use Symfony\Component\Console\Input\InputArgument; -class RollbackCommand extends PhpMigCommand +class MigrateRollbackCommand extends PhpMigCommand { protected string $name = 'migrate:rollback'; diff --git a/src/Console/Commands/StatusCommand.php b/src/Console/Commands/MigrateStatusCommand.php similarity index 97% rename from src/Console/Commands/StatusCommand.php rename to src/Console/Commands/MigrateStatusCommand.php index d8f77ff..e6f8fab 100644 --- a/src/Console/Commands/StatusCommand.php +++ b/src/Console/Commands/MigrateStatusCommand.php @@ -10,7 +10,7 @@ use function sprintf; -class StatusCommand extends PhpMigCommand +class MigrateStatusCommand extends PhpMigCommand { protected string $name = 'migrate:status'; diff --git a/src/Console/Commands/UpCommand.php b/src/Console/Commands/MigrateUpCommand.php similarity index 97% rename from src/Console/Commands/UpCommand.php rename to src/Console/Commands/MigrateUpCommand.php index 062b889..d497fbe 100644 --- a/src/Console/Commands/UpCommand.php +++ b/src/Console/Commands/MigrateUpCommand.php @@ -8,7 +8,7 @@ use Qubus\Exception\Exception; use Symfony\Component\Console\Input\InputArgument; -class UpCommand extends PhpMigCommand +class MigrateUpCommand extends PhpMigCommand { protected string $name = 'migrate:up'; diff --git a/src/Console/Commands/ListCommand.php b/src/Console/Commands/ScheduleListCommand.php similarity index 100% rename from src/Console/Commands/ListCommand.php rename to src/Console/Commands/ScheduleListCommand.php From 23ab4410e2d331eb1c9e2afdedf874040aa775ab Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 14:00:36 -0700 Subject: [PATCH 150/161] Fixed help text. Signed-off-by: Joshua Parker --- src/Console/Commands/ScheduleListCommand.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Console/Commands/ScheduleListCommand.php b/src/Console/Commands/ScheduleListCommand.php index e513d14..6575e76 100644 --- a/src/Console/Commands/ScheduleListCommand.php +++ b/src/Console/Commands/ScheduleListCommand.php @@ -12,13 +12,16 @@ use Exception; use Symfony\Component\Console\Helper\Table; -class ListCommand extends ConsoleCommand +class ScheduleListCommand extends ConsoleCommand { protected string $name = 'schedule:list'; protected string $description = 'Lists the existing jobs/tasks.'; - protected string $help = 'This command displays the list of registered jobs/tasks.'; + protected string $help = <<schedule:list command prints a table with jobs/tasks to run. +php codex schedule:list +EOT; public function __construct(protected Schedule $schedule, protected Application $codefy) { From 529da649ab0befe0faf22a67ad4342b1c78045ea Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 14:01:27 -0700 Subject: [PATCH 151/161] Renamed commands and new queue commands. Signed-off-by: Joshua Parker --- src/Console/Commands/QueueListCommand.php | 81 +++++++++++++++++++++++ src/Console/Commands/QueueRunCommand.php | 60 +++++++++++++++++ src/Support/DefaultCommands.php | 16 +++-- 3 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 src/Console/Commands/QueueListCommand.php create mode 100644 src/Console/Commands/QueueRunCommand.php diff --git a/src/Console/Commands/QueueListCommand.php b/src/Console/Commands/QueueListCommand.php new file mode 100644 index 0000000..3dce262 --- /dev/null +++ b/src/Console/Commands/QueueListCommand.php @@ -0,0 +1,81 @@ +queue:list command prints a table of available queues. +php codex queue:list +EOT; + + protected function configure(): void + { + parent::configure(); + + $this + ->addOption( + name: 'name', + shortcut: null, + mode: InputOption::VALUE_REQUIRED, + description: 'List the queued jobs by node.' + ); + } + + public function handle(): int + { + try { + $table = new Table($this->output); + $table->setHeaders([ + "Queue Name", + "Class", + "Expression", + "Next Execution", + ]); + + $db = Node::open(database_path($this->getOptions('name') ?? 'nodequeue')); + $queues = $db->all(); + + foreach ($queues as $queue) { + $object = new JsonSerializer()->unserialize($queue['object']); + + $nextRun = new CronExpression($object->schedule); + + $table->addRow([ + $queue['name'], + get_class($object), + $nextRun->getExpression(), + $nextRun->getNextRunDate()->format(format: 'd F Y h:i A'), + ]); + } + + $table->render(); + } catch (InvalidJsonException | ReflectionException | Exception $e) { + return ConsoleCommand::FAILURE; + } + + // return value is important when using CI + // to fail the build when the command fails + // 0 = success, other values = fail + return ConsoleCommand::SUCCESS; + } +} diff --git a/src/Console/Commands/QueueRunCommand.php b/src/Console/Commands/QueueRunCommand.php new file mode 100644 index 0000000..2e37558 --- /dev/null +++ b/src/Console/Commands/QueueRunCommand.php @@ -0,0 +1,60 @@ +queue:run command dispatches the queues that are due to run. +php codex queue:run +EOT; + + protected function configure(): void + { + parent::configure(); + + $this + ->addOption( + name: 'name', + shortcut: null, + mode: InputOption::VALUE_REQUIRED, + description: 'Runs the queue based on given node.' + ); + } + + public function handle(): int + { + try { + $db = Node::open(database_path($this->getOptions('name') ?? 'nodequeue')); + $queues = $db->all(); + + foreach ($queues as $queue) { + $object = new JsonSerializer()->unserialize($queue['object']); + queue($object)->dispatch(); + } + } catch (InvalidJsonException | ReflectionException $e) { + return ConsoleCommand::FAILURE; + } + + // return value is important when using CI + // to fail the build when the command fails + // 0 = success, other values = fail + return ConsoleCommand::SUCCESS; + } +} diff --git a/src/Support/DefaultCommands.php b/src/Support/DefaultCommands.php index 4640f98..fa97703 100644 --- a/src/Support/DefaultCommands.php +++ b/src/Support/DefaultCommands.php @@ -17,15 +17,15 @@ public function __construct(array $collection = []) \Codefy\Framework\Console\Commands\ScheduleRunCommand::class, \Codefy\Framework\Console\Commands\PasswordHashCommand::class, \Codefy\Framework\Console\Commands\InitCommand::class, - \Codefy\Framework\Console\Commands\StatusCommand::class, - \Codefy\Framework\Console\Commands\CheckCommand::class, + \Codefy\Framework\Console\Commands\MigrateStatusCommand::class, + \Codefy\Framework\Console\Commands\MigrateCheckCommand::class, \Codefy\Framework\Console\Commands\MigrateGenerateCommand::class, - \Codefy\Framework\Console\Commands\UpCommand::class, - \Codefy\Framework\Console\Commands\DownCommand::class, + \Codefy\Framework\Console\Commands\MigrateUpCommand::class, + \Codefy\Framework\Console\Commands\MigrateDownCommand::class, \Codefy\Framework\Console\Commands\MigrateCommand::class, - \Codefy\Framework\Console\Commands\RollbackCommand::class, - \Codefy\Framework\Console\Commands\RedoCommand::class, - \Codefy\Framework\Console\Commands\ListCommand::class, + \Codefy\Framework\Console\Commands\MigrateRollbackCommand::class, + \Codefy\Framework\Console\Commands\MigrateRedoCommand::class, + \Codefy\Framework\Console\Commands\ScheduleListCommand::class, \Codefy\Framework\Console\Commands\ServeCommand::class, \Codefy\Framework\Console\Commands\UuidCommand::class, \Codefy\Framework\Console\Commands\UlidCommand::class, @@ -34,6 +34,8 @@ public function __construct(array $collection = []) \Codefy\Framework\Console\Commands\GenerateEncryptionKeyCommand::class, \Codefy\Framework\Console\Commands\GenerateEncryptionKeyFileCommand::class, \Codefy\Framework\Console\Commands\EncryptEnvCommand::class, + \Codefy\Framework\Console\Commands\QueueListCommand::class, + \Codefy\Framework\Console\Commands\QueueRunCommand::class, ]; } } From 07ee0048c2358b42822774726f63f787651c5bdf Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 14:02:48 -0700 Subject: [PATCH 152/161] Fixed the queue. Signed-off-by: Joshua Parker --- src/Queue/NodeQueue.php | 33 ++++++++++++++++----------------- src/Queue/ShouldQueue.php | 4 ++-- src/Queue/Traits/QueueAware.php | 8 +++----- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/Queue/NodeQueue.php b/src/Queue/NodeQueue.php index 86b86cb..315b1e6 100644 --- a/src/Queue/NodeQueue.php +++ b/src/Queue/NodeQueue.php @@ -82,13 +82,6 @@ protected function doCreateItem(): string { $lastId = ''; - /** - * Check if queue is due or not due. - */ - if (!$this->isDue($this->queue->schedule)) { - return ''; - } - $query = $this->db; $query->begin(); try { @@ -325,27 +318,33 @@ protected function catchException(Exception $e): void public function dispatch(): bool { - $this->createItem(); + /** + * Check if queue is due or not due. + */ + if (!$this->isDue($this->queue->schedule)) { + return false; + } + + $item = $this->claimItem(); try { - if (false !== $item = $this->claimItem()) { + if (false !== $item) { $object = new JsonSerializer()->unserialize($item['object']); + if (!$object instanceof ShouldQueue) { + $this->deleteItem($item); return false; }; - if (!method_exists(object_or_class: $object, method: 'handle')) { - return false; - } - - if (false === call_user_func([$object, 'handle'])) { + if (false !== call_user_func([$object, 'handle'])) { + $this->deleteItem($item); + } else { $this->releaseItem($item); return false; } - - $this->deleteItem($item); - return true; } + + return true; } catch (Exception $e) { $this->catchException($e); } diff --git a/src/Queue/ShouldQueue.php b/src/Queue/ShouldQueue.php index f7b825a..831c572 100644 --- a/src/Queue/ShouldQueue.php +++ b/src/Queue/ShouldQueue.php @@ -9,9 +9,9 @@ interface ShouldQueue /** * The code/task that should be executed. * - * @return bool + * @return void */ - public function handle(): bool; + public function handle(): void; public function node(): string; } diff --git a/src/Queue/Traits/QueueAware.php b/src/Queue/Traits/QueueAware.php index 22d8aba..e6730fd 100644 --- a/src/Queue/Traits/QueueAware.php +++ b/src/Queue/Traits/QueueAware.php @@ -9,17 +9,15 @@ trait QueueAware /** * The name of the queue this instance is working with. */ - protected string $name = ''; + public string $name = ''; /** * How long the processing is expected to take in seconds. */ - protected int $leaseTime = 3600; - - protected bool $debug = false; + public int $leaseTime = 3600; /** * When should the process run. */ - protected string $schedule = '* * * * *'; + public string $schedule = '* * * * *'; } From 3e11742032dfff0e5557bdcd55969d8d99cdba87 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 14:03:12 -0700 Subject: [PATCH 153/161] Bumped version value. Signed-off-by: Joshua Parker --- src/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 0f61b7e..15e59da 100644 --- a/src/Application.php +++ b/src/Application.php @@ -64,7 +64,7 @@ final class Application extends Container use InvokerAware; use LoggerAware; - public const string APP_VERSION = '3.0.0-rc.1'; + public const string APP_VERSION = '3.0.0-rc.2'; public const string MIN_PHP_VERSION = '8.4'; From 5b130d03b12aa123976a74126d154117da922dde Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 14:15:34 -0700 Subject: [PATCH 154/161] Fixed path to nodes. Signed-off-by: Joshua Parker --- src/Console/Commands/QueueListCommand.php | 4 +++- src/Console/Commands/QueueRunCommand.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Console/Commands/QueueListCommand.php b/src/Console/Commands/QueueListCommand.php index 3dce262..acaf614 100644 --- a/src/Console/Commands/QueueListCommand.php +++ b/src/Console/Commands/QueueListCommand.php @@ -52,7 +52,9 @@ public function handle(): int "Next Execution", ]); - $db = Node::open(database_path($this->getOptions('name') ?? 'nodequeue')); + $path = "" !== $this->getOptions('name') ? $this->getOptions('name') : 'nodequeue'; + + $db = Node::open(database_path(path: $path)); $queues = $db->all(); foreach ($queues as $queue) { diff --git a/src/Console/Commands/QueueRunCommand.php b/src/Console/Commands/QueueRunCommand.php index 2e37558..38a5de7 100644 --- a/src/Console/Commands/QueueRunCommand.php +++ b/src/Console/Commands/QueueRunCommand.php @@ -41,7 +41,9 @@ protected function configure(): void public function handle(): int { try { - $db = Node::open(database_path($this->getOptions('name') ?? 'nodequeue')); + $path = "" !== $this->getOptions('name') ? $this->getOptions('name') : 'nodequeue'; + + $db = Node::open(database_path(path: $path)); $queues = $db->all(); foreach ($queues as $queue) { From a80c96d0b5b7fc7e2542bd7909233caa02a31615 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Sun, 12 Oct 2025 16:39:40 -0700 Subject: [PATCH 155/161] Last few fixes and additions. Signed-off-by: Joshua Parker --- src/Queue/NodeQueue.php | 18 +++++++++++++++--- src/Queue/Traits/QueueAware.php | 6 ++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Queue/NodeQueue.php b/src/Queue/NodeQueue.php index 315b1e6..a8863f1 100644 --- a/src/Queue/NodeQueue.php +++ b/src/Queue/NodeQueue.php @@ -89,7 +89,8 @@ protected function doCreateItem(): string 'name' => $this->queue->name, 'object' => new JsonSerializer()->serialize($this->queue), 'created' => time(), - 'expire' => (int) 0 + 'expire' => (int) 0, + 'executions' => (int) 0, ]); $query->commit(); $lastId = $query->lastInsertId(); @@ -215,7 +216,8 @@ public function releaseItem(mixed $item): bool try { $update->where('_id', $item['_id']) ->update([ - 'expire' => (int) 0 + 'expire' => (int) 0, + 'executions' => +1, ]); $update->commit(); @@ -268,7 +270,8 @@ public function garbageCollection(): void /** * Clean up the queue for failed batches. */ - $delete->where('created', '<', $_SERVER['REQUEST_TIME'] - 864000) + $delete + ->where('executions', '>=', $this->queue->executions) ->where('name', $this->queue->name) ->delete(); $delete->commit(); @@ -316,8 +319,17 @@ protected function catchException(Exception $e): void ); } + /** + * @throws ReflectionException + * @throws TypeException + */ public function dispatch(): bool { + /** + * Delete queues that are considered dead. + */ + $this->garbageCollection(); + /** * Check if queue is due or not due. */ diff --git a/src/Queue/Traits/QueueAware.php b/src/Queue/Traits/QueueAware.php index e6730fd..7165890 100644 --- a/src/Queue/Traits/QueueAware.php +++ b/src/Queue/Traits/QueueAware.php @@ -20,4 +20,10 @@ trait QueueAware * When should the process run. */ public string $schedule = '* * * * *'; + + /** + * How many times should a job execute before + * considered dead. + */ + public int $executions = 3; } From 331539a1355b926647e729a211f4b0b252cbfe20 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 14 Oct 2025 08:37:59 -0700 Subject: [PATCH 156/161] Removed minimum stability. Signed-off-by: Joshua Parker --- composer.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index c65f055..e3b592a 100644 --- a/composer.json +++ b/composer.json @@ -13,14 +13,14 @@ "require": { "php": ">=8.4", "ext-pdo": "*", - "codefyphp/domain-driven-core": "^3.0.x-dev", - "composer/class-map-generator": "^1.0@dev", + "codefyphp/domain-driven-core": "^3", + "composer/class-map-generator": "^1", "dragonmantank/cron-expression": "^3", - "melbahja/seo": "^2.1", - "middlewares/cache": "^3.1", - "middlewares/minifier": "^2.1", - "middlewares/referrer-spam": "^2.1", - "middlewares/whoops": "dev-master", + "melbahja/seo": "^2", + "middlewares/cache": "^3", + "middlewares/minifier": "^2", + "middlewares/referrer-spam": "^2", + "middlewares/whoops": "^2", "paragonie/csp-builder": "^3", "php-debugbar/php-debugbar": "^2.2", "qubus/cache": "^4", @@ -31,7 +31,7 @@ "qubus/filesystem": "^4", "qubus/inheritance": "^4", "qubus/mail": "^5", - "qubus/nosql": "dev-master", + "qubus/nosql": "^4", "qubus/router": "^4", "qubus/security": "^4", "qubus/support": "^4", @@ -65,7 +65,6 @@ "cs-check": "phpcs", "cs-fix": "phpcbf" }, - "minimum-stability": "dev", "config": { "optimize-autoloader": true, "sort-packages": true, From 15036ec47ff07b9c0d8cc9a3b79e32f680c34cd7 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 14 Oct 2025 08:40:48 -0700 Subject: [PATCH 157/161] Bumped version value. Signed-off-by: Joshua Parker --- src/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application.php b/src/Application.php index 15e59da..a278a40 100644 --- a/src/Application.php +++ b/src/Application.php @@ -64,7 +64,7 @@ final class Application extends Container use InvokerAware; use LoggerAware; - public const string APP_VERSION = '3.0.0-rc.2'; + public const string APP_VERSION = '3.0.0'; public const string MIN_PHP_VERSION = '8.4'; From 15e20ae8eb98159d12c5d9906081169e8ff66e1a Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 14 Oct 2025 10:20:08 -0700 Subject: [PATCH 158/161] Updated release schedule. Signed-off-by: Joshua Parker --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 16dbe75..534674c 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ CodefyPHP is __not__ a framework such as the likes of Symfony, Laravel, Codeigni light framework providing interfaces and implementations for architecting a Domain Driven project with CQRS, Event Sourcing and implementations of [PSR-3](https://www.php-fig.org/psr/psr-3), [PSR-6](https://www.php-fig.org/psr/psr-6), [PSR-7](https://www.php-fig.org/psr/psr-7), -[PSR-11](https://www.php-fig.org/psr/psr-11), [PSR-12](https://www.php-fig.org/psr/psr-12/), -[PSR-15](https://www.php-fig.org/psr/psr-15), [PSR-16](https://www.php-fig.org/psr/psr-16) +[PSR-11](https://www.php-fig.org/psr/psr-11), [PSR-12](https://www.php-fig.org/psr/psr-12/), +[PSR-14](https://www.php-fig.org/psr/psr-14/), [PSR-15](https://www.php-fig.org/psr/psr-15), [PSR-16](https://www.php-fig.org/psr/psr-16) and [PSR-17](https://www.php-fig.org/psr/psr-17). The philosophy of Codefy is that code should be systematized, maintainable, and follow OOP (Object-Oriented Programming). @@ -61,7 +61,7 @@ composer require codefyphp/codefy |---------|---------------------|----------------|-----------------|----------------------| | 1 | 8.2 | September 2023 | July 2024 | EOL | | 2 - LTS | 8.2 | September 2024 | September 2027 | January 2028 | -| 3.0 | 8.4 | December 2025 | August 2026 | December 2027 | +| 3.0 | 8.4 | October 2025 | August 2026 | December 2027 | | 3.1 | 8.4 | June 2025 | February 2027 | June 2028 | ## 📘 Documentation From 77828c78e23b4f288ae20c7c3744d18710ce2dee Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 14 Oct 2025 10:20:53 -0700 Subject: [PATCH 159/161] Updated catch block. Signed-off-by: Joshua Parker --- src/Console/Commands/QueueRunCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Console/Commands/QueueRunCommand.php b/src/Console/Commands/QueueRunCommand.php index 38a5de7..e12d77f 100644 --- a/src/Console/Commands/QueueRunCommand.php +++ b/src/Console/Commands/QueueRunCommand.php @@ -5,6 +5,7 @@ namespace Codefy\Framework\Console\Commands; use Codefy\Framework\Console\ConsoleCommand; +use Qubus\Exception\Data\TypeException; use Qubus\NoSql\Exceptions\InvalidJsonException; use Qubus\NoSql\Node; use Qubus\Support\Serializer\JsonSerializer; @@ -50,7 +51,7 @@ public function handle(): int $object = new JsonSerializer()->unserialize($queue['object']); queue($object)->dispatch(); } - } catch (InvalidJsonException | ReflectionException $e) { + } catch (InvalidJsonException | ReflectionException | TypeException $e) { return ConsoleCommand::FAILURE; } From fcda3ccd1193a93458b1ad3d524ea6ca38e1f205 Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 14 Oct 2025 10:50:06 -0700 Subject: [PATCH 160/161] Updated docblock Signed-off-by: Joshua Parker --- src/Console/ConsoleApplication.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Console/ConsoleApplication.php b/src/Console/ConsoleApplication.php index 137cd38..1867a14 100644 --- a/src/Console/ConsoleApplication.php +++ b/src/Console/ConsoleApplication.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; class ConsoleApplication extends SymfonyApplication { @@ -24,6 +25,9 @@ public function __construct(protected Application $codefy) parent::__construct(name: 'CodefyPHP', version: Application::APP_VERSION); } + /** + * @throws Throwable + */ public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { $this->getCommandName(input: $input = $input ?: new ArgvInput()); @@ -33,6 +37,7 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu /** * @throws Exception + * @throws Throwable */ public function call($command, array $parameters = [], bool|OutputInterface|null $outputBuffer = null): int { From a1794418521cfeb52d44343e280ab68f29ca092b Mon Sep 17 00:00:00 2001 From: Joshua Parker Date: Tue, 14 Oct 2025 10:50:33 -0700 Subject: [PATCH 161/161] Fixed bug in ConsoleKernel. Signed-off-by: Joshua Parker --- src/Console/ConsoleKernel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/ConsoleKernel.php b/src/Console/ConsoleKernel.php index 65e1bc5..eca4301 100644 --- a/src/Console/ConsoleKernel.php +++ b/src/Console/ConsoleKernel.php @@ -104,7 +104,7 @@ protected function commands(): void */ public function registerCommand(callable|Command $command): void { - $this->getCodex()->addCommand(command: $command); + $this->getCodex()->add(command: $command); } /**