Skip to content
1 change: 1 addition & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// Syntax & consistency
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => ['default' => 'single_space'],
'cast_spaces' => ['space' => 'single'],
'concat_space' => ['spacing' => 'one'],
'single_quote' => true,
'trailing_comma_in_multiline' => true,
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ 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 model architecture:
- Introduced a base Model class for non-database models
- Refactored QtModel into DbModel with persistence-only responsibilities
- Database fetch methods (first(), findOne(), findOneBy(), get()) now return new model instances instead of mutating existing ones
- Calling create() resets model state to ensure clean inserts
- Database-generated primary keys are now synced into the model after save()
- Models are hydrated only on fetch operations, not implicitly via getters

### Fixed
- PHP 8.1 compatibility: Fixed null parameter deprecations in `explode()`, `parse_url()`, and `str_replace()`
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Thanks for your interest in contributing to Quantum PHP Framework 💡 Whether y

## Before You Start

- Familiarize yourself with the codebase: Quantum PHP Framework is modular — most features live under `/src` (core logic) and `/modules` (demo templates and optional components). Start by reviewing the `Router`, `Controller`, and `QtModel` classes to understand the framework flow.
- Familiarize yourself with the codebase: Quantum PHP Framework is modular — most features live under `/src` (core logic) and `/modules` (demo templates and optional components).
- Start by reviewing the `Router`, `Controller`, and `Model` classes to understand the framework flow.
- Check existing issues and milestones: look for tickets labeled `good first issue`, `help wanted`, or assigned to an upcoming version.
- Don’t hesitate to open a new issue if you find something worth improving.

Expand Down
2 changes: 1 addition & 1 deletion src/App/Helpers/misc.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function random_number(int $length = 10): int
for ($i = 0; $i < $length; $i++) {
$randomString .= random_int(0, 9);
}
return (int)$randomString;
return (int) $randomString;
}

/**
Expand Down
14 changes: 0 additions & 14 deletions src/App/Traits/AppTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
use Quantum\Libraries\Logger\Factories\LoggerFactory;
use Quantum\Libraries\Lang\Exceptions\LangException;
use Quantum\Libraries\Lang\Factories\LangFactory;
use Quantum\Environment\Exceptions\EnvException;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\App\Exceptions\BaseException;
use Quantum\Di\Exceptions\DiException;
use Quantum\Environment\Environment;
use Quantum\Tracer\ErrorHandler;
use Quantum\Loader\Loader;
use Quantum\Config\Config;
Expand Down Expand Up @@ -125,18 +123,6 @@ protected function loadModuleHelperFunctions(): void
$loader->loadDir(App::getBaseDir() . DS . 'modules' . DS . '*' . DS . 'helpers');
}

/**
* @return void
* @throws DiException
* @throws ReflectionException
* @throws EnvException
* @throws BaseException
*/
protected function loadEnvironment()
{
Environment::getInstance()->load(new Setup('config', 'env'));
}

/**
* @return void
* @throws DiException
Expand Down
18 changes: 18 additions & 0 deletions src/App/Traits/ConsoleAppTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@

namespace Quantum\App\Traits;

use Quantum\Environment\Exceptions\EnvException;
use Symfony\Component\Console\Application;
use Quantum\App\Exceptions\BaseException;
use Quantum\Di\Exceptions\DiException;
use Quantum\Console\CommandDiscovery;
use Quantum\Environment\Environment;
use Quantum\Loader\Setup;
use ReflectionException;
use Exception;

Expand All @@ -25,6 +30,19 @@
*/
trait ConsoleAppTrait
{
/**
* @throws DiException
* @throws ReflectionException
* @throws EnvException
* @throws BaseException
*/
protected function loadEnvironment()
{
Environment::getInstance()
->setMutable(true)
->load(new Setup('config', 'env'));
}

/**
* @param string $name
* @param string $version
Expand Down
13 changes: 13 additions & 0 deletions src/App/Traits/WebAppTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
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;
Expand All @@ -40,6 +42,17 @@
*/
trait WebAppTrait
{
/**
* @throws DiException
* @throws ReflectionException
* @throws EnvException
* @throws BaseException
*/
protected function loadEnvironment()
{
Environment::getInstance()->load(new Setup('config', 'env'));
}

/**
* @param Request $request
* @param Response $response
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Commands/KeyGenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@ public function exec()
*/
private function generateRandomKey(): string
{
return bin2hex(random_bytes((int)$this->getOption('length')));
return bin2hex(random_bytes((int) $this->getOption('length')));
}
}
2 changes: 1 addition & 1 deletion src/Console/Commands/MigrationMigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function exec()
return;
}

