Merge branch '7.0'

* 7.0:
  Update tools
  Do not use Phive to manage Composer
This commit is contained in:
Sebastian Bergmann 2026-05-13 13:32:02 +02:00
commit 8d0e9c4c32
No known key found for this signature in database
100 changed files with 1480 additions and 718 deletions

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="php-cs-fixer" version="^3.71" installed="3.92.3" location="./tools/php-cs-fixer" copy="true"/>
<phar name="composer" version="^2.8" installed="2.9.3" location="./tools/composer" copy="true"/>
<phar name="php-cs-fixer" version="^3.71" installed="3.95.1" location="./tools/php-cs-fixer" copy="true"/>
<phar name="infection" version="^0.29" installed="0.29.14" location="./tools/infection" copy="true"/>
</phive>

View File

@ -27,6 +27,13 @@
<arg value="update"/>
</exec>
<exec executable="${basedir}/tools/composer" taskname="composer">
<arg value="self-update"/>
<arg value="--no-interaction"/>
<arg value="--no-progress"/>
<arg value="--no-ansi"/>
</exec>
<exec executable="${basedir}/tools/composer" dir="${basedir}/tools/.phpstan" taskname="composer">
<arg value="update"/>
<arg value="--no-interaction"/>

View File

@ -1,10 +1,10 @@
{
"require-dev": {
"phpstan/phpstan": "^2.1.33",
"phpstan/phpstan": "^2.1.54",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-strict-rules": "^2.0.7",
"phpstan/phpstan-strict-rules": "^2.0.11",
"tomasvotruba/type-coverage": "^2.1.0",
"ergebnis/phpstan-rules": "^2.12.0"
"ergebnis/phpstan-rules": "^2.13.1"
},
"config": {
"allow-plugins": {

View File

@ -4,40 +4,40 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4409cdc49212f7e878b14b7ae9285bb2",
"content-hash": "5352dc46c7b6a0ee40e2263c4eac895f",
"packages": [],
"packages-dev": [
{
"name": "ergebnis/phpstan-rules",
"version": "2.12.0",
"version": "2.13.1",
"source": {
"type": "git",
"url": "https://github.com/ergebnis/phpstan-rules.git",
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea"
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/c4e0121a937b3b551f800a86e7d78794da2783ea",
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea",
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/f69db86b98595c34fc1f61c89fe3b380141aa519",
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"phpstan/phpstan": "^2.1.8"
"phpstan/phpstan": "^2.1.35"
},
"require-dev": {
"codeception/codeception": "^4.0.0 || ^5.0.0",
"doctrine/orm": "^2.20.0 || ^3.3.0",
"ergebnis/composer-normalize": "^2.47.0",
"ergebnis/license": "^2.6.0",
"ergebnis/php-cs-fixer-config": "^6.54.0",
"ergebnis/composer-normalize": "^2.49.0",
"ergebnis/license": "^2.7.0",
"ergebnis/php-cs-fixer-config": "^6.59.0",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
"fakerphp/faker": "^1.24.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpstan/phpstan-strict-rules": "^2.0.6",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpunit/phpunit": "^9.6.21",
"psr/container": "^2.0.2",
"symfony/finder": "^5.4.45",
@ -78,20 +78,20 @@
"security": "https://github.com/ergebnis/phpstan-rules/blob/main/.github/SECURITY.md",
"source": "https://github.com/ergebnis/phpstan-rules"
},
"time": "2025-09-07T13:31:33+00:00"
"time": "2026-01-27T17:13:06+00:00"
},
{
"name": "nette/utils",
"version": "v4.1.1",
"version": "v4.1.4",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
"reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
"reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
"url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"shasum": ""
},
"require": {
@ -103,8 +103,10 @@
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5",
"phpstan/phpstan-nette": "^2.0@stable",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@ -165,9 +167,9 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.1.1"
"source": "https://github.com/nette/utils/tree/v4.1.4"
},
"time": "2025-12-22T12:14:32+00:00"
"time": "2026-05-11T20:49:54+00:00"
},
{
"name": "phpstan/extension-installer",
@ -219,11 +221,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.33",
"version": "2.1.54",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"shasum": ""
},
"require": {
@ -268,25 +270,25 @@
"type": "github"
}
],
"time": "2025-12-05T10:24:31+00:00"
"time": "2026-04-29T13:31:09+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "2.0.7",
"version": "2.0.11",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538"
"reference": "9b000a578b85b32945b358b172c7b20e91189024"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538",
"reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024",
"reference": "9b000a578b85b32945b358b172c7b20e91189024",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.29"
"phpstan/phpstan": "^2.1.39"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
@ -312,11 +314,14 @@
"MIT"
],
"description": "Extra strict and opinionated rules for PHPStan",
"keywords": [
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7"
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11"
},
"time": "2025-09-26T11:19:08+00:00"
"time": "2026-05-02T06:54:10+00:00"
},
{
"name": "tomasvotruba/type-coverage",

View File

@ -49,6 +49,9 @@ return array(
'Nette\\Utils\\JsonException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => $vendorDir . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\Paginator' => $vendorDir . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Process' => $vendorDir . '/nette/utils/src/Utils/Process.php',
'Nette\\Utils\\ProcessFailedException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ProcessTimeoutException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Random' => $vendorDir . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => $vendorDir . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => $vendorDir . '/nette/utils/src/Utils/ReflectionMethod.php',

View File

@ -97,6 +97,9 @@ class ComposerStaticInitf9e7218f71d5874b5632927df4f72bd7
'Nette\\Utils\\JsonException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\Paginator' => __DIR__ . '/..' . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Process' => __DIR__ . '/..' . '/nette/utils/src/Utils/Process.php',
'Nette\\Utils\\ProcessFailedException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ProcessTimeoutException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Random' => __DIR__ . '/..' . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => __DIR__ . '/..' . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => __DIR__ . '/..' . '/nette/utils/src/Utils/ReflectionMethod.php',

View File

@ -2,42 +2,42 @@
"packages": [
{
"name": "ergebnis/phpstan-rules",
"version": "2.12.0",
"version_normalized": "2.12.0.0",
"version": "2.13.1",
"version_normalized": "2.13.1.0",
"source": {
"type": "git",
"url": "https://github.com/ergebnis/phpstan-rules.git",
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea"
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/c4e0121a937b3b551f800a86e7d78794da2783ea",
"reference": "c4e0121a937b3b551f800a86e7d78794da2783ea",
"url": "https://api.github.com/repos/ergebnis/phpstan-rules/zipball/f69db86b98595c34fc1f61c89fe3b380141aa519",
"reference": "f69db86b98595c34fc1f61c89fe3b380141aa519",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"phpstan/phpstan": "^2.1.8"
"phpstan/phpstan": "^2.1.35"
},
"require-dev": {
"codeception/codeception": "^4.0.0 || ^5.0.0",
"doctrine/orm": "^2.20.0 || ^3.3.0",
"ergebnis/composer-normalize": "^2.47.0",
"ergebnis/license": "^2.6.0",
"ergebnis/php-cs-fixer-config": "^6.54.0",
"ergebnis/composer-normalize": "^2.49.0",
"ergebnis/license": "^2.7.0",
"ergebnis/php-cs-fixer-config": "^6.59.0",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
"fakerphp/faker": "^1.24.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpstan/phpstan-strict-rules": "^2.0.6",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpunit/phpunit": "^9.6.21",
"psr/container": "^2.0.2",
"symfony/finder": "^5.4.45",
"symfony/process": "^5.4.47"
},
"time": "2025-09-07T13:31:33+00:00",
"time": "2026-01-27T17:13:06+00:00",
"type": "phpstan-extension",
"extra": {
"phpstan": {
@ -78,17 +78,17 @@
},
{
"name": "nette/utils",
"version": "v4.1.1",
"version_normalized": "4.1.1.0",
"version": "v4.1.4",
"version_normalized": "4.1.4.0",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
"reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
"reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
"url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
"shasum": ""
},
"require": {
@ -100,8 +100,10 @@
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5",
"phpstan/phpstan-nette": "^2.0@stable",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
@ -112,7 +114,7 @@
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"time": "2025-12-22T12:14:32+00:00",
"time": "2026-05-11T20:49:54+00:00",
"type": "library",
"extra": {
"branch-alias": {
@ -164,7 +166,7 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.1.1"
"source": "https://github.com/nette/utils/tree/v4.1.4"
},
"install-path": "../nette/utils"
},
@ -221,12 +223,12 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.33",
"version_normalized": "2.1.33.0",
"version": "2.1.54",
"version_normalized": "2.1.54.0",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f",
"reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"shasum": ""
},
"require": {
@ -235,7 +237,7 @@
"conflict": {
"phpstan/phpstan-shim": "*"
},
"time": "2025-12-05T10:24:31+00:00",
"time": "2026-04-29T13:31:09+00:00",
"bin": [
"phpstan",
"phpstan.phar"
@ -277,22 +279,22 @@
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "2.0.7",
"version_normalized": "2.0.7.0",
"version": "2.0.11",
"version_normalized": "2.0.11.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538"
"reference": "9b000a578b85b32945b358b172c7b20e91189024"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538",
"reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024",
"reference": "9b000a578b85b32945b358b172c7b20e91189024",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.29"
"phpstan/phpstan": "^2.1.39"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
@ -300,7 +302,7 @@
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6"
},
"time": "2025-09-26T11:19:08+00:00",
"time": "2026-05-02T06:54:10+00:00",
"type": "phpstan-extension",
"extra": {
"phpstan": {
@ -320,9 +322,12 @@
"MIT"
],
"description": "Extra strict and opinionated rules for PHPStan",
"keywords": [
"static analysis"
],
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7"
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11"
},
"install-path": "../phpstan/phpstan-strict-rules"
},

View File

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'd4d9bba3c5904cdebea5c9bebab27d8abd6c1af1',
'pretty_version' => '7.0.x-dev',
'version' => '7.0.9999999.9999999-dev',
'reference' => '150ed9dd5535041d546fe5416a20dd4be31f6c36',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -11,27 +11,27 @@
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'd4d9bba3c5904cdebea5c9bebab27d8abd6c1af1',
'pretty_version' => '7.0.x-dev',
'version' => '7.0.9999999.9999999-dev',
'reference' => '150ed9dd5535041d546fe5416a20dd4be31f6c36',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'ergebnis/phpstan-rules' => array(
'pretty_version' => '2.12.0',
'version' => '2.12.0.0',
'reference' => 'c4e0121a937b3b551f800a86e7d78794da2783ea',
'pretty_version' => '2.13.1',
'version' => '2.13.1.0',
'reference' => 'f69db86b98595c34fc1f61c89fe3b380141aa519',
'type' => 'phpstan-extension',
'install_path' => __DIR__ . '/../ergebnis/phpstan-rules',
'aliases' => array(),
'dev_requirement' => true,
),
'nette/utils' => array(
'pretty_version' => 'v4.1.1',
'version' => '4.1.1.0',
'reference' => 'c99059c0315591f1a0db7ad6002000288ab8dc72',
'pretty_version' => 'v4.1.4',
'version' => '4.1.4.0',
'reference' => '7da6c396d7ebe142bc857c20479d5e70a5e1aac7',
'type' => 'library',
'install_path' => __DIR__ . '/../nette/utils',
'aliases' => array(),
@ -47,18 +47,18 @@
'dev_requirement' => true,
),
'phpstan/phpstan' => array(
'pretty_version' => '2.1.33',
'version' => '2.1.33.0',
'reference' => '9e800e6bee7d5bd02784d4c6069b48032d16224f',
'pretty_version' => '2.1.54',
'version' => '2.1.54.0',
'reference' => '8be50c3992107dc837b17da4d140fbbdf9a5c5bd',
'type' => 'library',
'install_path' => __DIR__ . '/../phpstan/phpstan',
'aliases' => array(),
'dev_requirement' => true,
),
'phpstan/phpstan-strict-rules' => array(
'pretty_version' => '2.0.7',
'version' => '2.0.7.0',
'reference' => 'd6211c46213d4181054b3d77b10a5c5cb0d59538',
'pretty_version' => '2.0.11',
'version' => '2.0.11.0',
'reference' => '9b000a578b85b32945b358b172c7b20e91189024',
'type' => 'phpstan-extension',
'install_path' => __DIR__ . '/../phpstan/phpstan-strict-rules',
'aliases' => array(),

View File

@ -6,7 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## Unreleased
For a full diff see [`2.12.0...main`][2.12.0...main].
For a full diff see [`2.13.1...main`][2.13.1...main].
## [`2.13.1`][2.13.1]
For a full diff see [`2.13.0...2.13.1`][2.13.0...2.13.1].
### Fixed
- Adjusted `Files\NoPhpstanIgnoreRule` to traverse into all nodes ([#1011]), by [@localheinz]
## [`2.13.0`][2.13.0]
For a full diff see [`2.12.0...2.13.0`][2.12.0...2.13.0].
### Added
- Added `Files\NoPhpstanIgnoreRule`, which reports an error when a `@phpstan-ignore`, `@phpstan-ignore-line`, or `@phpstan-ignore-next-line` tag is used to suppress an error ([#1009]), by [@localheinz]
### Changed
- Required `phpstan/phpstan:^2.1.35` ([#1010]), by [@localheinz]
## [`2.12.0`][2.12.0]
@ -588,6 +608,8 @@ For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
[2.10.5]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.10.5
[2.11.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.11.0
[2.12.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.12.0
[2.13.0]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.13.0
[2.13.1]: https://github.com/ergebnis/phpstan-rules/releases/tag/2.13.1
[362c7ea...0.1.0]: https://github.com/ergebnis/phpstan-rules/compare/362c7ea...0.1.0
[0.1.0...0.2.0]: https://github.com/ergebnis/phpstan-rules/compare/0.1.0...0.2.0
@ -638,7 +660,9 @@ For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
[2.10.4...2.10.5]: https://github.com/ergebnis/phpstan-rules/compare/2.10.4...2.10.5
[2.10.5...2.11.0]: https://github.com/ergebnis/phpstan-rules/compare/2.10.5...2.11.0
[2.11.0...2.12.0]: https://github.com/ergebnis/phpstan-rules/compare/2.11.0...2.12.0
[2.12.0...main]: https://github.com/ergebnis/phpstan-rules/compare/2.12.0...main
[2.12.0...2.13.0]: https://github.com/ergebnis/phpstan-rules/compare/2.12.0...2.13.0
[2.13.0...2.13.1]: https://github.com/ergebnis/phpstan-rules/compare/2.13.0...2.13.1
[2.13.1...main]: https://github.com/ergebnis/phpstan-rules/compare/2.13.1...main
[#1]: https://github.com/ergebnis/phpstan-rules/pull/1
[#4]: https://github.com/ergebnis/phpstan-rules/pull/4
@ -736,6 +760,9 @@ For a full diff see [`362c7ea...0.1.0`][362c7ea...0.1.0].
[#958]: https://github.com/ergebnis/phpstan-rules/pull/958
[#972]: https://github.com/ergebnis/phpstan-rules/pull/972
[#977]: https://github.com/ergebnis/phpstan-rules/pull/977
[#1009]: https://github.com/ergebnis/phpstan-rules/pull/1009
[#1010]: https://github.com/ergebnis/phpstan-rules/pull/1010
[#1011]: https://github.com/ergebnis/phpstan-rules/pull/1011
[@cosmastech]: https://github.com/cosmastech
[@enumag]: https://github.com/enumag

View File

@ -1,6 +1,6 @@
# The MIT License (MIT)
Copyright (c) 2018-2025 Andreas Möller
Copyright (c) 2018-2026 Andreas Möller
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the _Software_), to deal in the Software without restriction, including without limitation the

View File

@ -58,6 +58,7 @@ This package provides the following rules for use with [`phpstan/phpstan`](https
- [`Ergebnis\PHPStan\Rules\Expressions\NoEvalRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoevalrule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoIssetRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoissetrule)
- [`Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule`](https://github.com/ergebnis/phpstan-rules#filesdeclarestricttypesrule)
- [`Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule`](https://github.com/ergebnis/phpstan-rules#filesnophpstanignorerule)
- [`Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#functionsnonullablereturntypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterpassedbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Functions\NoParameterWithNullableTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#functionsnoparameterwithnullabletypedeclarationrule)
@ -348,6 +349,21 @@ parameters:
enabled: false
```
#### `Files\NoPhpstanIgnoreRule`
This rule reports an error when a `@phpstan-ignore`, `@phpstan-ignore-line`, or `@phpstan-ignore-next-line` tag is used to [ignore errors reported by `phpstan/phpstan`](https://phpstan.org/user-guide/ignoring-errors#ignoring-in-code-using-phpdocs).
##### Disabling the rule
You can set the `enabled` parameter to `false` to disable this rule.
```neon
parameters:
ergebnis:
noPhpstanIgnore:
enabled: false
```
### Functions
#### `Functions\NoNullableReturnTypeDeclarationRule`

View File

@ -23,20 +23,20 @@
"require": {
"php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-mbstring": "*",
"phpstan/phpstan": "^2.1.8"
"phpstan/phpstan": "^2.1.35"
},
"require-dev": {
"codeception/codeception": "^4.0.0 || ^5.0.0",
"doctrine/orm": "^2.20.0 || ^3.3.0",
"ergebnis/composer-normalize": "^2.47.0",
"ergebnis/license": "^2.6.0",
"ergebnis/php-cs-fixer-config": "^6.54.0",
"ergebnis/composer-normalize": "^2.49.0",
"ergebnis/license": "^2.7.0",
"ergebnis/php-cs-fixer-config": "^6.59.0",
"ergebnis/phpunit-slow-test-detector": "^2.20.0",
"fakerphp/faker": "^1.24.1",
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.7",
"phpstan/phpstan-strict-rules": "^2.0.6",
"phpstan/phpstan-phpunit": "^2.0.12",
"phpstan/phpstan-strict-rules": "^2.0.8",
"phpunit/phpunit": "^9.6.21",
"psr/container": "^2.0.2",
"symfony/finder": "^5.4.45",

View File

@ -25,6 +25,8 @@ conditionalTags:
phpstan.rules.rule: %ergebnis.noIsset.enabled%
Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule:
phpstan.rules.rule: %ergebnis.declareStrictTypes.enabled%
Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule:
phpstan.rules.rule: %ergebnis.noPhpstanIgnore.enabled%
Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule:
phpstan.rules.rule: %ergebnis.noNullableReturnTypeDeclaration.enabled%
Ergebnis\PHPStan\Rules\Functions\NoParameterPassedByReferenceRule:
@ -102,6 +104,8 @@ parameters:
enabled: %ergebnis.allRules%
noParameterWithNullDefaultValue:
enabled: %ergebnis.allRules%
noPhpstanIgnore:
enabled: %ergebnis.allRules%
noReturnByReference:
enabled: %ergebnis.allRules%
noSwitch:
@ -176,6 +180,9 @@ parametersSchema:
noParameterWithNullDefaultValue: structure([
enabled: bool(),
])
noPhpstanIgnore: structure([
enabled: bool(),
])
noReturnByReference: structure([
enabled: bool(),
])
@ -238,6 +245,9 @@ services:
-
class: Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule
-
class: Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule
-
class: Ergebnis\PHPStan\Rules\Functions\NoNullableReturnTypeDeclarationRule

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
@ -110,6 +110,11 @@ final class ErrorIdentifier
return new self('noNullableReturnTypeDeclaration');
}
public static function noPhpstanIgnore(): self
{
return new self('noPhpstanIgnore');
}
public static function noReturnByReference(): self
{
return new self('noReturnByReference');

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/
namespace Ergebnis\PHPStan\Rules\Files;
use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PhpParser\NodeFinder;
use PHPStan\Analyser;
use PHPStan\Node\FileNode;
use PHPStan\Rules;
/**
* @implements Rules\Rule<FileNode>
*/
final class NoPhpstanIgnoreRule implements Rules\Rule
{
public function getNodeType(): string
{
return FileNode::class;
}
public function processNode(
Node $node,
Analyser\Scope $scope
): array {
$nodeFinder = new NodeFinder();
$foundNodes = $nodeFinder->find($node->getNodes(), static function (Node $node): bool {
return true;
});
$errors = [];
foreach ($foundNodes as $foundNode) {
foreach ($foundNode->getComments() as $comment) {
foreach (\explode("\n", $comment->getText()) as $index => $line) {
if (0 === \preg_match('/@phpstan-ignore(-line|-next-line)?(?=\s|$|,)/', $line, $matches)) {
continue;
}
$tag = $matches[0];
$message = \sprintf(
'Errors reported by phpstan/phpstan should not be ignored via "%s", fix the error or use the baseline instead.',
$tag,
);
$errors[] = Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noPhpstanIgnore()->toString())
->line($comment->getStartLine() + $index)
->nonIgnorable()
->build();
}
}
}
return $errors;
}
}

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -3,7 +3,7 @@
declare(strict_types=1);
/**
* Copyright (c) 2018-2025 Andreas Möller
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.

View File

@ -1,6 +1,4 @@
<?php
declare(strict_types=1);
<?php declare(strict_types=1);
namespace PHPSTORM_META;

View File

@ -20,7 +20,9 @@
"require-dev": {
"nette/tester": "^2.5",
"tracy/tracy": "^2.9",
"phpstan/phpstan-nette": "^2.0@stable",
"phpstan/phpstan": "^2.1@stable",
"phpstan/extension-installer": "^1.4@stable",
"nette/phpstan-rules": "^1.0",
"jetbrains/phpstorm-attributes": "^1.2"
},
"conflict": {
@ -50,5 +52,10 @@
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
}
}
}

View File

@ -1,19 +1,20 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;
/**
* Represents object convertible to HTML string.
*/
interface HtmlStringable
{
/**
* Returns string in HTML format
* Returns string in HTML format.
*/
function __toString(): string;
}

View File

@ -1,28 +1,29 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Iterators;
use Nette;
/**
* Smarter caching iterator.
* Enhanced caching iterator with first/last/counter tracking.
*
* @template TKey
* @template TValue
* @extends \CachingIterator<TKey, TValue, \Iterator<TKey, TValue>>
* @property-read bool $first
* @property-read bool $last
* @property-read bool $empty
* @property-read bool $odd
* @property-read bool $even
* @property-read int $counter
* @property-read mixed $nextKey
* @property-read mixed $nextValue
* @property-read TKey $nextKey
* @property-read TValue $nextValue
*/
class CachingIterator extends \CachingIterator implements \Countable
{
@ -31,6 +32,7 @@ class CachingIterator extends \CachingIterator implements \Countable
private int $counter = 0;
/** @param iterable<TKey, TValue>|\stdClass $iterable */
public function __construct(iterable|\stdClass $iterable)
{
$iterable = $iterable instanceof \stdClass
@ -58,45 +60,30 @@ class CachingIterator extends \CachingIterator implements \Countable
}
/**
* Is the iterator empty?
*/
public function isEmpty(): bool
{
return $this->counter === 0;
}
/**
* Is the counter odd?
*/
public function isOdd(): bool
{
return $this->counter % 2 === 1;
}
/**
* Is the counter even?
*/
public function isEven(): bool
{
return $this->counter % 2 === 0;
}
/**
* Returns the counter.
*/
public function getCounter(): int
{
return $this->counter;
}
/**
* Returns the count of elements.
*/
public function count(): int
{
$inner = $this->getInnerIterator();
@ -131,18 +118,14 @@ class CachingIterator extends \CachingIterator implements \Countable
}
/**
* Returns the next key.
*/
/** @return TKey */
public function getNextKey(): mixed
{
return $this->getInnerIterator()->key();
}
/**
* Returns the next element.
*/
/** @return TValue */
public function getNextValue(): mixed
{
return $this->getInnerIterator()->current();

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Iterators;
@ -15,14 +13,13 @@ namespace Nette\Iterators;
*/
class Mapper extends \IteratorIterator
{
/** @var callable */
private $callback;
private \Closure $callback;
public function __construct(\Traversable $iterator, callable $callback)
{
parent::__construct($iterator);
$this->callback = $callback;
$this->callback = $callback(...);
}

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;
use Nette\Utils\ObjectHelpers;
@ -22,6 +20,7 @@ use Nette\Utils\ObjectHelpers;
trait SmartObject
{
/**
* @param mixed[] $args
* @return mixed
* @throws MemberAccessException
*/
@ -47,6 +46,8 @@ trait SmartObject
/**
* @param mixed[] $args
* @return never
* @throws MemberAccessException
*/
public static function __callStatic(string $name, array $args)

View File

@ -1,17 +1,15 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;
/**
* Static class.
* Prevents instantiation.
*/
trait StaticClass
{

View File

@ -1,17 +1,15 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Localization;
/**
* Translator adapter.
* Translation provider.
*/
interface Translator
{

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -14,7 +12,7 @@ use function count, is_array, is_scalar, sprintf;
/**
* Provides objects to work as array.
* Array-like object with property access.
* @template T
* @implements \IteratorAggregate<array-key, T>
* @implements \ArrayAccess<array-key, T>
@ -39,7 +37,6 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator
/**
* Returns an iterator over all items.
* @return \Iterator<array-key, T>
*/
public function &getIterator(): \Iterator
@ -50,9 +47,6 @@ class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \Iterator
}
/**
* Returns items count.
*/
public function count(): int
{
return count((array) $this);

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -14,13 +12,14 @@ use function array_slice, array_splice, count, is_int;
/**
* Provides the base class for a generic list (items can be accessed by index).
* Generic list with integer indices.
* @template T
* @implements \IteratorAggregate<int, T>
* @implements \ArrayAccess<int, T>
*/
class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
{
/** @var list<T> */
private array $list = [];
@ -41,7 +40,6 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/**
* Returns an iterator over all items.
* @return \Iterator<int, T>
*/
public function &getIterator(): \Iterator
@ -52,9 +50,6 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
}
/**
* Returns items count.
*/
public function count(): int
{
return count($this->list);
@ -63,7 +58,7 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
/**
* Replaces or appends an item.
* @param int|null $index
* @param ?int $index
* @param T $value
* @throws Nette\OutOfRangeException
*/

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use JetBrains\PhpStorm\Language;
@ -16,7 +14,7 @@ use const PREG_GREP_INVERT, PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
/**
* Array tools library.
* Array manipulation utilities.
*/
class Arrays
{
@ -72,14 +70,14 @@ class Arrays
/**
* Recursively merges two fields. It is useful, for example, for merging tree structures. It behaves as
* the + operator for array, ie. it adds a key/value pair from the second array to the first one and retains
* the value from the first array in the case of a key collision.
* Recursively merges two arrays. Useful for merging tree structures. Behaves like the + operator:
* key/value pairs from the second array are added to the first, with the first array's values taking
* precedence on key collisions. Nested arrays are merged recursively instead of replaced.
* @template T1
* @template T2
* @param array<T1> $array1
* @param array<T2> $array2
* @return array<T1|T2>
* @return array<T1|T2|array<mixed>>
*/
public static function mergeTree(array $array1, array $array2): array
{
@ -96,6 +94,7 @@ class Arrays
/**
* Returns zero-indexed position of given array key. Returns null if key is not found.
* @param array<mixed> $array
*/
public static function getKeyOffset(array $array, string|int $key): ?int
{
@ -104,9 +103,10 @@ class Arrays
/**
* @param array<mixed> $array
* @deprecated use getKeyOffset()
*/
public static function searchKey(array $array, $key): ?int
public static function searchKey(array $array, string|int $key): ?int
{
return self::getKeyOffset($array, $key);
}
@ -114,10 +114,11 @@ class Arrays
/**
* Tests an array for the presence of value.
* @param array<mixed> $array
*/
public static function contains(array $array, mixed $value): bool
{
return in_array($value, $array, true);
return in_array($value, $array, strict: true);
}
@ -125,9 +126,11 @@ class Arrays
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K of int|string
* @template V
* @template E
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
* @param ?callable(): E $else
* @return ($else is null ? ?V : V|E)
*/
public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{
@ -142,9 +145,11 @@ class Arrays
* Returns the last item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K of int|string
* @template V
* @template E
* @param array<K, V> $array
* @param ?callable(V, K, array<K, V>): bool $predicate
* @return ?V
* @param ?callable(): E $else
* @return ($else is null ? ?V : V|E)
*/
public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed
{
@ -194,8 +199,10 @@ class Arrays
/**
* Inserts the contents of the $inserted array into the $array immediately after the $key.
* Inserts the contents of the $inserted array into the $array immediately before the $key.
* If $key is null (or does not exist), it is inserted at the beginning.
* @param array<mixed> $array
* @param array<mixed> $inserted
*/
public static function insertBefore(array &$array, string|int|null $key, array $inserted): void
{
@ -207,8 +214,10 @@ class Arrays
/**
* Inserts the contents of the $inserted array into the $array before the $key.
* Inserts the contents of the $inserted array into the $array immediately after the $key.
* If $key is null (or does not exist), it is inserted at the end.
* @param array<mixed> $array
* @param array<mixed> $inserted
*/
public static function insertAfter(array &$array, string|int|null $key, array $inserted): void
{
@ -224,6 +233,7 @@ class Arrays
/**
* Renames key in array.
* @param array<mixed> $array
*/
public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool
{
@ -260,6 +270,8 @@ class Arrays
/**
* Transforms multidimensional array to flat array.
* @param array<mixed> $array
* @return array<mixed>
*/
public static function flatten(array $array, bool $preserveKeys = false): array
{
@ -283,17 +295,19 @@ class Arrays
/**
* Reformats table to associative tree. Path looks like 'field|field[]field->field=field'.
* @param string|string[] $path
* Transforms a flat array of rows into an associative tree using a path expression like 'field|field[]field->field=field'.
* @param array<mixed> $array
* @param string|list<string> $path
* @return array<mixed>|\stdClass
*/
public static function associate(array $array, $path): array|\stdClass
public static function associate(array $array, string|array $path): array|\stdClass
{
$parts = is_array($path)
? $path
: preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') {
throw new Nette\InvalidArgumentException("Invalid path '$path'.");
throw new Nette\InvalidArgumentException("Invalid path '" . (is_array($path) ? implode('', $path) : $path) . "'.");
}
$res = $parts[0] === '->' ? new \stdClass : [];
@ -312,6 +326,8 @@ class Arrays
$x = $row[$parts[$i]];
$row = null;
}
break; // '=' is always the final operation
} elseif ($part === '->') {
if (isset($parts[++$i])) {
if ($x === null) {
@ -337,7 +353,9 @@ class Arrays
/**
* Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling.
* Converts array to associative: items with numeric keys are converted to keys, with $filling as their value.
* @param array<mixed> $array
* @return array<string, mixed>
*/
public static function normalize(array $array, mixed $filling = null): array
{
@ -480,9 +498,10 @@ class Arrays
/**
* Invokes all callbacks and returns array of results.
* @param callable[] $callbacks
* @param iterable<callable> $callbacks
* @return array<mixed>
*/
public static function invoke(iterable $callbacks, ...$args): array
public static function invoke(iterable $callbacks, mixed ...$args): array
{
$res = [];
foreach ($callbacks as $k => $cb) {
@ -495,9 +514,10 @@ class Arrays
/**
* Invokes method on every object in an array and returns array of results.
* @param object[] $objects
* @param iterable<object> $objects
* @return array<mixed>
*/
public static function invokeMethod(iterable $objects, string $method, ...$args): array
public static function invokeMethod(iterable $objects, string $method, mixed ...$args): array
{
$res = [];
foreach ($objects as $k => $obj) {
@ -511,6 +531,7 @@ class Arrays
/**
* Copies the elements of the $array array to the $object object and then returns it.
* @template T of object
* @param iterable<mixed> $array
* @param T $object
* @return T
*/
@ -534,8 +555,7 @@ class Arrays
/**
* Returns copy of the $array where every item is converted to string
* and prefixed by $prefix and suffixed by $suffix.
* Returns a copy of $array where every item is cast to string and wrapped with $prefix and $suffix.
* @param string[] $array
* @return string[]
*/

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -22,21 +20,24 @@ final class Callback
/**
* Invokes internal PHP function with own error handler.
* @param callable-string $function
* @param list<mixed> $args
* @param callable(string, int): (bool|void|null) $onError
*/
public static function invokeSafe(string $function, array $args, callable $onError): mixed
{
$prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
$prev = set_error_handler(function (int $severity, string $message, string $file, int $line) use ($onError, &$prev, $function): bool {
if ($file === __FILE__) {
$msg = ini_get('html_errors')
? Html::htmlToText($message)
: $message;
$msg = preg_replace("#^$function\\(.*?\\): #", '', $msg);
$msg = (string) preg_replace("#^$function\\(.*?\\): #", '', $msg);
if ($onError($msg, $severity) !== false) {
return null;
return true;
}
}
return $prev ? $prev(...func_get_args()) : false;
return $prev ? $prev(...func_get_args()) !== false : false;
});
try {
@ -48,12 +49,12 @@ final class Callback
/**
* Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies
* that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists.
* Checks that $callable is a valid PHP callback and returns it. With $syntax set to true, only verifies
* the structural validity without checking whether the class or method actually exists.
* @return callable
* @throws Nette\InvalidArgumentException
*/
public static function check(mixed $callable, bool $syntax = false)
public static function check(mixed $callable, bool $syntax = false): mixed
{
if (!is_callable($callable, $syntax)) {
throw new Nette\InvalidArgumentException(
@ -87,7 +88,7 @@ final class Callback
* @param callable $callable type check is escalated to ReflectionException
* @throws \ReflectionException if callback is not valid
*/
public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
public static function toReflection(mixed $callable): \ReflectionMethod|\ReflectionFunction
{
if ($callable instanceof \Closure) {
$callable = self::unwrap($callable);
@ -100,6 +101,7 @@ final class Callback
} elseif (is_object($callable) && !$callable instanceof \Closure) {
return new ReflectionMethod($callable, '__invoke');
} else {
assert($callable instanceof \Closure || is_string($callable));
return new \ReflectionFunction($callable);
}
}
@ -116,8 +118,9 @@ final class Callback
/**
* Unwraps closure created by Closure::fromCallable().
* @return callable|array{object|class-string, string}|string
*/
public static function unwrap(\Closure $closure): callable|array
public static function unwrap(\Closure $closure): callable|array|string
{
$r = new \ReflectionFunction($closure);
$class = $r->getClosureScopeClass()?->name;

View File

@ -1,19 +1,17 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use function array_merge, checkdate, implode, is_numeric, is_string, preg_replace_callback, sprintf, time, trim;
/**
* DateTime.
* Extends PHP's DateTime with strict validation and additional factory methods.
*/
class DateTime extends \DateTime implements \JsonSerializable
{
@ -142,7 +140,7 @@ class DateTime extends \DateTime implements \JsonSerializable
}
private function apply(string $datetime, $timezone = null, bool $ctr = false): void
private function apply(string $datetime, ?\DateTimeZone $timezone = null, bool $ctr = false): void
{
$relPart = '';
$absPart = preg_replace_callback(
@ -189,7 +187,7 @@ class DateTime extends \DateTime implements \JsonSerializable
/**
* You'd better use: (clone $dt)->modify(...)
* Returns a modified copy of the object. Use (clone $dt)->modify(...) for better type safety.
*/
public function modifyClone(string $modify = ''): static
{

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -19,14 +17,12 @@ use const DIRECTORY_SEPARATOR;
*/
final class FileInfo extends \SplFileInfo
{
private readonly string $relativePath;
public function __construct(string $file, string $relativePath = '')
{
public function __construct(
string $file,
private readonly string $relativePath = '',
) {
parent::__construct($file);
$this->setInfoClass(static::class);
$this->relativePath = $relativePath;
$this->setInfoClass(self::class);
}

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -52,14 +50,15 @@ final class FileSystem
} elseif (is_dir($origin)) {
static::createDir($target);
foreach (new \FilesystemIterator($target) as $item) {
\assert($item instanceof \SplFileInfo);
static::delete($item->getPathname());
}
foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
if ($item->isDir()) {
static::createDir($target . '/' . $iterator->getSubPathName());
static::createDir($target . '/' . $iterator->getSubPathname());
} else {
static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName());
static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathname());
}
}
} else {
@ -112,6 +111,7 @@ final class FileSystem
}
} elseif (is_dir($path)) {
foreach (new \FilesystemIterator($path) as $item) {
\assert($item instanceof \SplFileInfo);
static::delete($item->getPathname());
}
@ -208,7 +208,7 @@ final class FileSystem
/**
* Writes the string to a file.
* Writes the string to a file. Creates the parent directory if it does not exist. Pass null as $mode to skip chmod.
* @throws Nette\IOException on error occurred
*/
public static function write(string $file, string $content, ?int $mode = 0o666): void
@ -251,6 +251,7 @@ final class FileSystem
}
} elseif (is_dir($path)) {
foreach (new \FilesystemIterator($path) as $item) {
\assert($item instanceof \SplFileInfo);
static::makeWritable($item->getPathname(), $dirMode, $fileMode);
}
@ -277,6 +278,19 @@ final class FileSystem
}
/**
* Determines whether the string is a valid cross-platform filename without any path information.
*/
public static function isValidFilename(string $name): bool
{
[$stem] = explode('.', $name, 2);
return $name !== '' && $name !== '.' && $name !== '..'
&& !preg_match('#[\x00-\x1F<>:"|?*\\\/]#', $name) // control and reserved characters
&& !str_ends_with($name, '.') && !str_ends_with($name, ' ') // trailing dots/spaces
&& !preg_match('#^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$#i', $stem); // Windows reserved device names
}
/**
* Normalizes `..` and `.` and directory separators in path.
*/

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -15,7 +13,7 @@ use const GLOB_NOESCAPE, GLOB_NOSORT, GLOB_ONLYDIR;
/**
* Finder allows searching through directory trees using iterator.
* Searches for files and directories in directory trees.
*
* Finder::findFiles('*.php')
* ->size('> 10kB')
@ -32,24 +30,25 @@ class Finder implements \IteratorAggregate
/** @var string[] */
private array $in = [];
/** @var \Closure[] */
/** @var array<\Closure(FileInfo): bool> */
private array $filters = [];
/** @var \Closure[] */
/** @var array<\Closure(FileInfo): bool> */
private array $descentFilters = [];
/** @var array<string|self> */
private array $appends = [];
private bool $childFirst = false;
/** @var ?callable */
private $sort;
/** @var ?(\Closure(FileInfo, FileInfo): int) */
private ?\Closure $sort = null;
private int $maxDepth = -1;
private bool $ignoreUnreadableDirs = true;
/**
* Begins search for files and directories matching mask.
* @param string|list<string> $masks
*/
public static function find(string|array $masks = ['*']): static
{
@ -60,6 +59,7 @@ class Finder implements \IteratorAggregate
/**
* Begins search for files matching mask.
* @param string|list<string> $masks
*/
public static function findFiles(string|array $masks = ['*']): static
{
@ -70,6 +70,7 @@ class Finder implements \IteratorAggregate
/**
* Begins search for directories matching mask.
* @param string|list<string> $masks
*/
public static function findDirectories(string|array $masks = ['*']): static
{
@ -80,6 +81,7 @@ class Finder implements \IteratorAggregate
/**
* Finds files matching the specified masks.
* @param string|list<string> $masks
*/
public function files(string|array $masks = ['*']): static
{
@ -89,6 +91,7 @@ class Finder implements \IteratorAggregate
/**
* Finds directories matching the specified masks.
* @param string|list<string> $masks
*/
public function directories(string|array $masks = ['*']): static
{
@ -96,6 +99,7 @@ class Finder implements \IteratorAggregate
}
/** @param list<string> $masks */
private function addMask(array $masks, string $mode): static
{
foreach ($masks as $mask) {
@ -117,6 +121,7 @@ class Finder implements \IteratorAggregate
/**
* Searches in the given directories. Wildcards are allowed.
* @param string|list<string> $paths
*/
public function in(string|array $paths): static
{
@ -128,6 +133,7 @@ class Finder implements \IteratorAggregate
/**
* Searches recursively from the given directories. Wildcards are allowed.
* @param string|list<string> $paths
*/
public function from(string|array $paths): static
{
@ -137,6 +143,7 @@ class Finder implements \IteratorAggregate
}
/** @param list<string> $paths */
private function addLocation(array $paths, string $ext): void
{
foreach ($paths as $path) {
@ -170,12 +177,12 @@ class Finder implements \IteratorAggregate
/**
* Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory.
* Sets a comparison function for sorting entries within each directory.
* @param callable(FileInfo, FileInfo): int $callback
*/
public function sortBy(callable $callback): static
{
$this->sort = $callback;
$this->sort = $callback(...);
return $this;
}
@ -191,7 +198,8 @@ class Finder implements \IteratorAggregate
/**
* Adds the specified paths or appends a new finder that returns.
* Appends the specified file paths to results. Passing null creates and returns a new sub-finder whose results are appended.
* @param string|list<string>|null $paths
*/
public function append(string|array|null $paths = null): static
{
@ -209,6 +217,7 @@ class Finder implements \IteratorAggregate
/**
* Skips entries that matches the given masks relative to the ones defined with the in() or from() methods.
* @param string|list<string> $masks
*/
public function exclude(string|array $masks): static
{
@ -239,7 +248,7 @@ class Finder implements \IteratorAggregate
*/
public function filter(callable $callback): static
{
$this->filters[] = \Closure::fromCallable($callback);
$this->filters[] = $callback(...);
return $this;
}
@ -250,7 +259,7 @@ class Finder implements \IteratorAggregate
*/
public function descentFilter(callable $callback): static
{
$this->descentFilters[] = \Closure::fromCallable($callback);
$this->descentFilters[] = $callback(...);
return $this;
}
@ -267,6 +276,7 @@ class Finder implements \IteratorAggregate
/**
* Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB
* @param '>'|'>='|'<'|'<='|'='|'=='|'==='|'!='|'!=='|'<>' $operator or predicate string
*/
public function size(string $operator, ?int $size = null): static
{
@ -277,7 +287,7 @@ class Finder implements \IteratorAggregate
[, $operator, $size, $unit] = $matches;
$units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
$size *= $units[strtolower($unit)];
$size = (float) $size * $units[strtolower($unit)];
$operator = $operator ?: '=';
}
@ -287,6 +297,7 @@ class Finder implements \IteratorAggregate
/**
* Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23
* @param '>'|'>='|'<'|'<='|'='|'=='|'==='|'!='|'!=='|'<>' $operator or predicate string
*/
public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static
{
@ -401,6 +412,7 @@ class Finder implements \IteratorAggregate
}
/** @param iterable<string> $pathNames */
private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator
{
foreach ($pathNames as $pathName) {
@ -413,6 +425,10 @@ class Finder implements \IteratorAggregate
}
/**
* @param (\Closure(FileInfo): bool)[] $filters
* @param array<int, bool> $cache
*/
private function proveFilters(array $filters, FileInfo $file, array &$cache): bool
{
foreach ($filters as $filter) {
@ -468,6 +484,7 @@ class Finder implements \IteratorAggregate
/**
* Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal.
* @return array{string, string, bool}
*/
private static function splitRecursivePart(string $path): array
{

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -14,6 +12,9 @@ use function array_unique, ini_get, levenshtein, max, min, ob_end_clean, ob_get_
use const PHP_OS_FAMILY;
/**
* Miscellaneous utilities.
*/
class Helpers
{
public const IsWindows = PHP_OS_FAMILY === 'Windows';
@ -21,6 +22,7 @@ class Helpers
/**
* Executes a callback and returns the captured output as a string.
* @param callable(): void $func
*/
public static function capture(callable $func): string
{
@ -37,7 +39,7 @@ class Helpers
/**
* Returns the last occurred PHP error or an empty string if no error occurred. Unlike error_get_last(),
* it is nit affected by the PHP directive html_errors and always returns text, not HTML.
* it is not affected by the PHP directive html_errors and always returns text, not HTML.
*/
public static function getLastError(): string
{
@ -59,6 +61,7 @@ class Helpers
/**
* Returns value clamped to the inclusive range of min and max.
* @return ($value is float ? float : ($min is float ? float : ($max is float ? float : int)))
*/
public static function clamp(int|float $value, int|float $min, int|float $max): int|float
{
@ -71,7 +74,7 @@ class Helpers
/**
* Looks for a string from possibilities that is most similar to value, but not the same (for 8-bit encoding).
* Finds the string from $possibilities most similar to $value using Levenshtein distance, or null if none is close enough.
* @param string[] $possibilities
*/
public static function getSuggestion(array $possibilities, string $value): ?string
@ -91,6 +94,7 @@ class Helpers
/**
* Compares two values in the same way that PHP does. Recognizes operators: >, >=, <, <=, =, ==, ===, !=, !==, <>
* @param '>'|'>='|'<'|'<='|'='|'=='|'==='|'!='|'!=='|'<>' $operator
*/
public static function compare(mixed $left, string $operator, mixed $right): bool
{

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette\HtmlStringable;
@ -15,115 +13,115 @@ use const ENT_HTML5, ENT_NOQUOTES, ENT_QUOTES;
/**
* HTML helper.
* Generates HTML elements with automatic attribute escaping.
*
* @property string|null $accept
* @property string|null $accesskey
* @property string|null $action
* @property string|null $align
* @property string|null $allow
* @property string|null $alt
* @property bool|null $async
* @property string|null $autocapitalize
* @property string|null $autocomplete
* @property bool|null $autofocus
* @property bool|null $autoplay
* @property string|null $charset
* @property bool|null $checked
* @property string|null $cite
* @property string|null $class
* @property int|null $cols
* @property int|null $colspan
* @property string|null $content
* @property bool|null $contenteditable
* @property bool|null $controls
* @property string|null $coords
* @property string|null $crossorigin
* @property string|null $data
* @property string|null $datetime
* @property string|null $decoding
* @property bool|null $default
* @property bool|null $defer
* @property string|null $dir
* @property string|null $dirname
* @property bool|null $disabled
* @property bool|null $download
* @property string|null $draggable
* @property string|null $dropzone
* @property string|null $enctype
* @property string|null $for
* @property string|null $form
* @property string|null $formaction
* @property string|null $formenctype
* @property string|null $formmethod
* @property bool|null $formnovalidate
* @property string|null $formtarget
* @property string|null $headers
* @property int|null $height
* @property bool|null $hidden
* @property float|null $high
* @property string|null $href
* @property string|null $hreflang
* @property string|null $id
* @property string|null $integrity
* @property string|null $inputmode
* @property bool|null $ismap
* @property string|null $itemprop
* @property string|null $kind
* @property string|null $label
* @property string|null $lang
* @property string|null $list
* @property bool|null $loop
* @property float|null $low
* @property float|null $max
* @property int|null $maxlength
* @property int|null $minlength
* @property string|null $media
* @property string|null $method
* @property float|null $min
* @property bool|null $multiple
* @property bool|null $muted
* @property string|null $name
* @property bool|null $novalidate
* @property bool|null $open
* @property float|null $optimum
* @property string|null $pattern
* @property string|null $ping
* @property string|null $placeholder
* @property string|null $poster
* @property string|null $preload
* @property string|null $radiogroup
* @property bool|null $readonly
* @property string|null $rel
* @property bool|null $required
* @property bool|null $reversed
* @property int|null $rows
* @property int|null $rowspan
* @property string|null $sandbox
* @property string|null $scope
* @property bool|null $selected
* @property string|null $shape
* @property int|null $size
* @property string|null $sizes
* @property string|null $slot
* @property int|null $span
* @property string|null $spellcheck
* @property string|null $src
* @property string|null $srcdoc
* @property string|null $srclang
* @property string|null $srcset
* @property int|null $start
* @property float|null $step
* @property string|null $style
* @property int|null $tabindex
* @property string|null $target
* @property string|null $title
* @property string|null $translate
* @property string|null $type
* @property string|null $usemap
* @property string|null $value
* @property int|null $width
* @property string|null $wrap
* @property ?string $accept
* @property ?string $accesskey
* @property ?string $action
* @property ?string $align
* @property ?string $allow
* @property ?string $alt
* @property ?bool $async
* @property ?string $autocapitalize
* @property ?string $autocomplete
* @property ?bool $autofocus
* @property ?bool $autoplay
* @property ?string $charset
* @property ?bool $checked
* @property ?string $cite
* @property ?string $class
* @property ?int $cols
* @property ?int $colspan
* @property ?string $content
* @property ?bool $contenteditable
* @property ?bool $controls
* @property ?string $coords
* @property ?string $crossorigin
* @property ?string $data
* @property ?string $datetime
* @property ?string $decoding
* @property ?bool $default
* @property ?bool $defer
* @property ?string $dir
* @property ?string $dirname
* @property ?bool $disabled
* @property ?bool $download
* @property ?string $draggable
* @property ?string $dropzone
* @property ?string $enctype
* @property ?string $for
* @property ?string $form
* @property ?string $formaction
* @property ?string $formenctype
* @property ?string $formmethod
* @property ?bool $formnovalidate
* @property ?string $formtarget
* @property ?string $headers
* @property ?int $height
* @property ?bool $hidden
* @property ?float $high
* @property ?string $href
* @property ?string $hreflang
* @property ?string $id
* @property ?string $integrity
* @property ?string $inputmode
* @property ?bool $ismap
* @property ?string $itemprop
* @property ?string $kind
* @property ?string $label
* @property ?string $lang
* @property ?string $list
* @property ?bool $loop
* @property ?float $low
* @property ?float $max
* @property ?int $maxlength
* @property ?int $minlength
* @property ?string $media
* @property ?string $method
* @property ?float $min
* @property ?bool $multiple
* @property ?bool $muted
* @property ?string $name
* @property ?bool $novalidate
* @property ?bool $open
* @property ?float $optimum
* @property ?string $pattern
* @property ?string $ping
* @property ?string $placeholder
* @property ?string $poster
* @property ?string $preload
* @property ?string $radiogroup
* @property ?bool $readonly
* @property ?string $rel
* @property ?bool $required
* @property ?bool $reversed
* @property ?int $rows
* @property ?int $rowspan
* @property ?string $sandbox
* @property ?string $scope
* @property ?bool $selected
* @property ?string $shape
* @property ?int $size
* @property ?string $sizes
* @property ?string $slot
* @property ?int $span
* @property ?string $spellcheck
* @property ?string $src
* @property ?string $srcdoc
* @property ?string $srclang
* @property ?string $srcset
* @property ?int $start
* @property ?float $step
* @property ?string $style
* @property ?int $tabindex
* @property ?string $target
* @property ?string $title
* @property ?string $translate
* @property ?string $type
* @property ?string $usemap
* @property ?string $value
* @property ?int $width
* @property ?string $wrap
*
* @method self accept(?string $val)
* @method self accesskey(?string $val, bool $state = null)
@ -230,20 +228,23 @@ use const ENT_HTML5, ENT_NOQUOTES, ENT_QUOTES;
* @method self value(?string $val)
* @method self width(?int $val)
* @method self wrap(?string $val)
*
* @implements \IteratorAggregate<int, self|string>
* @implements \ArrayAccess<int, self|string>
*/
class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable
{
/** @var array<string, mixed> element's attributes */
public array $attrs = [];
/** void elements */
/** @var array<string, int> void elements */
public static array $emptyElements = [
'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1,
'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1,
'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1,
];
/** @var array<int, HtmlStringable|string> nodes */
/** @var array<int, self|string> nodes */
protected array $children = [];
/** element's name */
@ -254,7 +255,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Constructs new HTML element.
* @param array|string $attrs element's attributes or plain text content
* @param array<string, mixed>|string|null $attrs element's attributes or plain text content
*/
public static function el(?string $name = null, array|string|null $attrs = null): static
{
@ -345,7 +346,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Is element empty?
* Checks whether the element is a void (self-closing) element.
*/
final public function isEmpty(): bool
{
@ -355,6 +356,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Sets multiple attributes.
* @param array<string, mixed> $attrs
*/
public function addAttributes(array $attrs): static
{
@ -417,6 +419,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Unsets element's attributes.
* @param list<string> $attributes
*/
public function removeAttributes(array $attributes): static
{
@ -429,7 +432,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Overloaded setter for element's attribute.
* Sets element's attribute via property assignment.
*/
final public function __set(string $name, mixed $value): void
{
@ -438,7 +441,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Overloaded getter for element's attribute.
* Returns element's attribute via property access.
*/
final public function &__get(string $name): mixed
{
@ -447,7 +450,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Overloaded tester for element's attribute.
* Checks if element's attribute is set.
*/
final public function __isset(string $name): bool
{
@ -456,7 +459,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Overloaded unsetter for element's attribute.
* Unsets element's attribute via property unset.
*/
final public function __unset(string $name): void
{
@ -465,7 +468,8 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Overloaded setter for element's attribute.
* Sets or returns element's attribute via method call.
* @param mixed[] $args
*/
final public function __call(string $m, array $args): mixed
{
@ -496,6 +500,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Special setter for element's attribute.
* @param array<string, mixed> $query
*/
final public function href(string $path, array $query = []): static
{
@ -594,6 +599,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Creates and adds a new Html child.
* @param array<string, mixed>|string|null $attrs
*/
final public function create(string $name, array|string|null $attrs = null): static
{
@ -621,7 +627,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Inserts (replaces) child node (\ArrayAccess implementation).
* @param int|null $index position or null for appending
* @param ?int $index position or null for appending
* @param Html|string $child Html node or raw HTML string
*/
final public function offsetSet($index, $child): void
@ -634,7 +640,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
* Returns child node (\ArrayAccess implementation).
* @param int $index
*/
final public function offsetGet($index): HtmlStringable|string
final public function offsetGet($index): self|string
{
return $this->children[$index];
}
@ -682,7 +688,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Iterates over elements.
* @return \ArrayIterator<int, HtmlStringable|string>
* @return \ArrayIterator<int, self|string>
*/
final public function getIterator(): \ArrayIterator
{
@ -692,6 +698,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Returns all children.
* @return array<int, self|string>
*/
final public function getChildren(): array
{
@ -700,7 +707,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
/**
* Renders element's start tag, content and end tag.
* Renders element's start tag, content and end tag. Pass indent level to enable pretty-printing.
*/
final public function render(?int $indent = null): string
{
@ -764,10 +771,6 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
*/
final public function attributes(): string
{
if (!is_array($this->attrs)) {
return '';
}
$s = '';
$attrs = $this->attrs;
foreach ($attrs as $key => $value) {
@ -780,7 +783,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab
continue;
} elseif (is_array($value)) {
if (strncmp($key, 'data-', 5) === 0) {
if (str_starts_with($key, 'data-')) {
$value = Json::encode($value);
} else {

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -24,7 +22,7 @@ use const IMG_BMP, IMG_FLIP_BOTH, IMG_FLIP_HORIZONTAL, IMG_FLIP_VERTICAL, IMG_GI
* $image->send();
* </code>
*
* @method Image affine(array $affine, ?array $clip = null)
* @method Image affine(array<int, float|int> $affine, ?array{x: int, y: int, width: int, height: int} $clip = null)
* @method void alphaBlending(bool $enable)
* @method void antialias(bool $enable)
* @method void arc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color)
@ -41,51 +39,51 @@ use const IMG_BMP, IMG_FLIP_BOTH, IMG_FLIP_HORIZONTAL, IMG_FLIP_VERTICAL, IMG_GI
* @method int colorResolve(int $red, int $green, int $blue)
* @method int colorResolveAlpha(int $red, int $green, int $blue, int $alpha)
* @method void colorSet(int $index, int $red, int $green, int $blue, int $alpha = 0)
* @method array colorsForIndex(int $color)
* @method array{red: int, green: int, blue: int, alpha: int} colorsForIndex(int $color)
* @method int colorsTotal()
* @method int colorTransparent(?int $color = null)
* @method void convolution(array $matrix, float $div, float $offset)
* @method void convolution(array<int, array<int, float>> $matrix, float $div, float $offset)
* @method void copy(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH)
* @method void copyMerge(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
* @method void copyMergeGray(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct)
* @method void copyResampled(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
* @method void copyResized(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH)
* @method Image cropAuto(int $mode = IMG_CROP_DEFAULT, float $threshold = .5, ?ImageColor $color = null)
* @method Image cropAuto(int $mode = 0, float $threshold = .5, ?ImageColor $color = null)
* @method void ellipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
* @method void fill(int $x, int $y, ImageColor $color)
* @method void filledArc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color, int $style)
* @method void filledEllipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color)
* @method void filledPolygon(array $points, ImageColor $color)
* @method void filledPolygon(array<int, int> $points, ImageColor $color)
* @method void filledRectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void fillToBorder(int $x, int $y, ImageColor $borderColor, ImageColor $color)
* @method void filter(int $filter, ...$args)
* @method void flip(int $mode)
* @method array ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array $options = [])
* @method array<int, int> ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array<string, mixed> $options = [])
* @method void gammaCorrect(float $inputgamma, float $outputgamma)
* @method array getClip()
* @method array{int, int, int, int} getClip()
* @method int getInterpolation()
* @method int interlace(?bool $enable = null)
* @method bool isTrueColor()
* @method void layerEffect(int $effect)
* @method void line(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method void openPolygon(array $points, ImageColor $color)
* @method void openPolygon(array<int, int> $points, ImageColor $color)
* @method void paletteCopy(Image $source)
* @method void paletteToTrueColor()
* @method void polygon(array $points, ImageColor $color)
* @method void polygon(array<int, int> $points, ImageColor $color)
* @method void rectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color)
* @method mixed resolution(?int $resolutionX = null, ?int $resolutionY = null)
* @method Image rotate(float $angle, ImageColor $backgroundColor)
* @method void saveAlpha(bool $enable)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED)
* @method Image scale(int $newWidth, int $newHeight = -1, int $mode = 3)
* @method void setBrush(Image $brush)
* @method void setClip(int $x1, int $y1, int $x2, int $y2)
* @method void setInterpolation(int $method = IMG_BILINEAR_FIXED)
* @method void setInterpolation(int $method = 3)
* @method void setPixel(int $x, int $y, ImageColor $color)
* @method void setStyle(array $style)
* @method void setStyle(array<int, int> $style)
* @method void setThickness(int $thickness)
* @method void setTile(Image $tile)
* @method void trueColorToPalette(bool $dither, int $ncolors)
* @method array ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array $options = [])
* @method array<int, int> ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array<string, mixed> $options = [])
* @property-read positive-int $width
* @property-read positive-int $height
* @property-read \GdImage $imageResource
@ -139,6 +137,7 @@ class Image
public const EmptyGIF = "GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;";
private const Formats = [ImageType::JPEG => 'jpeg', ImageType::PNG => 'png', ImageType::GIF => 'gif', ImageType::WEBP => 'webp', ImageType::AVIF => 'avif', ImageType::BMP => 'bmp'];
private const Sentinel = "\0";
private \GdImage $image;
@ -146,6 +145,7 @@ class Image
/**
* Returns RGB color (0..255) and transparency (0..127).
* @deprecated use ImageColor::rgb()
* @return array{red: int, green: int, blue: int, alpha: int}
*/
public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array
{
@ -159,11 +159,13 @@ class Image
/**
* Reads an image from a file and returns its type in $type.
* Reads an image from a file and returns its type in $type. If $warnings is passed, recoverable decoder
* warnings are returned in it instead of being raised as a PHP warning.
* @param-out ?string $warnings
* @throws Nette\NotSupportedException if gd extension is not loaded
* @throws UnknownImageFileException if file not found or file type is not known
*/
public static function fromFile(string $file, ?int &$type = null): static
public static function fromFile(string $file, ?int &$type = null, ?string &$warnings = self::Sentinel): static
{
self::ensureExtension();
$type = self::detectTypeFromFile($file);
@ -171,16 +173,18 @@ class Image
throw new UnknownImageFileException(is_file($file) ? "Unknown type of file '$file'." : "File '$file' not found.");
}
return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__);
return self::invokeSafe('imagecreatefrom' . self::Formats[$type], $file, "Unable to open file '$file'.", __METHOD__, $warnings);
}
/**
* Reads an image from a string and returns its type in $type.
* Reads an image from a string and returns its type in $type. If $warnings is passed, recoverable decoder
* warnings are returned in it instead of being raised as a PHP warning.
* @param-out ?string $warnings
* @throws Nette\NotSupportedException if gd extension is not loaded
* @throws ImageException
*/
public static function fromString(string $s, ?int &$type = null): static
public static function fromString(string $s, ?int &$type = null, ?string &$warnings = self::Sentinel): static
{
self::ensureExtension();
$type = self::detectTypeFromString($s);
@ -188,21 +192,31 @@ class Image
throw new UnknownImageFileException('Unknown type of image.');
}
return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__);
return self::invokeSafe('imagecreatefromstring', $s, 'Unable to open image from string.', __METHOD__, $warnings);
}
private static function invokeSafe(string $func, string $arg, string $message, string $callee): static
/** @param callable-string $func */
private static function invokeSafe(
string $func,
string $arg,
string $message,
string $callee,
?string &$warnings = self::Sentinel,
): static
{
$errors = [];
$res = Callback::invokeSafe($func, [$arg], function (string $message) use (&$errors): void {
$errors[] = $message;
});
$raiseWarning = $warnings === self::Sentinel;
$warnings = $errors ? implode(', ', $errors) : null;
if (!$res) {
throw new ImageException($message . ' Errors: ' . implode(', ', $errors));
} elseif ($errors) {
trigger_error($callee . '(): ' . implode(', ', $errors), E_USER_WARNING);
throw new ImageException($message . ' Errors: ' . $warnings);
} elseif ($errors && $raiseWarning) {
trigger_error($callee . '(): ' . $warnings, E_USER_WARNING);
}
return new static($res);
@ -213,6 +227,7 @@ class Image
* Creates a new true color image of the given dimensions. The default color is black.
* @param positive-int $width
* @param positive-int $height
* @param ImageColor|array{red: int, green: int, blue: int, alpha?: int}|null $color
* @throws Nette\NotSupportedException if gd extension is not loaded
*/
public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static
@ -224,9 +239,9 @@ class Image
$image = new static(imagecreatetruecolor($width, $height));
if ($color) {
$image->alphablending(false);
$image->filledrectangle(0, 0, $width - 1, $height - 1, $color);
$image->alphablending(true);
$image->alphaBlending(false);
$image->filledRectangle(0, 0, $width - 1, $height - 1, self::normalizeColor($color));
$image->alphaBlending(true);
}
return $image;
@ -235,9 +250,11 @@ class Image
/**
* Returns the type of image from file.
* @return ImageType::*|null
* @param-out ?int $width
* @param-out ?int $height
* @return ?ImageType::*
*/
public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int
public static function detectTypeFromFile(string $file, mixed &$width = null, mixed &$height = null): ?int
{
[$width, $height, $type] = Helpers::falseToNull(@getimagesize($file)); // @ - files smaller than 12 bytes causes read error
return $type && isset(self::Formats[$type]) ? $type : null;
@ -246,9 +263,11 @@ class Image
/**
* Returns the type of image from string.
* @return ImageType::*|null
* @param-out ?int $width
* @param-out ?int $height
* @return ?ImageType::*
*/
public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int
public static function detectTypeFromString(string $s, mixed &$width = null, mixed &$height = null): ?int
{
[$width, $height, $type] = Helpers::falseToNull(@getimagesizefromstring($s)); // @ - strings smaller than 12 bytes causes read error
return $type && isset(self::Formats[$type]) ? $type : null;
@ -297,6 +316,7 @@ class Image
/**
* Checks whether the given image type is supported by the GD extension.
* @param ImageType::* $type
*/
public static function isTypeSupported(int $type): bool
@ -314,7 +334,10 @@ class Image
}
/** @return ImageType[] */
/**
* Returns list of image types supported by the GD extension.
* @return ImageType::*[]
*/
public static function getSupportedTypes(): array
{
self::ensureExtension();
@ -386,6 +409,10 @@ class Image
public function resize(int|string|null $width, int|string|null $height, int $mode = self::OrSmaller): static
{
if ($mode & self::Cover) {
if ($width === null || $height === null) {
throw new Nette\InvalidArgumentException('Both width and height must be set for Cover mode.');
}
return $this->resize($width, $height, self::OrBigger)->crop('50%', '50%', $width, $height);
}
@ -419,12 +446,13 @@ class Image
/**
* Calculates dimensions of resized image. Width and height accept pixels or percent.
* @param int-mask-of<self::OrSmaller|self::OrBigger|self::Stretch|self::Cover|self::ShrinkOnly> $mode
* @return array{int<1, max>, int<1, max>}
*/
public static function calculateSize(
int $srcWidth,
int $srcHeight,
$newWidth,
$newHeight,
int|string|null $newWidth,
int|string|null $newHeight,
int $mode = self::OrSmaller,
): array
{
@ -468,19 +496,19 @@ class Image
}
if ($mode & self::OrBigger) {
$scale = [max($scale)];
$scale = [max($scale ?: [1])];
}
if ($mode & self::ShrinkOnly) {
$scale[] = 1;
}
$scale = min($scale);
$scale = min($scale ?: [1]);
$newWidth = (int) round($srcWidth * $scale);
$newHeight = (int) round($srcHeight * $scale);
}
return [max($newWidth, 1), max($newHeight, 1)];
return [max((int) $newWidth, 1), max((int) $newHeight, 1)];
}
@ -495,7 +523,7 @@ class Image
$this->image = imagecrop($this->image, $r);
imagesavealpha($this->image, true);
} else {
$newImage = static::fromBlank($r['width'], $r['height'], ImageColor::rgb(0, 0, 0, 0))->getImageResource();
$newImage = static::fromBlank(max(1, $r['width']), max(1, $r['height']), ImageColor::rgb(0, 0, 0, 0))->getImageResource();
imagecopy($newImage, $this->image, 0, 0, $r['x'], $r['y'], $r['width'], $r['height']);
$this->image = $newImage;
}
@ -506,6 +534,7 @@ class Image
/**
* Calculates dimensions of cutout in image. Arguments accepts pixels or percent.
* @return array{int, int, int, int}
*/
public static function calculateCutout(
int $srcWidth,
@ -516,21 +545,10 @@ class Image
int|string $newHeight,
): array
{
if (self::isPercent($newWidth)) {
$newWidth = (int) round($srcWidth / 100 * $newWidth);
}
if (self::isPercent($newHeight)) {
$newHeight = (int) round($srcHeight / 100 * $newHeight);
}
if (self::isPercent($left)) {
$left = (int) round(($srcWidth - $newWidth) / 100 * $left);
}
if (self::isPercent($top)) {
$top = (int) round(($srcHeight - $newHeight) / 100 * $top);
}
$newWidth = (int) (self::isPercent($newWidth) ? round($srcWidth / 100 * $newWidth) : $newWidth);
$newHeight = (int) (self::isPercent($newHeight) ? round($srcHeight / 100 * $newHeight) : $newHeight);
$left = (int) (self::isPercent($left) ? round(($srcWidth - $newWidth) / 100 * $left) : $left);
$top = (int) (self::isPercent($top) ? round(($srcHeight - $newHeight) / 100 * $top) : $top);
if ($left < 0) {
$newWidth += $left;
@ -575,14 +593,8 @@ class Image
$width = $image->getWidth();
$height = $image->getHeight();
if (self::isPercent($left)) {
$left = (int) round(($this->getWidth() - $width) / 100 * $left);
}
if (self::isPercent($top)) {
$top = (int) round(($this->getHeight() - $height) / 100 * $top);
}
$left = (int) (self::isPercent($left) ? round(($this->getWidth() - $width) / 100 * $left) : $left);
$top = (int) (self::isPercent($top) ? round(($this->getHeight() - $height) / 100 * $top) : $top);
$output = $input = $image->image;
if ($opacity < 100) {
@ -595,7 +607,7 @@ class Image
imagealphablending($output, false);
if (!$image->isTrueColor()) {
$input = $output;
imagefilledrectangle($output, 0, 0, $width, $height, imagecolorallocatealpha($output, 0, 0, 0, 127));
imagefilledrectangle($output, 0, 0, $width, $height, (int) imagecolorallocatealpha($output, 0, 0, 0, 127));
imagecopy($output, $image->image, 0, 0, 0, 0, $width, $height);
}
@ -626,6 +638,8 @@ class Image
/**
* Calculates the bounding box for a TrueType text. Returns keys left, top, width and height.
* @param array<string, mixed> $options
* @return array{left: int, top: int, width: int, height: int}
*/
public static function calculateTextBox(
string $text,
@ -647,7 +661,7 @@ class Image
/**
* Draw a rectangle.
* Draws a rectangle using top-left coordinates and dimensions instead of two corner coordinates.
*/
public function rectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
{
@ -658,7 +672,7 @@ class Image
/**
* Draw a filled rectangle.
* Draws a filled rectangle using top-left coordinates and dimensions instead of two corner coordinates.
*/
public function filledRectangleWH(int $x, int $y, int $width, int $height, ImageColor $color): void
{
@ -670,7 +684,7 @@ class Image
/**
* Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9).
* @param ImageType::*|null $type
* @param ?ImageType::* $type
* @throws ImageException
*/
public function save(string $file, ?int $quality = null, ?int $type = null): void
@ -746,6 +760,7 @@ class Image
/**
* Call to undefined method.
* @param mixed[] $args
* @throws Nette\MemberAccessException
*/
public function __call(string $name, array $args): mixed
@ -760,6 +775,7 @@ class Image
$args[$key] = $value->getImageResource();
} elseif ($value instanceof ImageColor || (is_array($value) && isset($value['red']))) {
/** @var ImageColor|array{red: int, green: int, blue: int, alpha?: int} $value */
$args[$key] = $this->resolveColor($value);
}
}
@ -775,10 +791,11 @@ class Image
{
ob_start(fn() => '');
imagepng($this->image, null, 0);
$this->setImageResource(imagecreatefromstring(ob_get_clean()));
$this->setImageResource(imagecreatefromstring(ob_get_clean()) ?: throw new Nette\ShouldNotHappenException);
}
/** @param-out int|float $num */
private static function isPercent(int|string &$num): bool
{
if (is_string($num) && str_ends_with($num, '%')) {
@ -802,13 +819,31 @@ class Image
}
/**
* Resolves a color to a GD color index for the current image.
* @param ImageColor|array{red: int, green: int, blue: int, alpha?: int} $color
*/
public function resolveColor(ImageColor|array $color): int
{
$color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color);
$color = self::normalizeColor($color)->toRGBA();
return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color);
}
/** @param ImageColor|array{red: int, green: int, blue: int, alpha?: int} $color */
private static function normalizeColor(ImageColor|array $color): ImageColor
{
return $color instanceof ImageColor
? $color
: ImageColor::rgb(
$color['red'],
$color['green'],
$color['blue'],
(127 - ($color['alpha'] ?? 0)) / 127,
);
}
private static function ensureExtension(): void
{
if (!extension_loaded('gd')) {

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -18,6 +16,9 @@ use function hexdec, ltrim, max, min, round, strlen;
*/
class ImageColor
{
/**
* Creates a color from RGB components (0..255) and opacity (0..1).
*/
public static function rgb(int $red, int $green, int $blue, float $opacity = 1): self
{
return new self($red, $green, $blue, $opacity);
@ -64,6 +65,10 @@ class ImageColor
}
/**
* Returns GD-compatible color array [R, G, B, alpha].
* @return array{int<0, 255>, int<0, 255>, int<0, 255>, int<0, 127>}
*/
public function toRGBA(): array
{
return [

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use const IMAGETYPE_BMP, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -22,6 +20,7 @@ final class Iterables
/**
* Tests for the presence of value.
* @param iterable<mixed> $iterable
*/
public static function contains(iterable $iterable, mixed $value): bool
{
@ -36,6 +35,7 @@ final class Iterables
/**
* Tests for the presence of key.
* @param iterable<mixed> $iterable
*/
public static function containsKey(iterable $iterable, mixed $key): bool
{
@ -52,9 +52,11 @@ final class Iterables
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K
* @template V
* @template E
* @param iterable<K, V> $iterable
* @param ?callable(V, K, iterable<K, V>): bool $predicate
* @return ?V
* @param ?callable(): E $else
* @return ($else is null ? ?V : V|E)
*/
public static function first(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
@ -71,9 +73,11 @@ final class Iterables
* Returns the key of first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* @template K
* @template V
* @template E
* @param iterable<K, V> $iterable
* @param ?callable(V, K, iterable<K, V>): bool $predicate
* @return ?K
* @param ?callable(): E $else
* @return ($else is null ? ?K : K|E)
*/
public static function firstKey(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
@ -87,7 +91,7 @@ final class Iterables
/**
* Tests whether at least one element in the iterator passes the test implemented by the provided function.
* Tests whether at least one element in the iterable passes the test implemented by the provided function.
* @template K
* @template V
* @param iterable<K, V> $iterable
@ -105,7 +109,7 @@ final class Iterables
/**
* Tests whether all elements in the iterator pass the test implemented by the provided function.
* Tests whether all elements in the iterable pass the test implemented by the provided function.
* @template K
* @template V
* @param iterable<K, V> $iterable
@ -123,7 +127,7 @@ final class Iterables
/**
* Iterator that filters elements according to a given $predicate. Maintains original keys.
* Returns a generator that yields only elements matching the given $predicate. Maintains original keys.
* @template K
* @template V
* @param iterable<K, V> $iterable
@ -141,7 +145,7 @@ final class Iterables
/**
* Iterator that transforms values by calling $transformer. Maintains original keys.
* Returns a generator that transforms values by calling $transformer. Maintains original keys.
* @template K
* @template V
* @template R
@ -158,14 +162,14 @@ final class Iterables
/**
* Iterator that transforms keys and values by calling $transformer. If it returns null, the element is skipped.
* Returns a generator that transforms keys and values by calling $transformer. If it returns null, the element is skipped.
* @template K
* @template V
* @template ResV
* @template ResK
* @template ResV
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): ?array{ResV, ResK} $transformer
* @return \Generator<ResV, ResK>
* @param callable(V, K, iterable<K, V>): ?array{ResK, ResV} $transformer
* @return \Generator<ResK, ResV>
*/
public static function mapWithKeys(iterable $iterable, callable $transformer): \Generator
{
@ -188,9 +192,10 @@ final class Iterables
*/
public static function repeatable(callable $factory): \IteratorAggregate
{
return new class ($factory) implements \IteratorAggregate {
return new class ($factory(...)) implements \IteratorAggregate {
public function __construct(
private $factory,
/** @var \Closure(): iterable<mixed, mixed> */
private \Closure $factory,
) {
}
@ -215,7 +220,8 @@ final class Iterables
{
return new class (self::toIterator($iterable)) implements \IteratorAggregate {
public function __construct(
private \Iterator $iterator,
private readonly \Iterator $iterator,
/** @var array<array{mixed, mixed}> */
private array $cache = [],
) {
}

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -32,8 +30,8 @@ final class Json
/**
* Converts value to JSON format. Use $pretty for easier reading and clarity, $asciiSafe for ASCII output
* and $htmlSafe for HTML escaping, $forceObjects enforces the encoding of non-associateve arrays as objects.
* Converts value to JSON format. Use $pretty for formatted output, $asciiSafe for ASCII-only output,
* $htmlSafe for HTML-safe output, and $forceObjects to encode non-associative arrays as objects.
* @throws JsonException
*/
public static function encode(
@ -66,7 +64,7 @@ final class Json
/**
* Parses JSON to PHP value. The $forceArrays enforces the decoding of objects as arrays.
* Decodes a JSON string to a PHP value. Use $forceArrays to decode objects as arrays.
* @throws JsonException
*/
public static function decode(string $json, bool|int $forceArrays = false): mixed

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -17,17 +15,17 @@ use Nette;
*
* @property int $page
* @property-read int $firstPage
* @property-read int|null $lastPage
* @property-read ?int $lastPage
* @property-read int<0,max> $firstItemOnPage
* @property-read int<0,max> $lastItemOnPage
* @property int $base
* @property-read bool $first
* @property-read bool $last
* @property-read int<0,max>|null $pageCount
* @property-read ?int<0,max> $pageCount
* @property positive-int $itemsPerPage
* @property int<0,max>|null $itemCount
* @property ?int<0,max> $itemCount
* @property-read int<0,max> $offset
* @property-read int<0,max>|null $countdownOffset
* @property-read ?int<0,max> $countdownOffset
* @property-read int<0,max> $length
*/
class Paginator
@ -41,13 +39,10 @@ class Paginator
private int $page = 1;
/** @var int<0, max>|null */
/** @var ?int<0, max> */
private ?int $itemCount = null;
/**
* Sets current page number.
*/
public function setPage(int $page): static
{
$this->page = $page;
@ -55,27 +50,18 @@ class Paginator
}
/**
* Returns current page number.
*/
public function getPage(): int
{
return $this->base + $this->getPageIndex();
}
/**
* Returns first page number.
*/
public function getFirstPage(): int
{
return $this->base;
}
/**
* Returns last page number.
*/
public function getLastPage(): ?int
{
return $this->itemCount === null
@ -85,7 +71,7 @@ class Paginator
/**
* Returns the sequence number of the first element on the page
* Returns the 1-based index of the first item on the current page, or 0 if the page is empty.
* @return int<0, max>
*/
public function getFirstItemOnPage(): int
@ -97,7 +83,7 @@ class Paginator
/**
* Returns the sequence number of the last element on the page
* Returns the 1-based index of the last item on the current page.
* @return int<0, max>
*/
public function getLastItemOnPage(): int
@ -106,9 +92,6 @@ class Paginator
}
/**
* Sets first page (base) number.
*/
public function setBase(int $base): static
{
$this->base = $base;
@ -116,9 +99,6 @@ class Paginator
}
/**
* Returns first page (base) number.
*/
public function getBase(): int
{
return $this->base;
@ -138,18 +118,12 @@ class Paginator
}
/**
* Is the current page the first one?
*/
public function isFirst(): bool
{
return $this->getPageIndex() === 0;
}
/**
* Is the current page the last one?
*/
public function isLast(): bool
{
return $this->itemCount === null
@ -159,20 +133,16 @@ class Paginator
/**
* Returns the total number of pages.
* @return int<0, max>|null
* @return ?int<0, max>
*/
public function getPageCount(): ?int
{
return $this->itemCount === null
? null
: (int) ceil($this->itemCount / $this->itemsPerPage);
: max(0, (int) ceil($this->itemCount / $this->itemsPerPage));
}
/**
* Sets the number of items to display on a single page.
*/
public function setItemsPerPage(int $itemsPerPage): static
{
$this->itemsPerPage = max(1, $itemsPerPage);
@ -181,7 +151,6 @@ class Paginator
/**
* Returns the number of items to display on a single page.
* @return positive-int
*/
public function getItemsPerPage(): int
@ -190,9 +159,6 @@ class Paginator
}
/**
* Sets the total number of items.
*/
public function setItemCount(?int $itemCount = null): static
{
$this->itemCount = $itemCount === null ? null : max(0, $itemCount);
@ -201,8 +167,7 @@ class Paginator
/**
* Returns the total number of items.
* @return int<0, max>|null
* @return ?int<0, max>
*/
public function getItemCount(): ?int
{
@ -222,7 +187,7 @@ class Paginator
/**
* Returns the absolute index of the first item on current page in countdown paging.
* @return int<0, max>|null
* @return ?int<0, max>
*/
public function getCountdownOffset(): ?int
{
@ -240,6 +205,6 @@ class Paginator
{
return $this->itemCount === null
? $this->itemsPerPage
: min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage);
: max(0, min($this->itemsPerPage, $this->itemCount - $this->getPageIndex() * $this->itemsPerPage));
}
}

View File

@ -0,0 +1,507 @@
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
namespace Nette\Utils;
use Nette;
/**
* Represents a process, which can be started and controlled (reading output, writing input, waiting for completion).
*/
final class Process
{
private const PollInterval = 10_000;
private const DefaultTimeout = 60;
private const StdIn = 0;
private const StdOut = 1;
private const StdErr = 2;
/** @var resource */
private mixed $process;
/** @var array<string, mixed> result of proc_get_status() */
private array $status = ['running' => true];
/** @var resource */
private mixed $inputPipe;
/** @var array<int, resource|string[]> pipe resources, or ['file', path, mode] descriptors for discarded output */
private array $outputPipes = [];
/** @var string[] */
private array $outputBuffers = [];
/** @var int[] Number of bytes already read from buffers. */
private array $outputBufferOffsets = [];
/** @var array<int, true> Output IDs whose target resource was supplied by the caller and must not be closed here. */
private array $callerOutputs = [];
private float $startTime;
/**
* Starts an executable with given arguments. Because the arguments are passed as an array, the shell is
* never involved, so they need no escaping and there is no risk of shell injection.
* @param string $executable Path to the executable binary.
* @param list<string> $arguments Arguments passed to the executable.
* @param string[]|null $env Environment variables or null to use the same environment as the current process.
* @param array<string, mixed> $options Additional options for proc_open(). On Windows the executable is launched directly, without cmd.exe.
* @param mixed $stdin Input: string, a readable resource (its content is copied to STDIN), a Process (its STDOUT is piped in), or null (STDIN stays open for writeStdInput()).
* @param mixed $stdout Output target: string filename, a writable resource backed by a real OS file descriptor (not php://memory etc.), false to discard, or null to capture into memory.
* @param mixed $stderr Error output target (same options as $stdout).
* @param string|null $directory Working directory.
* @param float|null $timeout Time limit in seconds, checked while waiting for or reading the process; null disables it.
*/
public static function runExecutable(
string $executable,
array $arguments = [],
?array $env = null,
array $options = [],
mixed $stdin = '',
mixed $stdout = null,
mixed $stderr = null,
?string $directory = null,
?float $timeout = self::DefaultTimeout,
): self
{
return new self([$executable, ...$arguments], $env, $options, $directory, $stdin, $stdout, $stderr, $timeout);
}
/**
* Starts a process from a command string interpreted by the shell (/bin/sh on POSIX, cmd.exe on Windows).
* Because the shell parses the string, NEVER pass unescaped user input here - use runExecutable() for that.
* @param string $command Shell command to run.
* @param string[]|null $env Environment variables or null to use the same environment as the current process.
* @param array<string, mixed> $options Options for proc_open(), e.g. ['bypass_shell' => true] on Windows to skip cmd.exe.
* @param mixed $stdin Input: string, a readable resource (its content is copied to STDIN), a Process (its STDOUT is piped in), or null (STDIN stays open for writeStdInput()).
* @param mixed $stdout Output target: string filename, a writable resource backed by a real OS file descriptor (not php://memory etc.), false to discard, or null to capture into memory.
* @param mixed $stderr Error output target (same options as $stdout).
* @param string|null $directory Working directory.
* @param float|null $timeout Time limit in seconds, checked while waiting for or reading the process; null disables it.
*/
public static function runCommand(
string $command,
?array $env = null,
array $options = [],
mixed $stdin = '',
mixed $stdout = null,
mixed $stderr = null,
?string $directory = null,
?float $timeout = self::DefaultTimeout,
): self
{
return new self($command, $env, $options, $directory, $stdin, $stdout, $stderr, $timeout);
}
/**
* @param list<string>|string $command
* @param array<string, string>|null $env
* @param array<string, mixed> $options
*/
private function __construct(
string|array $command,
?array $env,
array $options,
?string $directory,
mixed $stdin,
mixed $stdout,
mixed $stderr,
private ?float $timeout,
) {
$descriptors = [
self::StdIn => $this->createInputDescriptor($stdin),
self::StdOut => $this->createOutputDescriptor(self::StdOut, $stdout),
self::StdErr => $this->createOutputDescriptor(self::StdErr, $stderr),
];
$process = @proc_open($command, $descriptors, $pipes, $directory, $env, $options);
if (!is_resource($process)) {
throw new ProcessFailedException('Failed to start process: ' . Helpers::getLastError());
}
$this->process = $process;
[$this->inputPipe, $this->outputPipes[self::StdOut], $this->outputPipes[self::StdErr]] = $pipes + $descriptors;
if ($stdin instanceof self) {
// the source process hands over its STDOUT pipe; from now on this process owns it
unset(
$stdin->outputBuffers[self::StdOut],
$stdin->outputBufferOffsets[self::StdOut],
$stdin->outputPipes[self::StdOut],
);
}
$this->writeInitialInput($stdin);
$this->startTime = microtime(true);
}
public function __destruct()
{
$this->outputBuffers = [];
$this->terminate();
}
/**
* Checks if the process is currently running.
*/
public function isRunning(): bool
{
if (!$this->status['running']) {
return false;
}
$this->status = proc_get_status($this->process);
if (!$this->status['running']) {
$this->close();
}
return $this->status['running'];
}
/**
* Finishes the process by waiting for its completion. While waiting, the captured output is read
* continuously and kept in memory; an optional callback is invoked with each new output/error chunk.
*
* @param (\Closure(string, string): void)|null $callback
*/
public function wait(?\Closure $callback = null): void
{
while ($this->isRunning()) {
$this->enforceTimeout();
$this->drainPipes();
$this->dispatchCallback($callback);
usleep(self::PollInterval);
}
$this->dispatchCallback($callback);
}
/**
* Reads any new data from the captured pipes into the buffers, so a process producing more output
* than the OS pipe buffer holds does not block. (On Windows the captured output is a file and never blocks.)
*/
private function drainPipes(): void
{
foreach ([self::StdOut, self::StdErr] as $id) {
$this->readFromPipe($id);
}
}
/**
* Terminates the running process if it is still running.
*/
public function terminate(): void
{
if (!$this->isRunning()) {
return;
} elseif (Helpers::IsWindows) {
exec("taskkill /F /T /PID {$this->getPid()} 2>&1");
} else {
proc_terminate($this->process, 9); // 9 = SIGKILL: cannot be trapped, so the following proc_close() won't hang
}
$this->status['running'] = false;
$this->close();
}
/**
* Returns the process exit code. If the process is still running, waits until it finishes.
*/
public function getExitCode(): int
{
$this->wait();
return $this->status['exitcode'] ?? -1;
}
/**
* Returns true if the process terminated with exit code 0.
*/
public function isSuccess(): bool
{
return $this->getExitCode() === 0;
}
/**
* Waits for the process to finish and throws ProcessFailedException if exit code is not zero.
*/
public function ensureSuccess(): void
{
$code = $this->getExitCode();
if ($code !== 0) {
throw new ProcessFailedException("Process failed with non-zero exit code: $code");
}
}
/**
* Returns the PID of the running process, or null if it is not running.
*/
public function getPid(): ?int
{
return $this->isRunning() ? $this->status['pid'] : null;
}
/**
* Waits for the process to finish and returns everything it wrote to STDOUT.
*/
public function getStdOutput(): string
{
$this->wait();
return $this->outputBuffers[self::StdOut] ?? throw new Nette\InvalidStateException('Cannot read output: it is not captured (it was redirected, discarded or piped).');
}
/**
* Waits for the process to finish and returns everything it wrote to STDERR.
*/
public function getStdError(): string
{
$this->wait();
return $this->outputBuffers[self::StdErr] ?? throw new Nette\InvalidStateException('Cannot read output: it is not captured (it was redirected, discarded or piped).');
}
/**
* Returns the STDOUT data produced since the previous consumeStdOutput() call.
* To read everything incrementally, poll `while ($p->isRunning())` calling this, then call it once more
* after the loop; that last call returns whatever the process wrote just before the loop noticed it had exited.
*/
public function consumeStdOutput(): string
{
return $this->consumeBuffer(self::StdOut);
}
/**
* Returns the STDERR data produced since the previous consumeStdError() call. See consumeStdOutput().
*/
public function consumeStdError(): string
{
return $this->consumeBuffer(self::StdErr);
}
/**
* Returns newly available data from the specified buffer and advances the read pointer.
*/
private function consumeBuffer(int $id): string
{
if (!isset($this->outputBuffers[$id])) {
throw new Nette\InvalidStateException('Cannot read output: it is not captured (it was redirected, discarded or piped).');
} elseif ($this->isRunning()) {
$this->enforceTimeout();
$this->readFromPipe($id);
}
return $this->extractNewData($id);
}
/**
* Returns the buffered data not returned yet and advances the read pointer.
*/
private function extractNewData(int $id): string
{
if (!isset($this->outputBuffers[$id])) {
return '';
}
$res = substr($this->outputBuffers[$id], $this->outputBufferOffsets[$id]);
$this->outputBufferOffsets[$id] = strlen($this->outputBuffers[$id]);
return $res;
}
/**
* Writes data into the process' STDIN. If STDIN is closed, throws exception.
*/
public function writeStdInput(string $string): void
{
if (!is_resource($this->inputPipe)) {
throw new Nette\InvalidStateException('Cannot write to process: STDIN pipe is closed');
}
$this->writeToPipe($string);
}
/**
* Writes the whole string to STDIN, handling partial writes. Stops (and lets fwrite() warn) on a broken pipe,
* i.e. when the process stopped reading its STDIN.
*/
private function writeToPipe(string $string): void
{
$length = strlen($string);
for ($written = 0; $written < $length; $written += $bytes) {
$bytes = fwrite($this->inputPipe, substr($string, $written));
if (!$bytes) {
break;
}
}
}
/**
* Closes the STDIN pipe, indicating no more data will be sent.
*/
public function closeStdInput(): void
{
if (is_resource($this->inputPipe)) {
fclose($this->inputPipe);
}
}
/**
* If a callback is given, invokes it with the output/error produced since the previous call.
* @param (\Closure(string, string): void)|null $callback
*/
private function dispatchCallback(?\Closure $callback): void
{
if (!$callback) {
return;
}
$output = $this->extractNewData(self::StdOut);
$error = $this->extractNewData(self::StdErr);
if ($output !== '' || $error !== '') {
$callback($output, $error);
}
}
/**
* Checks if the timeout has expired. If yes, terminates the process.
*/
private function enforceTimeout(): void
{
if ($this->timeout !== null && (microtime(true) - $this->startTime) >= $this->timeout) {
$this->terminate();
throw new ProcessTimeoutException('Process exceeded the time limit of ' . $this->timeout . ' seconds');
}
}
/**
* Reads any new data from the specified pipe and appends it to the buffer. Does nothing if the output
* is not captured or the pipe is already closed (or handed over to another process).
*/
private function readFromPipe(int $id): void
{
if (!isset($this->outputBuffers[$id]) || !is_resource($this->outputPipes[$id] ?? null)) {
return;
} elseif (Helpers::IsWindows) {
fseek($this->outputPipes[$id], strlen($this->outputBuffers[$id]));
} else {
stream_set_blocking($this->outputPipes[$id], false);
}
$this->outputBuffers[$id] .= stream_get_contents($this->outputPipes[$id]);
}
/**
* Sends the initial input to the process: writes and closes a string or stream input,
* or leaves STDIN open when input is null (until closeStdInput()) or another Process (fed by that process).
* The input type was already validated by createInputDescriptor().
*
* Note: a string or stream input is written upfront, so if it is large and the process does not read it
* while filling its own output, this can block; in that case pass null and feed STDIN via writeStdInput().
*/
private function writeInitialInput(mixed $input): void
{
if ($input === null || $input instanceof self) {
// STDIN stays open
} elseif (is_string($input)) {
$this->writeToPipe($input);
$this->closeStdInput();
} elseif (is_resource($input)) {
stream_copy_to_stream($input, $this->inputPipe);
$this->closeStdInput();
}
}
/**
* Validates the input and determines the STDIN descriptor based on its type.
*/
private function createInputDescriptor(mixed $input): mixed
{
if ($input === null || is_string($input) || is_resource($input)) {
return ['pipe', 'r'];
} elseif (!$input instanceof self) {
throw new Nette\InvalidArgumentException('Input must be string, resource, Process or null, ' . get_debug_type($input) . ' given.');
} elseif (Helpers::IsWindows) {
throw new Nette\NotSupportedException('Process piping is not supported on Windows.');
} elseif (!isset($input->outputBuffers[self::StdOut]) || !is_resource($input->outputPipes[self::StdOut] ?? null)) {
throw new Nette\InvalidStateException('Cannot pipe from the given process: its STDOUT must be captured (it must not be redirected elsewhere).');
}
return $input->outputPipes[self::StdOut];
}
/**
* Determines the descriptor for STDOUT or STDERR based on the specified output target.
*/
private function createOutputDescriptor(int $id, mixed $output): mixed
{
if (is_resource($output)) {
$this->callerOutputs[$id] = true;
return $output;
} elseif (is_string($output)) {
return FileSystem::open($output, 'w');
} elseif ($output === false) {
return ['file', Helpers::IsWindows ? 'NUL' : '/dev/null', 'w'];
} elseif ($output === null) {
$this->outputBuffers[$id] = '';
$this->outputBufferOffsets[$id] = 0;
// On Windows anonymous pipes are blocking and cannot be polled without freezing the process,
// so captured output is backed by a temporary file that can be read non-blockingly (needed for timeouts).
return Helpers::IsWindows ? tmpfile() : ['pipe', 'w'];
} else {
throw new Nette\InvalidArgumentException('Output must be string, resource, bool or null, ' . get_debug_type($output) . ' given.');
}
}
/**
* Closes all pipes and the process resource.
*/
private function close(): void
{
$this->drainPipes();
$this->closeStdInput();
$this->closeOutputPipes();
proc_close($this->process);
}
/**
* Closes the output pipes that this class opened; resources supplied by the caller are left untouched.
* (The temporary file backing captured output on Windows is removed by fclose() itself.)
*/
private function closeOutputPipes(): void
{
foreach ($this->outputPipes as $id => $pipe) {
if (is_resource($pipe) && !isset($this->callerOutputs[$id])) {
fclose($pipe);
}
}
}
}

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -35,10 +33,14 @@ final class Reflection
}
/**
* Returns the default value of a parameter. Resolves constants and class constants used as default values.
* @throws \ReflectionException if the constant cannot be resolved
*/
public static function getParameterDefaultValue(\ReflectionParameter $param): mixed
{
if ($param->isDefaultValueConstant()) {
$const = $orig = $param->getDefaultValueConstantName();
$const = $orig = $param->getDefaultValueConstantName() ?? throw new Nette\ShouldNotHappenException;
$pair = explode('::', $const);
if (isset($pair[1])) {
$pair[0] = Type::resolve($pair[0], $param);
@ -68,6 +70,7 @@ final class Reflection
/**
* Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait.
* @return \ReflectionClass<object>
*/
public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass
{
@ -130,6 +133,9 @@ final class Reflection
}
/**
* Returns a human-readable string representation of a reflection object.
*/
public static function toString(\Reflector $ref): string
{
if ($ref instanceof \ReflectionClass) {
@ -151,6 +157,7 @@ final class Reflection
/**
* Expands the name of the class to full name in the given context of given class.
* Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context.
* @param \ReflectionClass<object> $context
* @throws Nette\InvalidArgumentException
*/
public static function expandClassName(string $name, \ReflectionClass $context): string
@ -189,7 +196,11 @@ final class Reflection
}
/** @return array<string, class-string> of [alias => class] */
/**
* Returns the use statements from the file where the class is defined.
* @param \ReflectionClass<object> $class
* @return array<string, class-string> Map of alias to fully qualified class name
*/
public static function getUseStatements(\ReflectionClass $class): array
{
if ($class->isAnonymous()) {
@ -201,7 +212,7 @@ final class Reflection
if ($class->isInternal()) {
$cache[$name] = [];
} else {
$code = file_get_contents($class->getFileName());
$code = (string) file_get_contents((string) $class->getFileName());
$cache = self::parseUseStatements($code, $name) + $cache;
}
}
@ -212,6 +223,7 @@ final class Reflection
/**
* Parses PHP code to [class => [alias => class, ...]]
* @return array<string, array<string, string>>
*/
private static function parseUseStatements(string $code, ?string $forClass = null): array
{
@ -256,8 +268,8 @@ final class Reflection
$name = ltrim($name, '\\');
if (self::fetch($tokens, '{')) {
while ($suffix = self::fetch($tokens, $nameTokens)) {
if (self::fetch($tokens, T_AS)) {
$uses[self::fetch($tokens, T_STRING)] = $name . $suffix;
if (self::fetch($tokens, T_AS) && ($alias = self::fetch($tokens, T_STRING))) {
$uses[$alias] = $name . $suffix;
} else {
$tmp = explode('\\', $suffix);
$uses[end($tmp)] = $name . $suffix;
@ -267,8 +279,8 @@ final class Reflection
break;
}
}
} elseif (self::fetch($tokens, T_AS)) {
$uses[self::fetch($tokens, T_STRING)] = $name;
} elseif (self::fetch($tokens, T_AS) && ($alias = self::fetch($tokens, T_STRING))) {
$uses[$alias] = $name;
} else {
$tmp = explode('\\', $name);
@ -301,6 +313,10 @@ final class Reflection
}
/**
* @param \PhpToken[] $tokens
* @param string|int|int[] $take
*/
private static function fetch(array &$tokens, string|int|array $take): ?string
{
$res = null;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use function explode, is_string, str_contains;
@ -18,9 +16,11 @@ use function explode, is_string, str_contains;
*/
final class ReflectionMethod extends \ReflectionMethod
{
private \ReflectionClass $originalClass;
/** @var \ReflectionClass<object> */
private readonly \ReflectionClass $originalClass;
/** @param class-string|object $objectOrMethod */
public function __construct(object|string $objectOrMethod, ?string $method = null)
{
if (is_string($objectOrMethod) && str_contains($objectOrMethod, '::')) {
@ -31,6 +31,7 @@ final class ReflectionMethod extends \ReflectionMethod
}
/** @return \ReflectionClass<object> */
public function getOriginalClass(): \ReflectionClass
{
return $this->originalClass;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use JetBrains\PhpStorm\Language;
@ -59,7 +57,8 @@ class Strings
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
return iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code));
$res = iconv('UTF-32BE', 'UTF-8//IGNORE', pack('N', $code));
return $res === false ? throw new Nette\ShouldNotHappenException : $res;
}
@ -73,11 +72,11 @@ class Strings
}
$tmp = iconv('UTF-8', 'UTF-32BE//IGNORE', $c);
if (!$tmp) {
if ($tmp === false || $tmp === '') {
throw new Nette\InvalidArgumentException('Invalid UTF-8 character "' . ($c === '' ? '' : '\x' . strtoupper(bin2hex($c))) . '".');
}
return unpack('N', $tmp)[1];
return unpack('N', $tmp)[1] ?? throw new Nette\ShouldNotHappenException;
}
@ -124,7 +123,8 @@ class Strings
$start += self::length($s); // unifies iconv_substr behavior with mb_substr
}
return iconv_substr($s, $start, $length, 'UTF-8');
$res = iconv_substr($s, $start, $length, 'UTF-8');
return $res === false ? throw new Nette\InvalidStateException('iconv_substr() failed.') : $res;
}
@ -135,7 +135,7 @@ class Strings
public static function normalize(string $s): string
{
// convert to compressed normal form (NFC)
if (class_exists('Normalizer', false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) {
if (class_exists('Normalizer', autoload: false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) {
$s = $n;
}
@ -201,14 +201,23 @@ class Strings
$s = strtr($s, ["\u{AE}" => '(R)', "\u{A9}" => '(c)', "\u{2026}" => '...', "\u{AB}" => '<<', "\u{BB}" => '>>', "\u{A3}" => 'lb', "\u{A5}" => 'yen', "\u{B2}" => '^2', "\u{B3}" => '^3', "\u{B5}" => 'u', "\u{B9}" => '^1', "\u{BA}" => 'o', "\u{BF}" => '?', "\u{2CA}" => "'", "\u{2CD}" => '_', "\u{2DD}" => '"', "\u{1FEF}" => '', "\u{20AC}" => 'EUR', "\u{2122}" => 'TM', "\u{212E}" => 'e', "\u{2190}" => '<-', "\u{2191}" => '^', "\u{2192}" => '->', "\u{2193}" => 'V', "\u{2194}" => '<->']); // ® © … « » £ ¥ ² ³ µ ¹ º ¿ ˊ ˍ ˝ € ™ ← ↑ → ↓ ↔
}
$s = \Transliterator::create('Any-Latin; Latin-ASCII')->transliterate($s);
$s = \Transliterator::create('Any-Latin; Latin-ASCII')?->transliterate($s)
?? throw new Nette\InvalidStateException('Transliterator::transliterate() failed.');
// use iconv because The transliterator leaves some characters out of ASCII, eg → ʾ
if ($iconv === 'glibc') {
$s = strtr($s, '?', "\x01"); // temporarily hide ? to distinguish them from the garbage that iconv creates
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
if ($s === false) {
throw new Nette\InvalidStateException('iconv() failed.');
}
$s = str_replace(['?', "\x01"], ['', '?'], $s); // remove garbage and restore ? characters
} elseif ($iconv === 'libiconv') {
$s = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
if ($s === false) {
throw new Nette\InvalidStateException('iconv() failed.');
}
} else { // null or 'unknown' (#216)
$s = self::pcre('preg_replace', ['#[^\x00-\x7F]++#', '', $s]); // remove non-ascii chars
}
@ -323,7 +332,7 @@ class Strings
*/
public static function compare(string $left, string $right, ?int $length = null): bool
{
if (class_exists('Normalizer', false)) {
if (class_exists('Normalizer', autoload: false)) {
$left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster
$right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster
}
@ -347,6 +356,10 @@ class Strings
public static function findPrefix(array $strings): string
{
$first = array_shift($strings);
if ($first === null) {
return '';
}
for ($i = 0; $i < strlen($first); $i++) {
foreach ($strings as $s) {
if (!isset($s[$i]) || $first[$i] !== $s[$i]) {
@ -370,8 +383,8 @@ class Strings
public static function length(string $s): int
{
return match (true) {
extension_loaded('mbstring') => mb_strlen($s, 'UTF-8'),
extension_loaded('iconv') => iconv_strlen($s, 'UTF-8'),
extension_loaded('mbstring') => (int) mb_strlen($s, 'UTF-8'),
extension_loaded('iconv') => (int) iconv_strlen($s, 'UTF-8'),
default => strlen(@utf8_decode($s)), // deprecated
};
}
@ -420,7 +433,10 @@ class Strings
throw new Nette\NotSupportedException(__METHOD__ . '() requires ICONV extension that is not loaded.');
}
return iconv('UTF-32LE', 'UTF-8', strrev(iconv('UTF-8', 'UTF-32BE', $s)));
$tmp = iconv('UTF-8', 'UTF-32BE', $s);
return $tmp === false
? throw new Nette\InvalidStateException('iconv() failed.')
: (string) iconv('UTF-32LE', 'UTF-8', strrev($tmp));
}
@ -498,7 +514,8 @@ class Strings
/**
* Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well.
* Splits the string by a regular expression. Expressions in parentheses will be captured and returned as well.
* @return list<string>
*/
public static function split(
string $subject,
@ -523,8 +540,9 @@ class Strings
/**
* Searches the string for the part matching the regular expression and returns
* an array with the found expression and individual subexpressions, or `null`.
* Searches the string for the first match of the regular expression and returns
* an array with the found expression and individual subexpressions, or null.
* @return ?array<string>
*/
public static function match(
string $subject,
@ -545,6 +563,7 @@ class Strings
$pattern .= 'u';
}
$m = [];
if ($offset > strlen($subject)) {
return null;
} elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) {
@ -558,9 +577,9 @@ class Strings
/**
* Searches the string for all occurrences matching the regular expression and
* returns an array of arrays containing the found expression and each subexpression.
* @return ($lazy is true ? \Generator<int, array> : array[])
* Searches the string for all occurrences matching the regular expression and returns
* an array of arrays containing the found expression and each subexpression.
* @return ($lazy is true ? \Generator<int, array<string>> : list<array<string>>)
*/
public static function matchAll(
string $subject,
@ -583,10 +602,12 @@ class Strings
$flags = PREG_OFFSET_CAPTURE | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
return (function () use ($utf8, $captureOffset, $flags, $subject, $pattern, $offset) {
$counter = 0;
$m = [];
while (
$offset <= strlen($subject) - ($counter ? 1 : 0)
&& self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])
) {
/** @var list<array{string, int}> $m */
$offset = $m[0][1] + max(1, strlen($m[0][0]));
if (!$captureOffset) {
$m = array_map(fn($item) => $item[0], $m);
@ -606,6 +627,7 @@ class Strings
? $captureOffset
: ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0);
$m = [];
self::pcre('preg_match_all', [
$pattern, $subject, &$m,
($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER),
@ -618,7 +640,8 @@ class Strings
/**
* Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`.
* Replaces all occurrences matching the regular expression $pattern, which can be a string or array in the form `pattern => replacement`.
* @param string|array<string, string> $pattern
*/
public static function replace(
string $subject,
@ -638,7 +661,7 @@ class Strings
$flags = ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0);
if ($utf8) {
$pattern .= 'u';
$pattern = is_array($pattern) ? array_map(fn($item) => $item . 'u', $pattern) : $pattern . 'u';
if ($captureOffset) {
$replacement = fn($m) => $replacement(self::bytesToChars($subject, [$m])[0]);
}
@ -659,6 +682,10 @@ class Strings
}
/**
* @param list<array<array{string, int}>> $groups
* @return list<array<array{string, int}>>
*/
private static function bytesToChars(string $s, array $groups): array
{
$lastBytes = $lastChars = 0;
@ -679,8 +706,12 @@ class Strings
}
/** @internal */
public static function pcre(string $func, array $args)
/**
* @param callable-string $func
* @param list<mixed> $args
* @internal
*/
public static function pcre(string $func, array $args): mixed
{
$res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void {
// compile-time error, not detectable by preg_last_error
@ -688,7 +719,7 @@ class Strings
});
if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars
&& ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true))
&& ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], strict: true))
) {
throw new RegexpException(preg_last_error_msg()
. ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code);

View File

@ -1,16 +1,14 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
use function array_map, array_search, array_splice, count, explode, implode, is_a, is_resource, is_string, strcasecmp, strtolower, substr, trim;
use function array_map, array_search, array_splice, array_values, count, explode, implode, is_a, is_resource, is_string, strcasecmp, strtolower, substr, trim;
/**
@ -18,9 +16,9 @@ use function array_map, array_search, array_splice, count, explode, implode, is_
*/
final readonly class Type
{
/** @var array<int, string|self> */
/** @var list<string|self> */
private array $types;
private bool $simple;
private ?string $singleName;
private string $kind; // | &
@ -40,7 +38,12 @@ final readonly class Type
}
private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string
/** @return ($asObject is true ? self : self|string) */
private static function fromReflectionType(
\ReflectionType $type,
\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of,
bool $asObject,
): self|string
{
if ($type instanceof \ReflectionNamedType) {
$name = self::resolve($type->getName(), $of);
@ -107,34 +110,40 @@ final readonly class Type
*/
public static function resolve(
string $type,
\ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of,
\ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $of,
): string
{
$lower = strtolower($type);
if ($of instanceof \ReflectionFunction) {
return $type;
}
$class = $of->getDeclaringClass();
if ($class === null) {
return $type;
} elseif ($lower === 'self') {
return $of->getDeclaringClass()->name;
return $class->name;
} elseif ($lower === 'static') {
return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $of->getDeclaringClass())->name;
} elseif ($lower === 'parent' && $of->getDeclaringClass()->getParentClass()) {
return $of->getDeclaringClass()->getParentClass()->name;
return ($of instanceof ReflectionMethod ? $of->getOriginalClass() : $class)->name;
} elseif ($lower === 'parent' && $class->getParentClass()) {
return $class->getParentClass()->name;
} else {
return $type;
}
}
/** @param array<string|self> $types */
private function __construct(array $types, string $kind = '|')
{
$o = array_search('null', $types, strict: true);
if ($o !== false) { // null as last
array_splice($types, $o, 1);
array_splice($types, (int) $o, 1);
$types[] = 'null';
}
$this->types = $types;
$this->simple = is_string($types[0]) && ($types[1] ?? 'null') === 'null';
$this->types = array_values($types);
$this->singleName = is_string($types[0]) && ($types[1] ?? 'null') === 'null' ? $types[0] : null;
$this->kind = count($types) > 1 ? $kind : '';
}
@ -142,8 +151,8 @@ final readonly class Type
public function __toString(): string
{
$multi = count($this->types) > 1;
if ($this->simple) {
return ($multi ? '?' : '') . $this->types[0];
if ($this->singleName !== null) {
return ($multi ? '?' : '') . $this->singleName;
}
$res = [];
@ -155,7 +164,7 @@ final readonly class Type
/**
* Returns a type that accepts both the current type and the given type.
* Returns a union type that accepts both the current type and the given type.
*/
public function with(string|self $type): self
{
@ -173,7 +182,7 @@ final readonly class Type
/**
* Returns the array of subtypes that make up the compound type as strings.
* @return array<int, string|string[]>
* @return list<string|array<string|array<mixed>>>
*/
public function getNames(): array
{
@ -182,8 +191,8 @@ final readonly class Type
/**
* Returns the array of subtypes that make up the compound type as Type objects:
* @return self[]
* Returns the array of subtypes that make up the compound type as Type objects.
* @return list<self>
*/
public function getTypes(): array
{
@ -196,9 +205,7 @@ final readonly class Type
*/
public function getSingleName(): ?string
{
return $this->simple
? $this->types[0]
: null;
return $this->singleName;
}
@ -221,36 +228,36 @@ final readonly class Type
/**
* Returns true whether it is a simple type. Single nullable types are also considered to be simple types.
* Checks whether it is a simple (non-compound) type. Single nullable types such as ?int are also considered simple.
*/
public function isSimple(): bool
{
return $this->simple;
return $this->singleName !== null;
}
#[\Deprecated('use isSimple()')]
public function isSingle(): bool
{
return $this->simple;
return $this->singleName !== null;
}
/**
* Returns true whether the type is both a simple and a PHP built-in type.
* Checks whether it is a simple PHP built-in type (int, string, bool, etc.).
*/
public function isBuiltin(): bool
{
return $this->simple && Validators::isBuiltinType($this->types[0]);
return $this->singleName !== null && Validators::isBuiltinType($this->singleName);
}
/**
* Returns true whether the type is both a simple and a class name.
* Checks whether it is a simple class or interface name (not a built-in type).
*/
public function isClass(): bool
{
return $this->simple && !Validators::isBuiltinType($this->types[0]);
return $this->singleName !== null && !Validators::isBuiltinType($this->singleName);
}
@ -259,12 +266,12 @@ final readonly class Type
*/
public function isClassKeyword(): bool
{
return $this->simple && Validators::isClassKeyword($this->types[0]);
return $this->singleName !== null && Validators::isClassKeyword($this->singleName);
}
/**
* Verifies type compatibility. For example, it checks if a value of a certain type could be passed as a parameter.
* Checks whether a value of the given type could be assigned to this type.
*/
public function allows(string|self $type): bool
{
@ -279,6 +286,7 @@ final readonly class Type
}
/** @param array<string> $givenTypes */
private function allowsAny(array $givenTypes): bool
{
return $this->isUnion()
@ -287,13 +295,17 @@ final readonly class Type
}
/**
* @param array<string> $ourTypes
* @param array<string> $givenTypes
*/
private function allowsAll(array $ourTypes, array $givenTypes): bool
{
return Arrays::every(
$ourTypes,
fn($ourType) => Arrays::some(
fn(string $ourType) => Arrays::some(
$givenTypes,
fn($givenType) => Validators::isBuiltinType($ourType)
fn(string $givenType) => Validators::isBuiltinType($ourType)
? strcasecmp($ourType, $givenType) === 0
: is_a($givenType, $ourType, allow_string: true),
),

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;
@ -26,7 +24,7 @@ class Validators
'never' => 1, 'true' => 1,
];
/** @var array<string,?callable> */
/** @var array<string, ?(callable(mixed): bool)> */
protected static $validators = [
// PHP types
'array' => 'is_array',
@ -76,7 +74,7 @@ class Validators
'type' => [self::class, 'isType'],
];
/** @var array<string,callable> */
/** @var array<string, callable(mixed): int> */
protected static $counters = [
'string' => 'strlen',
'unicode' => [Strings::class, 'length'],
@ -114,22 +112,22 @@ class Validators
/**
* Verifies that element $key in array is of expected types separated by pipe.
* Verifies that item $key in array exists and is of expected types separated by pipe.
* @param mixed[] $array
* @throws AssertionException
*/
public static function assertField(
array $array,
$key,
int|string $key,
?string $expected = null,
string $label = "item '%' in array",
): void
{
if (!array_key_exists($key, $array)) {
throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.');
throw new AssertionException('Missing ' . str_replace('%', (string) $key, $label) . '.');
} elseif ($expected) {
static::assert($array[$key], $expected, str_replace('%', $key, $label));
static::assert($array[$key], $expected, str_replace('%', (string) $key, $label));
}
}
@ -159,7 +157,7 @@ class Validators
if (!static::$validators[$type]($value)) {
continue;
}
} catch (\TypeError $e) {
} catch (\TypeError) {
continue;
}
} elseif ($type === 'pattern') {
@ -197,7 +195,7 @@ class Validators
/**
* Finds whether all values are of expected types separated by pipe.
* @param mixed[] $values
* @param iterable<mixed> $values
*/
public static function everyIs(iterable $values, string $expected): bool
{
@ -261,7 +259,7 @@ class Validators
/**
* Checks if the value is 0, '', false or null.
* @return ($value is 0|''|false|null ? true : false)
* @return ($value is 0|0.0|''|false|null ? true : false)
*/
public static function isNone(mixed $value): bool
{
@ -290,6 +288,7 @@ class Validators
/**
* Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null).
* Numbers, strings and DateTime objects can be compared.
* @param array{int|float|string|\DateTimeInterface|null, int|float|string|\DateTimeInterface|null} $range
*/
public static function isInRange(mixed $value, array $range): bool
{

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
@ -48,3 +46,19 @@ class RegexpException extends \Exception
class AssertionException extends \Exception
{
}
/**
* The process failed to run successfully.
*/
class ProcessFailedException extends \RuntimeException
{
}
/**
* The process execution exceeded its timeout limit.
*/
class ProcessTimeoutException extends \RuntimeException
{
}

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Utils;
use Nette;

View File

@ -1,12 +1,10 @@
<?php
<?php declare(strict_types=1);
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette;

View File

@ -21,8 +21,8 @@ final class GeneratedConfig
0 => 'rules.neon',
),
),
'version' => '2.12.0',
'phpstanVersionConstraint' => '>=2.1.8.0-dev, <3.0.0.0-dev',
'version' => '2.13.1',
'phpstanVersionConstraint' => '>=2.1.35.0-dev, <3.0.0.0-dev',
),
'phpstan/phpstan-strict-rules' =>
array (
@ -35,8 +35,8 @@ final class GeneratedConfig
0 => 'rules.neon',
),
),
'version' => '2.0.7',
'phpstanVersionConstraint' => '>=2.1.29.0-dev, <3.0.0.0-dev',
'version' => '2.0.11',
'phpstanVersionConstraint' => '>=2.1.39.0-dev, <3.0.0.0-dev',
),
'tomasvotruba/type-coverage' =>
array (
@ -58,7 +58,7 @@ final class GeneratedConfig
);
/** @var string|null */
public const PHPSTAN_VERSION_CONSTRAINT = '>=2.1.29.0-dev, <3.0.0.0-dev';
public const PHPSTAN_VERSION_CONSTRAINT = '>=2.1.39.0-dev, <3.0.0.0-dev';
private function __construct()
{

View File

@ -1,27 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.{php,phpt}]
indent_style = tab
indent_size = 4
[*.xml]
indent_style = tab
indent_size = 4
[*.neon]
indent_style = tab
indent_size = 4
[*.{yaml,yml}]
indent_style = space
indent_size = 2
[composer.json]
indent_style = tab
indent_size = 4

View File

@ -10,13 +10,14 @@
|:---------------------------------------|:--------------------------------------------------------------------------------------------------------|
| `booleansInConditions` | Require booleans in `if`, `elseif`, ternary operator, after `!`, and on both sides of `&&` and `\|\|`. |
| `booleansInLoopConditions` | Require booleans in `while` and `do while` loop conditions. |
| `numericOperandsInArithmeticOperators` | Require numeric operand in `+$var`, `-$var`, `$var++`, `$var--`, `++$var` and `--$var`. |
| `numericOperandsInArithmeticOperators` | Require numeric operand in `$var++`, `$var--`, `++$var`and `--$var`. |
| `uselessCast` | Detect useless casts — e.g. casting to `int` when the expression is already `int`. |
| `numericOperandsInArithmeticOperators` | Require numeric operands in `+`, `-`, `*`, `/`, `%`, `**`, `+$var`, `-$var`, `$var++`, `$var--`, `++$var` and `--$var`. |
| `strictFunctionCalls` | These functions contain a `$strict` parameter for better type safety, it must be set to `true`:<br>* `in_array` (3rd parameter)<br>* `array_search` (3rd parameter)<br>* `array_keys` (3rd parameter; only if the 2nd parameter `$search_value` is provided)<br>* `base64_decode` (2nd parameter). |
| `overwriteVariablesWithLoop` | * Disallow overwriting variables with `foreach` key and value variables.<br>* Disallow overwriting variables with `for` loop initial assignment. |
| `switchConditionsMatchingType` | Types in `switch` condition and `case` value must match. PHP compares them loosely by default and that can lead to unexpected results. |
| `dynamicCallOnStaticMethod` | Check that statically declared methods are called statically. |
| `disallowedEmpty` | Disallow `empty()` - it's a very loose comparison (see [manual](https://php.net/empty)), it's recommended to use more strict one. |
| `disallowedLooseComparison` | Disallow loose comparison via `==` and `!=`. |
| `disallowedShortTernary` | Disallow short ternary operator (`?:`) - implies weak comparison, it's recommended to use null coalesce operator (`??`) or ternary operator with strict condition. |
| `noVariableVariables` | Disallow variable variables (`$$foo`, `$this->$method()` etc.). |
| `checkAlwaysTrueInstanceof`, `checkAlwaysTrueCheckTypeFunctionCall`, `checkAlwaysTrueStrictComparison` | Always true `instanceof`, type-checking `is_*` functions and strict comparisons `===`/`!==`. These checks can be turned off by setting `checkAlwaysTrueInstanceof`, `checkAlwaysTrueCheckTypeFunctionCall` and `checkAlwaysTrueStrictComparison` to false. |
@ -26,6 +27,9 @@
| | Check LSP even for static methods. |
| `requireParentConstructorCall` | Require calling parent constructor. |
| `disallowedBacktick` | Disallow usage of backtick operator (`` $ls = `ls -la` ``). |
| `disallowedImplicitArrayCreation` | Disallow implicit array creation through `$var[] =` when the variable does not exist yet. |
| `strictArrayFilter` | Require `array_filter()` to have a callback parameter to avoid loose comparison semantics. |
| `illegalConstructorMethodCall` | Disallow calling `__construct()` on an existing object or as a static call outside of parent constructor. |
| `closureUsesThis` | Closure should use `$this` directly instead of using `$this` variable indirectly. |
Additional rules are coming in subsequent releases!

View File

@ -5,9 +5,10 @@
"license": [
"MIT"
],
"keywords": ["static analysis"],
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.29"
"phpstan/phpstan": "^2.1.39"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",

View File

@ -12,6 +12,7 @@ parameters:
reportMaybesInPropertyPhpDocTypes: true
reportWrongPhpDocTypeInVarTag: true
checkStrictPrintfPlaceholderTypes: true
reportNonIntStringArrayKey: %featureToggles.bleedingEdge%
strictRules:
allRules: true
disallowedLooseComparison: %strictRules.allRules%
@ -164,6 +165,8 @@ services:
-
class: PHPStan\Rules\DisallowedConstructs\DisallowedLooseComparisonRule
arguments:
includeOperandTypesInErrorMessage: %featureToggles.bleedingEdge%
-
class: PHPStan\Rules\BooleansInConditions\BooleanInBooleanAndRule

View File

@ -4,6 +4,7 @@ namespace PHPStan\Rules\Cast;
use PhpParser\Node;
use PhpParser\Node\Expr\Cast;
use PhpParser\Node\Expr\Cast\Void_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
@ -38,6 +39,10 @@ class UselessCastRule implements Rule
public function processNode(Node $node, Scope $scope): array
{
if ($node instanceof Void_) {
return [];
}
$castType = $scope->getType($node);
if ($castType instanceof ErrorType) {
return [];

View File

@ -9,6 +9,8 @@ use PhpParser\Node\Expr\BinaryOp\NotEqual;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
/**
* @implements Rule<BinaryOp>
@ -16,6 +18,13 @@ use PHPStan\Rules\RuleErrorBuilder;
class DisallowedLooseComparisonRule implements Rule
{
private bool $includeOperandTypesInErrorMessage;
public function __construct(bool $includeOperandTypesInErrorMessage)
{
$this->includeOperandTypesInErrorMessage = $includeOperandTypesInErrorMessage;
}
public function getNodeType(): string
{
return BinaryOp::class;
@ -23,26 +32,34 @@ class DisallowedLooseComparisonRule implements Rule
public function processNode(Node $node, Scope $scope): array
{
if (!$node instanceof Equal && !$node instanceof NotEqual) {
return [];
}
$left = $scope->getType($node->left)->describe(VerbosityLevel::typeOnly());
$right = $scope->getType($node->right)->describe(VerbosityLevel::typeOnly());
if ($node instanceof Equal) {
return [
RuleErrorBuilder::message(
'Loose comparison via "==" is not allowed.',
$this->includeOperandTypesInErrorMessage
? sprintf('Loose comparison via "==" between %s and %s is not allowed.', $left, $right)
: 'Loose comparison via "==" is not allowed.',
)->tip('Use strict comparison via "===" instead.')
->identifier('equal.notAllowed')
->build(),
];
}
if ($node instanceof NotEqual) {
return [
RuleErrorBuilder::message(
'Loose comparison via "!=" is not allowed.',
)->tip('Use strict comparison via "!==" instead.')
->identifier('notEqual.notAllowed')
->build(),
];
}
return [];
return [
RuleErrorBuilder::message(
$this->includeOperandTypesInErrorMessage
? sprintf('Loose comparison via "!=" between %s and %s is not allowed.', $left, $right)
: 'Loose comparison via "!=" is not allowed.',
)->tip('Use strict comparison via "!==" instead.')
->identifier('notEqual.notAllowed')
->build(),
];
}
}

View File

@ -44,6 +44,10 @@ class VariablePropertyFetchRule implements Rule
return [];
}
if ($scope->getType($node->name)->isLiteralString()->yes()) {
return [];
}
$fetchedOnType = $scope->getType($node->var);
foreach ($fetchedOnType->getObjectClassNames() as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {

View File

@ -27,8 +27,9 @@ final class PharAutoloader
if (self::$composerAutoloader === null) {
self::$composerAutoloader = require 'phar://' . __DIR__ . '/phpstan.phar/vendor/autoload.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/jetbrains/phpstorm-stubs/PhpStormStubsMap.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/react/async/src/functions_include.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/react/promise/src/functions_include.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/ralouphie/getallheaders/src/getallheaders.php';
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/guzzlehttp/guzzle/src/functions_include.php';
}
self::$composerAutoloader->loadClass($class);
@ -106,7 +107,7 @@ final class PharAutoloader
if (
PHP_VERSION_ID < 80400
&& empty ($GLOBALS['__composer_autoload_files']['9d2b9fc6db0f153a0a149fefb182415e'])
&& !class_exists(\Symfony\Polyfill\Php83\Php84::class, false)
&& !class_exists(\Symfony\Polyfill\Php84\Php84::class, false)
) {
$GLOBALS['__composer_autoload_files']['9d2b9fc6db0f153a0a149fefb182415e'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php84/Php84.php';
@ -116,7 +117,7 @@ final class PharAutoloader
if (
PHP_VERSION_ID < 80500
&& empty ($GLOBALS['__composer_autoload_files']['606a39d89246991a373564698c2d8383'])
&& !class_exists(\Symfony\Polyfill\Php83\Php85::class, false)
&& !class_exists(\Symfony\Polyfill\Php85\Php85::class, false)
) {
$GLOBALS['__composer_autoload_files']['606a39d89246991a373564698c2d8383'] = true;
require_once 'phar://' . __DIR__ . '/phpstan.phar/vendor/symfony/polyfill-php85/Php85.php';

View File

@ -1,16 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEynwsejDI6OEnSoR2UcZzBf/C5cAFAmkysr4ACgkQUcZzBf/C
5cDE+RAAlbDueQxsbyo+wqm1sURcXT4fPbwdFAv1RXSYfZl7XraoagxYHssIHt9n
TcKuBSOPOmlKgZ4FmVW05TRr0yoLZtckpRDCWuJoPzI9ilW2tiVi6AbEVhV0EqwL
xVToGIzZXmti+X5Mm2OwQMCdpAbqpAy0mwmv/TudFiKdgZBzl5zHWDUSNCwP8s2m
pLGBupQis6sA45+u28vLpyR7vn3mIq3F3hJgb5nrZlWt33jVJ7MDFnpGpeJ6+jBn
/1BmefK0sXwN4EqaJbrRkHdRePISt2+jQXa/V/L3oJ0a3hVFufcGze41ARecSQ/0
203aB0XcBtIFlshE5OIpXW+ibmgBabd+ggZEkRoS3OXtV5cJizLh49B+16A97QOr
H0NWgLfY/W1u2t0Iu+WDT37Na5LPsuucW9vv9PwS6WY9eCr4Jbbmq1PtF0T17aqx
STyPWNjs5A8NNjVUQ+Ufj/AAdH3hmNj4B5H6psxHD5UsFHoOLVFtwih4GdHrKXIZ
N2lznVo8HDCcvKo6C4F8ejG1t4f5qBHNpYeEC9aVQVvXrBockQQz4lUclibW7MIe
g33fXgO/GIm0dn2u0NCzjYof+TAVOfvtbK4GY0Z9BOlJcQnzu7V1LxqFamG5ef+u
A2ABQa0HVEeKTomB3jzNQQVKw9QzWyfI21xSskfpFs4FXW3A5xc=
=edJd
iQIzBAABCgAdFiEEynwsejDI6OEnSoR2UcZzBf/C5cAFAmnyB+sACgkQUcZzBf/C
5cCc/w//RkO2T6MfMe/Fh1MeWAtdvqVCBDG0x1SwP0Z4UmhW2/EyxPlgP7LtyZOr
IiivLmTQlv9VgNsyKrVJjvo6WH4mQ39s6yyGHsRJxtQqeZRHQWFkCz/SykwCFJ8e
X7vhJsW6UzvboA1/zvjkuRnOr1JVt2DWRVN2P5STK+hk/hizBE/ec404Boc/iD7A
0WGEnhnDGsyKfwB1NHhiWiqUrZ3Wo8x17wyeZsvmH/SL2oimfEjPe3oGm37CKbjr
ztSGnCXfO5EWHxFOIs5O1xbW7Lz13EboQNzpsJyQVpjVwEdLq8xt2LWxnRN7gQTd
5JV5pygmAFhrf44xoFa20HLK2ia14izA+dMTXvTBRHDPJzPaVo2wW3Nt4qg+WCdT
TrN7IAWx88OjZ/4rAKIp3SDzNj3TsN9KxzriCokzZmExipkZL4C+rDLkqhwEhrwE
W+kSGTz0egQeNwBA1FBWKmiBzMYr+rF2uwy7Oud2BHwnq1N0n5tpS5Djsti0fFcp
YQdNt27e7yjV4LO6O7h7uK0r7u0QZ7Pw0En9sU9FC5PQsEmzP22aUoFpqEnzNI2g
7MbHRO5qKmYI7f991RFOj/utbuk5d17ByAOLroJ8Ub8B2qIVEczTPwtYJ06Wf59I
ZRQDRoWjv4q8JosQ9xclS2nt6S//SMmeXrJLMh5t3u65vQN9OcI=
=H59Q
-----END PGP SIGNATURE-----

Binary file not shown.

Binary file not shown.