Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
f4f22f4
[#379] Add `Di::has()` method to check for resolved instances in the …
armanist Apr 7, 2026
6c61080
[#374] Define application execution boundary by calling Di::reset() i…
armanist Apr 7, 2026
a422a30
[#378] Introduce AppContext class for application execution state
armanist Apr 7, 2026
9939253
[#375] Introduce BootStageInterface and BootPipeline abstractions
armanist Apr 7, 2026
8047476
[#376] Extract RegisterCoreDependenciesStage from AppTrait
armanist Apr 7, 2026
23064ad
[#376] Extract LoadHelpersStage from AppTrait
armanist Apr 7, 2026
82c224c
[#376] Extract LoadEnvironmentStage from WebAppTrait and ConsoleAppTrait
armanist Apr 7, 2026
b596f87
[#376] Extract LoadAppConfigStage from AppTrait
armanist Apr 7, 2026
0a62026
[#376] Extract SetupErrorHandlerStage from AppTrait
armanist Apr 7, 2026
7eea2e7
[#376] Extract LoadLanguageStage from AppTrait
armanist Apr 7, 2026
bcb9a2f
[#377] Wire boot pipeline into adapters, replace trait calls with stages
armanist Apr 7, 2026
df4fb00
[#377] Remove deprecated boot methods from AppTrait, WebAppTrait, Con…
armanist Apr 7, 2026
135dc23
[#380] Register Config as DI dependency and route all access through DI
armanist Apr 7, 2026
8f0a41f
[#380] Remove Config singleton, convert static state to instance prop…
armanist Apr 7, 2026
de4713f
[#381] Enhance Di::get() to auto-register instantiable concrete classes
armanist Apr 7, 2026
c5246d8
[#381] Migrate LangFactory from static singleton to DI-managed instance
armanist Apr 7, 2026
f064b91
[#381] Migrate ViewFactory from static singleton to DI-managed instance
armanist Apr 7, 2026
c20c9fd
[#381] Migrate LoggerFactory from static instances cache to DI-manage…
armanist Apr 7, 2026
1692dc2
[#381] Migrate SessionFactory from static instances cache to DI-manag…
armanist Apr 7, 2026
f101a3c
[#381] Migrate CacheFactory from static instances cache to DI-managed…
armanist Apr 7, 2026
4254907
[#381] Migrate FileSystemFactory from static instances cache to DI-ma…
armanist Apr 7, 2026
a91afc3
[#381] Migrate AuthFactory from static instances cache to DI-managed …
armanist Apr 7, 2026
e711bb1
[#381] Migrate RendererFactory from static instances cache to DI-mana…
armanist Apr 7, 2026
12ef4ff
[#381] Migrate MailerFactory from static instances cache to DI-manage…
armanist Apr 7, 2026
cd93ad6
[#381] Migrate CaptchaFactory from static instances cache to DI-manag…
armanist Apr 7, 2026
a42e948
[#381] Migrate CryptorFactory from static instances cache to DI-manag…
armanist Apr 7, 2026
4edcf9c
[#381] Migrate ArchiveFactory from static instances cache to DI-manag…
armanist Apr 7, 2026
1cb59fb
[#381] Remove redundant core dependencies.php and RegisterCoreDepende…
armanist Apr 7, 2026
b4bbf54
[#382] Migrate Server singleton to DI ownership
armanist Apr 7, 2026
1691ced
[#382] Migrate AssetManager singleton to DI ownership
armanist Apr 8, 2026
507a6da
[#382] Migrate Csrf singleton to DI ownership
armanist Apr 8, 2026
b3ee4fc
[#382] Migrate Database singleton to DI ownership
armanist Apr 8, 2026
5793234
[#382] Add unit tests for Csrf helper functions
armanist Apr 8, 2026
3759333
[#382] Migrate MailTrap singleton to DI ownership
armanist Apr 8, 2026
530f433
[#382] Migrate Debugger singleton to DI ownership
armanist Apr 8, 2026
e326da7
[#382] Migrate ViewCache singleton to DI ownership
armanist Apr 8, 2026
ee71a0e
[#382] Migrate ErrorHandler singleton to DI ownership
armanist Apr 9, 2026
3b2ee51
[#382] Migrate HookManager singleton to DI ownership
armanist Apr 9, 2026
8894b98
[#382] Replace fs()->exists() with file_exists() in Environment::load()
armanist Apr 9, 2026
6ab41c5
[#382] Migrate Cookie singleton to DI ownership
armanist Apr 9, 2026
fc883f0
[#382] Migrate ModuleLoader singleton to DI ownership
armanist Apr 9, 2026
17e12e1
Merge branch 'issue/382-singleton-to-di' into issue/373-app-bootstrap…
armanist Apr 9, 2026
bd518fb
[#373] Clean up docblocks, add missing @throws annotations, and moder…
armanist Apr 9, 2026
2fd77b2
[#373] Move SetupErrorHandlerStage into boot pipeline for both adapters
armanist Apr 9, 2026
97362a5
[#454] Merge HttpRequest/HttpResponse into Request/Response, remove f…
armanist Apr 11, 2026
a623729
[#454] Convert Request/Response traits from static to instance-based,…
armanist Apr 13, 2026
7daac46
[#454] Replace static Request calls with request() helper in DemoWeb …
armanist Apr 14, 2026
bfd360a
[#455] Create instance-based DiContainer class extracted from Di
armanist Apr 14, 2026
96951cf
[#455] Refactor Di to static facade delegating to DiContainer
armanist Apr 14, 2026
3926ca1
[#455] Update AppFactory to create DiContainer per execution
armanist Apr 14, 2026
6bf5957
[#455] Update DiTest and add DiContainer isolation tests
armanist Apr 14, 2026
173009b
[#455] Split DiTest into facade and container tests, fix PHPStan throws
armanist Apr 14, 2026
f79fbbd
Merge branch 'issue/455-di-facade-split' into issue/373-app-bootstrap…
armanist Apr 14, 2026
75f6aa4
[#456] Expand AppContext with baseDir, DiContainer, and typed accessors
armanist Apr 14, 2026
869cc14
[#456] Wire AppContext through AppFactory and adapters
armanist Apr 14, 2026
aecfc7b
[#456] Replace App::setBaseDir with App::setContext/getContext, delet…
armanist Apr 14, 2026
8f33766
[#456] Register Environment in DI, migrate all callers from getInstan…
armanist Apr 14, 2026
40a2e8f
[#456] Remove Environment singleton, make appEnv an instance property
armanist Apr 14, 2026
2a70ce7
[#456] Use createArrayBacked for dotenv, simplify Environment key loo…
armanist Apr 14, 2026
be00b16
[#456] Add environment() helper and env check shorthand methods
armanist Apr 14, 2026
8573b62
[#456] Expand AppContext as execution state holder
armanist Apr 14, 2026
c50c0b9
[#456] Guard current_module() against early boot when Request is not …
armanist Apr 14, 2026
8d245a6
[#456] Add getRoutes() accessor to AppContext and tests for all typed…
armanist Apr 15, 2026
6ab9dc5
[#452] Add explicit Di::register() guard to all 12 factory get() methods
armanist Apr 15, 2026
355836b
[#452] Add lazy registration guards to all helper functions
armanist Apr 15, 2026
022285c
[#452] Add explicit registration for Loader, ErrorHandler, Request, R…
armanist Apr 15, 2026
ba7f235
[#452] Add registration guards for Loader, ModuleLoader, Debugger, Vi…
armanist Apr 15, 2026
ed235d8
[#452] Enforce strict Di::get() contract - throw if dependency not re…
armanist Apr 15, 2026
fcf2dd2
[#452] Fix test failures from strict Di::get() contract - add explici…
armanist Apr 15, 2026
c355805
[#373] Extract InitDebuggerStage into boot pipeline
armanist Apr 15, 2026
d597567
[#373] Extract LoadModulesStage for module loading and route building
armanist Apr 15, 2026
cb7f982
[#373] Refactor WebAppAdapter start() into focused private methods in…
armanist Apr 15, 2026
e8749c2
[#373] Update CHANGELOG.md with app bootstrapping and DI ownership re…
armanist Apr 15, 2026
56e3ce9
[#373] Fix PHP 8.x ReflectionProperty::setValue() for static properti…
armanist Apr 15, 2026
c641d74
[#373] Remove redundant Di::get() calls from InitHttpStage
armanist Apr 17, 2026
261977b
[#373] Make AppContext the single owner of DiContainer, Di becomes a …
armanist Apr 17, 2026
a7c960a
[#373] Extract loadConfig() in Config to remove Loader registration d…
armanist Apr 17, 2026
62c7c79
[#373] Remove redundant Debugger registration guard from ViewFactory
armanist Apr 17, 2026
b1a2fa0
[#373] Remove mode from AppContext, move console-specific behavior to…
armanist Apr 17, 2026
7318132
[#373] Clean up import ordering and consolidate docblock @throws anno…
armanist Apr 17, 2026
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
## [3.0.0] - TBD

### Changed
- **BREAKING:** Refactored app bootstrapping and DI ownership model (#373):
- Introduced `AppContext` as the central execution state holder (mode, baseDir, DiContainer, Environment, Config, Request, Response, Routes)
- Split `Di` into a static facade delegating to an instance-based `DiContainer`, one container per application execution
- Added `Di::set()`, `Di::has()`, and `Di::isRegistered()` for explicit service management
- `Di::get()` now enforces a strict contract: throws if the dependency is not explicitly registered (no more implicit auto-registration)
- Introduced `BootPipeline` and `BootStageInterface` for explicit, ordered boot sequences
- Extracted boot stages: `LoadHelpersStage`, `LoadEnvironmentStage`, `LoadAppConfigStage`, `SetupErrorHandlerStage`, `InitHttpStage`, `InitDebuggerStage`
- Removed `AppTrait`; all adapter boot logic moved to pipeline stages and focused private methods in `WebAppTrait`/`ConsoleAppTrait`
- **BREAKING:** Converted `Request` and `Response` from static facades to instance-based classes (#454):
- Merged `HttpRequest`/`HttpResponse` wrapper layer directly into `Request`/`Response`
- All request/response access now goes through `request()` and `response()` helper functions
- **BREAKING:** Removed all static singleton patterns from core services (#381, #382):
- Migrated all 12 first-party factories (Auth, Archive, Cache, Captcha, Cryptor, FileSystem, Lang, Logger, Mailer, Renderer, Session, View) from static instance caches to DI-managed lifetimes
- Migrated service singletons to DI ownership: Cookie, Config, Environment, Server, AssetManager, Csrf, Database, MailTrap, Debugger, ViewCache, ErrorHandler, HookManager, ModuleLoader
- **BREAKING:** `Environment` class is no longer a static singleton (#456):
- Uses `Dotenv::createArrayBacked()` for isolated, deterministic env loading
- New `environment()` helper function and shorthand check methods: `isProduction()`, `isTesting()`, `isStaging()`, `isDevelopment()`, `isLocal()`
- `env()` helper now delegates through `environment()->getValue()`

- **BREAKING:** Minimum PHP version requirement raised from 7.3 to 7.4
- Modernized codebase with PHP 7.4+ syntax using Rector:
- Array destructuring: `list()` → `[]`
Expand Down Expand Up @@ -48,6 +67,11 @@
- Fixed cURL error message assertions for cross-version compatibility

### Added
- `AppContext` class representing the runtime identity of a single application execution
- `DiContainer` instance-based dependency injection container, isolated per execution
- `BootPipeline` and `BootStageInterface` for declarative, ordered boot sequences
- `environment()` global helper and `Environment` shorthand methods (`isProduction()`, `isTesting()`, etc.)
- Lazy registration guards (`Di::register()` + `Di::isRegistered()`) at all DI call sites for explicit dependency management
- 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`
Expand Down Expand Up @@ -75,3 +99,9 @@
### Removed
- Support for PHP 7.3 and earlier versions
- Legacy routing static state and implicit controller resolution via `RouteController`
- `AppTrait` — replaced by boot pipeline stages and adapter-specific traits
- Static singleton patterns from all core services and factories
- `Environment::getInstance()` static singleton accessor
- `HttpRequest`/`HttpResponse` static facade wrapper classes (merged into `Request`/`Response`)
- Implicit auto-registration in `Di::get()` — all dependencies must be explicitly registered
- `RegisterCoreDependenciesStage` and `dependencies.php` — replaced by lazy registration at call sites
4,904 changes: 0 additions & 4,904 deletions coverage.xml

This file was deleted.

21 changes: 4 additions & 17 deletions src/App/Adapters/AppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,19 @@

namespace Quantum\App\Adapters;

use Quantum\App\Exceptions\BaseException;
use Quantum\App\Contracts\AppInterface;
use Quantum\Di\Exceptions\DiException;
use Quantum\App\Traits\AppTrait;
use ReflectionException;
use Quantum\App\AppContext;

/**
* Class AppAdapter
* @package Quantum\App
*/
abstract class AppAdapter implements AppInterface
{
use AppTrait;
protected AppContext $context;

private static string $baseDir;

/**
* @throws BaseException
* @throws DiException
* @throws ReflectionException
*/
public function __construct()
public function __construct(AppContext $context)
{
$this->loadCoreDependencies();
$this->loadComponentHelperFunctions();
$this->loadAppHelperFunctions();
$this->loadModuleHelperFunctions();
$this->context = $context;
}
}
46 changes: 25 additions & 21 deletions src/App/Adapters/ConsoleAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@

use Symfony\Component\Console\Output\ConsoleOutput;
use Quantum\App\Exceptions\StopExecutionException;
use Quantum\Environment\Exceptions\EnvException;
use Symfony\Component\Console\Input\ArgvInput;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\App\Stages\SetupErrorHandlerStage;
use Quantum\App\Stages\LoadEnvironmentStage;
use Symfony\Component\Console\Application;
use Quantum\Lang\Exceptions\LangException;
use Quantum\App\Exceptions\BaseException;
use Quantum\App\Stages\LoadAppConfigStage;
use Quantum\App\Stages\LoadHelpersStage;
use Quantum\App\Traits\ConsoleAppTrait;
use Quantum\Di\Exceptions\DiException;
use ReflectionException;
use Quantum\App\BootPipeline;
use Quantum\App\AppContext;
use Exception;

if (!defined('DS')) {
define('DS', DIRECTORY_SEPARATOR);
Expand All @@ -46,18 +47,30 @@ class ConsoleAppAdapter extends AppAdapter

protected Application $application;

public function __construct()
public function __construct(AppContext $context)
{
parent::__construct();
parent::__construct($context);

$this->input = new ArgvInput();
$this->output = new ConsoleOutput();

$commandName = $this->input->getFirstArgument();

$stages = [
new LoadHelpersStage(),
];

if ($commandName !== 'core:env') {
$this->loadEnvironment();
$this->loadAppConfig();
$stages[] = new LoadEnvironmentStage();
$stages[] = new LoadAppConfigStage();
$stages[] = new SetupErrorHandlerStage();
}

$pipeline = new BootPipeline($stages);
$pipeline->run($this->context);

if ($commandName !== 'core:env') {
environment()->setMutable(true);
}

$this->application = $this->createApplication(
Expand All @@ -67,23 +80,14 @@ public function __construct()
}

/**
* @throws DiException
* @throws EnvException
* @throws BaseException
* @throws ConfigException
* @throws LangException
* @throws ReflectionException
*/
* @throws Exception
*/
public function start(): ?int
{
try {
$this->loadLanguage();

$this->registerCoreCommands();
$this->registerAppCommands();

$this->setupErrorHandler();

$this->validateCommand();

$exitCode = $this->application->run($this->input, $this->output);
Expand Down
128 changes: 32 additions & 96 deletions src/App/Adapters/WebAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,27 @@
namespace Quantum\App\Adapters;

use Quantum\Middleware\Exceptions\MiddlewareException;
use Quantum\Database\Exceptions\DatabaseException;
use Quantum\App\Exceptions\StopExecutionException;
use Quantum\Session\Exceptions\SessionException;
use Quantum\Environment\Exceptions\EnvException;
use Quantum\Loader\Exceptions\LoaderException;
use Quantum\Module\Exceptions\ModuleException;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\App\Stages\SetupErrorHandlerStage;
use Quantum\Router\Exceptions\RouteException;
use Quantum\Http\Exceptions\HttpException;
use Quantum\App\Stages\LoadEnvironmentStage;
use Quantum\Csrf\Exceptions\CsrfException;
use Quantum\Lang\Exceptions\LangException;
use Quantum\Middleware\MiddlewareManager;
use Quantum\App\Stages\LoadAppConfigStage;
use Quantum\App\Exceptions\BaseException;
use Quantum\App\Stages\InitDebuggerStage;
use Quantum\Middleware\MiddlewareManager;
use Quantum\App\Stages\LoadHelpersStage;
use Quantum\App\Stages\InitHttpStage;
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;
use Quantum\Http\Request;
use Quantum\App\BootPipeline;
use Quantum\App\AppContext;
use ReflectionException;
use Quantum\Di\Di;

/**
* Class WebAppAdapter
Expand All @@ -52,110 +47,51 @@ class WebAppAdapter extends AppAdapter
{
use WebAppTrait;

/**
* @var Request
*/
private $request;

/**
* @var Response
*/
private $response;

/**
* @throws BaseException
* @throws ConfigException
* @throws DiException
* @throws EnvException
* @throws ReflectionException
*/
public function __construct()
public function __construct(AppContext $context)
{
parent::__construct();

$this->loadEnvironment();
$this->loadAppConfig();

$this->request = Di::get(Request::class);
$this->response = Di::get(Response::class);
parent::__construct($context);

$pipeline = new BootPipeline([
new LoadHelpersStage(),
new LoadEnvironmentStage(),
new LoadAppConfigStage(),
new SetupErrorHandlerStage(),
new InitHttpStage(),
new InitDebuggerStage(),
]);

$pipeline->run($this->context);
}

/**
* Starts the web app
* @throws BaseException
* @throws ConfigException
* @throws CsrfException
* @throws DatabaseException
* @throws DebugBarException
* @throws DiException
* @throws HttpException
* @throws LangException
* @throws ModuleException
* @throws ReflectionException
* @throws RouteException
* @throws SessionException
* @throws MiddlewareException
* @throws ModuleException|MiddlewareException|LangException|RouteException|CsrfException|ConfigException|DiException|BaseException|LoaderException|ReflectionException
*/
public function start(): ?int
{
try {
$this->initializeRequestResponse($this->request, $this->response);

if ($this->request->isMethod('OPTIONS')) {
if (request()->isMethod('OPTIONS')) {
stop();
}

$this->setupErrorHandler();
$this->initializeDebugger();
$this->loadModules();

$moduleLoader = ModuleLoader::getInstance();

$builder = new RouteBuilder();

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

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);
$matchedRoute = $this->resolveRoute();

$this->loadLanguage();

$debugger = Debugger::getInstance();
if ($debugger->isEnabled()) {
$debugger->addToStoreCell(Debugger::HOOKS, 'info', HookManager::getInstance()->getRegistered());
}

$middlewareManager = new MiddlewareManager($matchedRoute);

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

$viewCache = $this->setupViewCache();
[$request, $response] = (new MiddlewareManager($matchedRoute))->applyMiddlewares(request(), response());

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

$dispatcher = new RouteDispatcher();
$dispatcher->dispatch($matchedRoute, $this->request);
(new RouteDispatcher())->dispatch($matchedRoute, $request);
stop();
} catch (StopExecutionException $exception) {
$this->handleCors($this->response);
$this->response->send();
$this->sendResponse();

return $exception->getCode();
}
Expand Down
19 changes: 12 additions & 7 deletions src/App/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/
class App
{
private static ?string $baseDir = null;
private static ?AppContext $context = null;

private AppInterface $adapter;

Expand All @@ -38,18 +38,23 @@ public function __construct(AppInterface $adapter)
$this->adapter = $adapter;
}

public static function setBaseDir(string $baseDir): void
public static function setContext(AppContext $context): void
{
self::$baseDir = $baseDir;
self::$context = $context;
}

public static function getBaseDir(): string
public static function getContext(): AppContext
{
if (self::$baseDir === null || self::$baseDir === '') {
throw new RuntimeException('Base directory is not initialized.');
if (self::$context === null) {
throw new RuntimeException('AppContext is not initialized.');
}

return self::$baseDir;
return self::$context;
}

public static function getBaseDir(): string
{
return self::getContext()->getBaseDir();
}

public function getAdapter(): AppInterface
Expand Down
Loading
Loading