$step = (int)$this->getOption('step') ?: null;
$step = (int) $this->getOption('step') ?: null;

$migrationManager = new MigrationManager();

Expand Down
1 change: 1 addition & 0 deletions src/Console/Commands/ModuleGenerateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public function exec()
$this->info($moduleName . ' module successfully created');
} catch (Exception $e) {
$this->error($e->getMessage());
exit(1);
}
}
}
82 changes: 58 additions & 24 deletions src/Environment/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Quantum\Environment;

use Quantum\Environment\Exceptions\EnvException;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\App\Exceptions\BaseException;
use Quantum\Di\Exceptions\DiException;
use Quantum\Environment\Enums\Env;
Expand All @@ -36,26 +37,34 @@ class Environment
* Environment file
* @var string
*/
private $envFile = '.env';
private string $envFile = '.env';

/**
* @var bool
*/
private $isMutable = false;
private bool $isMutable = false;

/**
* Loaded env content
* @var array
*/
private $envContent = [];
private array $envContent = [];

private static $appEnv = Env::PRODUCTION;
/**
* @var bool
*/
private bool $loaded = false;

/**
* @var string
*/
private static string $appEnv = Env::PRODUCTION;

/**
* Instance of Environment
* @var Environment
* @var Environment|null
*/
private static $instance = null;
private static ?Environment $instance = null;

