Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
- Raised the static analysis baseline to PHPStan level `8` with PHP `8.0` target settings (#532)
- Kept Rector aligned to the PHP 8 baseline and applied bounded PHP 8-safe modernization updates (#532)
- Simplified the standard framework file banner across core sources, templates, and affected tests (#532)
- Made Twig an optional renderer dependency so the framework can retain PHP 8.0 support without requiring vulnerable Twig releases (#532)
- Upgraded the PHP 8.0-compatible runtime lines for `psr/log`, `php-curl-class`, `php-debugbar`, `phpmailer`, `voku/html-min`, `swagger-ui`, and `swagger-php` (#532)

### Notes
- OpenAPI metadata remains on Doctrine-style annotations for now because the currently used `swagger-php` attribute scanning path requires PHP `8.1+`; `doctrine/annotations` is intentionally retained on the PHP `8.0` baseline (#532)
- Twig rendering requires a separately installed, security-supported Twig release; Twig `^3.27` requires PHP 8.1 or later, while the default HTML renderer remains available on PHP 8.0 (#532)

## [3.0.0] - 2026-05-28

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
<h2>Features</h2>
<img src="https://quantumphp.io/assets/img/features.png" alt="Quantum" />

<h2>Requirements</h2>

- PHP 8.0 or later

The HTML renderer is available by default and does not require Twig. Twig rendering remains supported as an optional
integration. Install a currently supported Twig release separately before selecting the `twig` renderer:

```bash
composer require twig/twig:^3.27
```

Twig 3.27 and later require PHP 8.1. Applications that must remain on PHP 8.0 should use the default HTML renderer.

<h2>Installation</h2>

<h3> 1. Create new project </h3>
Expand Down
25 changes: 10 additions & 15 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,17 @@
"symfony/console": "^5.4",
"vlucas/phpdotenv": "^5.0",
"dflydev/dot-access-data": "^3.0",
"php-debugbar/php-debugbar": "^1.8",
"phpmailer/phpmailer": "^6.0",
"twig/twig": ">=3.0 <3.26",
"php-curl-class/php-curl-class": "^11.0",
"psr/log": "^1.1",
"php-debugbar/php-debugbar": "^2.2",
"phpmailer/phpmailer": "^7.1",
"php-curl-class/php-curl-class": "^13.0",
"psr/log": "^2.0",
"rakibtg/sleekdb": "^2.13",
"swagger-api/swagger-ui": "^4.14",
"zircote/swagger-php": "~4.4",
"swagger-api/swagger-ui": "^5.32",
"zircote/swagger-php": "^5.8",
"psr/simple-cache": "^1.0",
"vaibhavpandeyvpz/phemail": "^1.1",
"doctrine/annotations": "^2.0",
"voku/html-min": "^4.5",
"voku/html-min": "^5.0",
Comment thread
armanist marked this conversation as resolved.
"league/commonmark": "^2.0",
"ezyang/htmlpurifier": "^4.18",
"povils/figlet": "^0.1.0",
Expand All @@ -69,7 +68,8 @@
"ext-dom": "Required for XML/HTML processing",
"ext-zip": "Required for ZIP file handling",
"ext-memcached": "Required for Memcached support",
"ext-redis": "Required for Redis support"
"ext-redis": "Required for Redis support",
"twig/twig": "Required for the optional Twig renderer; secure Twig ^3.27 releases require PHP 8.1 or later"
},
"autoload": {
"psr-4": {
Expand All @@ -90,10 +90,5 @@
"test": "vendor/bin/phpunit --stderr --coverage-clover coverage.xml"
},
"minimum-stability": "stable",
"prefer-stable": true,
"config": {
"audit": {
"block-insecure": false
}
}
"prefer-stable": true
}
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ parameters:
phpVersion: 80000
level: 8

scanFiles:
- stubs/Twig.stub

paths:
- src

Expand Down
3 changes: 2 additions & 1 deletion src/Logger/Adapters/MessageAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@

use Quantum\Logger\Contracts\ReportableInterface;
use Quantum\Debugger\Debugger;
use Stringable;

/**
* Class MessageAdapter
* @package Quantum\Logger
*/
class MessageAdapter implements ReportableInterface
{
public function report(string $level, string $message, ?array $context = []): void
public function report(string $level, string|Stringable $message, ?array $context = []): void
{
$tab = $context['tab'] ?? Debugger::MESSAGES;

Expand Down
4 changes: 3 additions & 1 deletion src/Logger/Contracts/ReportableInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace Quantum\Logger\Contracts;

use Stringable;

/**
* Interface ReportableInterface
* @package Quantum\Logger
Expand All @@ -20,6 +22,6 @@ interface ReportableInterface
* Reports a message
* @param array<string, mixed>|null $context
*/
public function report(string $level, string $message, ?array $context = []): void;
public function report(string $level, string|Stringable $message, ?array $context = []): void;

}
7 changes: 4 additions & 3 deletions src/Logger/Traits/LoggerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

use Quantum\Logger\Exceptions\LoggerException;
use Quantum\Storage\FileSystem;
use Stringable;

/**
* Trait LoggerTrait
Expand All @@ -33,14 +34,14 @@ abstract protected function initialize(array $params): void;
* Reports a log message
* @param array<string, mixed>|null $context
*/
public function report(string $level, string $message, ?array $context = []): void
public function report(string $level, string|Stringable $message, ?array $context = []): void
{
$this->fs->append($this->logFile, $this->formatMessage($level, $message, $context));
}

/**
* Formats the log message
* @param string|array<string, mixed> $message
* @param string|Stringable|array<string, mixed> $message
* @param array<string, mixed>|null $context
*/
protected function formatMessage(string $level, $message, ?array $context = []): string
Expand All @@ -49,7 +50,7 @@ protected function formatMessage(string $level, $message, ?array $context = []):
'[%s] %s: %s%s',
date('Y-m-d H:i:s'),
ucfirst($level),
is_array($message) ? json_encode($message, JSON_PRETTY_PRINT) : $message,
is_array($message) ? json_encode($message, JSON_PRETTY_PRINT) : (string) $message,
isset($context['trace']) ? (PHP_EOL . $context['trace'] . PHP_EOL) : PHP_EOL
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/Renderer/Adapters/TwigAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class TwigAdapter implements TemplateRendererInterface
*/
public function __construct(?array $configs = [])
{
if (!class_exists(Environment::class)) {
throw RendererException::twigNotInstalled();
}

$this->configs = $configs;

$this->fs = FileSystemFactory::get();
Expand Down
8 changes: 8 additions & 0 deletions src/Renderer/Exceptions/RendererException.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@
*/
class RendererException extends BaseException
{
public static function twigNotInstalled(): self
{
return new self(
'The Twig renderer requires the optional `twig/twig` package. ' .
'Install a security-supported release separately; Twig ^3.27 requires PHP 8.1 or later: ' .
'`composer require twig/twig:^3.27`.'
);
}
}
58 changes: 58 additions & 0 deletions stubs/Twig.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Twig;

use Twig\Loader\FilesystemLoader;

class Environment
{
/**
* @param array<string, mixed> $options
*/
public function __construct(FilesystemLoader $loader, array $options = [])
{
}

/**
* @param array<string, mixed> $context
*/
public function render(string $name, array $context = []): string
{
}

public function addFunction(TwigFunction $function): void
{
}
}

class TwigFunction
{
public function __construct(string $name, callable $callable)
{
}
}

namespace Twig\Loader;

class FilesystemLoader
{
public function __construct(string $path)
{
}
}

namespace Twig\Error;

use Exception;

class LoaderError extends Exception
{
}

class RuntimeError extends Exception
{
}

class SyntaxError extends Exception
{
}
22 changes: 21 additions & 1 deletion tests/Unit/Renderer/Adapters/TwigAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Quantum\Tests\Unit\Renderer\Adapters;

use Quantum\Renderer\Adapters\TwigAdapter;
use Quantum\Renderer\Exceptions\RendererException;
use Quantum\Tests\Unit\AppTestCase;
use Quantum\Router\MatchedRoute;
use Quantum\Router\Route;
Expand All @@ -27,8 +28,12 @@ public function setUp(): void
request()->setMatchedRoute($matchedRoute);
}

public function testHtmlAdapterRenderView(): void
public function testTwigAdapterRenderView(): void
{
if (!class_exists('Twig\Environment')) {
$this->markTestSkipped('The optional twig/twig package is not installed.');
}

$adapter = new TwigAdapter();

$output = $adapter->render('index.twig', ['name' => 'Tester']);
Expand All @@ -38,4 +43,19 @@ public function testHtmlAdapterRenderView(): void
$this->assertSame('<p>Hello Tester, this is rendered twig view</p>', $output);
}

public function testTwigAdapterThrowsActionableExceptionWhenTwigIsMissing(): void
{
if (class_exists('Twig\Environment')) {
$this->markTestSkipped('The optional twig/twig package is installed.');
}

$this->expectException(RendererException::class);
$this->expectExceptionMessage(
'The Twig renderer requires the optional `twig/twig` package. ' .
'Install a security-supported release separately; Twig ^3.27 requires PHP 8.1 or later: ' .
'`composer require twig/twig:^3.27`.'
);

new TwigAdapter();
}
}
4 changes: 4 additions & 0 deletions tests/Unit/Renderer/Factories/RendererFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public function testRendererFactoryGetHtmlAdapter(): void

public function testRendererFactoryTwigAdapter(): void
{
if (!class_exists('Twig\Environment')) {
$this->markTestSkipped('The optional twig/twig package is not installed.');
}

$renderer = RendererFactory::get(RendererType::TWIG);

$this->assertInstanceOf(TemplateRendererInterface::class, $renderer->getAdapter());
Expand Down
4 changes: 4 additions & 0 deletions tests/Unit/Renderer/RendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public function testRendererGetHtmlAdapter(): void

public function testRendererGetTwigAdapter(): void
{
if (!class_exists('Twig\Environment')) {
$this->markTestSkipped('The optional twig/twig package is not installed.');
}

$renderer = new Renderer(new TwigAdapter());

$this->assertInstanceOf(TwigAdapter::class, $renderer->getAdapter());
Expand Down
4 changes: 4 additions & 0 deletions tests/Unit/View/ViewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ public function testRenderPartial(): void

public function testRenderViewWithTwig(): void
{
if (!class_exists('Twig\Environment')) {
$this->markTestSkipped('The optional twig/twig package is not installed.');
}

$factory = Di::get(ViewFactory::class);
$this->setPrivateProperty($factory, 'instance', null);

Expand Down
Loading