Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
build:
environment:
php:
version: 7.4
version: 8.0
dependencies:
before:
- curl -sS https://getcomposer.org/installer | php -- --2
Expand Down
43 changes: 27 additions & 16 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [3.0.0] - TBD

### Changed
Expand All @@ -21,6 +12,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated CI pipeline to test against PHP 7.4, 8.0, and 8.1
- CI now fails on PHP warnings and deprecations for stricter quality control
- Added `declare(strict_types=1)` to all Exception classes for improved type safety

- **BREAKING:** Refactored routing system internals:
- Routes are now represented as first-class objects (`Route`, `RouteCollection`, `MatchedRoute`)
- Introduced `RouteBuilder` as a fluent DSL interpreter and route composition engine
- Introduced `PatternCompiler` for centralized route pattern compilation
- Introduced `RouteDispatcher` for explicit controller / closure dispatching
- Route matching now produces a `MatchedRoute` object containing the route and extracted parameters
- Routing is executed once per request; matched route is stored on the `Request`
- Global route helper functions (`current_*`, `route_*`) now rely on `MatchedRoute` instead of legacy static route state
- Middleware ordering is strictly prepend-based (later middleware wraps earlier)
- Nested route groups are no longer supported
- Route name uniqueness is now enforced at build time

- **BREAKING:** Controller resolution behavior changed:
- Routes no longer implicitly resolve controller class names via legacy `RouteController` logic
- Controllers are now instantiated directly based on the handler defined in the route
- Projects relying on legacy controller resolution or `RouteController` static state must update accordingly

- **BREAKING:** Refactored model architecture:
- Introduced a base Model class for non-database models
- Refactored QtModel into DbModel with persistence-only responsibilities
Expand All @@ -42,6 +51,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rector as dev dependency for automated code refactoring
- Additional PHP extensions required in CI: `bcmath`, `gd`, `zip`
- PHPUnit strict testing flags: `--fail-on-warning`, `--fail-on-risky`

- **Routing test coverage**:
- Comprehensive unit tests for route building, grouping, middleware ordering, caching, and naming
- Unit tests for route helpers and request–route integration
- Unit tests for dispatcher behavior with controller and closure routes

- **Cron Scheduler**: New CLI command `php qt cron:run` for running scheduled tasks
- Task definition via PHP files in `cron/` directory
- Cron expression parsing using `dragonmantank/cron-expression` library
Expand All @@ -50,17 +65,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for force mode and specific task execution
- Automatic cleanup of stale locks (older than 24 hours)
- Full documentation in `docs/cron-scheduler.md`

- **Opt-in Model Timestamps**: Introduced `HasTimestamps` trait for `DbModel`:
- Automatically sets `created_at` on insert
- Automatically sets `created_at` on insert
- Automatically sets `updated_at` on insert and update
- Supports custom timestamp column names via model constants (`CREATED_AT`, `UPDATED_AT`)
- Supports datetime and unix timestamp formats via `TIMESTAMP_TYPE`

### Removed
- Support for PHP 7.3 and earlier versions

---

## [2.x.x] - Previous versions

See Git history for changes in earlier versions.
- Legacy routing static state and implicit controller resolution via `RouteController`
45 changes: 35 additions & 10 deletions src/App/Adapters/WebAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