/**
* GetInstance
Expand Down Expand Up @@ -91,7 +100,7 @@ public function setMutable(bool $isMutable): Environment
*/
public function load(Setup $setup)
{
if (!empty($this->envContent)) {
if ($this->loaded) {
return;
}

Expand All @@ -101,16 +110,13 @@ public function load(Setup $setup)

$this->envFile = '.env' . ($appEnv !== Env::PRODUCTION ? ".$appEnv" : '');

if (!file_exists(App::getBaseDir() . DS . $this->envFile)) {
if (!fs()->exists($this->getEnvFilePath())) {
throw EnvException::fileNotFound($this->envFile);
}

$dotenv = $this->isMutable
? Dotenv::createMutable(App::getBaseDir(), $this->envFile)
: Dotenv::createImmutable(App::getBaseDir(), $this->envFile);

$this->envContent = $dotenv->load();
$this->envContent = $this->loadDotenvFile();

$this->loaded = true;
self::$appEnv = $appEnv;
}

Expand All @@ -132,17 +138,15 @@ public function getAppEnv(): string
*/
public function getValue(string $key, $default = null)
{
if (empty($this->envContent)) {
if (!$this->loaded) {
throw EnvException::environmentNotLoaded();
}

if (array_key_exists($key, $this->envContent)) {
return $this->envContent[$key];
}

$val = getenv($key);

return $val !== false ? $val : $default;
return $default;
}

/**
Expand All @@ -169,31 +173,36 @@ public function getRow(string $key): ?string
* Creates or updates the row in .env
* @param string $key
* @param string|null $value
* @return void
* @throws BaseException
* @throws DiException
* @throws EnvException
* @throws ReflectionException
* @throws ConfigException
*/
public function updateRow(string $key, ?string $value)
{
if (!$this->isMutable) {
throw EnvException::environmentImmutable();
}

if (empty($this->envContent)) {
if (!$this->loaded) {
throw EnvException::environmentNotLoaded();
}

$envFilePath = $this->getEnvFilePath();
$row = $this->getRow($key);

$envFilePath = App::getBaseDir() . DS . $this->envFile;

if ($row) {
$envFileContent = file_get_contents($envFilePath);
$envFileContent = fs()->get($envFilePath);
$envFileContent = preg_replace('/^' . preg_quote($row, '/') . '/m', $key . '=' . $value, $envFileContent);
file_put_contents($envFilePath, $envFileContent);

fs()->put($envFilePath, $envFileContent);
} else {
file_put_contents($envFilePath, PHP_EOL . $key . '=' . $value . PHP_EOL, FILE_APPEND);
fs()->append($envFilePath, PHP_EOL . $key . '=' . $value . PHP_EOL);
}

$this->envContent = Dotenv::createMutable(App::getBaseDir(), $this->envFile)->load();
$this->envContent = $this->loadDotenvFile(true);
}

/**
Expand All @@ -211,4 +220,29 @@ private function findKeyRow(string $key): ?string

return null;
}

/**
* @param bool $forceMutableReload
* @return array
*/
private function loadDotenvFile(bool $forceMutableReload = false): array
{
$baseDir = App::getBaseDir();

$dotenv = ($forceMutableReload || $this->isMutable)
? Dotenv::createMutable($baseDir, $this->envFile)
: Dotenv::createImmutable($baseDir, $this->envFile);

$loadedVars = $dotenv->load();

return is_array($loadedVars) ? $loadedVars : [];
}

/**
* @return string
*/
private function getEnvFilePath(): string
{
return App::getBaseDir() . DS . $this->envFile;
}
}
4 changes: 2 additions & 2 deletions src/Http/Request/HttpRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public static function getCsrfToken(): ?string
$csrfToken = null;

if (self::has(Csrf::TOKEN_KEY)) {
$csrfToken = (string)self::get(Csrf::TOKEN_KEY);
$csrfToken = (string) self::get(Csrf::TOKEN_KEY);
} elseif (self::hasHeader('X-' . Csrf::TOKEN_KEY)) {
$csrfToken = self::getHeader('X-' . Csrf::TOKEN_KEY);
}
Expand Down Expand Up @@ -275,6 +275,6 @@ private static function setContentType()
*/
private static function setRequestHeaders()
{
self::$__headers = array_change_key_case((array)getallheaders());
self::$__headers = array_change_key_case((array) getallheaders());
}
}
4 changes: 2 additions & 2 deletions src/Http/Traits/Request/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static function getAuthorizationBearer(): ?string
{
$bearerToken = null;

$authorization = (string)self::getHeader('Authorization');
$authorization = (string) self::getHeader('Authorization');

if (self::hasHeader('Authorization') && preg_match('/Bearer\s(\S+)/', $authorization, $matches)) {
$bearerToken = $matches[1];
Expand All @@ -117,7 +117,7 @@ public static function getBasicAuthCredentials(): ?array
return null;
}

$authorization = (string)self::getHeader('Authorization');
$authorization = (string) self::getHeader('Authorization');

if (preg_match('/Basic\s(\S+)/', $authorization, $matches)) {
$decoded = base64_decode($matches[1], true);
Expand Down
2 changes: 1 addition & 1 deletion src/Libraries/Auth/Factories/AuthFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,6 @@ private static function createAuthService(string $adapter): AuthServiceInterface
*/
private static function createJwtInstance(string $adapter): ?JwtToken
{
return $adapter === Auth::JWT ? (new JwtToken())->setLeeway(1)->setClaims((array)config()->get('auth.claims')) : null;
return $adapter === Auth::JWT ? (new JwtToken())->setLeeway(1)->setClaims((array) config()->get('auth.claims')) : null;
}
}
Loading