From 4fd649802f12b123df9c5e12338329bd76d08ce1 Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 19:39:06 -0500 Subject: [PATCH 1/7] Add PHPStan static analysis for the modules at level 0 Analyses the classes and models of the three modules. Winter's modules are not composer-autoloaded and extend runtime class aliases, so a small bootstrap registers the alias map from modules/system/aliases.php and the module directories are scanned for symbol discovery. The first run surfaced real defects, fixed here: - ThemeExport and ThemeImport built their not-intended-to-be-saved exception message with a broken sprintf placeholder ("The % model"), so the model class name never appeared in the message. - FilterScope declared every configurable property except $default, which was created dynamically on assignment (deprecated since PHP 8.2). - Eleven docblocks promised a return value on paths that return nothing, or the wrong type entirely: FormTabs::getIcon() and getPaneCssClass(), WidgetBase::render(), CmsObject::save(), ComponentManager's registerComponents() and makeComponent(), Router's findByUrl() and setParameters(), the four yaml-backed PluginBase register methods, and UpdateManager's downloadPlugin() and downloadTheme(), which claim to return self but return nothing (no caller chains on them). CombineAssets::getDeepHashFromAssets() claimed void while returning the hash string its only caller concatenates. The baseline carries three deliberate entries: BundleManager's setup handler calls (the closures are rebound to the console command with Closure::bind, so the methods exist at runtime), the new.static warnings on non-final constructors, and CodeParser::handleCorruptCache(), which is a real defect with a fix already in flight in #1511. --- .github/workflows/code-quality.yaml | 20 +++++++ modules/backend/classes/FilterScope.php | 5 ++ modules/backend/classes/FormTabs.php | 4 +- modules/backend/classes/WidgetBase.php | 2 +- modules/cms/classes/CmsObject.php | 2 +- modules/cms/classes/ComponentManager.php | 4 +- modules/cms/classes/Router.php | 4 +- modules/cms/models/ThemeExport.php | 2 +- modules/cms/models/ThemeImport.php | 2 +- modules/system/classes/CombineAssets.php | 2 +- modules/system/classes/PluginBase.php | 8 +-- modules/system/classes/UpdateManager.php | 4 +- phpstan-baseline.neon | 73 ++++++++++++++++++++++++ phpstan-bootstrap.php | 14 +++++ phpstan.neon.dist | 17 ++++++ 15 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 phpstan-baseline.neon create mode 100644 phpstan-bootstrap.php create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index 1147541209..fc1f59dfea 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -37,6 +37,26 @@ jobs: - name: Run code quality checks (on pull request) if: github.event_name == 'pull_request' run: ./.github/workflows/utilities/phpcs-pr ${{ github.base_ref }} + staticAnalysis: + runs-on: ubuntu-latest + name: PHPStan + steps: + - name: Checkout changes + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip + + - name: Install Composer dependencies + run: composer install --no-interaction --no-progress + + - name: Run static analysis + run: | + wget -q -O phpstan.phar https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar + php phpstan.phar analyse --no-progress codeQualityJS: runs-on: ubuntu-latest name: JavaScript diff --git a/modules/backend/classes/FilterScope.php b/modules/backend/classes/FilterScope.php index 1fa39c4e5b..822ef87163 100644 --- a/modules/backend/classes/FilterScope.php +++ b/modules/backend/classes/FilterScope.php @@ -56,6 +56,11 @@ class FilterScope */ public $dependsOn; + /** + * @var mixed Default value for this filter scope. + */ + public $default; + /** * @var string Specifies contextual visibility of this form scope. */ diff --git a/modules/backend/classes/FormTabs.php b/modules/backend/classes/FormTabs.php index 381b82b920..9b3462586e 100644 --- a/modules/backend/classes/FormTabs.php +++ b/modules/backend/classes/FormTabs.php @@ -203,7 +203,7 @@ public function getAllFields() /** * Returns an icon for the tab based on the tab's name. * @param string $name - * @return string + * @return string|null */ public function getIcon($name) { @@ -216,7 +216,7 @@ public function getIcon($name) * Returns a tab pane CSS class. * @param string $index * @param string $label - * @return string + * @return string|null */ public function getPaneCssClass($index = null, $label = null) { diff --git a/modules/backend/classes/WidgetBase.php b/modules/backend/classes/WidgetBase.php index 91f1ec57ae..5e18edc160 100644 --- a/modules/backend/classes/WidgetBase.php +++ b/modules/backend/classes/WidgetBase.php @@ -91,7 +91,7 @@ public function init() /** * Renders the widget's primary contents. - * @return string HTML markup supplied by this widget. + * @return string|null HTML markup supplied by this widget, or null when the widget renders nothing. */ public function render() { diff --git a/modules/cms/classes/CmsObject.php b/modules/cms/classes/CmsObject.php index 4411d61d94..cc6da6e730 100644 --- a/modules/cms/classes/CmsObject.php +++ b/modules/cms/classes/CmsObject.php @@ -199,7 +199,7 @@ public static function inTheme($theme) * Save the object to the theme. * * @param array $options - * @return bool + * @return void */ public function save(?array $options = null) { diff --git a/modules/cms/classes/ComponentManager.php b/modules/cms/classes/ComponentManager.php index 01858ce7c1..101389dc62 100644 --- a/modules/cms/classes/ComponentManager.php +++ b/modules/cms/classes/ComponentManager.php @@ -81,7 +81,7 @@ protected function loadComponents() * }); * * @param callable $definitions - * @return array Array values are class names. + * @return void */ public function registerComponents(callable $definitions) { @@ -195,7 +195,7 @@ public function hasComponent($name) * @param array $properties The properties set by the Page or Layout. * @param bool $isSoftComponent Defines if this is a soft component. * - * @return ComponentBase The component object. + * @return ComponentBase|null The component object, or null for an unresolvable soft component. * @throws SystemException If the (hard) component cannot be found or is not registered. */ public function makeComponent($name, $cmsObject = null, $properties = [], $isSoftComponent = false) diff --git a/modules/cms/classes/Router.php b/modules/cms/classes/Router.php index 04b0f429b4..fcc70a9b49 100644 --- a/modules/cms/classes/Router.php +++ b/modules/cms/classes/Router.php @@ -72,7 +72,7 @@ public function __construct(Theme $theme) /** * Finds a page by its URL. Returns the page object and sets the $parameters property. * @param string $url The requested URL string. - * @return \Cms\Classes\Page Returns \Cms\Classes\Page object or null if the page cannot be found. + * @return \Cms\Classes\Page|null Returns \Cms\Classes\Page object or null if the page cannot be found. */ public function findByUrl($url) { @@ -275,7 +275,7 @@ public function clearCache() /** * Sets the current routing parameters. * @param array $parameters - * @return array + * @return void */ public function setParameters(array $parameters) { diff --git a/modules/cms/models/ThemeExport.php b/modules/cms/models/ThemeExport.php index 83856bd39e..733670a7d1 100644 --- a/modules/cms/models/ThemeExport.php +++ b/modules/cms/models/ThemeExport.php @@ -61,7 +61,7 @@ class ThemeExport extends Model */ public function save(?array $options = null, $sessionKey = null) { - throw new ApplicationException(sprintf("The % model is not intended to be saved, please use %s instead", get_class($this), 'ThemeData')); + throw new ApplicationException(sprintf("The %s model is not intended to be saved, please use %s instead", get_class($this), 'ThemeData')); } public function getFoldersOptions() diff --git a/modules/cms/models/ThemeImport.php b/modules/cms/models/ThemeImport.php index b31da568c7..5a544304c5 100644 --- a/modules/cms/models/ThemeImport.php +++ b/modules/cms/models/ThemeImport.php @@ -66,7 +66,7 @@ class ThemeImport extends Model */ public function save(?array $options = null, $sessionKey = null) { - throw new ApplicationException(sprintf("The % model is not intended to be saved, please use %s instead", get_class($this), 'ThemeData')); + throw new ApplicationException(sprintf("The %s model is not intended to be saved, please use %s instead", get_class($this), 'ThemeData')); } public function getFoldersOptions() diff --git a/modules/system/classes/CombineAssets.php b/modules/system/classes/CombineAssets.php index bb185bad4a..fb1418155a 100644 --- a/modules/system/classes/CombineAssets.php +++ b/modules/system/classes/CombineAssets.php @@ -521,7 +521,7 @@ protected function setHashOnCombinerFilters($hash) /** * Returns a deep hash on filters that support it. * @param array $assets List of asset files. - * @return void + * @return string */ protected function getDeepHashFromAssets($assets) { diff --git a/modules/system/classes/PluginBase.php b/modules/system/classes/PluginBase.php index 466279660b..4fcad0b9b0 100644 --- a/modules/system/classes/PluginBase.php +++ b/modules/system/classes/PluginBase.php @@ -118,7 +118,7 @@ public function registerComponents() /** * Registers back-end navigation items for this plugin. * - * @return array + * @return array|null */ public function registerNavigation() { @@ -141,7 +141,7 @@ public function registerNavigation() /** * Registers back-end quick actions for this plugin. * - * @return array + * @return array|null */ public function registerQuickActions() { @@ -164,7 +164,7 @@ public function registerQuickActions() /** * Registers any back-end permissions used by this plugin. * - * @return array + * @return array|null */ public function registerPermissions() { @@ -177,7 +177,7 @@ public function registerPermissions() /** * Registers any back-end configuration links used by this plugin. * - * @return array + * @return array|null */ public function registerSettings() { diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index b02f2a9699..6d9946ce58 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -655,7 +655,7 @@ public function rollbackPlugin(string $name, ?string $stopOnVersion = null) * @param string $name Plugin name. * @param string $hash Expected file hash. * @param boolean $installation Indicates whether this is a plugin installation request. - * @return self + * @return void */ public function downloadPlugin($name, $hash, $installation = false) { @@ -699,7 +699,7 @@ public function requestThemeDetails($name) * Downloads a theme from the update server. * @param string $name Theme name. * @param string $hash Expected file hash. - * @return self + * @return void */ public function downloadTheme($name, $hash) { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000000..3090b6bbce --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,73 @@ +parameters: + ignoreErrors: + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: modules/backend/classes/MainMenuItem.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: modules/backend/classes/QuickActionItem.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: modules/backend/classes/SideMenuItem.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 2 + path: modules/cms/classes/Asset.php + + - + message: '#^Result of method Cms\\Classes\\CodeParser\:\:handleCorruptCache\(\) \(void\) is used\.$#' + identifier: method.void + count: 1 + path: modules/cms/classes/CodeParser.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 2 + path: modules/cms/classes/ComponentPartial.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 1 + path: modules/cms/classes/Controller.php + + - + message: '#^Unsafe usage of new static\(\)\.$#' + identifier: new.static + count: 3 + path: modules/system/classes/ImageResizer.php + + - + message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:argument\(\)\.$#' + identifier: method.notFound + count: 2 + path: modules/system/classes/asset/BundleManager.php + + - + message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:getFixture\(\)\.$#' + identifier: method.notFound + count: 7 + path: modules/system/classes/asset/BundleManager.php + + - + message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:option\(\)\.$#' + identifier: method.notFound + count: 1 + path: modules/system/classes/asset/BundleManager.php + + - + message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:writeFile\(\)\.$#' + identifier: method.notFound + count: 4 + path: modules/system/classes/asset/BundleManager.php diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php new file mode 100644 index 0000000000..b974adedc1 --- /dev/null +++ b/phpstan-bootstrap.php @@ -0,0 +1,14 @@ +register(); diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000..e94357cf7b --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,17 @@ +includes: + - phpstan-baseline.neon + +parameters: + paths: + - modules/system/classes + - modules/system/models + - modules/backend/classes + - modules/backend/models + - modules/cms/classes + - modules/cms/models + level: 0 + bootstrapFiles: + - phpstan-bootstrap.php + scanDirectories: + - modules + treatPhpDocTypesAsCertain: false From 2d4f7b29f0310769d8aa17b26c3928e93c2746f4 Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 19:47:47 -0500 Subject: [PATCH 2/7] Make the analysis environment reproducible in CI The workflow now installs with --no-scripts and resets the working tree the same way tests.yml does, since composer/installers replaces the checked-out modules with the packaged copies during install. The bootstrap gains an autoloader for the modules' class loader convention, so alias targets resolve without depending on how composer happened to install the module packages. --- .github/workflows/code-quality.yaml | 7 ++++++- phpstan-bootstrap.php | 29 ++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index fc1f59dfea..465809d7b8 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -51,7 +51,12 @@ jobs: extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - name: Install Composer dependencies - run: composer install --no-interaction --no-progress + run: composer install --no-interaction --no-progress --no-scripts + + - name: Reset modules + run: | + git reset --hard + git clean -fd - name: Run static analysis run: | diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index b974adedc1..8cfcf7b969 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -3,12 +3,35 @@ /* * Analysis bootstrap for PHPStan. * - * Winter registers global class aliases (Model, Db, and friends) while the application boots, and - * module classes extend those aliases directly. PHPStan reflects the module classes without an - * application, so the same aliases are registered here from the module's own alias map. + * Two things need registering that a plain composer autoload does not provide: + * + * - The modules follow Winter's class loader convention (StudlyCase namespaces over lowercase + * directories) rather than composer PSR-4, so a matching autoloader is registered here. PHPStan + * only needs it to load classes referenced indirectly, such as alias targets; the module code + * itself is discovered through the scanDirectories setting. + * - Winter registers global class aliases (Model, BackendAuth, and friends) while the application + * boots, and module code references the aliases directly. The same alias map is registered here. */ require __DIR__ . '/vendor/autoload.php'; +spl_autoload_register(function (string $class): void { + foreach (['System' => 'system', 'Backend' => 'backend', 'Cms' => 'cms'] as $prefix => $directory) { + if (str_starts_with($class, $prefix . '\\')) { + $parts = explode('\\', substr($class, strlen($prefix) + 1)); + $file = array_pop($parts) . '.php'; + $path = __DIR__ . '/modules/' . $directory + . '/' . strtolower(implode('/', $parts)) + . ($parts === [] ? '' : '/') . $file; + + if (is_file($path)) { + require_once $path; + } + + return; + } + } +}); + Illuminate\Foundation\AliasLoader::getInstance( require __DIR__ . '/modules/system/aliases.php' )->register(); From d7499cc452da987f54742b6f1c5cc9096e3f1c2c Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 19:57:49 -0500 Subject: [PATCH 3/7] Use larastan, matching storm The lock file is not tracked, so the dependency costs one composer.json line, and storm already maintains the same setup. Larastan's rules immediately paid their way: two vestigial single-argument with() wrappers in UpdateManager and VersionManager, replaced with the direct (new ...)->render() call the syntax has supported since PHP 8.0. The baseline is unchanged. --- .github/workflows/code-quality.yaml | 4 +--- composer.json | 3 ++- modules/system/classes/UpdateManager.php | 2 +- modules/system/classes/VersionManager.php | 2 +- phpstan.neon.dist | 2 ++ 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index 465809d7b8..e511df064d 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -59,9 +59,7 @@ jobs: git clean -fd - name: Run static analysis - run: | - wget -q -O phpstan.phar https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar - php phpstan.phar analyse --no-progress + run: vendor/bin/phpstan analyse --no-progress codeQualityJS: runs-on: ubuntu-latest name: JavaScript diff --git a/composer.json b/composer.json index 49915a8696..6b6a95eba9 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,8 @@ "fakerphp/faker": "^1.9.2", "squizlabs/php_codesniffer": "^3.2", "php-parallel-lint/php-parallel-lint": "^1.0", - "dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support" + "dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support", + "larastan/larastan": "^3.6" }, "repositories": [ { diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index 6d9946ce58..12116ed415 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -896,7 +896,7 @@ public function requestChangelog() protected function write($component, ...$arguments) { if ($this->notesOutput !== null) { - with(new $component($this->notesOutput))->render(...$arguments); + (new $component($this->notesOutput))->render(...$arguments); } return $this; diff --git a/modules/system/classes/VersionManager.php b/modules/system/classes/VersionManager.php index ea2e7735a9..f159ca1dfc 100644 --- a/modules/system/classes/VersionManager.php +++ b/modules/system/classes/VersionManager.php @@ -614,7 +614,7 @@ protected function hasDatabaseHistory($code, $version, $script = null) protected function write($component, ...$arguments) { if ($this->notesOutput !== null) { - with(new $component($this->notesOutput))->render(...$arguments); + (new $component($this->notesOutput))->render(...$arguments); } return $this; diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e94357cf7b..2adaa31660 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,4 +1,5 @@ includes: + - vendor/larastan/larastan/extension.neon - phpstan-baseline.neon parameters: @@ -15,3 +16,4 @@ parameters: scanDirectories: - modules treatPhpDocTypesAsCertain: false + disableSchemaScan: true From d37b34657bd22d8c5ef7d24bacd99d2cecb26c5c Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 22:07:23 -0500 Subject: [PATCH 4/7] Address review: real ClassLoader in the bootstrap, and a smaller baseline - The bootstrap now registers Winter's own ClassLoader for the modules, mirroring modules/system/tests/bootstrap/app.php, instead of a hand-rolled approximation of its convention. - checkout@v7 and a 2G memory limit in the workflow job. - One more baseline entry resolved: CodeParser::handleCorruptCache() declared @return void while returning the repaired cache data on every path, which is what made its caller's use of the result look like a defect. The BundleManager entries were investigated rather than assumed: typing $this inside the handlers converts the errors to protected-method violations, because the closures' static scope stays BundleManager while the runtime rebinding through Closure::call() is what legitimizes the access, so those stay baselined. --- .github/workflows/code-quality.yaml | 4 ++-- modules/cms/classes/CodeParser.php | 2 +- phpstan-baseline.neon | 6 ----- phpstan-bootstrap.php | 36 +++++++++++------------------ 4 files changed, 16 insertions(+), 32 deletions(-) diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml index e511df064d..e00c18212f 100644 --- a/.github/workflows/code-quality.yaml +++ b/.github/workflows/code-quality.yaml @@ -42,7 +42,7 @@ jobs: name: PHPStan steps: - name: Checkout changes - uses: actions/checkout@v2 + uses: actions/checkout@v7 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -59,7 +59,7 @@ jobs: git clean -fd - name: Run static analysis - run: vendor/bin/phpstan analyse --no-progress + run: vendor/bin/phpstan analyse --no-progress --memory-limit=2G codeQualityJS: runs-on: ubuntu-latest name: JavaScript diff --git a/modules/cms/classes/CodeParser.php b/modules/cms/classes/CodeParser.php index 3d022ad7a8..f91ae73818 100644 --- a/modules/cms/classes/CodeParser.php +++ b/modules/cms/classes/CodeParser.php @@ -193,7 +193,7 @@ public function source($page, $layout, $controller) * In some rare cases the cache file will not contain the class * name we expect. When this happens, destroy the corrupt file, * flush the request cache, and repeat the cycle. - * @return void + * @return array */ protected function handleCorruptCache($data) { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3090b6bbce..0971282b68 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -24,12 +24,6 @@ parameters: count: 2 path: modules/cms/classes/Asset.php - - - message: '#^Result of method Cms\\Classes\\CodeParser\:\:handleCorruptCache\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: modules/cms/classes/CodeParser.php - - message: '#^Unsafe usage of new static\(\)\.$#' identifier: new.static diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 8cfcf7b969..024975de72 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -3,34 +3,24 @@ /* * Analysis bootstrap for PHPStan. * - * Two things need registering that a plain composer autoload does not provide: - * - * - The modules follow Winter's class loader convention (StudlyCase namespaces over lowercase - * directories) rather than composer PSR-4, so a matching autoloader is registered here. PHPStan - * only needs it to load classes referenced indirectly, such as alias targets; the module code - * itself is discovered through the scanDirectories setting. - * - Winter registers global class aliases (Model, BackendAuth, and friends) while the application - * boots, and module code references the aliases directly. The same alias map is registered here. + * The modules are loaded through Winter's own class loader rather than composer, so the same + * loader is registered here, mirroring modules/system/tests/bootstrap/app.php. Winter also + * registers global class aliases (Model, BackendAuth, and friends) while the application boots, + * and module code references the aliases directly, so the alias map is registered as well. */ require __DIR__ . '/vendor/autoload.php'; -spl_autoload_register(function (string $class): void { - foreach (['System' => 'system', 'Backend' => 'backend', 'Cms' => 'cms'] as $prefix => $directory) { - if (str_starts_with($class, $prefix . '\\')) { - $parts = explode('\\', substr($class, strlen($prefix) + 1)); - $file = array_pop($parts) . '.php'; - $path = __DIR__ . '/modules/' . $directory - . '/' . strtolower(implode('/', $parts)) - . ($parts === [] ? '' : '/') . $file; +$classLoader = new Winter\Storm\Support\ClassLoader( + new Winter\Storm\Filesystem\Filesystem(), + __DIR__, + __DIR__ . '/storage/framework/classes.php' +); - if (is_file($path)) { - require_once $path; - } +$classLoader->register(); - return; - } - } -}); +foreach (glob(__DIR__ . '/modules/*', GLOB_ONLYDIR) as $modulePath) { + $classLoader->autoloadPackage(basename($modulePath), $modulePath); +} Illuminate\Foundation\AliasLoader::getInstance( require __DIR__ . '/modules/system/aliases.php' From 1671c6ac17f659f5f94f279d9823af81e712e9fb Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 22:16:04 -0500 Subject: [PATCH 5/7] Resolve every baseline entry; delete the baseline Both remaining categories had proper answers after all: - @param-closure-this on registerSetupHandler() and registerScaffoldHandler() declares what Closure::call() does at runtime, resolving the fourteen undefined-method reports inside the handlers and giving handler authors a typed $this in the bargain. - @phpstan-consistent-constructor on the seven classes using new static() documents the constructor contract those factories already rely on, without the API change final constructors would be. PHPStan now reports zero errors with no ignores anywhere. --- modules/backend/classes/MainMenuItem.php | 2 + modules/backend/classes/QuickActionItem.php | 2 + modules/backend/classes/SideMenuItem.php | 2 + modules/cms/classes/Asset.php | 2 + modules/cms/classes/ComponentPartial.php | 2 + modules/cms/classes/Controller.php | 2 + modules/system/classes/ImageResizer.php | 2 + .../system/classes/asset/BundleManager.php | 8 +++ phpstan-baseline.neon | 67 ------------------- phpstan.neon.dist | 1 - 10 files changed, 22 insertions(+), 68 deletions(-) delete mode 100644 phpstan-baseline.neon diff --git a/modules/backend/classes/MainMenuItem.php b/modules/backend/classes/MainMenuItem.php index ef64f562b6..09062f2eba 100644 --- a/modules/backend/classes/MainMenuItem.php +++ b/modules/backend/classes/MainMenuItem.php @@ -6,6 +6,8 @@ * Class MainMenuItem * * @package Backend\Classes + * + * @phpstan-consistent-constructor */ class MainMenuItem { diff --git a/modules/backend/classes/QuickActionItem.php b/modules/backend/classes/QuickActionItem.php index 8dab2e7251..da1b38ff82 100644 --- a/modules/backend/classes/QuickActionItem.php +++ b/modules/backend/classes/QuickActionItem.php @@ -4,6 +4,8 @@ * Class QuickActionItem * * @package Backend\Classes + * + * @phpstan-consistent-constructor */ class QuickActionItem { diff --git a/modules/backend/classes/SideMenuItem.php b/modules/backend/classes/SideMenuItem.php index a7fd9b647d..e397101dc1 100644 --- a/modules/backend/classes/SideMenuItem.php +++ b/modules/backend/classes/SideMenuItem.php @@ -4,6 +4,8 @@ * Class SideMenuItem * * @package Backend\Classes + * + * @phpstan-consistent-constructor */ class SideMenuItem { diff --git a/modules/cms/classes/Asset.php b/modules/cms/classes/Asset.php index 302b0544b0..55f6f5ea7f 100644 --- a/modules/cms/classes/Asset.php +++ b/modules/cms/classes/Asset.php @@ -15,6 +15,8 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @phpstan-consistent-constructor */ class Asset extends Extendable { diff --git a/modules/cms/classes/ComponentPartial.php b/modules/cms/classes/ComponentPartial.php index 3df5200011..a88008b589 100644 --- a/modules/cms/classes/ComponentPartial.php +++ b/modules/cms/classes/ComponentPartial.php @@ -12,6 +12,8 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @phpstan-consistent-constructor */ class ComponentPartial extends Extendable implements CmsObjectContract { diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index d3e855ca33..480ab2895d 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -32,6 +32,8 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @phpstan-consistent-constructor */ class Controller { diff --git a/modules/system/classes/ImageResizer.php b/modules/system/classes/ImageResizer.php index 5f178f4c43..166ca95de4 100644 --- a/modules/system/classes/ImageResizer.php +++ b/modules/system/classes/ImageResizer.php @@ -40,6 +40,8 @@ * @see System\Twig\Extension Twig filters for this class defined * @package winter\wn-system-module * @author Luke Towers + * + * @phpstan-consistent-constructor */ class ImageResizer { diff --git a/modules/system/classes/asset/BundleManager.php b/modules/system/classes/asset/BundleManager.php index 4da992350a..dcc09e8db2 100644 --- a/modules/system/classes/asset/BundleManager.php +++ b/modules/system/classes/asset/BundleManager.php @@ -261,6 +261,10 @@ public function registerBundle(string $name, array $definition): static /** * Registers a single bundle setup handler. + * + * The handler runs bound to the asset command invoking it, through Closure::call(). + * + * @param-closure-this \System\Console\Asset\AssetCreate $closure */ public function registerSetupHandler(string $name, Closure $closure): static { @@ -271,6 +275,10 @@ public function registerSetupHandler(string $name, Closure $closure): static /** * Registers a single bundle scaffold handler. + * + * The handler runs bound to the asset command invoking it, through Closure::call(). + * + * @param-closure-this \System\Console\Asset\AssetCreate $closure */ public function registerScaffoldHandler(string $name, Closure $closure): static { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 0971282b68..0000000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,67 +0,0 @@ -parameters: - ignoreErrors: - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 1 - path: modules/backend/classes/MainMenuItem.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 1 - path: modules/backend/classes/QuickActionItem.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 1 - path: modules/backend/classes/SideMenuItem.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 2 - path: modules/cms/classes/Asset.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 2 - path: modules/cms/classes/ComponentPartial.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 1 - path: modules/cms/classes/Controller.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 3 - path: modules/system/classes/ImageResizer.php - - - - message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:argument\(\)\.$#' - identifier: method.notFound - count: 2 - path: modules/system/classes/asset/BundleManager.php - - - - message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:getFixture\(\)\.$#' - identifier: method.notFound - count: 7 - path: modules/system/classes/asset/BundleManager.php - - - - message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:option\(\)\.$#' - identifier: method.notFound - count: 1 - path: modules/system/classes/asset/BundleManager.php - - - - message: '#^Call to an undefined method System\\Classes\\Asset\\BundleManager\:\:writeFile\(\)\.$#' - identifier: method.notFound - count: 4 - path: modules/system/classes/asset/BundleManager.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 2adaa31660..3425fbd7b9 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,5 @@ includes: - vendor/larastan/larastan/extension.neon - - phpstan-baseline.neon parameters: paths: From 8c5f05f09cfe067e297a96279b4579fdd41282d2 Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 22:46:00 -0500 Subject: [PATCH 6/7] Return the save result from CmsObject::save() Halcyon's inherited update() propagates save()'s return value, and Model::save() answers bool, so swallowing the result here made every successful CmsObject update indistinguishable from a vetoed one: callers checking the documented bool always received null. Returning the parent result restores the contract the docblock now correctly states. throwHalcyonSaveException() gains a @return never annotation; every branch of it throws. --- modules/cms/classes/CmsObject.php | 5 +++-- themes/apitest/testobjects/anotherobj.htm | 1 + themes/apitest/testobjects/compound-markup-settings.htm | 3 +++ themes/apitest/testobjects/compound-markup.htm | 1 + themes/apitest/testobjects/compound.htm | 7 +++++++ themes/apitest/testobjects/existingobj.htm | 1 + themes/apitest/testobjects/testsubdir/mytestobj.htm | 1 + 7 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 themes/apitest/testobjects/anotherobj.htm create mode 100644 themes/apitest/testobjects/compound-markup-settings.htm create mode 100644 themes/apitest/testobjects/compound-markup.htm create mode 100644 themes/apitest/testobjects/compound.htm create mode 100644 themes/apitest/testobjects/existingobj.htm create mode 100644 themes/apitest/testobjects/testsubdir/mytestobj.htm diff --git a/modules/cms/classes/CmsObject.php b/modules/cms/classes/CmsObject.php index cc6da6e730..faef7bdb7f 100644 --- a/modules/cms/classes/CmsObject.php +++ b/modules/cms/classes/CmsObject.php @@ -199,12 +199,12 @@ public static function inTheme($theme) * Save the object to the theme. * * @param array $options - * @return void + * @return bool */ public function save(?array $options = null) { try { - parent::save($options); + return parent::save($options); } catch (Exception $ex) { $this->throwHalcyonSaveException($ex); @@ -322,6 +322,7 @@ public function getTwigCacheKey() /** * Converts an exception type thrown by Halcyon to a native CMS exception. * @param Exception $ex + * @return never */ protected function throwHalcyonSaveException(Exception $ex) { diff --git a/themes/apitest/testobjects/anotherobj.htm b/themes/apitest/testobjects/anotherobj.htm new file mode 100644 index 0000000000..47d2739ba2 --- /dev/null +++ b/themes/apitest/testobjects/anotherobj.htm @@ -0,0 +1 @@ +new content \ No newline at end of file diff --git a/themes/apitest/testobjects/compound-markup-settings.htm b/themes/apitest/testobjects/compound-markup-settings.htm new file mode 100644 index 0000000000..bfee53b07a --- /dev/null +++ b/themes/apitest/testobjects/compound-markup-settings.htm @@ -0,0 +1,3 @@ +var = "value" +== +

Hello, world!

\ No newline at end of file diff --git a/themes/apitest/testobjects/compound-markup.htm b/themes/apitest/testobjects/compound-markup.htm new file mode 100644 index 0000000000..1f19a39b35 --- /dev/null +++ b/themes/apitest/testobjects/compound-markup.htm @@ -0,0 +1 @@ +

Hello, world!

\ No newline at end of file diff --git a/themes/apitest/testobjects/compound.htm b/themes/apitest/testobjects/compound.htm new file mode 100644 index 0000000000..cf46eaeb6f --- /dev/null +++ b/themes/apitest/testobjects/compound.htm @@ -0,0 +1,7 @@ +var = "value" +== + +== +

Hello, world!

\ No newline at end of file diff --git a/themes/apitest/testobjects/existingobj.htm b/themes/apitest/testobjects/existingobj.htm new file mode 100644 index 0000000000..1979fe4583 --- /dev/null +++ b/themes/apitest/testobjects/existingobj.htm @@ -0,0 +1 @@ +str \ No newline at end of file diff --git a/themes/apitest/testobjects/testsubdir/mytestobj.htm b/themes/apitest/testobjects/testsubdir/mytestobj.htm new file mode 100644 index 0000000000..e9bdfffd0b --- /dev/null +++ b/themes/apitest/testobjects/testsubdir/mytestobj.htm @@ -0,0 +1 @@ +mytestcontent \ No newline at end of file From dd97c228c667806f445194afb13840e7db7521b1 Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Thu, 13 Aug 2026 22:41:25 -0500 Subject: [PATCH 7/7] Raise PHPStan to level 1 Level 1 adds undefined-variable, unknown-method and argument-count analysis, which surfaced 180 errors. Every one is accounted for: Real defects fixed: - CmsException took the wrong branch for unknown error codes: the switch has no default, so $result stayed undefined and null !== false passed the is-CMS-exception check. It now starts false. - MediaLibrary passed undefined $type and $key into item construction for directory contents that are neither files nor folders; those entries are now skipped. - Config::package() and the FileManifest constructor were invoked with a vestigial extra argument PHP silently discards. - Uninitialized variables read on edge paths: $success in AutoDatasource, $result in Backend\Controller, $branch in UpdateManager, $resizer in SystemController, and $pluginId in PluginManager's catch blocks. - An unused closure import in BundleManager. Magic contracts documented: - The settings models carry @mixin and @method annotations for the SettingsModel behavior's API, and the attachOne relations, Halcyon builder forwards and the applyKey scope are annotated where used. - Model attributes resolve through magic accessors, declared via universalObjectCratesClasses for the two model base classes; typing them per model is the next ratchet, not a level-1 gate. - A stub corrects the Event facade's listen() arity (storm accepts a priority). - View partials inside model directories are excluded, matching phpcs. Two narrowly scoped, commented ignores remain where Winter's extendable dispatch defeats annotation: the SettingsModel get($key, $default) arity collision with the query builder, and parent::resetDefault() reaching the behavior through __call. --- modules/backend/classes/Controller.php | 2 ++ modules/backend/models/BrandSetting.php | 6 ++++ modules/backend/models/EditorSetting.php | 6 ++++ modules/backend/models/ImportModel.php | 2 ++ modules/backend/models/Preference.php | 6 ++++ modules/cms/classes/AutoDatasource.php | 2 ++ modules/cms/classes/CmsCompoundObject.php | 2 ++ modules/cms/classes/CmsException.php | 2 ++ modules/cms/classes/Page.php | 2 ++ modules/cms/models/MaintenanceSetting.php | 6 ++++ modules/cms/models/ThemeImport.php | 2 ++ modules/system/classes/MediaLibrary.php | 2 ++ modules/system/classes/PluginManager.php | 6 +++- modules/system/classes/SystemController.php | 4 ++- modules/system/classes/UpdateManager.php | 4 ++- .../system/classes/asset/BundleManager.php | 2 +- modules/system/models/LogSetting.php | 6 ++++ modules/system/models/MailBrandSetting.php | 6 ++++ modules/system/models/MailSetting.php | 6 ++++ modules/system/models/Parameter.php | 2 ++ phpstan-stubs/EventFacade.stub | 13 ++++++++ phpstan.neon.dist | 30 ++++++++++++++++++- 22 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 phpstan-stubs/EventFacade.stub diff --git a/modules/backend/classes/Controller.php b/modules/backend/classes/Controller.php index 2259e16509..9d95ef2015 100644 --- a/modules/backend/classes/Controller.php +++ b/modules/backend/classes/Controller.php @@ -307,6 +307,8 @@ public function run($action = null, $params = []) */ $this->setNavigationContext($action, $params); + $result = null; + /* * Execute AJAX event */ diff --git a/modules/backend/models/BrandSetting.php b/modules/backend/models/BrandSetting.php index bd3ae7d8d2..2fd0b9ce31 100644 --- a/modules/backend/models/BrandSetting.php +++ b/modules/backend/models/BrandSetting.php @@ -20,6 +20,12 @@ * @package winter\wn-backend-module * @author Alexey Bobkov, Samuel Georges * @author Winter CMS + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class BrandSetting extends Model { diff --git a/modules/backend/models/EditorSetting.php b/modules/backend/models/EditorSetting.php index 0f6d21ab84..666538dc42 100644 --- a/modules/backend/models/EditorSetting.php +++ b/modules/backend/models/EditorSetting.php @@ -14,6 +14,12 @@ * * @package winter\wn-backend-module * @author Alexey Bobkov, Samuel Georges + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class EditorSetting extends Model { diff --git a/modules/backend/models/ImportModel.php b/modules/backend/models/ImportModel.php index 8552d08b4d..4ed6bf9b56 100644 --- a/modules/backend/models/ImportModel.php +++ b/modules/backend/models/ImportModel.php @@ -12,6 +12,8 @@ * * @package winter\wn-backend-module * @author Alexey Bobkov, Samuel Georges + * + * @method \Winter\Storm\Database\Relations\AttachOne import_file() */ abstract class ImportModel extends Model { diff --git a/modules/backend/models/Preference.php b/modules/backend/models/Preference.php index ee25b9294c..c6655ceab2 100644 --- a/modules/backend/models/Preference.php +++ b/modules/backend/models/Preference.php @@ -17,6 +17,12 @@ * * @package winter\wn-backend-module * @author Alexey Bobkov, Samuel Georges + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class Preference extends Model { diff --git a/modules/cms/classes/AutoDatasource.php b/modules/cms/classes/AutoDatasource.php index 63852ba8ba..524cb4a8ad 100644 --- a/modules/cms/classes/AutoDatasource.php +++ b/modules/cms/classes/AutoDatasource.php @@ -490,6 +490,8 @@ public function update(string $dirName, string $fileName, string $extension, str */ public function delete(string $dirName, string $fileName, string $extension): bool { + $success = false; + try { // Delete from only the active datasource if ($this->forceDeleting) { diff --git a/modules/cms/classes/CmsCompoundObject.php b/modules/cms/classes/CmsCompoundObject.php index 33a516d0a4..b10a1de7cc 100644 --- a/modules/cms/classes/CmsCompoundObject.php +++ b/modules/cms/classes/CmsCompoundObject.php @@ -18,6 +18,8 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @method \Winter\Storm\Halcyon\Collection get(array $columns = ['*']) */ class CmsCompoundObject extends CmsObject { diff --git a/modules/cms/classes/CmsException.php b/modules/cms/classes/CmsException.php index 3a2f05fa39..4ffaa1a344 100644 --- a/modules/cms/classes/CmsException.php +++ b/modules/cms/classes/CmsException.php @@ -67,6 +67,8 @@ public function __construct($message = null, $code = 100, ?Throwable $previous = */ public function processCompoundObject(Throwable $exception) { + $result = false; + switch ($this->code) { case 200: $result = $this->processIni($exception); diff --git a/modules/cms/classes/Page.php b/modules/cms/classes/Page.php index 6bf807eea4..f46587ec41 100644 --- a/modules/cms/classes/Page.php +++ b/modules/cms/classes/Page.php @@ -10,6 +10,8 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @method static \Winter\Storm\Halcyon\Builder sortBy(string $column, string $direction = 'asc') */ class Page extends CmsCompoundObject { diff --git a/modules/cms/models/MaintenanceSetting.php b/modules/cms/models/MaintenanceSetting.php index e81514f87b..c27f3b4a25 100644 --- a/modules/cms/models/MaintenanceSetting.php +++ b/modules/cms/models/MaintenanceSetting.php @@ -12,6 +12,12 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class MaintenanceSetting extends Model { diff --git a/modules/cms/models/ThemeImport.php b/modules/cms/models/ThemeImport.php index 5a544304c5..5bf3d545df 100644 --- a/modules/cms/models/ThemeImport.php +++ b/modules/cms/models/ThemeImport.php @@ -13,6 +13,8 @@ * * @package winter\wn-cms-module * @author Alexey Bobkov, Samuel Georges + * + * @method \Winter\Storm\Database\Relations\AttachOne uploaded_file() */ class ThemeImport extends Model { diff --git a/modules/system/classes/MediaLibrary.php b/modules/system/classes/MediaLibrary.php index 0a28ea0230..3e788b41cd 100644 --- a/modules/system/classes/MediaLibrary.php +++ b/modules/system/classes/MediaLibrary.php @@ -695,6 +695,8 @@ protected function scanFolderContents($fullFolderPath) } elseif ($content['type'] === 'dir') { $type = MediaLibraryItem::TYPE_FOLDER; $key = 'folders'; + } else { + continue; } $libraryItem = $this->initLibraryItem($content, $type); diff --git a/modules/system/classes/PluginManager.php b/modules/system/classes/PluginManager.php index 4715e848ec..78c5b5b3bf 100644 --- a/modules/system/classes/PluginManager.php +++ b/modules/system/classes/PluginManager.php @@ -258,6 +258,8 @@ public function registerAll(bool $force = false): void return; } + $pluginId = null; + try { foreach ($this->plugins as $pluginId => $plugin) { $this->registerPlugin($plugin, $pluginId); @@ -330,7 +332,7 @@ public function registerPlugin(PluginBase $plugin, ?string $pluginId = null): vo */ $configPath = $pluginPath . '/config'; if (File::isDirectory($configPath)) { - Config::package($pluginNamespace, $configPath, $pluginNamespace); + Config::package($pluginNamespace, $configPath); } /* @@ -381,6 +383,8 @@ public function bootAll(bool $force = false): void return; } + $pluginId = null; + try { foreach ($this->plugins as $pluginId => $plugin) { $this->bootPlugin($plugin); diff --git a/modules/system/classes/SystemController.php b/modules/system/classes/SystemController.php index 2b430ceb21..6aa73e0078 100644 --- a/modules/system/classes/SystemController.php +++ b/modules/system/classes/SystemController.php @@ -58,6 +58,8 @@ public function resizer(string $identifier, string $encodedUrl) } // Attempt to process the resize + $resizer = null; + try { $resizer = ImageResizer::fromIdentifier($identifier); $resizer->resize(); @@ -72,7 +74,7 @@ public function resizer(string $identifier, string $encodedUrl) } catch (Exception $ex) { // If it failed for any other reason, restore the config so that // the resizer route will continue to work until it succeeds - if (!empty($resizer)) { + if ($resizer !== null) { $resizer->storeConfig(); } diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index 12116ed415..8902923359 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -424,7 +424,7 @@ public function uninstall() public function getBuildNumberManually($detailed = false) { $source = new SourceManifest(); - $manifest = new FileManifest(null, null, true); + $manifest = new FileManifest(); // Find build by comparing with source manifest return $source->compare($manifest, $detailed); @@ -853,6 +853,8 @@ public function requestChangelog() $build = Parameter::get('system::core.build'); // Determine branch + $branch = null; + if (!is_null($build)) { $branch = explode('.', $build); array_pop($branch); diff --git a/modules/system/classes/asset/BundleManager.php b/modules/system/classes/asset/BundleManager.php index dcc09e8db2..fc83d05007 100644 --- a/modules/system/classes/asset/BundleManager.php +++ b/modules/system/classes/asset/BundleManager.php @@ -75,7 +75,7 @@ public function init(): void ); }); - $manager->registerSetupHandler('react', function (string $packagePath, string $packageType) use ($manager) { + $manager->registerSetupHandler('react', function (string $packagePath, string $packageType) { if ($this->option('no-stubs')) { return; } diff --git a/modules/system/models/LogSetting.php b/modules/system/models/LogSetting.php index 50766acaf8..2421d2caf7 100644 --- a/modules/system/models/LogSetting.php +++ b/modules/system/models/LogSetting.php @@ -7,6 +7,12 @@ * * @package winter\wn-system-module * @author Alexey Bobkov, Samuel Georges + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class LogSetting extends Model { diff --git a/modules/system/models/MailBrandSetting.php b/modules/system/models/MailBrandSetting.php index 8f161d43c8..8109e44020 100644 --- a/modules/system/models/MailBrandSetting.php +++ b/modules/system/models/MailBrandSetting.php @@ -14,6 +14,12 @@ * * @package winter\wn-system-module * @author Alexey Bobkov, Samuel Georges + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class MailBrandSetting extends Model { diff --git a/modules/system/models/MailSetting.php b/modules/system/models/MailSetting.php index 44147f69ff..b919ee1290 100644 --- a/modules/system/models/MailSetting.php +++ b/modules/system/models/MailSetting.php @@ -8,6 +8,12 @@ * * @package winter\wn-system-module * @author Alexey Bobkov, Samuel Georges + * + * @mixin \System\Behaviors\SettingsModel + * @method static static instance() + * @method static bool isConfigured() + * @method static mixed get(string $key, mixed $default = null) + * @method static void resetDefault() */ class MailSetting extends Model { diff --git a/modules/system/models/Parameter.php b/modules/system/models/Parameter.php index 8d7ed42c09..ad7cfdd336 100644 --- a/modules/system/models/Parameter.php +++ b/modules/system/models/Parameter.php @@ -12,6 +12,8 @@ * * @package winter\wn-system-module * @author Alexey Bobkov, Samuel Georges + * + * @method static \Winter\Storm\Database\Builder applyKey(string $key) */ class Parameter extends Model { diff --git a/phpstan-stubs/EventFacade.stub b/phpstan-stubs/EventFacade.stub new file mode 100644 index 0000000000..0a9514d71c --- /dev/null +++ b/phpstan-stubs/EventFacade.stub @@ -0,0 +1,13 @@ +