use Quantum\Libraries\Database\Exceptions\DatabaseException;
use Quantum\Libraries\Session\Exceptions\SessionException;
use Quantum\Router\Exceptions\RouteControllerException;
use Quantum\Middleware\Exceptions\MiddlewareException;
use Quantum\Libraries\Csrf\Exceptions\CsrfException;
use Quantum\Libraries\Lang\Exceptions\LangException;
use Quantum\App\Exceptions\StopExecutionException;
Expand All @@ -29,8 +29,12 @@
use Quantum\App\Exceptions\BaseException;
use Quantum\Di\Exceptions\DiException;
use Quantum\App\Traits\WebAppTrait;
use Quantum\Router\RouteCollection;
use Quantum\Router\RouteDispatcher;
use Quantum\Router\RouteBuilder;
use Quantum\Module\ModuleLoader;
use DebugBar\DebugBarException;
use Quantum\Router\RouteFinder;
use Quantum\Debugger\Debugger;
use Quantum\Hook\HookManager;
use Quantum\Http\Response;
Expand Down Expand Up @@ -85,11 +89,11 @@ public function __construct()
* @throws DiException
* @throws HttpException
* @throws LangException
* @throws ModuleException
* @throws ReflectionException
* @throws RouteControllerException
* @throws RouteException
* @throws SessionException
* @throws ModuleException
* @throws MiddlewareException
*/
public function start(): ?int
{
Expand All @@ -103,26 +107,47 @@ public function start(): ?int
$this->setupErrorHandler();
$this->initializeDebugger();

$this->loadModules();
$moduleLoader = ModuleLoader::getInstance();

$builder = new RouteBuilder();

$collection = $builder->build(
$moduleLoader->loadModulesRoutes(),
$moduleLoader->getModuleConfigs()
);

$this->initializeRouter($this->request);
Di::set(RouteCollection::class, $collection);

$routeFinder = new RouteFinder($collection);

$matchedRoute = $routeFinder->find($this->request);

if ($matchedRoute === null) {
page_not_found();
stop();
}

$this->request->setMatchedRoute($matchedRoute);

$this->loadLanguage();

info(HookManager::getInstance()->getRegistered(), ['tab' => Debugger::HOOKS]);

if (current_middlewares()) {
[$this->request, $this->response] = (new MiddlewareManager())->applyMiddlewares($this->request, $this->response);
}
$middlewareManager = new MiddlewareManager($matchedRoute);

[$this->request, $this->response] = $middlewareManager->applyMiddlewares(
$this->request,
$this->response
);

$viewCache = $this->setupViewCache();

if ($viewCache->serveCachedView(route_uri(), $this->response)) {
stop();
}

RouteDispatcher::handle($this->request);

$dispatcher = new RouteDispatcher();
$dispatcher->dispatch($matchedRoute, $this->request, $this->response);
stop();
} catch (StopExecutionException $exception) {
$this->handleCors($this->response);
Expand Down
40 changes: 0 additions & 40 deletions src/App/Traits/WebAppTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,20 @@

namespace Quantum\App\Traits;

use Quantum\App\Exceptions\StopExecutionException;
use Quantum\Environment\Exceptions\EnvException;
use Quantum\Libraries\ResourceCache\ViewCache;
use Quantum\Module\Exceptions\ModuleException;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\Router\Exceptions\RouteException;
use Quantum\Http\Exceptions\HttpException;
use Quantum\App\Exceptions\BaseException;
use Quantum\Di\Exceptions\DiException;
use Quantum\Environment\Environment;
use Quantum\Module\ModuleLoader;
use Quantum\Router\RouteBuilder;
use DebugBar\DebugBarException;
use Quantum\Environment\Server;
use Quantum\Debugger\Debugger;
use Quantum\Http\Response;
use Quantum\Router\Router;
use Quantum\Http\Request;
use Quantum\Loader\Setup;
use ReflectionException;
use Quantum\Di\Di;

/**
* Trait WebAppTrait
Expand Down Expand Up @@ -76,23 +69,6 @@ private function initializeDebugger()
$debugger->initStore();
}

/**
* Load modules
* @throws ModuleException
*/
private function loadModules()
{
$moduleLoader = ModuleLoader::getInstance();

$modulesDependencies = $moduleLoader->loadModulesDependencies();
Di::registerDependencies($modulesDependencies);

$builder = new RouteBuilder();
$allRoutes = $builder->build($moduleLoader->loadModulesRoutes(), $moduleLoader->getModuleConfigs());

Router::setRoutes($allRoutes);
}

/**
* @return ViewCache
* @throws ConfigException
Expand All @@ -110,22 +86,6 @@ private function setupViewCache(): ViewCache
return $viewCache;
}

/**
* @param $request
* @throws BaseException
* @throws ConfigException
* @throws DebugBarException
* @throws DiException
* @throws ReflectionException
* @throws RouteException
* @throws StopExecutionException
*/
private function initializeRouter($request)
{
$router = new Router($request);
$router->findRoute();
}

/**
* @param Response $response
* @throws ConfigException
Expand Down
36 changes: 18 additions & 18 deletions src/Console/Commands/OpenApiCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@
namespace Quantum\Console\Commands;

use Quantum\Libraries\Storage\Factories\FileSystemFactory;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\Module\Exceptions\ModuleException;
use Quantum\Router\Exceptions\RouteException;
use Quantum\App\Exceptions\BaseException;
use Quantum\Libraries\Storage\FileSystem;
use Quantum\Di\Exceptions\DiException;
use Quantum\Router\RouteCollection;
use Quantum\Module\ModuleLoader;
use Quantum\Router\RouteBuilder;
use Quantum\Console\QtCommand;
use Quantum\Router\Router;
use ReflectionException;
use Quantum\Di\Di;
use OpenApi\Generator;

/**
Expand All @@ -38,7 +37,14 @@ class OpenApiCommand extends QtCommand
* File System
* @var FileSystem
*/
protected $fs;
protected FileSystem $fs;

public function __construct()
{
parent::__construct();

$this->fs = FileSystemFactory::get();
}

/**
* Command name
Expand Down Expand Up @@ -70,45 +76,39 @@ class OpenApiCommand extends QtCommand
* Path to public debug bar resources
* @var string
*/
private $publicOpenApiFolderPath = 'public/assets/OpenApiUi';
private string $publicOpenApiFolderPath = 'public/assets/OpenApiUi';

/**
* Path to vendor debug bar resources
* @var string
*/
private $vendorOpenApiFolderPath = 'vendor/swagger-api/swagger-ui/dist';
private string $vendorOpenApiFolderPath = 'vendor/swagger-api/swagger-ui/dist';

/**
* Exclude File Names
* @var array
*/
private $excludeFileNames = ['index.html', 'swagger-initializer.js', 'favicon-16x16.png', 'favicon-32x32.png'];
private array $excludeFileNames = ['index.html', 'swagger-initializer.js', 'favicon-16x16.png', 'favicon-32x32.png'];

/**
* Executes the command and generate Open API specifications
* @throws ModuleException
* @throws RouteException
* @throws BaseException
*/

/**
* @throws BaseException
* @throws ModuleException
* @throws RouteException
* @throws DiException
* @throws ConfigException
* @throws ReflectionException
*/
public function exec()
{
$moduleLoader = ModuleLoader::getInstance();

$builder = new RouteBuilder();
$allRoutes = $builder->build($moduleLoader->loadModulesRoutes(), $moduleLoader->getModuleConfigs());

Router::setRoutes($allRoutes);
$routeCollection = $builder->build(
$moduleLoader->loadModulesRoutes(),
$moduleLoader->getModuleConfigs()
);

$this->fs = FileSystemFactory::get();
Di::set(RouteCollection::class, $routeCollection);

$module = $this->getArgument('module');

Expand Down
Loading