diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index f71bc26..ef35c67 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -31,5 +31,15 @@ jobs: - name: Install dependencies run: composer install --no-interaction --no-progress --no-suggest --prefer-dist + - name: Install dependencies for basic example + working-directory: examples/basic + run: composer install --no-interaction --no-progress --no-suggest --prefer-dist + - name: PHP CS Fixer run: vendor/bin/php-cs-fixer fix --dry-run --diff + + - name: PHPStan + run: vendor/bin/phpstan analyse --no-progress + + - name: PHPStan (examples) + run: vendor/bin/phpstan analyse --no-progress -c phpstan.examples.neon diff --git a/chandler/Bootstrap.php b/chandler/Bootstrap.php index 38812c9..c8753a3 100644 --- a/chandler/Bootstrap.php +++ b/chandler/Bootstrap.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Tracy\Debugger; -define("CHANDLER_VER", "0.1.0", false); +define("CHANDLER_VER", "0.1.0"); /** * Bootstrap class, that is called during framework starting phase. @@ -27,18 +27,22 @@ public function __construct(?string $projectRoot = null, bool $skipExtensions = private function ensureDirectoriesCreated(): void { - function makeDir($path) - { - return is_dir($path) || mkdir($path); + $dirs = [ + "/logs", + "/tmp", + "/tmp/cache", + "/tmp/cache/database", + "/tmp/cache/templates", + "/tmp/cache/yaml", + "/tmp/plugins-artifacts", + ]; + + foreach ($dirs as $dir) { + $path = $this->projectRoot . $dir; + if (!is_dir($path)) { + mkdir($path); + } } - - makeDir($this->projectRoot . "/logs"); - makeDir($this->projectRoot . "/tmp"); - makeDir($this->projectRoot . "/tmp/cache"); - makeDir($this->projectRoot . "/tmp/cache/database"); - makeDir($this->projectRoot . "/tmp/cache/templates"); - makeDir($this->projectRoot . "/tmp/cache/yaml"); - makeDir($this->projectRoot . "/tmp/plugins-artifacts"); } /** @@ -118,7 +122,7 @@ private function defineIP(): void $ip = $_SERVER["REMOTE_ADDR"]; } - define("CONNECTING_IP", $ip, false); + define("CONNECTING_IP", $ip); } /** @@ -190,7 +194,7 @@ private function initCaptcha(): void public function ignite(bool $headless = false): void { if (!defined("CHANDLER_ROOT")) { - define("CHANDLER_ROOT", $this->projectRoot, false); + define("CHANDLER_ROOT", $this->projectRoot); } chandler_init_yaml_cache(); diff --git a/chandler/Captcha/CaptchaManager.php b/chandler/Captcha/CaptchaManager.php index 3e70cc4..7ea1a4f 100644 --- a/chandler/Captcha/CaptchaManager.php +++ b/chandler/Captcha/CaptchaManager.php @@ -8,7 +8,7 @@ use Chandler\Session\Session; use Chandler\Patterns\TSimpleSingleton; -class CaptchaManager +final class CaptchaManager { use TSimpleSingleton; diff --git a/chandler/Database/DatabaseConnection.php b/chandler/Database/DatabaseConnection.php index 182d657..12fd254 100644 --- a/chandler/Database/DatabaseConnection.php +++ b/chandler/Database/DatabaseConnection.php @@ -8,7 +8,7 @@ use Nette\Caching\Storages\FileStorage; use Nette\Database\Conventions\DiscoveredConventions; -class DatabaseConnection +final class DatabaseConnection { private static $self = null; diff --git a/chandler/Database/Log.php b/chandler/Database/Log.php index 6854201..fcbfe63 100644 --- a/chandler/Database/Log.php +++ b/chandler/Database/Log.php @@ -54,12 +54,7 @@ public function getTypeNom(): string public function getObjectType(): string { - $type = tr("log_" . $this->getObjectTable()); - if ($type === "@log_" . $this->getObjectTable()) { - return str_replace(CHANDLER_ROOT_CONF["preferences"]["logs"]["entitiesNamespace"], "", $this->getRecord()->object_model); - } else { - return $type; - } + return str_replace(CHANDLER_ROOT_CONF["preferences"]["logs"]["entitiesNamespace"], "", $this->getRecord()->object_model); } public function getObjectName(): string diff --git a/chandler/Eventing/EventDispatcher.php b/chandler/Eventing/EventDispatcher.php index 4317065..ff2cba7 100644 --- a/chandler/Eventing/EventDispatcher.php +++ b/chandler/Eventing/EventDispatcher.php @@ -6,7 +6,7 @@ use Chandler\Patterns\TSimpleSingleton; -class EventDispatcher +final class EventDispatcher { use TSimpleSingleton; private $hooks = []; @@ -20,7 +20,7 @@ public function addListener($hook): bool public function pushEvent(Events\Event $event): Events\Event { - foreach ($hooks as $hook) { + foreach ($this->hooks as $hook) { if ($event instanceof Events\Cancelable) { if ($event->isCancelled()) { break; @@ -28,7 +28,7 @@ public function pushEvent(Events\Event $event): Events\Event } $method = "on" . str_replace("Event", "", get_class($event)); - if (!method_exists($hook, $methodName)) { + if (!method_exists($hook, $method)) { continue; } diff --git a/chandler/Eventing/Events/Cancelable.php b/chandler/Eventing/Events/Cancelable.php index 71362b2..ae44c33 100644 --- a/chandler/Eventing/Events/Cancelable.php +++ b/chandler/Eventing/Events/Cancelable.php @@ -6,8 +6,6 @@ interface Cancelable { - protected $cancelled; - public function cancel(): void; public function isCancelled(): bool; diff --git a/chandler/Extensions/ExtensionManager.php b/chandler/Extensions/ExtensionManager.php index 465e691..27d877d 100644 --- a/chandler/Extensions/ExtensionManager.php +++ b/chandler/Extensions/ExtensionManager.php @@ -7,7 +7,7 @@ use Chandler\Patterns\TSimpleSingleton; use Chandler\MVC\Routing\Router; -class ExtensionManager +final class ExtensionManager { use TSimpleSingleton; private $extensions = []; @@ -68,10 +68,10 @@ private function init(): void $constName = str_replace("-", "_", mb_strtoupper($name)); if (!defined($constName . "_ROOT")) { - define($constName . "_ROOT", $extPath, false); + define($constName . "_ROOT", $extPath); } if (!defined($constName . "_ROOT_CONF")) { - define($constName . "_ROOT_CONF", chandler_parse_yaml("$extPath/$name.yml"), false); + define($constName . "_ROOT_CONF", chandler_parse_yaml("$extPath/$name.yml")); } Router::setExtensionPath($name, $extPath); diff --git a/chandler/MVC/Routing/Router.php b/chandler/MVC/Routing/Router.php index 186c0fd..e2b5464 100644 --- a/chandler/MVC/Routing/Router.php +++ b/chandler/MVC/Routing/Router.php @@ -11,7 +11,7 @@ use Chandler\MVC\IPresenter; use Nette\DI; -class Router +final class Router { use TSimpleSingleton; public const HANDLER_DELIMITER = "%([#@❤]|\->)%"; @@ -221,7 +221,8 @@ public function delegateStatic(string $namespace, string $path, ?array $queryPar $hash = "W/\"" . hash_file("snefru", $file) . "\""; if (isset($_SERVER["HTTP_IF_NONE_MATCH"])) { if ($_SERVER["HTTP_IF_NONE_MATCH"] === $hash) { - exit(header("HTTP/1.1 304")); + header("HTTP/1.1 304"); + exit; } } diff --git a/chandler/Patterns/TSimpleSingleton.php b/chandler/Patterns/TSimpleSingleton.php index 6548046..a35a31f 100644 --- a/chandler/Patterns/TSimpleSingleton.php +++ b/chandler/Patterns/TSimpleSingleton.php @@ -6,13 +6,13 @@ trait TSimpleSingleton { - private static $self = null; + private static ?self $self = null; private function __construct() {} private function __clone() {} public function __wakeup() {} - public static function i() + public static function i(): static { return static::$self ?? static::$self = new static(); } diff --git a/chandler/Security/Authenticator.php b/chandler/Security/Authenticator.php index 1c31c38..9849a82 100644 --- a/chandler/Security/Authenticator.php +++ b/chandler/Security/Authenticator.php @@ -8,7 +8,7 @@ use Chandler\Patterns\TSimpleSingleton; use Chandler\Database\DatabaseConnection; -class Authenticator +final class Authenticator { use TSimpleSingleton; private $db; @@ -23,8 +23,6 @@ private function __construct() $this->session = Session::i(); } - private function verifySuRights(string $uId): bool {} - private function makeToken(string $user, string $ip, string $ua): string { $data = ["user" => $user, "ip" => $ip, "ua" => $ua]; @@ -74,7 +72,7 @@ public function getUser(): ?User $su = $this->session->get("_su"); $cacheKey = $token . "\x00" . ($su ?? ""); - return @$this->cache[$cacheKey] ??= $this->resolveUser($token, $su); + return @self::$cache[$cacheKey] ??= $this->resolveUser($token, $su); } private function resolveUser(string $token, $su) diff --git a/chandler/Security/User.php b/chandler/Security/User.php index 7c6e29b..e155751 100644 --- a/chandler/Security/User.php +++ b/chandler/Security/User.php @@ -14,6 +14,7 @@ * User class. * * @author kurotsun + * @phpstan-consistent-constructor */ class User { diff --git a/chandler/Session/Session.php b/chandler/Session/Session.php index f0e37e8..8b0f3ae 100644 --- a/chandler/Session/Session.php +++ b/chandler/Session/Session.php @@ -13,7 +13,7 @@ * * @author kurotsun */ -class Session +final class Session { use TSimpleSingleton; /** diff --git a/chandler/Signaling/SignalManager.php b/chandler/Signaling/SignalManager.php index 658183b..b042239 100644 --- a/chandler/Signaling/SignalManager.php +++ b/chandler/Signaling/SignalManager.php @@ -14,7 +14,7 @@ * @author kurotsun * @author Vladimir Barinov */ -class SignalManager +final class SignalManager { use TSimpleSingleton; @@ -163,7 +163,7 @@ public function listen(\Closure $callback, int $for, int $time = 25): void "updates" => [], ])); } - } catch (Exception $e) { + } catch (\Exception $e) { error_log("Couldn't connect to Redis server, fallback to old sqlite method. Exception Message: " . $e->getMessage()); } @@ -291,7 +291,7 @@ public function triggerEvent(object $event, int $for): bool try { $redisClient = new RedisClient(CHANDLER_ROOT_CONF["redisUrl"]); $redisClient->publish('im' . $for, json_encode([$id, $event])); - } catch (Exception $e) { + } catch (\Exception $e) { error_log("Couldn't connect to Redis server and push the event. Exception Message: " . $e->getMessage()); } } diff --git a/composer.json b/composer.json index 67d74ba..f4fd80d 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,9 @@ }, "scripts": { "fix": "php-cs-fixer fix", - "lint": "php-cs-fixer fix --dry-run --diff --verbose" + "lint": "php-cs-fixer fix --dry-run --diff --verbose", + "analyse": "phpstan analyse --no-progress", + "analyse:examples": "phpstan analyse --no-progress -c phpstan.examples.neon" }, "require": { "php": "~8.2", @@ -43,6 +45,7 @@ ] }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.90" + "friendsofphp/php-cs-fixer": "^3.90", + "phpstan/phpstan": "^2.2" } } diff --git a/composer.lock b/composer.lock index d6cbbe6..5382034 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8a305c3d22aba624739164d06c741ca1", + "content-hash": "17b66472712481e6935c31a5698c1943", "packages": [ { "name": "doctrine/deprecations", @@ -3401,6 +3401,70 @@ ], "time": "2026-07-30T15:46:02+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.2.8", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e285254e60f33c21902efef4a926ca0987c06804", + "reference": "e285254e60f33c21902efef4a926ca0987c06804", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-08-04T22:21:45+00:00" + }, { "name": "react/cache", "version": "v1.2.0", diff --git a/examples/basic/bootstrap.php b/examples/basic/bootstrap.php index 16e1d99..fc30f80 100644 --- a/examples/basic/bootstrap.php +++ b/examples/basic/bootstrap.php @@ -19,7 +19,7 @@ // ── Project root ───────────────────────────────────────────────── // Tells Chandler where to find logs/, tmp/, cache/, etc. -define("CHANDLER_ROOT", __DIR__, false); +define("CHANDLER_ROOT", __DIR__); // ── YAML cache ─────────────────────────────────────────────────── // Must be called before any config parsing. @@ -29,7 +29,7 @@ // A single YAML file holds both Chandler framework settings // (chandler:) and app-specific settings (helloapp:). $config = chandler_parse_yaml(__DIR__ . "/helloapp.yml"); -define("CHANDLER_ROOT_CONF", $config["chandler"], false); +define("CHANDLER_ROOT_CONF", $config["chandler"]); // ── Register the app as a builtin extension ────────────────────── // Once registered, ExtensionManager will: diff --git a/phpstan.examples.neon b/phpstan.examples.neon new file mode 100644 index 0000000..5ba4da4 --- /dev/null +++ b/phpstan.examples.neon @@ -0,0 +1,11 @@ +parameters: + level: 0 + # Avoid mentiong a path that may lead to examples/basic/vendor + # as it contains a cyclical symlink to Chandler and bloats the check. + # "excludePaths" does not help. + paths: + - examples/basic/bootstrap.php + - examples/basic/init.php + - examples/basic/htdocs/ + - examples/basic/Web/ + tmpDir: tmp/phpstan-examples \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..6d992a2 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 0 + paths: + - chandler/ + - bin/ + tmpDir: tmp/phpstan