diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 00000000..fad4940e --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +exclude_paths: + - ".github/**" + - "example/**" + - "test/**" + - "go.php" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e73723df..96fc1012 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,19 +1,25 @@ name: CI -on: [push] +on: + push: + pull_request: + +permissions: + contents: read + actions: read jobs: composer: runs-on: ubuntu-latest strategy: matrix: - php: [ 8.3, 8.4 ] + php: [ 8.4, 8.5 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Cache Composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v5 with: path: /tmp/composer-cache key: ${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} @@ -22,139 +28,149 @@ jobs: uses: php-actions/composer@v6 with: php_version: ${{ matrix.php }} + php_extensions: pcntl - name: Archive build - run: mkdir /tmp/github-actions/ && tar -cvf /tmp/github-actions/build.tar ./ + run: mkdir /tmp/github-actions/ && tar --exclude=".git" -cvf /tmp/github-actions/build.tar ./ - name: Upload build archive for test runners uses: actions/upload-artifact@v4 with: name: build-artifact-${{ matrix.php }} path: /tmp/github-actions -# -# phpunit: -# runs-on: ubuntu-latest -# needs: [ composer ] -# strategy: -# matrix: -# php: [ 8.1, 8.2, 8.3 ] -# -# outputs: -# coverage: ${{ steps.store-coverage.outputs.coverage_text }} -# -# steps: -# - uses: actions/download-artifact@v4 -# with: -# name: build-artifact-${{ matrix.php }} -# path: /tmp/github-actions -# -# - name: Extract build archive -# run: tar -xvf /tmp/github-actions/build.tar ./ -# -# - name: PHP Unit tests -# uses: php-actions/phpunit@v3 -# env: -# XDEBUG_MODE: cover -# with: -# version: 10 -# php_version: ${{ matrix.php }} -# php_extensions: xdebug -# coverage_text: _coverage/coverage.txt -# coverage_clover: _coverage/clover.xml -# -# - name: Store coverage data -# uses: actions/upload-artifact@v4 -# with: -# name: code-coverage-${{ matrix.php }}-${{ github.run_number }} -# path: _coverage -# -# coverage: -# runs-on: ubuntu-latest -# needs: [ phpunit ] -# strategy: -# matrix: -# php: [ 8.1, 8.2, 8.3 ] -# -# steps: -# - uses: actions/download-artifact@v4 -# with: -# name: code-coverage-${{ matrix.php }}-${{ github.run_number }} -# path: _coverage -# -# - name: Output coverage -# run: cat "_coverage/coverage.txt" -# -# - name: Upload to Codecov -# uses: codecov/codecov-action@v4 -# with: -# token: ${{ secrets.CODECOV_TOKEN }} -# -# phpstan: -# runs-on: ubuntu-latest -# needs: [ composer ] -# strategy: -# matrix: -# php: [ 8.1, 8.2, 8.3 ] -# -# steps: -# - uses: actions/download-artifact@v4 -# with: -# name: build-artifact-${{ matrix.php }} -# path: /tmp/github-actions -# -# - name: Extract build archive -# run: tar -xvf /tmp/github-actions/build.tar ./ -# -# - name: PHP Static Analysis -# uses: php-actions/phpstan@v3 -# with: -# php_version: ${{ matrix.php }} -# path: src/ -# level: 6 -# -# phpmd: -# runs-on: ubuntu-latest -# needs: [ composer ] -# strategy: -# matrix: -# php: [ 8.1, 8.2, 8.3 ] -# -# steps: -# - uses: actions/download-artifact@v4 -# with: -# name: build-artifact-${{ matrix.php }} -# path: /tmp/github-actions -# -# - name: Extract build archive -# run: tar -xvf /tmp/github-actions/build.tar ./ -# -# - name: PHP Mess Detector -# uses: php-actions/phpmd@v1 -# with: -# php_version: ${{ matrix.php }} -# path: src/ -# output: text -# ruleset: phpmd.xml -# -# phpcs: -# runs-on: ubuntu-latest -# needs: [ composer ] -# strategy: -# matrix: -# php: [ 8.1, 8.2, 8.3 ] -# -# steps: -# - uses: actions/download-artifact@v4 -# with: -# name: build-artifact-${{ matrix.php }} -# path: /tmp/github-actions -# -# - name: Extract build archive -# run: tar -xvf /tmp/github-actions/build.tar ./ -# -# - name: PHP Code Sniffer -# uses: php-actions/phpcs@v1 -# with: -# php_version: ${{ matrix.php }} -# path: src/ -# standard: phpcs.xml + retention-days: 1 + + phpunit: + runs-on: ubuntu-latest + needs: [ composer ] + strategy: + matrix: + php: [ 8.4, 8.5 ] + + outputs: + coverage: ${{ steps.store-coverage.outputs.coverage_text }} + + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifact-${{ matrix.php }} + path: /tmp/github-actions + + - name: Extract build archive + run: tar -xvf /tmp/github-actions/build.tar ./ + + - name: PHP Unit tests + uses: php-actions/phpunit@v4 + env: + XDEBUG_MODE: cover + with: + php_version: ${{ matrix.php }} + php_extensions: xdebug + coverage_text: _coverage/coverage.txt + coverage_clover: _coverage/clover.xml + + - name: Store coverage data + uses: actions/upload-artifact@v4 + with: + name: code-coverage-${{ matrix.php }}-${{ github.run_number }} + path: _coverage + + coverage: + runs-on: ubuntu-latest + needs: [ phpunit ] + strategy: + matrix: + php: [ 8.4, 8.5 ] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: code-coverage-${{ matrix.php }}-${{ github.run_number }} + path: _coverage + + - name: Output coverage + run: cat "_coverage/coverage.txt" + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + phpstan: + runs-on: ubuntu-latest + needs: [ composer ] + strategy: + matrix: + php: [ 8.4, 8.5 ] + + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifact-${{ matrix.php }} + path: /tmp/github-actions + + - name: Extract build archive + run: tar -xvf /tmp/github-actions/build.tar ./ + + - name: PHP Static Analysis + uses: php-actions/phpstan@v3 + with: + php_version: ${{ matrix.php }} + path: src/ + level: 6 + memory_limit: 256M + + phpmd: + runs-on: ubuntu-latest + needs: [ composer ] + strategy: + matrix: + php: [ 8.4, 8.5 ] + + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifact-${{ matrix.php }} + path: /tmp/github-actions + + - name: Extract build archive + run: tar -xvf /tmp/github-actions/build.tar ./ + + - name: PHP Mess Detector + uses: php-actions/phpmd@v1 + with: + php_version: ${{ matrix.php }} + path: src/ + output: text + ruleset: phpmd.xml + + phpcs: + runs-on: ubuntu-latest + needs: [ composer ] + strategy: + matrix: + php: [ 8.4, 8.5 ] + + steps: + - uses: actions/download-artifact@v4 + with: + name: build-artifact-${{ matrix.php }} + path: /tmp/github-actions + + - name: Extract build archive + run: tar -xvf /tmp/github-actions/build.tar ./ + + - name: PHP Code Sniffer + uses: php-actions/phpcs@v1 + with: + php_version: ${{ matrix.php }} + path: src/ + standard: phpcs.xml + + remove_old_artifacts: + runs-on: ubuntu-latest + + permissions: + actions: write diff --git a/.gitignore b/.gitignore index 3c59648a..3d5422a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea /vendor /test/behaviour/project/*/vendor -/test/unit/_coverage \ No newline at end of file +/test/phpunit/.phpunit.cache +.phpunit*.cache diff --git a/README.md b/README.md index 842489a3..3169b247 100644 --- a/README.md +++ b/README.md @@ -85,10 +85,6 @@ The [Github issue tracker][issues] is used to submit bug reports, feature reques It would be helpful if you could create your issue in the appropriate repository - for instance, if the issue/question is regarding using a database in WebEngine, https://github.com/phpgt/Database/issues would be the best place - but it's fine to create the issue on WebEngine's issue tracker, and someone can then move the issue if necessary. -### Chat to a developer - -A hands-on dev chat system is currently being planned - [quick-start]: https://github.com/PhpGt/WebEngine/wiki/Quick-start [tutorials]: https://github.com/PhpGt/WebEngine/wiki/hello-world-tutorial [contributing]: https://github.com/PhpGt/WebEngine/blob/master/CONTRIBUTING.md diff --git a/build.default.json b/build.default.json index 9032f7bb..e5afe8ee 100644 --- a/build.default.json +++ b/build.default.json @@ -15,7 +15,7 @@ }, "execute": { "command": "vendor/bin/sync", - "arguments": ["--pattern", "*.js", "script", "www/script"] + "arguments": ["--pattern", "**/*.js", "script", "www/script"] } }, @@ -35,7 +35,7 @@ }, "execute": { "command": "vendor/bin/sync", - "arguments": ["--pattern", "*.css", "style", "www/style"] + "arguments": ["--pattern", "**/*.css", "style", "www/style"] } }, diff --git a/composer.json b/composer.json index da38c229..a700f500 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "phpgt/webengine", - "description": "Rapid development engine.", + "description": "Minimalistic, ergonomic PHP toolkit.", "license": [ "MIT" @@ -10,7 +10,8 @@ "require": { "ext-dom": "*", "ext-json": "*", - "php": ">=8.1", + "ext-curl": "*", + "php": ">=8.4", "phpgt/async": "^1.0", "phpgt/build": "^1.2", @@ -28,38 +29,54 @@ "phpgt/domvalidation": "^1.0", "phpgt/fetch": "^1.2", "phpgt/filecache": "^1.2", - "phpgt/http": "^1.2", + "phpgt/http": "^1.3.5", "phpgt/input": "^1.3", "phpgt/logger": "^1.0", "phpgt/promise": "^2.4", - "phpgt/protectedglobal": "^1.1", - "phpgt/routing": "1.1.3", + "phpgt/protectedglobal": "^2.0", + "phpgt/routing": "^1.1", "phpgt/server": "^1.1", "phpgt/servicecontainer": "^1.3", - "phpgt/session": "1.2.4", + "phpgt/session": "^1.2", "phpgt/sync": "^1.3", "phpgt/ulid": "^1.0", - - "psr/http-server-middleware": "^1.0", "willdurand/negotiation": "^3.0" }, "require-dev": { - "phpstan/phpstan": "v1.8.0", - "phpunit/phpunit": "v9.5.21" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.4", + "phpmd/phpmd": "^2.13", + "squizlabs/php_codesniffer": "^3.7" }, "autoload": { + "files": [ + "./src/gt-compat.php" + ], "psr-4": { - "Gt\\WebEngine\\": "./src/" + "GT\\WebEngine\\": "./src/" } }, "autoload-dev": { "psr-4": { - "Gt\\WebEngine\\Test\\": "./test/phpunit" + "GT\\WebEngine\\Test\\": "./test/phpunit" } }, + "scripts": { + "phpunit": "vendor/bin/phpunit --configuration phpunit.xml", + "phpstan": "vendor/bin/phpstan analyse --memory-limit=512M --level 6 src", + "phpcs": "vendor/bin/phpcs src --standard=phpcs.xml", + "phpmd": "vendor/bin/phpmd src/ text phpmd.xml", + "test": [ + "@phpunit", + "@phpstan", + "@phpcs", + "@phpmd" + ] + }, + "keywords": [ "php", "phpgt", @@ -76,7 +93,7 @@ "funding": [ { "type": "github", - "url": "https://github.com/sponsors/PhpGt" + "url": "https://github.com/sponsors/phpgt" } ] } diff --git a/composer.lock b/composer.lock index bbf13664..21e1d0be 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "552cdd346040e7823d4e3d68c902c1d1", + "content-hash": "05f0c8c3a247b9c8dff3b867417afdbf", "packages": [ { "name": "composer/semver", @@ -207,6 +207,81 @@ }, "time": "2024-12-02T12:14:07+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "v6.7.2", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", + "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" + }, + "time": "2026-02-15T15:06:22+00:00" + }, { "name": "magicalex/write-ini-file", "version": "v1.2.4", @@ -258,6 +333,79 @@ }, "time": "2018-09-09T12:40:38+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, { "name": "phpgt/async", "version": "v1.0.1", @@ -286,7 +434,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Async\\": "./src" + "GT\\Async\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -340,7 +488,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Build\\": "./src" + "GT\\Build\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -389,7 +537,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Cli\\": "./src" + "GT\\Cli\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -423,26 +571,28 @@ }, { "name": "phpgt/config", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/phpgt/Config.git", - "reference": "e693bc69af7b844b9b393e8a88755de962b606fe" + "reference": "21fa25f3dca864ecc9c54800a4394eb5601f2c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Config/zipball/e693bc69af7b844b9b393e8a88755de962b606fe", - "reference": "e693bc69af7b844b9b393e8a88755de962b606fe", + "url": "https://api.github.com/repos/phpgt/Config/zipball/21fa25f3dca864ecc9c54800a4394eb5601f2c4d", + "reference": "21fa25f3dca864ecc9c54800a4394eb5601f2c4d", "shasum": "" }, "require": { "magicalex/write-ini-file": "v1.2.4", - "php": ">=8.2", + "php": ">=8.3", "phpgt/typesafegetter": "^v1.2" }, "require-dev": { + "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^12.4" + "phpunit/phpunit": "^12.4", + "squizlabs/php_codesniffer": "^4.0" }, "bin": [ "bin/config-generate" @@ -450,7 +600,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Config\\": "./src" + "GT\\Config\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -466,7 +616,7 @@ "description": "Manage configuration with ini files and environment variables.", "support": { "issues": "https://github.com/phpgt/Config/issues", - "source": "https://github.com/phpgt/Config/tree/v1.1.1" + "source": "https://github.com/phpgt/Config/tree/v1.2.0" }, "funding": [ { @@ -474,7 +624,7 @@ "type": "github" } ], - "time": "2025-10-19T12:31:15+00:00" + "time": "2026-03-16T11:04:06+00:00" }, { "name": "phpgt/cookie", @@ -502,7 +652,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Cookie\\": "./src" + "GT\\Cookie\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -559,7 +709,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Cron\\": "./src/" + "GT\\Cron\\": "./src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -602,7 +752,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Csrf\\": "./src" + "GT\\Csrf\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -660,7 +810,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\CssXPath\\": "./src" + "GT\\CssXPath\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -690,34 +840,34 @@ }, { "name": "phpgt/curl", - "version": "v3.1.1", + "version": "v3.2.0", "source": { "type": "git", - "url": "https://github.com/PhpGt/Curl.git", - "reference": "a7e0856d3735f8f69d6d5fbf2f6f26664e55e3b7" + "url": "https://github.com/phpgt/Curl.git", + "reference": "cbeb514a700253a94200b3e91107f1fb0bb32b03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Curl/zipball/a7e0856d3735f8f69d6d5fbf2f6f26664e55e3b7", - "reference": "a7e0856d3735f8f69d6d5fbf2f6f26664e55e3b7", + "url": "https://api.github.com/repos/phpgt/Curl/zipball/cbeb514a700253a94200b3e91107f1fb0bb32b03", + "reference": "cbeb514a700253a94200b3e91107f1fb0bb32b03", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", - "php": ">=8.1", - "phpgt/json": "^1.2" + "php": ">=8.4", + "phpgt/json": "^2.2" }, "require-dev": { "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "^1.9", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.0", "squizlabs/php_codesniffer": "^3.7" }, "type": "library", "autoload": { "psr-4": { - "Gt\\Curl\\": "./src" + "GT\\Curl\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -738,8 +888,8 @@ "interface" ], "support": { - "issues": "https://github.com/PhpGt/Curl/issues", - "source": "https://github.com/PhpGt/Curl/tree/v3.1.1" + "issues": "https://github.com/phpgt/Curl/issues", + "source": "https://github.com/phpgt/Curl/tree/v3.2.0" }, "funding": [ { @@ -747,20 +897,20 @@ "type": "github" } ], - "time": "2023-04-29T17:28:12+00:00" + "time": "2025-12-09T22:55:13+00:00" }, { "name": "phpgt/daemon", - "version": "v1.1.4", + "version": "v1.1.5", "source": { "type": "git", - "url": "https://github.com/PhpGt/Daemon.git", - "reference": "4609225cf82a1f13a5f7364870a2b5f52c6a2529" + "url": "https://github.com/phpgt/Daemon.git", + "reference": "413e16b54de6e1fd5c2b646b485f88a86dfedd9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Daemon/zipball/4609225cf82a1f13a5f7364870a2b5f52c6a2529", - "reference": "4609225cf82a1f13a5f7364870a2b5f52c6a2529", + "url": "https://api.github.com/repos/phpgt/Daemon/zipball/413e16b54de6e1fd5c2b646b485f88a86dfedd9a", + "reference": "413e16b54de6e1fd5c2b646b485f88a86dfedd9a", "shasum": "" }, "require": { @@ -776,14 +926,14 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Daemon\\": "./src" + "GT\\Daemon\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", "description": "Background script execution with cross-platform compatible streaming.", "support": { - "issues": "https://github.com/PhpGt/Daemon/issues", - "source": "https://github.com/PhpGt/Daemon/tree/v1.1.4" + "issues": "https://github.com/phpgt/Daemon/issues", + "source": "https://github.com/phpgt/Daemon/tree/v1.1.5" }, "funding": [ { @@ -791,20 +941,20 @@ "type": "github" } ], - "time": "2024-05-16T08:42:54+00:00" + "time": "2026-03-11T14:11:10+00:00" }, { "name": "phpgt/database", - "version": "v1.6.2", + "version": "v1.6.3", "source": { "type": "git", "url": "https://github.com/phpgt/Database.git", - "reference": "3d58f0841d8aebb47e576c40e473b19b2deddf6b" + "reference": "73f8cb414e7d2592dc2c9728db79f771cff6fec8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Database/zipball/3d58f0841d8aebb47e576c40e473b19b2deddf6b", - "reference": "3d58f0841d8aebb47e576c40e473b19b2deddf6b", + "url": "https://api.github.com/repos/phpgt/Database/zipball/73f8cb414e7d2592dc2c9728db79f771cff6fec8", + "reference": "73f8cb414e7d2592dc2c9728db79f771cff6fec8", "shasum": "" }, "require": { @@ -827,7 +977,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Database\\": "./src" + "GT\\Database\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -851,7 +1001,7 @@ "description": "Database API organisation.", "support": { "issues": "https://github.com/phpgt/Database/issues", - "source": "https://github.com/phpgt/Database/tree/v1.6.2" + "source": "https://github.com/phpgt/Database/tree/v1.6.3" }, "funding": [ { @@ -859,7 +1009,7 @@ "type": "github" } ], - "time": "2025-03-25T14:26:54+00:00" + "time": "2025-10-11T14:57:21+00:00" }, { "name": "phpgt/dataobject", @@ -889,7 +1039,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\DataObject\\": "./src" + "GT\\DataObject\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -917,16 +1067,16 @@ }, { "name": "phpgt/dom", - "version": "v4.1.8", + "version": "v4.1.9", "source": { "type": "git", "url": "https://github.com/phpgt/Dom.git", - "reference": "dfe4843e402b018141db50a3cf1fbe19406390c5" + "reference": "bdc8498330b2b7bee221e42f02f7714300111130" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Dom/zipball/dfe4843e402b018141db50a3cf1fbe19406390c5", - "reference": "dfe4843e402b018141db50a3cf1fbe19406390c5", + "url": "https://api.github.com/repos/phpgt/Dom/zipball/bdc8498330b2b7bee221e42f02f7714300111130", + "reference": "bdc8498330b2b7bee221e42f02f7714300111130", "shasum": "" }, "require": { @@ -947,7 +1097,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Dom\\": "./src" + "GT\\Dom\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1005,7 +1155,7 @@ "description": "Modern DOM API.", "support": { "issues": "https://github.com/phpgt/Dom/issues", - "source": "https://github.com/phpgt/Dom/tree/v4.1.8" + "source": "https://github.com/phpgt/Dom/tree/v4.1.9" }, "funding": [ { @@ -1013,35 +1163,37 @@ "type": "github" } ], - "time": "2025-04-21T13:43:50+00:00" + "time": "2026-03-02T16:12:49+00:00" }, { "name": "phpgt/domtemplate", - "version": "v3.4.2", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/PhpGt/DomTemplate.git", - "reference": "b469f1813bbd039bfb5ba0d2a65b9a4f0eeff224" + "url": "https://github.com/phpgt/DomTemplate.git", + "reference": "f724ad744ebd32f753c63782ebeebb756fd01f08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/DomTemplate/zipball/b469f1813bbd039bfb5ba0d2a65b9a4f0eeff224", - "reference": "b469f1813bbd039bfb5ba0d2a65b9a4f0eeff224", + "url": "https://api.github.com/repos/phpgt/DomTemplate/zipball/f724ad744ebd32f753c63782ebeebb756fd01f08", + "reference": "f724ad744ebd32f753c63782ebeebb756fd01f08", "shasum": "" }, "require": { "ext-dom": "*", - "php": ">=8.1", - "phpgt/dom": "^4.0" + "php": ">=8.2", + "phpgt/dom": "^4.1.9" }, "require-dev": { - "phpstan/phpstan": "^1.8", - "phpunit/phpunit": "^10.0" + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^11.5", + "squizlabs/php_codesniffer": "^4.0" }, "type": "library", "autoload": { "psr-4": { - "Gt\\DomTemplate\\": "./src" + "GT\\DomTemplate\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1050,8 +1202,8 @@ ], "description": "Bind dynamic data to reusable HTML components.", "support": { - "issues": "https://github.com/PhpGt/DomTemplate/issues", - "source": "https://github.com/PhpGt/DomTemplate/tree/v3.4.2" + "issues": "https://github.com/phpgt/DomTemplate/issues", + "source": "https://github.com/phpgt/DomTemplate/tree/v3.5.1" }, "funding": [ { @@ -1059,26 +1211,26 @@ "type": "github" } ], - "time": "2024-11-25T09:59:24+00:00" + "time": "2026-03-14T13:50:44+00:00" }, { "name": "phpgt/domvalidation", - "version": "v1.0.1", + "version": "v1.0.2", "source": { "type": "git", - "url": "https://github.com/PhpGt/DomValidation.git", - "reference": "28604ad1a22127e4c23a664f3558ae1dfdd3215a" + "url": "https://github.com/phpgt/DomValidation.git", + "reference": "573cfc00cbfa7eb75fb7b870e95adfd6dc54a733" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/DomValidation/zipball/28604ad1a22127e4c23a664f3558ae1dfdd3215a", - "reference": "28604ad1a22127e4c23a664f3558ae1dfdd3215a", + "url": "https://api.github.com/repos/phpgt/DomValidation/zipball/573cfc00cbfa7eb75fb7b870e95adfd6dc54a733", + "reference": "573cfc00cbfa7eb75fb7b870e95adfd6dc54a733", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "phpgt/cssxpath": "1.*", - "phpgt/dom": "^4.1" + "phpgt/dom": "^4.1.8" }, "require-dev": { "phpmd/phpmd": "^2.13", @@ -1089,7 +1241,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\DomValidation\\": "./src/" + "GT\\DomValidation\\": "./src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1098,8 +1250,8 @@ ], "description": "Server side form validation using web standards.", "support": { - "issues": "https://github.com/PhpGt/DomValidation/issues", - "source": "https://github.com/PhpGt/DomValidation/tree/v1.0.1" + "issues": "https://github.com/phpgt/DomValidation/issues", + "source": "https://github.com/phpgt/DomValidation/tree/v1.0.2" }, "funding": [ { @@ -1107,20 +1259,20 @@ "type": "github" } ], - "time": "2023-03-02T14:56:58+00:00" + "time": "2026-03-11T18:01:15+00:00" }, { "name": "phpgt/fetch", - "version": "v1.2.1", + "version": "v1.2.2", "source": { "type": "git", "url": "https://github.com/phpgt/Fetch.git", - "reference": "ccb3b3f11384ddd560331f9e46e5d534befa112b" + "reference": "bc3473624eed6ca87953755580b3a8b5d10aaf9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Fetch/zipball/ccb3b3f11384ddd560331f9e46e5d534befa112b", - "reference": "ccb3b3f11384ddd560331f9e46e5d534befa112b", + "url": "https://api.github.com/repos/phpgt/Fetch/zipball/bc3473624eed6ca87953755580b3a8b5d10aaf9f", + "reference": "bc3473624eed6ca87953755580b3a8b5d10aaf9f", "shasum": "" }, "require": { @@ -1128,9 +1280,9 @@ "ext-json": "*", "php": ">=8.3", "phpgt/async": "^1.0", - "phpgt/curl": "^3.1.1", + "phpgt/curl": "^3.2", "phpgt/http": "^1.3", - "phpgt/json": "^1.2", + "phpgt/json": "^2.2", "phpgt/promise": "^2.3", "phpgt/propfunc": "^1.0" }, @@ -1143,7 +1295,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Fetch\\": "./src" + "GT\\Fetch\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1172,7 +1324,7 @@ ], "support": { "issues": "https://github.com/phpgt/Fetch/issues", - "source": "https://github.com/phpgt/Fetch/tree/v1.2.1" + "source": "https://github.com/phpgt/Fetch/tree/v1.2.2" }, "funding": [ { @@ -1180,7 +1332,7 @@ "type": "github" } ], - "time": "2025-05-02T12:21:25+00:00" + "time": "2025-12-09T23:16:42+00:00" }, { "name": "phpgt/filecache", @@ -1209,7 +1361,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\FileCache\\": "./src" + "GT\\FileCache\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1239,27 +1391,27 @@ }, { "name": "phpgt/http", - "version": "v1.3.5", + "version": "v1.3.7", "source": { "type": "git", "url": "https://github.com/phpgt/Http.git", - "reference": "77d735191a9d11b7c48c89764998c80d04d7558d" + "reference": "7c92bd2d0c7b9d68aa29c39f01ce41a7fcb221ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Http/zipball/77d735191a9d11b7c48c89764998c80d04d7558d", - "reference": "77d735191a9d11b7c48c89764998c80d04d7558d", + "url": "https://api.github.com/repos/phpgt/Http/zipball/7c92bd2d0c7b9d68aa29c39f01ce41a7fcb221ee", + "reference": "7c92bd2d0c7b9d68aa29c39f01ce41a7fcb221ee", "shasum": "" }, "require": { "ext-curl": "*", "ext-dom": "*", "ext-fileinfo": "*", - "php": ">=8.3", + "php": ">=8.4", "phpgt/async": "^1.0", "phpgt/curl": "^3.1", "phpgt/input": "^1.3", - "phpgt/json": "^1.2", + "phpgt/json": "^2.2", "phpgt/promise": "^2.4", "phpgt/propfunc": "^1.0", "phpgt/typesafegetter": "^1.3", @@ -1273,9 +1425,14 @@ "squizlabs/php_codesniffer": "^3.7" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, "autoload": { "psr-4": { - "Gt\\Http\\": "./src" + "GT\\Http\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1285,7 +1442,7 @@ "description": "PSR-7 HTTP message implementation.", "support": { "issues": "https://github.com/phpgt/Http/issues", - "source": "https://github.com/phpgt/Http/tree/v1.3.5" + "source": "https://github.com/phpgt/Http/tree/v1.3.7" }, "funding": [ { @@ -1293,26 +1450,26 @@ "type": "github" } ], - "time": "2025-10-24T14:32:09+00:00" + "time": "2026-03-06T13:08:42+00:00" }, { "name": "phpgt/input", - "version": "v1.3.1", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/phpgt/Input.git", - "reference": "5386e51cc83896173c859c97557d16fb4b7b269c" + "reference": "cb6780cbed5f7de2366fba85bb6b695f6143298b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Input/zipball/5386e51cc83896173c859c97557d16fb4b7b269c", - "reference": "5386e51cc83896173c859c97557d16fb4b7b269c", + "url": "https://api.github.com/repos/phpgt/Input/zipball/cb6780cbed5f7de2366fba85bb6b695f6143298b", + "reference": "cb6780cbed5f7de2366fba85bb6b695f6143298b", "shasum": "" }, "require": { - "php": ">=8.1", - "phpgt/http": "^1.1", - "phpgt/json": "^1.2" + "php": ">=8.3", + "phpgt/http": "1.*", + "phpgt/json": "^2.1" }, "require-dev": { "phpmd/phpmd": "^2.13", @@ -1323,7 +1480,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Input\\": "./src" + "GT\\Input\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1333,7 +1490,7 @@ "description": "Encapsulated user input.", "support": { "issues": "https://github.com/phpgt/Input/issues", - "source": "https://github.com/phpgt/Input/tree/v1.3.1" + "source": "https://github.com/phpgt/Input/tree/v1.3.2" }, "funding": [ { @@ -1341,26 +1498,27 @@ "type": "github" } ], - "time": "2025-03-26T11:41:41+00:00" + "time": "2025-05-02T11:54:06+00:00" }, { "name": "phpgt/json", - "version": "v1.2.1", + "version": "v2.2.0", "source": { "type": "git", - "url": "https://github.com/PhpGt/Json.git", - "reference": "8938b374d550bc6114bf1d4e5c1cbe3283868e58" + "url": "https://github.com/phpgt/Json.git", + "reference": "522fb4fc665d658531ee287380adaaef1339b202" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Json/zipball/8938b374d550bc6114bf1d4e5c1cbe3283868e58", - "reference": "8938b374d550bc6114bf1d4e5c1cbe3283868e58", + "url": "https://api.github.com/repos/phpgt/Json/zipball/522fb4fc665d658531ee287380adaaef1339b202", + "reference": "522fb4fc665d658531ee287380adaaef1339b202", "shasum": "" }, "require": { "ext-json": "*", + "justinrainbow/json-schema": "^6.1", "php": ">=8.1", - "phpgt/dataobject": "^1.0.7" + "phpgt/dataobject": "^1.1" }, "require-dev": { "phpmd/phpmd": "^2.13", @@ -1371,7 +1529,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Json\\": "./src" + "GT\\Json\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1386,8 +1544,8 @@ ], "description": " Structured, type-safe, immutable JSON objects.", "support": { - "issues": "https://github.com/PhpGt/Json/issues", - "source": "https://github.com/PhpGt/Json/tree/v1.2.1" + "issues": "https://github.com/phpgt/Json/issues", + "source": "https://github.com/phpgt/Json/tree/v2.2.0" }, "funding": [ { @@ -1395,20 +1553,20 @@ "type": "github" } ], - "time": "2023-07-13T13:20:08+00:00" + "time": "2025-12-09T21:35:51+00:00" }, { "name": "phpgt/logger", - "version": "v1.0.1", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/phpgt/Logger.git", - "reference": "efa981ac32190ea5e432d7674afd0895e06d45e9" + "reference": "64676afa8c2f8ae27a81ba36a2cfc96795230d81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Logger/zipball/efa981ac32190ea5e432d7674afd0895e06d45e9", - "reference": "efa981ac32190ea5e432d7674afd0895e06d45e9", + "url": "https://api.github.com/repos/phpgt/Logger/zipball/64676afa8c2f8ae27a81ba36a2cfc96795230d81", + "reference": "64676afa8c2f8ae27a81ba36a2cfc96795230d81", "shasum": "" }, "require": { @@ -1421,7 +1579,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Logger\\": "./src" + "GT\\Logger\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1437,7 +1595,7 @@ "description": "PSR-3 logger and implementation.", "support": { "issues": "https://github.com/phpgt/Logger/issues", - "source": "https://github.com/phpgt/Logger/tree/v1.0.1" + "source": "https://github.com/phpgt/Logger/tree/v1.1.0" }, "funding": [ { @@ -1445,7 +1603,7 @@ "type": "github" } ], - "time": "2025-10-30T11:45:41+00:00" + "time": "2026-03-09T17:36:44+00:00" }, { "name": "phpgt/promise", @@ -1473,7 +1631,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Promise\\": "./src" + "GT\\Promise\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1528,7 +1686,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\PropFunc\\": "./src" + "GT\\PropFunc\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1558,29 +1716,29 @@ }, { "name": "phpgt/protectedglobal", - "version": "v1.1.2", + "version": "v2.0.0", "source": { "type": "git", "url": "https://github.com/phpgt/ProtectedGlobal.git", - "reference": "b8ac7187e1e3047877a68b21416fa545812ed96a" + "reference": "28c9ae979549759828fe0b732907a99d8b9c7e44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/ProtectedGlobal/zipball/b8ac7187e1e3047877a68b21416fa545812ed96a", - "reference": "b8ac7187e1e3047877a68b21416fa545812ed96a", + "url": "https://api.github.com/repos/phpgt/ProtectedGlobal/zipball/28c9ae979549759828fe0b732907a99d8b9c7e44", + "reference": "28c9ae979549759828fe0b732907a99d8b9c7e44", "shasum": "" }, "require": { - "php": ">7.4" + "php": ">=8.2" }, "require-dev": { - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.1" + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.5" }, "type": "library", "autoload": { "psr-4": { - "Gt\\ProtectedGlobal\\": "./src/" + "GT\\ProtectedGlobal\\": "./src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1590,22 +1748,22 @@ "description": "Protect against accidental use of superglobals.", "support": { "issues": "https://github.com/phpgt/ProtectedGlobal/issues", - "source": "https://github.com/phpgt/ProtectedGlobal/tree/v1.1.2" + "source": "https://github.com/phpgt/ProtectedGlobal/tree/v2.0.0" }, - "time": "2025-03-09T13:53:23+00:00" + "time": "2025-10-23T15:24:26+00:00" }, { "name": "phpgt/routing", - "version": "v1.1.3", + "version": "v1.1.6", "source": { "type": "git", "url": "https://github.com/phpgt/Routing.git", - "reference": "58aa9017517fae204cabffbb912ed9db2b8774eb" + "reference": "83e5ec5910c4c9cf8bd5df78a6fd62c3fb117276" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Routing/zipball/58aa9017517fae204cabffbb912ed9db2b8774eb", - "reference": "58aa9017517fae204cabffbb912ed9db2b8774eb", + "url": "https://api.github.com/repos/phpgt/Routing/zipball/83e5ec5910c4c9cf8bd5df78a6fd62c3fb117276", + "reference": "83e5ec5910c4c9cf8bd5df78a6fd62c3fb117276", "shasum": "" }, "require": { @@ -1626,7 +1784,8 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Routing\\": "./src" + "GT\\Routing\\": "./src", + "GT\\Routing\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1655,7 +1814,7 @@ ], "support": { "issues": "https://github.com/phpgt/Routing/issues", - "source": "https://github.com/phpgt/Routing/tree/v1.1.3" + "source": "https://github.com/phpgt/Routing/tree/v1.1.6" }, "funding": [ { @@ -1663,20 +1822,20 @@ "type": "github" } ], - "time": "2025-09-25T15:13:42+00:00" + "time": "2026-03-09T12:46:01+00:00" }, { "name": "phpgt/server", - "version": "v1.1.5", + "version": "v1.1.6", "source": { "type": "git", - "url": "https://github.com/PhpGt/Server.git", - "reference": "6e959fcf874d3363ddbe708f25f907b42dcb17bc" + "url": "https://github.com/phpgt/Server.git", + "reference": "162eeb581973e7c8de3da7c2117af659df39b582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Server/zipball/6e959fcf874d3363ddbe708f25f907b42dcb17bc", - "reference": "6e959fcf874d3363ddbe708f25f907b42dcb17bc", + "url": "https://api.github.com/repos/phpgt/Server/zipball/162eeb581973e7c8de3da7c2117af659df39b582", + "reference": "162eeb581973e7c8de3da7c2117af659df39b582", "shasum": "" }, "require": { @@ -1685,7 +1844,7 @@ }, "require-dev": { "phpmd/phpmd": "^2.13", - "phpstan/phpstan": "v1.10", + "phpstan/phpstan": "^2.1", "squizlabs/php_codesniffer": "^3.7" }, "bin": [ @@ -1694,14 +1853,14 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Server\\": "./src/" + "GT\\Server\\": "./src/" } }, "notification-url": "https://packagist.org/downloads/", "description": "Development HTTP server.", "support": { - "issues": "https://github.com/PhpGt/Server/issues", - "source": "https://github.com/PhpGt/Server/tree/v1.1.5" + "issues": "https://github.com/phpgt/Server/issues", + "source": "https://github.com/phpgt/Server/tree/v1.1.6" }, "funding": [ { @@ -1709,20 +1868,20 @@ "type": "github" } ], - "time": "2024-05-09T15:21:06+00:00" + "time": "2026-03-09T18:54:33+00:00" }, { "name": "phpgt/servicecontainer", - "version": "v1.3.4", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/phpgt/ServiceContainer.git", - "reference": "60e01f6279e2fcc792e905b70d1b33d01a4632b2" + "reference": "9f85224075716808f75a7d2da867fddefe9b4020" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/ServiceContainer/zipball/60e01f6279e2fcc792e905b70d1b33d01a4632b2", - "reference": "60e01f6279e2fcc792e905b70d1b33d01a4632b2", + "url": "https://api.github.com/repos/phpgt/ServiceContainer/zipball/9f85224075716808f75a7d2da867fddefe9b4020", + "reference": "9f85224075716808f75a7d2da867fddefe9b4020", "shasum": "" }, "require": { @@ -1741,7 +1900,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\ServiceContainer\\": "./src" + "GT\\ServiceContainer\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1751,7 +1910,7 @@ "description": "Centralised container of a project's core objects.", "support": { "issues": "https://github.com/phpgt/ServiceContainer/issues", - "source": "https://github.com/phpgt/ServiceContainer/tree/v1.3.4" + "source": "https://github.com/phpgt/ServiceContainer/tree/v1.4.0" }, "funding": [ { @@ -1759,20 +1918,20 @@ "type": "github" } ], - "time": "2025-10-28T10:34:11+00:00" + "time": "2026-03-09T16:32:02+00:00" }, { "name": "phpgt/session", - "version": "v1.2.4", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/phpgt/Session.git", - "reference": "acdf1f7e6bb7aafab7daf155fc3e8d55788b9e81" + "reference": "c9a50b491c53737dea9503ea297832138b89bfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpgt/Session/zipball/acdf1f7e6bb7aafab7daf155fc3e8d55788b9e81", - "reference": "acdf1f7e6bb7aafab7daf155fc3e8d55788b9e81", + "url": "https://api.github.com/repos/phpgt/Session/zipball/c9a50b491c53737dea9503ea297832138b89bfd5", + "reference": "c9a50b491c53737dea9503ea297832138b89bfd5", "shasum": "" }, "require": { @@ -1788,7 +1947,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Session\\": "./src" + "GT\\Session\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1798,7 +1957,7 @@ "description": "Encapsulated user sessions.", "support": { "issues": "https://github.com/phpgt/Session/issues", - "source": "https://github.com/phpgt/Session/tree/v1.2.4" + "source": "https://github.com/phpgt/Session/tree/v1.2.5" }, "funding": [ { @@ -1806,20 +1965,20 @@ "type": "github" } ], - "time": "2025-07-31T11:48:09+00:00" + "time": "2025-10-31T16:04:32+00:00" }, { "name": "phpgt/sync", - "version": "v1.3.0", + "version": "v1.3.1", "source": { "type": "git", - "url": "https://github.com/PhpGt/Sync.git", - "reference": "5d1ee6aaa0b97919629c83a90745a1407ddfd66c" + "url": "https://github.com/phpgt/Sync.git", + "reference": "147c2ae1f3670bc606093892a01175584c082e64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/Sync/zipball/5d1ee6aaa0b97919629c83a90745a1407ddfd66c", - "reference": "5d1ee6aaa0b97919629c83a90745a1407ddfd66c", + "url": "https://api.github.com/repos/phpgt/Sync/zipball/147c2ae1f3670bc606093892a01175584c082e64", + "reference": "147c2ae1f3670bc606093892a01175584c082e64", "shasum": "" }, "require": { @@ -1839,7 +1998,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Sync\\": "./src" + "GT\\Sync\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1855,8 +2014,8 @@ "synchronize" ], "support": { - "issues": "https://github.com/PhpGt/Sync/issues", - "source": "https://github.com/PhpGt/Sync/tree/v1.3.0" + "issues": "https://github.com/phpgt/Sync/issues", + "source": "https://github.com/phpgt/Sync/tree/v1.3.1" }, "funding": [ { @@ -1864,20 +2023,20 @@ "type": "github" } ], - "time": "2023-07-07T16:49:08+00:00" + "time": "2026-03-15T17:45:10+00:00" }, { "name": "phpgt/typesafegetter", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", - "url": "https://github.com/PhpGt/TypeSafeGetter.git", - "reference": "f760c05a37b1cc188dcbf800c5fdfab8a926b4b0" + "url": "https://github.com/phpgt/TypeSafeGetter.git", + "reference": "a0d339103828791989cbb81f760d252f3c2f8b8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhpGt/TypeSafeGetter/zipball/f760c05a37b1cc188dcbf800c5fdfab8a926b4b0", - "reference": "f760c05a37b1cc188dcbf800c5fdfab8a926b4b0", + "url": "https://api.github.com/repos/phpgt/TypeSafeGetter/zipball/a0d339103828791989cbb81f760d252f3c2f8b8c", + "reference": "a0d339103828791989cbb81f760d252f3c2f8b8c", "shasum": "" }, "require": { @@ -1892,7 +2051,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\TypeSafeGetter\\": "./src" + "GT\\TypeSafeGetter\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1907,8 +2066,8 @@ ], "description": "An interface for objects that expose type-safe getter methods.", "support": { - "issues": "https://github.com/PhpGt/TypeSafeGetter/issues", - "source": "https://github.com/PhpGt/TypeSafeGetter/tree/v1.3.2" + "issues": "https://github.com/phpgt/TypeSafeGetter/issues", + "source": "https://github.com/phpgt/TypeSafeGetter/tree/v1.3.3" }, "funding": [ { @@ -1916,7 +2075,7 @@ "type": "github" } ], - "time": "2023-04-28T14:42:27+00:00" + "time": "2026-03-10T22:28:01+00:00" }, { "name": "phpgt/ulid", @@ -1944,7 +2103,7 @@ "type": "library", "autoload": { "psr-4": { - "Gt\\Ulid\\": "./src" + "GT\\Ulid\\": "./src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2078,119 +2237,6 @@ }, "time": "2023-04-04T09:54:51+00:00" }, - { - "name": "psr/http-server-handler", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", - "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side request handler", - "keywords": [ - "handler", - "http", - "http-interop", - "psr", - "psr-15", - "psr-7", - "request", - "response", - "server" - ], - "support": { - "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" - }, - "time": "2023-04-10T20:06:20+00:00" - }, - { - "name": "psr/http-server-middleware", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", - "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "psr/http-message": "^1.0 || ^2.0", - "psr/http-server-handler": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Server\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP server-side middleware", - "keywords": [ - "http", - "http-interop", - "middleware", - "psr", - "psr-15", - "psr-7", - "request", - "response" - ], - "support": { - "issues": "https://github.com/php-fig/http-server-middleware/issues", - "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" - }, - "time": "2023-04-11T06:14:47+00:00" - }, { "name": "webmozart/assert", "version": "1.12.1", @@ -2409,84 +2455,112 @@ ], "packages-dev": [ { - "name": "doctrine/deprecations", - "version": "1.1.5", + "name": "composer/pcre", + "version": "3.3.2", "source": { "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^7.4 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpstan/phpstan": "<1.11.10" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { - "Doctrine\\Deprecations\\": "src" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" }, - "time": "2025-04-07T20:06:18+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" }, { - "name": "doctrine/instantiator", - "version": "1.5.0", + "name": "composer/xdebug-handler", + "version": "3.0.5", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Composer\\XdebugHandler\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2495,36 +2569,35 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Restarts a process without Xdebug.", "keywords": [ - "constructor", - "instantiate" + "Xdebug", + "performance" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/composer", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "myclabs/deep-copy", @@ -2588,16 +2661,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -2640,33 +2713,96 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.4", + "name": "pdepend/pdepend", + "version": "2.16.2", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "url": "https://github.com/pdepend/pdepend.git", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" }, - "type": "library", + "require-dev": { + "easy-doc/easy-doc": "0.0.0|^1.2.3", + "gregwar/rst": "^1.0", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", "extra": { "branch-alias": { "dev-master": "2.0.x-dev" @@ -2763,314 +2899,99 @@ "time": "2022-02-21T01:04:05+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.6.5", + "name": "phpmd/phpmd", + "version": "2.15.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" + "url": "https://github.com/phpmd/phpmd.git", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.1", - "ext-filter": "*", - "php": "^7.4 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.7", - "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", + "ext-xml": "*", + "pdepend/pdepend": "^2.16.1", + "php": ">=5.3.9" }, "require-dev": { - "mockery/mockery": "~1.3.5 || ~1.6.0", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-webmozart-assert": "^1.2", - "phpunit/phpunit": "^9.5", - "psalm/phar": "^5.26" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", + "gregwar/rst": "^1.0", + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } + "bin": [ + "src/bin/phpmd" ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" - }, - "time": "2025-11-27T19:50:05+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.12.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", - "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.18|^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" - }, "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" + "psr-0": { + "PHPMD\\": "src/main/php" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" - }, - "time": "2025-11-21T15:09:14+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.24.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "a24f1bda2d00a03877f7f99d9e6b150baf543f6d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/a24f1bda2d00a03877f7f99d9e6b150baf543f6d", - "reference": "a24f1bda2d00a03877f7f99d9e6b150baf543f6d", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2 || ^2.0", - "php": "8.2.* || 8.3.* || 8.4.* || 8.5.*", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/deprecation-contracts": "^2.5 || ^3.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.88", - "phpspec/phpspec": "^6.0 || ^7.0 || ^8.0", - "phpstan/phpstan": "^2.1.13", - "phpunit/phpunit": "^11.0 || ^12.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" }, { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "https://phpmd.org/", "keywords": [ - "Double", - "Dummy", "dev", - "fake", - "mock", - "spy", - "stub" + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" ], "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.24.0" - }, - "time": "2025-11-21T13:10:52+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", - "shasum": "" + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" - }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2023-12-11T08:22:20+00:00" }, { "name": "phpstan/phpstan", - "version": "1.8.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5" - }, + "version": "2.1.41", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b7648d4ee9321665acaf112e49da9fd93df8fbd5", - "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a2eae8f20856b3afe74bf1f9726ce8c11438e300", + "reference": "a2eae8f20856b3afe74bf1f9726ce8c11438e300", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -3090,9 +3011,16 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.0" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -3102,49 +3030,40 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2022-06-29T08:53:31+00:00" + "time": "2026-03-16T18:24:10+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "12.5.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", - "theseer/tokenizer": "^1.2.3" + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^12.5.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -3153,7 +3072,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "12.5.x-dev" } }, "autoload": { @@ -3182,40 +3101,52 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2024-08-22T04:23:01+00:00" + "time": "2026-02-06T06:01:44+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3242,36 +3173,49 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-pcntl": "*" @@ -3279,7 +3223,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3305,7 +3249,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" }, "funding": [ { @@ -3313,32 +3258,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2025-02-07T04:58:58+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3364,7 +3309,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" }, "funding": [ { @@ -3372,32 +3318,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2025-02-07T04:59:16+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -3423,7 +3369,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" }, "funding": [ { @@ -3431,58 +3378,49 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2025-02-07T04:59:38+00:00" }, { "name": "phpunit/phpunit", - "version": "9.5.21", + "version": "12.5.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", - "sebastian/version": "^3.0.2" - }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0.1" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" }, "bin": [ "phpunit" @@ -3490,7 +3428,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-main": "12.5-dev" } }, "autoload": { @@ -3521,7 +3459,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.21" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" }, "funding": [ { @@ -3531,90 +3470,96 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-06-19T12:14:25+00:00" + "time": "2026-02-18T12:38:40+00:00" }, { - "name": "sebastian/cli-parser", - "version": "1.0.2", + "name": "psr/log", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "3.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Log\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { - "name": "sebastian/code-unit", - "version": "1.0.8", + "name": "sebastian/cli-parser", + "version": "4.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.2-dev" } }, "autoload": { @@ -3633,101 +3578,64 @@ "role": "lead" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2025-09-14T09:36:45+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.1-dev" } }, "autoload": { @@ -3766,7 +3674,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ { @@ -3786,33 +3695,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3835,7 +3744,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" }, "funding": [ { @@ -3843,33 +3753,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2025-02-07T04:55:25+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -3901,7 +3811,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" }, "funding": [ { @@ -3909,27 +3820,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2025-02-07T04:55:46+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "8.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "suggest": { "ext-posix": "*" @@ -3937,7 +3848,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -3956,7 +3867,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -3964,42 +3875,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2026-03-15T07:05:40+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.8", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", - "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -4041,7 +3965,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" }, "funding": [ { @@ -4061,38 +3986,35 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:03:27+00:00" + "time": "2025-09-24T06:16:11+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.8", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", - "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -4111,13 +4033,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" }, "funding": [ { @@ -4137,33 +4060,33 @@ "type": "tidelift" } ], - "time": "2025-08-10T07:10:35+00:00" + "time": "2025-08-29T11:29:25+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -4186,7 +4109,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" }, "funding": [ { @@ -4194,34 +4118,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2025-02-07T04:57:28+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -4243,7 +4167,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" }, "funding": [ { @@ -4251,32 +4176,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2025-02-07T04:57:48+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -4298,7 +4223,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" }, "funding": [ { @@ -4306,32 +4232,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2025-02-07T04:58:17+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.6", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", - "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -4361,7 +4287,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" }, "funding": [ { @@ -4381,32 +4308,32 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:57:39+00:00" + "time": "2025-08-13T04:44:59+00:00" }, { - "name": "sebastian/resource-operations", - "version": "3.0.4", + "name": "sebastian/type", + "version": "6.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^12.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4421,46 +4348,58 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" } ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2025-08-09T06:57:12+00:00" }, { - "name": "sebastian/type", - "version": "3.2.1", + "name": "sebastian/version", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" + "php": ">=8.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -4479,11 +4418,12 @@ "role": "lead" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" }, "funding": [ { @@ -4491,91 +4431,177 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2025-02-07T05:00:38+00:00" }, { - "name": "sebastian/version", - "version": "3.0.2", + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" }, - "autoload": { - "classmap": [ - "src/" - ] + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "php": ">=8.1" + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/config", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5" }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/6c17162555bfb58957a55bb0e43e00035b6ae3d5", + "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", "autoload": { - "files": [ - "function.php" + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4584,18 +4610,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "A generic function and convention to trigger deprecation notices", + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/config/tree/v7.4.7" }, "funding": [ { @@ -4606,32 +4632,592 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-03-06T10:41:14+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.3.1", + "name": "symfony/dependency-injection", + "version": "v7.4.7", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", + "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.6", + "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-03T07:48:48+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "reference": "7345f46c251f2eb27c7b3ebdb5bb076b3ffcae04", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/property-access": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T18:53:00+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" }, "type": "library", "autoload": { @@ -4653,7 +5239,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { @@ -4661,7 +5247,7 @@ "type": "github" } ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2025-12-08T11:19:18+00:00" } ], "aliases": [], @@ -4672,7 +5258,8 @@ "platform": { "ext-dom": "*", "ext-json": "*", - "php": ">=8.1" + "ext-curl": "*", + "php": ">=8.4" }, "platform-dev": {}, "plugin-api-version": "2.9.0" diff --git a/config.default.ini b/config.default.ini index a88fc3aa..a7318976 100644 --- a/config.default.ini +++ b/config.default.ini @@ -1,5 +1,6 @@ [app] namespace=App +production=false class_dir=class service_loader=ServiceLoader slow_delta=0.25 @@ -11,10 +12,13 @@ globals_whitelist_get=xdebug globals_whitelist_post= globals_whitelist_files= globals_whitelist_cookies= +force_trailing_slash=true +error_page_dir=page/_error [router] router_file=router.php router_class=AppRouter +redirect_response_code=307 default_content_type=text/html redirect_response_code=307 @@ -23,18 +27,24 @@ component_directory=page/_component partial_directory=page/_partial [logger] +log_all_requests=true +log_static_requests=false +log_404_to_error_log=false +debug_to_javascript=true +stderr_min_level=ERROR type=stdout level=debug path= timestamp_format=Y-m-d H:i:s -log_format={TIMESTAMP}\t{LEVEL}\t{MESSAGE}\t{CONTEXT} +log_format={TIMESTAMP}\t{USER}\t{LEVEL}\t{MESSAGE}\t{CONTEXT} separator=\t newline=\n [session] handler=Gt\Session\FileHandler path=phpgt/session -name=WebEngineSession +name=GT +use_cookies=true [database] driver=sqlite diff --git a/go.php b/go.php index c92c08f7..d4e4be21 100644 --- a/go.php +++ b/go.php @@ -1,58 +1,69 @@ start(); -} -catch(Exception $e) { - if(function_exists("exception_handler")) { - call_user_func("exception_handler", $e); - } - else { - throw $e; - } +$app = new Application(); +$app->start(); + +if(file_exists("teardown.php")) { + require("teardown.php"); } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..f0c51f14 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,66 @@ + + + Created from PHP.Gt/Styleguide + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 00000000..19a67403 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,52 @@ + + + Custom ruleset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..46f9b112 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +parameters: + level: 6 + paths: + - src + reportUnmatchedIgnoredErrors: false + ignoreErrors: + - '#^Class Gt\\[a-zA-Z\\]+ referenced with incorrect case: GT\\[a-zA-Z\\]+\.$#' + - '#^Class GT\\[a-zA-Z\\]+ referenced with incorrect case: Gt\\[a-zA-Z\\]+\.$#' diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..1b2deb15 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + + + + ./test/phpunit/ + + + + + + + + src/ + + + + diff --git a/router.default.php b/router.default.php index c0d585e7..7c99667d 100644 --- a/router.default.php +++ b/router.default.php @@ -1,18 +1,14 @@ errorStatus) { + $uriPath = "_error/$this->errorStatus"; + } + else { + $uriPath = $request->getUri()->getPath(); + } + $matchingLogics = $pathMatcher->findForUriPath( - $request->getUri()->getPath(), + $uriPath, "page", "php" ); @@ -95,7 +98,7 @@ public function page(Request $request):void { } $matchingViews = $pathMatcher->findForUriPath( - $request->getUri()->getPath(), + $uriPath, "page", "html" ); @@ -111,7 +114,7 @@ public function api( ):void { $pathMatcher = new PathMatcher("api"); $this->pathMatcherFilter($pathMatcher); - $this->setViewClass(NullView::class); + $this->setViewClass(JSONView::class); $sortNestLevelCallback = fn(string $a, string $b) => substr_count($a, "/") > substr_count($b, "/") ? 1 diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 00000000..90fd197c --- /dev/null +++ b/src/Application.php @@ -0,0 +1,472 @@ +>> */ + private array $globals; + private Protection $globalProtection; + private Config $config; + private DispatcherFactory $dispatcherFactory; + private Dispatcher $dispatcher; + private static bool $loggerConfigured = false; + private bool $finished = false; + + /** + * @param null|array> $globals + * @SuppressWarnings("PHPMD.Superglobals") + */ + public function __construct( + ?Redirect $redirect = null, + ?Config $config = null, + ?Timer $timer = null, + ?OutputBuffer $outputBuffer = null, + ?RequestFactory $requestFactory = null, + ?DispatcherFactory $dispatcherFactory = null, + ?array $globals = null, + ?Closure $handleShutdown = null, + ?Protection $globalProtection = null, + ) { + $this->gtCompatibility(); + $this->redirect = $redirect ?? new Redirect(); + $this->config = $config ?? $this->loadConfig(); + $this->configureLoggerStreams(); + $this->timer = $timer ?? new Timer( + $this->config->getFloat("app.slow_delta"), + $this->config->getFloat("app.very_slow_delta"), + ); + $this->outputBuffer = $outputBuffer ?? new OutputBuffer( + $this->config->getBool("logger.debug_to_javascript") + ); + $this->requestFactory = $requestFactory ?? new RequestFactory(); + $this->dispatcherFactory = $dispatcherFactory ?? new DispatcherFactory(); + $this->globals = array_merge([ + "_SERVER" => [], + "_FILES" => [], + "_GET" => [], + "_POST" => [], + "_ENV" => [], + "_COOKIE" => [], + ], $globals ?? $GLOBALS); + $this->globalProtection = $globalProtection ?? new Protection(); + register_shutdown_function($handleShutdown ?? $this->handleShutdown(...)); + } + + public function start():void { +// Before we start, we check if the current URI should be redirected. If it +// should, we won't go any further into the lifecycle. + $this->redirect->execute(); + +// The first thing done within the WebEngine lifecycle is start a timer. +// This timer is only used again at the end of the call, when finish() is +// called - at which point the entire duration of the request is logged out (and +// slow requests are highlighted as a NOTICE). + $this->timer->start(); + +// Starting the output buffer is done before any logic is executed, so any calls +// to any area of code will not accidentally send output to the web browser. + $this->outputBuffer->start(); + +// PHP.GT provides object-oriented interfaces to all values stored in $_SERVER, +// $_FILES, $_GET, and $_POST - to enforce good encapsulation and safe variable +// usage, the globals are protected against accidental misuse. + $this->protectGlobals(); + +// The RequestFactory takes the necessary global arrays to construct a +// ServerRequest object. The $_SERVER array contains metadata about the request, +// such as headers and server variables. $_FILES contains any uploaded files, +// $_GET contains query parameters from the URL, and $_POST contains form data. +// These arrays are optional and will default to empty arrays if not provided, +// ensuring the ServerRequest can always be constructed safely. + $request = $this->requestFactory->createServerRequestFromGlobalState( + $this->globals["_SERVER"] ?? [], + $this->globals["_FILES"] ?? [], + $this->globals["_GET"] ?? [], + $this->globals["_POST"] ?? [], + ); + assert($request instanceof Request); + $this->request = $request; + +// The Dispatcher is a core component responsible for: +// 1. Executing the application's routing logic to match the incoming request +// 2. Running any middleware defined for the matched route +// 3. Executing the appropriate page logic functions +// 4. Generating and returning the HTTP response +// +// It's critical to store the Dispatcher as a class property because if an error +// occurs during request processing, the error handling system needs access to +// the same Dispatcher instance that has the original request context, +// configuration, and other dependencies required to properly generate and +// display error pages. This ensures errors can be handled consistently using +// the application's error templates and logging mechanisms. + $this->dispatcher = $this->dispatcherFactory->create( + $this->config, + $this->request, + $this->globals, + $this->finish(...), + ); + + try { + $response = $this->dispatcher->generateResponse(); + } + catch(Throwable $throwable) { + if ($errorScript = $this->config->getString('app.error_script')) { + $this->restoreGlobals(); + require($errorScript); + return; + } + + $this->logError($throwable); + $errorStatus = 500; + + if($throwable instanceof ResponseStatusException) { + $errorStatus = $throwable->getHttpCode(); + } + + $this->dispatcher = $this->dispatcherFactory->create( + $this->config, + $this->request, + $this->globals, + $this->finish(...), + $errorStatus, + $this->dispatcher->getSessionInit(), + ); + + try { + $response = $this->dispatcher->generateErrorResponse($throwable); + } + catch(Throwable $innerThrowable) { + $response = $this->dispatcher->generateBasicErrorResponse($throwable, $innerThrowable); + } + } + + $this->finish($response); + } + + private function finish( + Response $response, + ):void { + if($this->finished) { + return; + } + $this->finished = true; + + $this->outputHeaders( + $response->getStatusCode(), + $response->getHeaders(), + ); + + if($this->config->getBool("logger.log_all_requests")) { + Log::info( + "HTTP " . $response->getStatusCode(), + $this->getLogContext(), + ); + } + + /** @var Stream $responseBody */ + $responseBody = $response->getBody(); + $this->outputResponseBody( + $responseBody, + $this->outputBuffer->debugOutput(), + ); + + $this->timer->stop(); + $this->timer->logDelta(); + } + + /** + * Registers a namespace compatibility autoloader to bridge the + * legacy-to-GT namespace transition. + * + * As part of the PHP.GT rebranding for WebEngine v5, all references to + * "GT" are being standardised to uppercase. However, the framework + * consists of 40+ repositories that cannot all be refactored + * simultaneously. This compatibility layer allows new code to reference + * classes using the GT\ namespace while the underlying packages still + * define classes with the legacy namespace casing. + */ + private function gtCompatibility():void { + registerNamespaceCompatibilityAutoloader(); + } + + private function protectGlobals():void { + $this->globalProtection->overrideInternals( + $this->globalProtection->removeGlobals([ + "server" => $this->globals["_SERVER"], + "files" => $this->globals["_FILES"], + "get" => $this->globals["_GET"], + "post" => $this->globals["_POST"], + "env" => $this->globals["_ENV"], + "cookie" => $this->globals["_COOKIE"], + ], [ + "_ENV" => explode(",", $this->config->getString("app.globals_whitelist_env") ?? ""), + "_SERVER" => explode(",", $this->config->getString("app.globals_whitelist_server") ?? ""), + "_GET" => explode(",", $this->config->getString("app.globals_whitelist_get") ?? ""), + "_POST" => explode(",", $this->config->getString("app.globals_whitelist_post") ?? ""), + "_FILES" => explode(",", $this->config->getString("app.globals_whitelist_files") ?? ""), + "_COOKIE" => explode(",", $this->config->getString("app.globals_whitelist_cookies") ?? ""), + ]) + ); + } + + /** @SuppressWarnings("PHPMD.Superglobals") */ + public function restoreGlobals(): void { + foreach ($this->globals as $key => $value) { + $GLOBALS[$key] = $value; + + if (in_array($key, [ + "_GET", + "_POST", + "_SERVER", + "_COOKIE", + "_FILES", + "_ENV" + ])) { + $GLOBALS[substr($key, 1)] = $value; + } + } + } + + private function loadConfig():Config { + $configFactory = new ConfigFactory(); + return $configFactory->createForProject( + getcwd(), + "vendor/phpgt/webengine/config.default.ini" + ); + } + + private function configureLoggerStreams():void { + if(self::$loggerConfigured) { + return; + } + + $stderrMinLevel = $this->getStderrMinimumLogLevel(); + $stderrMinLevelIndex = array_search($stderrMinLevel, LogLevel::ALL_LEVELS, true); + if($stderrMinLevelIndex === false) { + return; + } + + if(!class_exists(StdErrHandler::class)) { + return; + } + + $addHandlerMethod = new ReflectionMethod(LogConfig::class, "addHandler"); + + if($addHandlerMethod->getNumberOfParameters() < 3) { + return; + } + + if($stderrMinLevelIndex > 0) { + $stdoutMaxLevel = LogLevel::ALL_LEVELS[$stderrMinLevelIndex - 1]; + LogConfig::addHandler( + LogConfig::getDefaultHandler(), + LogLevel::DEBUG, + $stdoutMaxLevel, + ); + } + LogConfig::addHandler( + new StdErrHandler(), + $stderrMinLevel, + LogLevel::EMERGENCY, + ); + self::$loggerConfigured = true; + } + + private function handleShutdown():void { + $error = error_get_last(); + if(!$error) { + return; + } + + $fatalErrors = [ + E_ERROR, + E_PARSE, + E_CORE_ERROR, + E_COMPILE_ERROR, + E_USER_ERROR, + ]; + + if(!in_array($error["type"], $fatalErrors)) { + return; + } + + if($this->finished) { + return; + } + + + $throwable = new ErrorException( + $error["message"], + 0, + $error["type"], + $error["file"], + $error["line"], + ); + $this->logError($throwable); + + if(!isset($this->dispatcher)) { + return; + } + + try { + $response = $this->dispatcher->generateErrorResponse($throwable); + $this->finish($response); + } + catch(Throwable $innerThrowable) { + $response = $this->dispatcher->generateBasicErrorResponse($innerThrowable, $throwable); + } + $this->outputHeaders( + $response->getStatusCode(), + $response->getHeaders(), + ); + /** @var Stream $responseBody */ + $responseBody = $response->getBody(); + $this->outputResponseBody($responseBody); + } + + private function logError(Throwable $throwable):void { + if($throwable instanceof ClientErrorException) { + return; + } + + if(self::$loggerConfigured) { + Log::error((string)$throwable); + return; + } + + $stderrMinLevel = $this->getStderrMinimumLogLevel(); + $stderrMinLevelIndex = array_search($stderrMinLevel, LogLevel::ALL_LEVELS, true); + $errorLevelIndex = array_search(LogLevel::ERROR, LogLevel::ALL_LEVELS, true); + if($stderrMinLevelIndex === false || $stderrMinLevelIndex > $errorLevelIndex) { + Log::error((string)$throwable); + return; + } + + $errorLine = (string)$throwable; + if(!str_ends_with($errorLine, PHP_EOL)) { + $errorLine .= PHP_EOL; + } + file_put_contents("php://stderr", $errorLine, FILE_APPEND); + } + + private function getStderrMinimumLogLevel():string { + $configuredLevel = strtoupper( + $this->config->getString("logger.stderr_min_level") ?: LogLevel::ERROR + ); + if(in_array($configuredLevel, LogLevel::ALL_LEVELS, true)) { + return $configuredLevel; + } + + return LogLevel::ERROR; + } + + /** + * @return array + * @SuppressWarnings("PHPMD.Superglobals") + */ + private function getLogContext():array { + $uri = $this->request->getUri(); + $uriPath = $uri->getPath(); + $uriQuery = $this->request->getQueryParams(); + $postBody = $this->request->getParsedBody(); + + $context = [ + "id" => $this->request->getServerParams()["REMOTE_ADDR"] . ":" . substr(session_id(), 0, 4), + "uri" => $uriPath, + ]; + if($uriQuery) { + $context["query"] = $uriQuery; + } + if($postBody) { + $context["post"] = $postBody; + } + + return $context; + } + + /** @param array> $headers */ + private function outputHeaders(int $statusCode, array $headers):void { + foreach($headers as $key => $value) { +// TODO: Is this how multi-value headers should be set? + $valueString = implode(";", $value); + header("$key: $valueString", true); + } + + http_response_code($statusCode); + + } + + /** + * The response body is not the same as the currently-held output + * buffer ($this->outputBuffer). The output buffer is used for debug + * purposes, to allow developers to use var_dump, echo, etc. without + * messing up their page. + * + * The response body is the actual response HTML, JSON, etc. that is + * to be rendered directly to the web client. + * + * `ob_*` functions are used here to ensure that the response body is + * flushed and doesn't get rendered into another open buffer. + */ + private function outputResponseBody(Stream $responseBody, ?string $debugScript = null):void { + $length = $this->config->getInt("app.render_buffer_size"); + + $responseBody->rewind(); + ob_start(); + while(!$responseBody->eof()) { + $content = $responseBody->read($length); + if($debugScript) { + $closingBody = strpos($content, ""); + if(false !== $closingBody) { + $content = substr_replace($content, $debugScript, $closingBody, 0); + } + } + echo $content; + + ob_flush(); + flush(); + } + + ob_end_flush(); + } +} diff --git a/src/Debug/OutputBuffer.php b/src/Debug/OutputBuffer.php new file mode 100644 index 00000000..ff63c457 --- /dev/null +++ b/src/Debug/OutputBuffer.php @@ -0,0 +1,62 @@ +obStartHandler = $obStartHandler ?? fn() => ob_start(); + $this->obGetCleanHandler = $obGetCleanHandler ?? fn() => ob_get_clean(); + } + + private function fillBuffer(string $buffer):string { + $this->buffer .= $buffer; + return $buffer; + } + + public function start():void { + ($this->obStartHandler)(); + } + + public function cleanBuffer():void { + // ob_get_clean can return false; normalise to empty string. + $this->fillBuffer(($this->obGetCleanHandler)() ?? ""); + } + + public function debugOutput():?string { + $this->cleanBuffer(); + if($this->buffer) { + if($this->debugToJavaScript) { + $html = << + console.group("PHP.GT/WebEngine"); + console.log(`%buffer%`); + console.groupEnd(); + + + HTML; + $buffer = trim($this->buffer); + $buffer = str_replace("`", "\\`", $buffer); + return str_replace("%buffer%", $buffer, $html); + } + } + + return null; + } +} diff --git a/src/Debug/Timer.php b/src/Debug/Timer.php index 6c732097..ff08301d 100644 --- a/src/Debug/Timer.php +++ b/src/Debug/Timer.php @@ -1,19 +1,72 @@ deltaLogCallback = $deltaLogCallback; + } - public function __construct() { - $this->startTime = microtime(true); + $this->timeGetter = $timeGetter ?? fn() => microtime(true); + } + + public function start():void { + $this->startTime = ($this->timeGetter)(); } public function stop():void { - $this->endTime = microtime(true); + $this->endTime = ($this->timeGetter)(); } public function getDelta():float { return $this->endTime - $this->startTime; } + + public function logDelta():void { + if(!isset($this->deltaLogCallback)) { + return; + } + + $delta = $this->getDelta(); + if($delta > $this->verySlowDelta) { + $message = "VERY SLOW"; + } + elseif($delta > $this->slowDelta) { + $message = "SLOW"; + } + else { + return; + } + + $this->deltaLogCallback->call( + $this, + "Timer ended with $message delta time: $delta seconds. " + . "https://www.php.gt/webengine/slow-delta" + ); + } + } diff --git a/src/Dispatch/Dispatcher.php b/src/Dispatch/Dispatcher.php new file mode 100644 index 00000000..2639bb4b --- /dev/null +++ b/src/Dispatch/Dispatcher.php @@ -0,0 +1,521 @@ +>> */ + private array $globals; + private Closure $finishCallback; + + private AppAutoloader $appAutoloader; + private LogicStreamHandler $logicStreamHandler; + private Input $input; + private Response $response; + private Container $serviceContainer; + + private Injector $injector; + private NullView|JSONView|HTMLView $view; + private HTMLDocument|JsonDocument $viewModel; + private ?ViewModelProcessor $viewModelProcessor; + private BaseRouter $router; + private ?SessionInit $sessionInit = null; + private Assembly $logicAssembly; + private Assembly $viewAssembly; + private LogicExecutor $logicExecutor; + private ViewStreamer $viewStreamer; + + private HeaderManager $headerManager; + private Closure $viewInitCb; + private bool $redirectPrepared = false; + + /** + * @param array>> $globals + * @SuppressWarnings("PHPMD.ExcessiveMethodLength") + * @SuppressWarnings("PHPMD.ExcessiveParameterList") + */ + // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh + public function __construct( + Config $config, + Request $request, + array $globals, + Closure $finishCallback, + ?int $errorStatus = null, + + ?AppAutoloader $appAutoloader = null, + ?LogicStreamHandler $logicStreamHandler = null, + + ?RequestInit $requestInit = null, + ?RouterInit $routerInit = null, + ?SessionInit $sessionInit = null, + ?ViewModelInit $viewModelInit = null, + + ?LogicExecutor $logicExecutor = null, + ?ViewStreamer $viewStreamer = null, + ?HeaderManager $headerManager = null, + ) { +// Set the first Dispatcher dependencies - these are required to be passed from +// the Application, so come first as non-nullable: + $this->config = $config; + $this->request = $request; + $this->globals = $globals; + $this->finishCallback = $finishCallback; + +// Next, we set up the response and service container, which will be used by the +// Dispatcher dependencies throughout the request-response lifecycle. + $this->response = $this->setupResponse(); + $containerFactory = new ContainerFactory(); + $this->serviceContainer = $containerFactory->create($this->config); + $this->injector = new Injector($this->serviceContainer); + $this->serviceContainer->set($this->request); + $this->serviceContainer->set($this->response); + $this->serviceContainer->set($this->request->getUri()); + +// The following Dispatcher dependencies are all nullable. They don't expect to +// be passed from the Application, so will default to null. The reason for this +// is that unit tests can pass mocked versions of all dependencies to produce a +// predictable application state without requiring a real web browser context. + $appNamespace = $config->getString("app.namespace"); + $this->appAutoloader = $appAutoloader ?? new AppAutoloader( + $appNamespace, + $config->getString("app.class_dir"), + ); + $this->appAutoloader->setup(); + +// TODO: I think it makes sense to initialise and setup the logic stream handler just before the logic executor is set up. + $this->logicStreamHandler = $logicStreamHandler ?? new LogicStreamHandler(); + $this->logicStreamHandler->setup(); + + $pathNormaliser = new PathNormaliser(); + /** @var \GT\Http\Uri $requestUri */ + $requestUri = $request->getUri(); + $requestInit = $requestInit ?? new RequestInit( + $pathNormaliser, + $requestUri, + $config->getBool("app.force_trailing_slash") ?? true, + $this->response->redirect(...), + $this->globals["_GET"], + $this->globals["_POST"], + $this->globals["_FILES"], + $this->globals["_SERVER"], + ); + $this->input = $requestInit->getInput(); + $this->serviceContainer->set($this->input); + $this->serviceContainer->set($requestInit->getServerInfo()); + if($this->isRedirectPrepared()) { + $this->redirectPrepared = true; + return; + } + + $routerInit = $routerInit ?? new RouterInit( + $this->request, + $this->response, + $this->serviceContainer, + $appNamespace, + $this->config->getString("router.router_file"), + $this->config->getString("router.router_class"), + dirname(__FILE__, 3) . DIRECTORY_SEPARATOR . "router.default.php", + "\\GT\\WebEngine\\DefaultRouter", + $this->config->getInt("router.redirect_response_code"), + $this->config->getString("router.default_content_type"), + $errorStatus, + ); + $view = $routerInit->getView(); + $this->serviceContainer->set($view); + $this->view = $view; + $this->viewModel = $routerInit->getViewModel(); + $this->serviceContainer->set($this->viewModel); + $this->router = $routerInit->getBaseRouter(); + $this->serviceContainer->set($this->router); + $dynamicPath = $routerInit->getDynamicPath(); + $this->serviceContainer->set($dynamicPath); + $this->viewAssembly = $routerInit->getViewAssembly(); + $this->logicAssembly = $routerInit->getLogicAssembly(); + + $sessionInit = $sessionInit ?? new SessionInit( + $this->config->getString("session.name"), + $this->config->getString("session.handler"), + $this->config->getString("session.path"), + $this->config->getBool("session.use_trans_sid") ?? false, + $this->config->getBool("session.use_cookies") ?? false, + $this->globals["_COOKIE"], + ); + $this->sessionInit = $sessionInit; + $this->serviceContainer->set($sessionInit->getSession()); + + $viewModelInit = $viewModelInit ?? new ViewModelInit( + $this->viewModel, + $this->config->getString("view.component_directory"), + $this->config->getString("view.partial_directory"), + ); + $this->viewModelProcessor = $viewModelInit->getViewModelProcessor(); + $this->viewInitCb = $this->viewModel instanceof HTMLDocument + ? function()use($viewModelInit):void { + $documentBinder = $this->serviceContainer->get(Binder::class); + assert($documentBinder instanceof DocumentBinder); + $viewModelInit->initHTMLDocument( + $documentBinder, + $this->serviceContainer->get(HTMLAttributeBinder::class), + $this->serviceContainer->get(ListBinder::class), + $this->serviceContainer->get(TableBinder::class), + $this->serviceContainer->get(ElementBinder::class), + $this->serviceContainer->get(PlaceholderBinder::class), + $this->serviceContainer->get(HTMLAttributeCollection::class), + $this->serviceContainer->get(ListElementCollection::class), + $this->serviceContainer->get(BindableCache::class), + ); + } + : static fn() => null; + + $this->logicExecutor = $logicExecutor ?? new LogicExecutor( + $appNamespace, + $this->injector, + ); + $this->viewStreamer = $viewStreamer ?? new ViewStreamer(); + $this->headerManager = $headerManager ?? new HeaderManager(); + } + // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh + + public function generateResponse():Response { + if($this->redirectPrepared) { + return $this->response; + } + +// The routing is now complete and all services are properly configured. This +// function's responsibility is to execute the logic that builds the response. +// Since this involves running user code that may throw errors, we execute each +// step individually to ensure proper error handling throughout the process. + if(!$this->viewAssembly->containsDistinctFile() && !$this->logicAssembly->containsDistinctFile()) { + $this->response = $this->response->withStatus(StatusCode::NOT_FOUND); + throw new HttpNotFound(); + } + + $this->processResponse(); + + if(!$this->response->getStatusCode()) { + $this->response = $this->response->withStatus(StatusCode::OK); + } + + return $this->response; + } + + public function generateErrorResponse(Throwable $throwable):Response { + $this->serviceContainer->set($throwable); + + $errorStatusCode = StatusCode::INTERNAL_SERVER_ERROR; + if($throwable instanceof ResponseStatusException) { + $errorStatusCode = $throwable->getHttpCode(); + } + + if(!$this->viewAssembly->containsDistinctFile()) { + throw new ErrorPageNotFoundException(code: $errorStatusCode); + } + + $this->processResponse($throwable); + $this->response = $this->response->withStatus($errorStatusCode); + return $this->response; + } + + public function generateBasicErrorResponse( + Throwable $actualThrowable, + Throwable $innerThrowable, + ):Response { +// TODO: Handle innerThrowable for if there's an error thrown in WebEngine itself. + $errorStatusCode = $this->response->getStatusCode(); + $errorType = get_class($actualThrowable); + $errorMessage = $actualThrowable->getMessage(); + $detail = ""; + + $errorPageDir = $this->config->getString("app.error_page_dir"); + + if(!$errorMessage) { + if($actualThrowable instanceof HttpNotFound) { + $errorMessage = "The server could not find the requested resource."; + + if(!$this->config->getBool("app.production")) { + $detail .= " Additionally, there was no error page found in your " + . "application at $errorPageDir/$errorStatusCode.html"; + } + } + } + + if($errorStatusCode >= 500 && !$this->config->getBool("app.production")) { + $detail .= implode(":", [ + $actualThrowable->getFile(), + $actualThrowable->getLine(), + ]) . "\n\n"; + foreach($actualThrowable->getTrace() as $i => $t) { + if(isset($t["file"]) && str_ends_with($t["file"], "/vendor/phpgt/servicecontainer/src/Injector.php")) { + break; + } + $detail .= "#$i\n"; + + foreach($t as $key => $value) { + $detail .= "$key:\t$value\n"; + } + } + } + + // TODO: Load this HTML from a file in the root of WebEngine! + $html = << +

Error $errorStatusCode

+

$errorType

+

$errorMessage

+

$detail

+ HTML; + + $body = new Stream(); + $body->write($html); + $response = new Response(null, null, $this->request); + $response = $response->withBody($body); + return $response->withStatus($errorStatusCode); + } + + private function setupResponse():Response { + $response = new Response(null, null, $this->request); + $response->setExitCallback(function() { + ($this->finishCallback)($this->response); + }); + return $response; + } + + private function handleLogicExecution( + Assembly $logicAssembly, + Input $input, + ?Element $component = null, + ):void { + $extraArgs = []; + + if($component) { + $binder = new ComponentBinder($this->viewModel); + $binder->setDependencies( + $this->serviceContainer->get(ElementBinder::class), + $this->serviceContainer->get(PlaceholderBinder::class), + $this->serviceContainer->get(TableBinder::class), + $this->serviceContainer->get(ListBinder::class), + $this->serviceContainer->get(ListElementCollection::class), + $this->serviceContainer->get(BindableCache::class), + ); + $binder->setComponentBinderDependencies($component); + $extraArgs[Binder::class] = $binder; + $extraArgs[Element::class] = $component; +// This is a temporary fix while repos transition to the GT namespace: + $legacyBinderClass = Binder::class[0] . strtolower(Binder::class[1]) . substr(Binder::class, 2); + $legacyElementClass = Element::class[0] . strtolower(Element::class[1]) . substr(Element::class, 2); + $extraArgs[$legacyBinderClass] = $binder; + $extraArgs[$legacyElementClass] = $component; + } + + foreach($this->logicExecutor->invoke($logicAssembly, "go_before", $extraArgs) as $file) { + // Force generator execution even when debug output is disabled. + continue; + } + +// TODO: No need to have the whole Input class. Just pass a nullable string in called $doMethod, from $input->getString("do") + $input->when("do")->call( + function(InputData $data)use($logicAssembly, $extraArgs) { + $doName = "do_" . str_replace( + "-", + "_", + $data->getString("do"), + ); + + foreach($this->logicExecutor->invoke($logicAssembly, $doName, $extraArgs) as $file) { + // Force generator execution even when debug output is disabled. + continue; + } + } + ); + + foreach($this->logicExecutor->invoke($logicAssembly, "go", $extraArgs) as $file) { + // Force generator execution even when debug output is disabled. + continue; + } + + foreach($this->logicExecutor->invoke($logicAssembly, "go_after", $extraArgs) as $file) { + // Force generator execution even when debug output is disabled. + continue; + } + } + + /** + * @return void + */ + // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh + public function processResponse( + ?Throwable $errorThrowable = null, + ):void { + $dynamicPath = $this->serviceContainer->get(DynamicPath::class); + + $this->viewModelProcessor?->processDynamicPath( + $this->viewModel, + $dynamicPath, + ); + + $componentList = $this->viewModelProcessor?->processPartialContent( + $this->viewModel, + ); + +// TODO: CSRF handling - needs to be done on any POST request. + ($this->viewInitCb)(); + if($errorThrowable) { + $this->bindErrorDetails($errorThrowable); + } + + foreach($componentList ?? [] as $componentLogic) { + $assembly = $componentLogic->assembly; + $componentElement = $componentLogic->component; + $this->serviceContainer->set($componentElement); + + try { + $this->handleLogicExecution( + $assembly, + $this->input, + $componentElement, + ); + } + catch(Throwable $throwable) { + if(!$errorThrowable) { + throw $throwable; + } + } + } + + try { + $this->handleLogicExecution( + $this->logicAssembly, + $this->input, + ); + } + catch(Throwable $throwable) { + if(!$errorThrowable) { + throw $throwable; + } + } + + if($responseWithHeader = $this->headerManager->applyWithHeader( + $this->response->getResponseHeaders(), + $this->response->withHeader(...) + )) { + $this->response = $responseWithHeader; + } + + $documentBinder = $this->serviceContainer->get(Binder::class); + assert($documentBinder instanceof DocumentBinder); + $documentBinder->cleanupDocument(); + + $this->viewStreamer->stream($this->view, $this->viewModel); + } + // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh + + public function getSessionInit():?SessionInit { + return $this->sessionInit; + } + + private function isRedirectPrepared():bool { + $status = $this->response->getStatusCode(); + if($status < 300 || $status >= 400) { + return false; + } + + return $this->response->hasHeader("Location"); + } + + // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh + private function bindErrorDetails(Throwable $throwable):void { + $trace = $throwable->getTrace(); + array_unshift($trace, [ + "file" => $throwable->getFile(), + "line" => $throwable->getLine(), + "class" => get_class($throwable) . "(\"" . $throwable->getMessage() . "\")", + ]); + foreach($trace as $i => $traceItem) { + if(isset($traceItem["file"])) { + $cwd = getcwd() . DIRECTORY_SEPARATOR; + if(str_starts_with($traceItem["file"], $cwd)) { + $trace[$i]["file"] = substr($traceItem["file"], strlen($cwd)); + } + } + + if(isset($traceItem["file"]) && str_starts_with($traceItem["file"], "gt-logic-stream://")) { + $trace = array_slice($trace, 0, $i + 1); + break; + } + } + + $binder = $this->serviceContainer->get(Binder::class); + $binder->bindValue($throwable->getMessage()); + if(!$this->config->getBool("app.production")) { + $traceString = ""; + foreach($trace as $i => $traceItem) { + $traceString .= "#$i "; + if(isset($traceItem["class"])) { + $traceString .= $traceItem["class"]; + if(isset($traceItem["function"])) { + $traceString .= "::"; + $traceString .= $traceItem["function"]; + } + $traceString .= " -> "; + } + if(isset($traceItem["file"])) { + $traceString .= $traceItem["file"]; + } + if(isset($traceItem["line"])) { + $traceString .= "(" . $traceItem["line"] . ")"; + } + $traceString .= "\n"; + } + $binder->bindKeyValue("trace", $traceString); + } + } + // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh +} diff --git a/src/Dispatch/DispatcherFactory.php b/src/Dispatch/DispatcherFactory.php new file mode 100644 index 00000000..246d3ac3 --- /dev/null +++ b/src/Dispatch/DispatcherFactory.php @@ -0,0 +1,30 @@ +>> $globals + */ + public function create( + Config $config, + Request $request, + array $globals, + Closure $finishCallback, + ?int $errorStatus = null, + ?SessionInit $sessionInit = null, + ):Dispatcher { + return new Dispatcher( + $config, + $request, + $globals, + $finishCallback, + $errorStatus, + sessionInit: $sessionInit, + ); + } +} diff --git a/src/Dispatch/ErrorPageNotFoundException.php b/src/Dispatch/ErrorPageNotFoundException.php new file mode 100644 index 00000000..59b5799a --- /dev/null +++ b/src/Dispatch/ErrorPageNotFoundException.php @@ -0,0 +1,6 @@ +asArray() as $name => $value) { + $response = $withHeaderCallback($name, $value); + } + + return $response; + } +} diff --git a/src/Dispatch/PathNormaliser.php b/src/Dispatch/PathNormaliser.php new file mode 100644 index 00000000..4c9d90cd --- /dev/null +++ b/src/Dispatch/PathNormaliser.php @@ -0,0 +1,27 @@ +getPath(); + + if($forceTrailingSlash) { + if(!str_ends_with($path, "/")) { + $redirect($uri->withPath("$path/")); + } + } + else { + if(str_ends_with($path, "/") && $path !== "/") { + $redirect($uri->withPath(rtrim($path, "/"))); + } + } + } + +} diff --git a/src/Dispatch/RouterFactory.php b/src/Dispatch/RouterFactory.php new file mode 100644 index 00000000..9fec4465 --- /dev/null +++ b/src/Dispatch/RouterFactory.php @@ -0,0 +1,40 @@ +setContainer($container); + return $router; + } +} diff --git a/src/Init/RequestInit.php b/src/Init/RequestInit.php new file mode 100644 index 00000000..8284378a --- /dev/null +++ b/src/Init/RequestInit.php @@ -0,0 +1,47 @@ + $get + * @param array $post + * @param array> $files + * @param array $server + */ + public function __construct( + private PathNormaliser $pathNormaliser, + private UriInterface $requestUri, + private bool $forceTrailingSlash, + private Closure $redirect, + array $get, + array $post, + array $files, + array $server, + ) { + $this->pathNormaliser->normaliseTrailingSlash( + $this->requestUri, + $this->forceTrailingSlash, + $this->redirect, + ); + + $this->input = new Input($get, $post, $files); + $this->serverInfo = new ServerInfo($server); + } + + public function getInput():Input { + return $this->input; + } + + public function getServerInfo():ServerInfo { + return $this->serverInfo; + } +} diff --git a/src/Init/RouterInit.php b/src/Init/RouterInit.php new file mode 100644 index 00000000..70c18563 --- /dev/null +++ b/src/Init/RouterInit.php @@ -0,0 +1,103 @@ +baseRouter = $routerFactory->create( + $container, + $appNamespace, + $appRouterFile, + $appRouterClass, + $defaultRouterFile, + $defaultRouterClass, + $redirectResponseCode, + $defaultContentType, + $errorStatus, + ); + $this->baseRouter->route($request); + $viewClass = $this->baseRouter->getViewClass() ?? NullView::class; + + // @codeCoverageIgnoreStart + if(strlen($viewClass) > 2 && $viewClass[0] === "G" && $viewClass[1] === "t" && $viewClass[2] === "\\") { + $viewClass[1] = "T"; + } + // @codeCoverageIgnoreEnd + + $this->view = new $viewClass($response->getBody()); + + $this->viewAssembly = $this->baseRouter->getViewAssembly(); + $this->logicAssembly = $this->baseRouter->getLogicAssembly(); + + foreach($this->viewAssembly as $viewFile) { + $this->view->addViewFile($viewFile); + } + $this->viewModel = $this->view->createViewModel(); + + $this->dynamicPath = new DynamicPath( + $request->getUri()->getPath(), + $this->viewAssembly, + $this->logicAssembly, + ); + } + + public function getView():NullView|HTMLView|JSONView { + return $this->view; + } + + public function getViewModel():HTMLDocument|JsonDocument { + return $this->viewModel; + } + + public function getBaseRouter():BaseRouter { + return $this->baseRouter; + } + + public function getDynamicPath():DynamicPath { + return $this->dynamicPath; + } + + public function getViewAssembly():Assembly { + return $this->viewAssembly; + } + + public function getLogicAssembly():Assembly { + return $this->logicAssembly; + } +} diff --git a/src/Init/SessionInit.php b/src/Init/SessionInit.php new file mode 100644 index 00000000..f401fd65 --- /dev/null +++ b/src/Init/SessionInit.php @@ -0,0 +1,58 @@ + $currentCookieArray + * @SuppressWarnings("PHPMD.Superglobals") + */ + public function __construct( + string $name, + string $handler, + string $savePath, + bool $useTransSid, + bool $useCookies, + array $currentCookieArray, + ?SessionSetup $sessionSetup = null, + string|Session $sessionClass = Session::class, + ) { + $originalCookie = $_COOKIE; + $_COOKIE = $currentCookieArray; + + $sessionConfig = [ + "name" => $name, + "handler" => $handler, + "save_path" => $savePath, + "use_trans_sid" => $useTransSid, + "use_cookies" => $useCookies, + ]; + + $sessionId = $_COOKIE[$sessionConfig["name"]] ?? null; + $sessionSetup = $sessionSetup ?? new SessionSetup(); + $sessionHandler = $sessionSetup->attachHandler($sessionConfig["handler"]); + + if($sessionClass instanceof Session) { + $this->session = $sessionClass; + } + else { +// @codeCoverageIgnoreStart + $this->session = new $sessionClass( + $sessionHandler, + $sessionConfig, + $sessionId, + ); +// @codeCoverageIgnoreEnd + } + + $_COOKIE = $originalCookie; + } + + public function getSession():Session { + return $this->session; + } +} diff --git a/src/Init/ViewModelInit.php b/src/Init/ViewModelInit.php new file mode 100644 index 00000000..e8549d05 --- /dev/null +++ b/src/Init/ViewModelInit.php @@ -0,0 +1,92 @@ +viewModelProcessor = new HTMLDocumentProcessor( + $componentDirectory, + $partialDirectory, + ); + } +// TODO: Handle other view model types. + } + + public function initHTMLDocument( + DocumentBinder $documentBinder, + HTMLAttributeBinder $htmlAttributeBinder, + ListBinder $listBinder, + TableBinder $tableBinder, + ElementBinder $elementBinder, + PlaceholderBinder $placeholderBinder, + HTMLAttributeCollection $attrCollection, + ListElementCollection $elementCollection, + BindableCache $bindableCache, + ):void { + if($this->initialised) { + return; + } + + $this->initialised = true; + + $htmlAttributeBinder->setDependencies( + $listBinder, + $tableBinder, + ); + $elementBinder->setDependencies( + $htmlAttributeBinder, + $attrCollection, + $placeholderBinder, + ); + $tableBinder->setDependencies( + $listBinder, + $elementCollection, + $elementBinder, + $htmlAttributeBinder, + $attrCollection, + $placeholderBinder, + ); + $listBinder->setDependencies( + $elementBinder, + $elementCollection, + $bindableCache, + $tableBinder, + ); + $documentBinder->setDependencies( + $elementBinder, + $placeholderBinder, + $tableBinder, + $listBinder, + $elementCollection, + $bindableCache, + ); + } + + public function getViewModelProcessor():?ViewModelProcessor { + return $this->viewModelProcessor ?? null; + } +} diff --git a/src/Logic/AppAutoloader.php b/src/Logic/AppAutoloader.php index 3288dd23..26c07895 100644 --- a/src/Logic/AppAutoloader.php +++ b/src/Logic/AppAutoloader.php @@ -1,7 +1,34 @@ -namespace) + 1 ); - $phpFilePath = "./" . $this->classDir; - foreach(explode("\\", $classNameWithoutAppNamespace) as $classPart) { + $phpFilePath = $this->classDir; + // If classDir is not absolute (neither POSIX '/' nor Windows drive letter), prefix with './' + if(!str_starts_with($phpFilePath, "/") && !preg_match('/^[A-Za-z]:[\\\\\/]/', $phpFilePath)) { + $phpFilePath = "./" . $phpFilePath; + } + foreach(explode("\\", $classNameWithoutNs) as $classPart) { $phpFilePath .= "/"; $phpFilePath .= ucfirst($classPart); } diff --git a/src/Logic/HTMLDocumentProcessor.php b/src/Logic/HTMLDocumentProcessor.php new file mode 100644 index 00000000..9ae5ce2c --- /dev/null +++ b/src/Logic/HTMLDocumentProcessor.php @@ -0,0 +1,84 @@ +getUrl("page/"); + $dynamicUri = str_replace("/", "--", $dynamicUri); + $dynamicUri = str_replace("@", "_", $dynamicUri); + $model->body->classList->add("uri" . $dynamicUri); + $bodyDirClass = "dir"; + foreach(explode("--", $dynamicUri) as $i => $pathPart) { + if($i === 0) { + continue; + } + $bodyDirClass .= "--$pathPart"; + $model->body->classList->add($bodyDirClass); + } + } + + function processPartialContent( + HTMLDocument $model, + ):LogicAssemblyComponentList { + $componentList = new LogicAssemblyComponentList(); + + try { +// TODO: Handle other model types in sub-functions. + $partial = new PartialContent(implode(DIRECTORY_SEPARATOR, [ + getcwd(), + $this->componentDirectory, + ])); + $componentExpander = new ComponentExpander( + $model, + $partial, + ); + + foreach($componentExpander->expand() as $componentElement) { + $filePath = $this->componentDirectory; + $filePath .= DIRECTORY_SEPARATOR; + $filePath .= strtolower($componentElement->tagName); + $filePath .= ".php"; + + if(!is_file($filePath)) { +// TODO: Log that a component has been detected but there's no HTML file to load. + continue; + } + + $componentAssembly = new Assembly(); + $componentAssembly->add($filePath); + $componentList->addAssemblyComponent( + $componentAssembly, + $componentElement, + ); + } + } + catch(PartialContentDirectoryNotFoundException) {} + + try { + $partial = new PartialContent(implode(DIRECTORY_SEPARATOR, [ + getcwd(), + $this->partialDirectory, + ])); + + $partialExpander = new PartialExpander( + $model, + $partial, + ); + $partialExpander->expand(); + } + catch(PartialContentDirectoryNotFoundException) {} + + return $componentList; + } +} diff --git a/src/Logic/LogicAssemblyComponent.php b/src/Logic/LogicAssemblyComponent.php new file mode 100644 index 00000000..9906316f --- /dev/null +++ b/src/Logic/LogicAssemblyComponent.php @@ -0,0 +1,12 @@ + */ +class LogicAssemblyComponentList extends ArrayIterator { + public function addAssemblyComponent(Assembly $assembly, Element $component):void { + $this->append( + new LogicAssemblyComponent( + $assembly, + $component, + ) + ); + } +} diff --git a/src/Logic/LogicExecutor.php b/src/Logic/LogicExecutor.php index 8c4b98f0..0ceed620 100644 --- a/src/Logic/LogicExecutor.php +++ b/src/Logic/LogicExecutor.php @@ -1,29 +1,35 @@ $extraArgs + * @return Generator filename::function() + */ + // phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh + // phpcs:disable Generic.Metrics.NestingLevel.TooHigh + public function invoke(Assembly $logicAssembly, string $name, array $extraArgs = []):Generator { + foreach($logicAssembly as $file) { $this->loadLogicFile($file); } - } - /** @return Generator filename::function() */ - public function invoke(string $name, array $extraArgs = []):Generator { - foreach(iterator_to_array($this->assembly) as $file) { +// TODO: Why convert to array? + foreach(iterator_to_array($logicAssembly) as $file) { $nsProject = (string)(new LogicProjectNamespace( - $file, + $this->relativePath($file), $this->appNamespace )); @@ -55,8 +61,7 @@ public function invoke(string $name, array $extraArgs = []):Generator { foreach($fnReferenceArray as $fnReference) { if(function_exists($fnReference)) { - $closure = $fnReference(...); - $refFunction = new ReflectionFunction($closure); + $refFunction = new ReflectionFunction($fnReference); foreach($refFunction->getAttributes() as $refAttr) { $functionReference .= "#"; $functionReference .= $refAttr->getName(); @@ -88,9 +93,60 @@ public function invoke(string $name, array $extraArgs = []):Generator { } } } + // phpcs:enable Generic.Metrics.CyclomaticComplexity.TooHigh + // phpcs:enable Generic.Metrics.NestingLevel.TooHigh private function loadLogicFile(string $file):void { + // If the target file already declares a namespace, load it directly. + // The LogicStreamWrapper injects a namespace for classless scripts, but + // passing an already-namespaced file through the wrapper can cause an + // extra namespace to be injected which leads to syntax errors when + // executing tests in isolation. + if($this->fileHasNamespace($file)) { + require_once($file); + return; + } + $streamPath = LogicStreamWrapper::STREAM_NAME . "://$file"; require_once($streamPath); } + + private function fileHasNamespace(string $file):bool { + if(!is_file($file)) { + return false; + } + + $fileHandle = fopen($file, "r"); + if($fileHandle === false) { + return false; + } + + $maxLines = 50; + $read = ""; + for($i = 0; $i < $maxLines; $i++) { + if(feof($fileHandle)) { + break; + } + + $line = fgets($fileHandle); + if($line === false) { + break; + } + $read .= $line; + } + fclose($fileHandle); + return (bool)preg_match('/^\s*namespace\s+[^;]+;/m', $read); + } + + private function relativePath(string $path):string { + if(!str_starts_with($path, "/") && !preg_match('/^[A-Za-z]:[\\\\\/]/', $path)) { + return $path; + } + $cwd = rtrim(getcwd(), "/"); + $real = realpath($path) ?: $path; + if(str_starts_with($real, $cwd . "/")) { + $path = ltrim(substr($real, strlen($cwd) + 1), "/"); + } + return $path; + } } diff --git a/src/Logic/LogicProjectNamespace.php b/src/Logic/LogicProjectNamespace.php index 20b117f3..a6d335cf 100644 --- a/src/Logic/LogicProjectNamespace.php +++ b/src/Logic/LogicProjectNamespace.php @@ -1,5 +1,5 @@ registerCallback = $registerCallback ?? + fn() => stream_wrapper_register($this->streamName, $this->logicStreamClassName); + } + + private function isProtocolDefined(string $protocol):bool { + return in_array($protocol, stream_get_wrappers(), true); + } + + public function setup():void { + if($this->isProtocolDefined($this->streamName)) { + return; + } + + ($this->registerCallback)( + $this->streamName, + $this->logicStreamClassName, + ); + } +} diff --git a/src/Logic/ViewModelProcessor.php b/src/Logic/ViewModelProcessor.php new file mode 100644 index 00000000..230e0947 --- /dev/null +++ b/src/Logic/ViewModelProcessor.php @@ -0,0 +1,22 @@ +getArray, - $this->postArray, - $this->filesArray, - $this->serverArray, - ); - } - - public function handle( - ServerRequestInterface $request - ):ResponseInterface { - $errorCode = 500; - if($this->throwable instanceof ResponseStatusException) { - $errorCode = $this->throwable->getHttpCode(); - } - - $this->originalUri = $request->getUri(); - $errorUri = new Uri("/_$errorCode"); - $errorRequest = $request->withUri($errorUri); - $this->completeRequestHandling($errorRequest, true); - $this->response = $this->response->withStatus($errorCode); - return $this->response; - } -} diff --git a/src/Middleware/Lifecycle.php b/src/Middleware/Lifecycle.php deleted file mode 100644 index 6d3743eb..00000000 --- a/src/Middleware/Lifecycle.php +++ /dev/null @@ -1,297 +0,0 @@ -handleRedirects(); - -// The first thing that's done within the WebEngine lifecycle is start a timer. -// This timer is only used again at the end of the call, when finish() is -// called - at which point the entire duration of the request is logged out (and -// slow requests are highlighted as a NOTICE). - $this->timer = new Timer(); - -// Starting the output buffer is done before any logic is executed, so any calls -// to any area of code will not accidentally send output to the client. - ob_start(); - - $originalGlobals = [ - "get" => $_GET, - "post" => $_POST, - "files" => $_FILES, - "server" => $_SERVER, - ]; -// A PSR-7 HTTP Request object is created from the current global state, ready -// for processing by the Handler. - $requestFactory = new RequestFactory(); - $request = $requestFactory->createServerRequestFromGlobalState( - $originalGlobals["server"], - $originalGlobals["files"], - $originalGlobals["get"], - $originalGlobals["post"], - ); - -// The handler is an individual component that processes a request and produces -// a response, as defined by PSR-7. It's where all your application's logic is -// executed - the brain of WebEngine. Here we pass in a reference to the finish -// function, so the RequestHandler can complete the request early if needed. - $handler = new RequestHandler( - ConfigFactory::createForProject( - getcwd(), - "vendor/phpgt/webengine/config.default.ini" - ), - $this->finish(...), - $originalGlobals["get"], - $originalGlobals["post"], - $originalGlobals["files"], - $originalGlobals["server"], - ); - -// The request and request handler are passed to the PSR-15 process function, -// which will return our PSR-7 HTTP Response. - try { - $response = $this->process($request, $handler); - } - catch(Throwable $throwable) { - Log::critical($throwable->getMessage(), ["uri" => $request->getUri()] + $request->getHeaders() + $throwable->getTrace()[0]); - - $errorHandler = new ErrorRequestHandler( - ConfigFactory::createForProject( - getcwd(), - "vendor/phpgt/webengine/config.default.ini" - ), - $this->finish(...), - $throwable, - $handler->getServiceContainer(), - $originalGlobals["get"], - $originalGlobals["post"], - $originalGlobals["files"], - $originalGlobals["server"], - ); - $response = $this->process($request, $errorHandler); - } - -// Now we can finish the HTTP lifecycle by providing the HTTP response for -// outputting to the browser, along with the buffer so we can display the -// contents in a debug area. - $this->finish( - $response, - $handler->getConfigSection("app"), - ); - } - - /** - * Process an incoming server request and return a response, - * delegating response creation to a handler. - */ - public function process( - ServerRequestInterface $request, - RequestHandlerInterface $handler, - ):ResponseInterface { - return $handler->handle($request); - } - - public function error( - int $errno, - string $errstr, - ?string $errfile = null, - ?int $errline = null, - ?array $errcontext = null, - ):bool { - $params = ["error", $errstr]; - if(isset($this->throwable)) { - array_push($params, $this->throwable, get_class($this->throwable)); - } - call_user_func_array($this->debugOutput(...), $params); - return true; - } - - public function debugOutput( - string $name, - string $message, - mixed $detail = null, - ?string $detailName = null, - ):void { - $detailJs = ""; - if(!is_null($detail)) { - if(!is_null($detailName)) { - $detailJs .= "console.group(\"$detailName\");"; - } - $detailJs .= "console.log(`" . print_r($detail, true) . "`)"; - if(!is_null($detailName)) { - $detailJs .= "console.groupEnd();"; - } - } - $js = << - console.group("%cphp.gt/webengine", "display: inline-block; padding: 0.5em 1em; background: #26a5e3; color: white; cursor: pointer"); - console.info(`$message`); - $detailJs - console.groupEnd(); - - JS; -// $js = str_replace("write("errrrrrrrrrrror!"); - $response = $response->withBody($body); - return $response; - } - - public function finish( - ResponseInterface $response, - ConfigSection $appConfig - ):never { - $buffer = trim(ob_get_clean()); - http_response_code($response->getStatusCode() ?? StatusCode::OK); - - foreach($response->getHeaders() as $key => $value) { - $stringValue = implode(", ", $value); - if(strtolower($key) === "location" && str_starts_with($stringValue, "/_")) { - continue; - } - header("$key: $stringValue", true); - } - - if(strlen($buffer) > 0) { - $newLine = str_contains($buffer, "\n") ? "\n" : ""; - Log::debug("Logic output: {$newLine}{$buffer}"); - } - - $renderBufferSize = $appConfig->getInt("render_buffer_size"); - $body = $response->getBody(); - $body->rewind(); - ob_start(); - while(!$body->eof()) { - echo $body->read($renderBufferSize); - ob_flush(); - flush(); - } - - if(strlen($buffer) > 0) { - $this->debugOutput("buffer", $buffer); - exit; - } - -// The very last thing that's done before the script ends is to stop the Timer, -// so we know exactly how long the request-response lifecycle has taken. - $this->timer->stop(); - $delta = number_format($this->timer->getDelta(), 2); - if($delta >= $appConfig->getFloat("slow_delta")) { - Log::warning("Lifecycle end with VERY SLOW delta time: $delta seconds. https://www.php.gt/webengine/slow-delta"); - } - elseif($delta >= $appConfig->getFloat("very_slow_delta")) { - Log::notice("Lifecycle end with SLOW delta time: $delta seconds. https://www.php.gt/webengine/slow-delta"); - } - - exit; - } - - private function handleRedirects():void { - $redirectFiles = [ - "\t" => "redirect.tsv", - "," => "redirect.csv", - ]; - foreach($redirectFiles as $separatorCharacter => $fileName) { - if(!is_file($fileName)) { - continue; - } - - $currentUri = $_SERVER["REQUEST_URI"]; - - $lines = file($fileName); - usort($lines, function(string $lineA, string $lineB):int { - $lineARegex = str_starts_with($lineA, "~"); - $lineBRegex = str_starts_with($lineB, "~"); - if($lineARegex && !$lineBRegex) { - return -1; - } - - if(!$lineARegex && $lineBRegex) { - return 1; - } - - return 0; - }); - - foreach($lines as $line) { - $row = str_getcsv($line, $separatorCharacter); - if(!$row || !$row[0]) { - continue; - } - - $matchingUri = $row[0]; - $redirectUri = $row[1]; - $responseCode = $row[2] ?? 302; - - $match = $currentUri === $matchingUri; - if($matchingUri[0] === "~") { - $matchingUri = substr($matchingUri, 1); - if(preg_match("~$matchingUri~", $currentUri, $matches)) { - $match = true; - $matchIndex = 1; - while(str_contains($redirectUri, '$' . $matchIndex)) { - $redirectUri = str_replace('$' . $matchIndex, $matches[$matchIndex], $redirectUri); - } - } - } - - if($match) { - Log::notice("Redirecting: $currentUri -> $redirectUri ($responseCode)"); - header("Location: $redirectUri", true, $responseCode); - exit; - } - } - return; - } - } - -} diff --git a/src/Middleware/RequestHandler.php b/src/Middleware/RequestHandler.php deleted file mode 100644 index b67b716b..00000000 --- a/src/Middleware/RequestHandler.php +++ /dev/null @@ -1,547 +0,0 @@ -finishCallback = $finishCallback; - - $this->setupLogger( - $this->config->getSection("logger") - ); - - $appAutoloader = new AppAutoloader( - $this->config->get("app.namespace"), - $this->config->get("app.class_dir"), - ); - $appAutoloader->setup(); - - if(!in_array("gt-logic-stream", stream_get_wrappers())) { - stream_wrapper_register( - "gt-logic-stream", - LogicStreamWrapper::class, - ); - } - } - - public function getConfigSection(string $sectionName):ConfigSection { - return $this->config->getSection($sectionName); - } - - public function getServiceContainer():Container { - return $this->serviceContainer; - } - - public function handle( - ServerRequestInterface $request - ):ResponseInterface { - $this->completeRequestHandling($request); - return $this->response; - } - - protected function completeRequestHandling( - ServerRequestInterface $request, - bool $ignoreLogicErrors = false, - ):void { - $this->setupResponse($request); - $this->forceTrailingSlashes($request); - $this->setupServiceContainer(); - - $input = new Input($this->getArray, $this->postArray, $this->filesArray); - - $this->serviceContainer->set( - $this->config, - $request, - $this->response, - $this->response->headers, - $input, - new ServerInfo($this->serverArray), - ); - $this->injector = new Injector($this->serviceContainer); - - $this->handleRouting($request); - if(!$this->serviceContainer->has(Session::class)) { - $this->handleSession(); - } - - $this->handleProtectedGlobals(); - - try { - if(isset($this->viewModel) && $this->viewModel instanceof HTMLDocument) { - $this->handleHTMLDocumentViewModel(); -// $this->handleCsrf($request); - } - - $this->handleLogicExecution($this->logicAssembly); - } - catch(Throwable $throwable) { - if(!$ignoreLogicErrors) { - throw $throwable; - } - } - -// TODO: Why is this in the handle function? - $documentBinder = $this->serviceContainer->get(Binder::class); - $documentBinder->cleanupDocument(); - - $this->view->stream($this->viewModel); - - $responseHeaders = $this->serviceContainer->get(ResponseHeaders::class); - foreach($responseHeaders->asArray() as $name => $value) { - $this->response = $this->response->withHeader( - $name, - $value, - ); - } - } - - protected function handleRouting(ServerRequestInterface $request) { - $router = $this->createRouter($this->serviceContainer); - $router->route($request); - - $viewClass = $router->getViewClass() ?? NullView::class; - $this->view = new $viewClass($this->response->getBody()); - - $this->viewAssembly = $router->getViewAssembly(); - $this->logicAssembly = $router->getLogicAssembly(); - - $this->dynamicPath = new DynamicPath( - $request->getUri()->getPath(), - $this->viewAssembly, - $this->logicAssembly, - ); - - $this->serviceContainer->set($this->dynamicPath); - - if(!$this->viewAssembly->containsDistinctFile()) { - $this->response = $this->response->withStatus(StatusCode::NOT_FOUND); - } - - foreach($this->viewAssembly as $viewFile) { - $this->view->addViewFile($viewFile); - } - if($viewModel = $this->view->createViewModel()) { - $this->serviceContainer->set($viewModel); - $this->viewModel = $viewModel; - } - } - - protected function handleHTMLDocumentViewModel():void { - $expandedLogicAssemblyList = []; - $expandedComponentList = []; - - try { - $partial = new PartialContent(implode(DIRECTORY_SEPARATOR, [ - getcwd(), - $this->config->getString("view.component_directory") - ])); - $componentExpander = new ComponentExpander( - $this->viewModel, - $partial, - ); - - foreach($componentExpander->expand() as $componentElement) { - $filePath = $this->config->getString("view.component_directory"); - $filePath .= "/"; - $filePath .= $componentElement->tagName; - $filePath .= ".php"; - - if(is_file($filePath)) { - $componentAssembly = new Assembly(); - $componentAssembly->add($filePath); - array_push($expandedLogicAssemblyList, $componentAssembly); - array_push($expandedComponentList, $componentElement); - } - } - } - catch(PartialContentDirectoryNotFoundException) {} - - try { - $partial = new PartialContent(implode(DIRECTORY_SEPARATOR, [ - getcwd(), - $this->config->getString("view.partial_directory") - ])); - - $partialExpander = new PartialExpander( - $this->viewModel, - $partial - ); - $partialExpander->expand(); - } - catch(PartialContentDirectoryNotFoundException) {} - - $dynamicUri = $this->dynamicPath->getUrl("page/"); - $dynamicUri = str_replace("/", "--", $dynamicUri); - $dynamicUri = str_replace("@", "_", $dynamicUri); - $this->viewModel->body->classList->add("uri" . $dynamicUri); - $bodyDirClass = "dir"; - foreach(explode("--", $dynamicUri) as $i => $pathPart) { - if($i === 0) { - continue; - } - $bodyDirClass .= "--$pathPart"; - $this->viewModel->body->classList->add($bodyDirClass); - } - - $this->serviceContainer->get(HTMLAttributeBinder::class)->setDependencies( - $this->serviceContainer->get(ListBinder::class), - $this->serviceContainer->get(TableBinder::class), - ); - $this->serviceContainer->get(ElementBinder::class)->setDependencies( - $this->serviceContainer->get(HTMLAttributeBinder::class), - $this->serviceContainer->get(HTMLAttributeCollection::class), - $this->serviceContainer->get(PlaceholderBinder::class), - ); - $this->serviceContainer->get(TableBinder::class)->setDependencies( - $this->serviceContainer->get(ListBinder::class), - $this->serviceContainer->get(ListElementCollection::class), - $this->serviceContainer->get(ElementBinder::class), - $this->serviceContainer->get(HTMLAttributeBinder::class), - $this->serviceContainer->get(HTMLAttributeCollection::class), - $this->serviceContainer->get(PlaceholderBinder::class), - ); - $this->serviceContainer->get(ListBinder::class)->setDependencies( - $this->serviceContainer->get(ElementBinder::class), - $this->serviceContainer->get(ListElementCollection::class), - $this->serviceContainer->get(BindableCache::class), - $this->serviceContainer->get(TableBinder::class), - ); - $this->serviceContainer->get(Binder::class)->setDependencies( - $this->serviceContainer->get(ElementBinder::class), - $this->serviceContainer->get(PlaceholderBinder::class), - $this->serviceContainer->get(TableBinder::class), - $this->serviceContainer->get(ListBinder::class), - $this->serviceContainer->get(ListElementCollection::class), - $this->serviceContainer->get(BindableCache::class), - ); - -// $listElementCollection = $this->serviceContainer->get(ListElementCollection::class); -// var_dump($listElementCollection);die(); - - foreach($expandedLogicAssemblyList as $i => $assembly) { - $componentElement = $expandedComponentList[$i]; - $this->handleLogicExecution($assembly, $componentElement); - } - } - - protected function handleSession():void { - $sessionConfig = $this->config->getSection("session"); - $sessionId = $_COOKIE[$sessionConfig["name"]] ?? null; - $sessionSetup = new SessionSetup(); - $sessionHandler = $sessionSetup->attachHandler( - $sessionConfig->getString("handler") - ); - - $session = new Session( - $sessionHandler, - $sessionConfig, - $sessionId, - ); - $this->serviceContainer->set($session); - } - - protected function handleCsrf(ServerRequestInterface $request):void { - $shouldVerifyCsrf = true; - $ignoredPathArray = explode(",", $this->config->getString("security.csrf_ignore_path") ?? ""); - foreach($ignoredPathArray as $ignoredPath) { - if(empty($ignoredPath)) { - continue; - } - - if(str_contains($ignoredPath, "*")) { - $pattern = strtr(rtrim($ignoredPath, "/"), [ - "*" => ".*", - ]); - if(preg_match("|$pattern|", rtrim($request->getUri()->getPath(), "/"))) { - $shouldVerifyCsrf = false; - } - } - else { - if(rtrim($request->getUri()->getPath(), "/") === rtrim($ignoredPath, "/")) { - $shouldVerifyCsrf = false; - } - } - } - - if($shouldVerifyCsrf) { - $session = $this->serviceContainer->get(Session::class); - $csrfTokenStore = new SessionTokenStore( - $session->getStore("webengine.csrf", true), - $this->config->getInt("security.csrf_max_tokens") - ); - $csrfTokenStore->setTokenLength( - $this->config->getInt("security.csrf_token_length") - ); - - if($request->getMethod() === "POST") { - $csrfTokenStore->verify($_POST); - } - - $sharing = match($this->config->getString("security.csrf_token_sharing")) { - "per-page" => HTMLDocumentProtector::ONE_TOKEN_PER_PAGE, - default => HTMLDocumentProtector::ONE_TOKEN_PER_FORM, - }; - $protector = new HTMLDocumentProtector( - $this->viewModel, - $csrfTokenStore - ); - $tokens = $protector->protect($sharing); - $this->response = $this->response->withHeader($this->config->getString("security.csrf_header"), $tokens); - } - - } - - protected function handleProtectedGlobals():void { - if($this instanceof ErrorRequestHandler) { -// ErrorRequestHandler extends RequestHander, so this has already been done. - return; - } - - Protection::overrideInternals( - Protection::removeGlobals($GLOBALS, [ - "_ENV" => explode(",", $this->config->getString("app.globals_whitelist_env") ?? ""), - "_SERVER" => explode(",", $this->config->getString("app.globals_whitelist_server") ?? ""), - "_GET" => explode(",", $this->config->getString("app.globals_whitelist_get") ?? ""), - "_POST" => explode(",", $this->config->getString("app.globals_whitelist_post") ?? ""), - "_FILES" => explode(",", $this->config->getString("app.globals_whitelist_files") ?? ""), - "_COOKIES" => explode(",", $this->config->getString("app.globals_whitelist_cookies") ?? ""), - ] - ) - ); - } - - protected function handleLogicExecution(Assembly $logicAssembly, ?Element $component = null):void { - $logicExecutor = new LogicExecutor( - $logicAssembly, - $this->injector, - $this->config->getString("app.namespace") - ); - $extraArgs = []; - - if($component) { - $binder = new ComponentBinder($this->viewModel); - $binder->setDependencies( - $this->serviceContainer->get(ElementBinder::class), - $this->serviceContainer->get(PlaceholderBinder::class), - $this->serviceContainer->get(TableBinder::class), - $this->serviceContainer->get(ListBinder::class), - $this->serviceContainer->get(ListElementCollection::class), - $this->serviceContainer->get(BindableCache::class), - ); - $binder->setComponentBinderDependencies($component); - $extraArgs[Binder::class] = $binder; - $extraArgs[Element::class] = $component; - } - - foreach($logicExecutor->invoke("go_before", $extraArgs) as $file) { - // TODO: Hook up to debug output - } - - $input = $this->serviceContainer->get(Input::class); - $input->when("do")->call( - function(InputData $data)use($logicExecutor, $extraArgs) { - $doName = "do_" . str_replace( - "-", - "_", - $data->getString("do"), - ); - - foreach($logicExecutor->invoke($doName, $extraArgs) as $file) { - // TODO: Hook up to debug output - } - } - ); - foreach($logicExecutor->invoke("go", $extraArgs) as $file) { - // TODO: Hook up to debug output - } - foreach($logicExecutor->invoke("go_after", $extraArgs) as $file) { - // TODO: Hook up to debug output - } - } - - protected function setupLogger(ConfigSection $logConfig):void { - $type = $logConfig->getString("type"); - $path = $logConfig->getString("path"); - $level = $logConfig->getString("level"); - $timestampFormat = $logConfig->getString("timestamp_format"); - $logFormat = explode("\\t", $logConfig->getString("log_format")); - $separator = $logConfig->getString("separator"); - $newLine = $logConfig->getString("newline"); - $logHandler = match($type) { - "file" => new FileHandler($path, $timestampFormat, $logFormat, $separator, $newLine), - "stream" => new StreamHandler($path), - default => new StdOutHandler() - }; - LogConfig::addHandler($logHandler, $level); - } - - protected function createRouter(Container $container):BaseRouter { - $routerConfig = $this->config->getSection("router"); - $namespace = $this->config->getString("app.namespace"); - $appRouterFile = $routerConfig->getString("router_file"); - $appRouterClass = $routerConfig->getString("router_class"); - $defaultRouterFile = dirname(__DIR__, 2) . "/router.default.php"; - - if(file_exists($appRouterFile)) { - require_once($appRouterFile); - $class = "\\$namespace\\$appRouterClass"; - } - else { - require_once($defaultRouterFile); - $class = "\\Gt\\WebEngine\\DefaultRouter"; - } - - /** @var BaseRouter $router */ - $router = new $class($routerConfig); - $router->setContainer($container); - return $router; - } - - private function setupResponse(ServerRequestInterface $request):void { - $this->response = new Response(request: $request); - - $this->response->setExitCallback(fn() => call_user_func( - $this->finishCallback, - $this->response, - $this->config->getSection("app") - )); - } - - private function setupServiceContainer():void { - if(isset($this->serviceContainer)) { - return; - } - $this->serviceContainer = new Container(); - $this->serviceContainer->addLoaderClass( - new DefaultServiceLoader( - $this->config, - $this->serviceContainer - ) - ); - $customServiceContainerClassName = implode("\\", [ - $this->config->get("app.namespace"), - $this->config->get("app.service_loader"), - ]); - if(class_exists($customServiceContainerClassName)) { - $constructorArgs = []; - if(is_a($customServiceContainerClassName, DefaultServiceLoader::class, true)) { - $constructorArgs = [ - $this->config, - $this->serviceContainer, - ]; - } - - $this->serviceContainer->addLoaderClass( - new $customServiceContainerClassName( - ...$constructorArgs - ) - ); - } - } - - /** - * Force trailing slashes in URLs. This is useful for consistency, but - * also helps identify that WebEngine requests do not match an actual - * static file, as file requests will never end in a slash. Another - * benefit is that links can behave relatively (e.g. - * ) 307 is used here to preserve any POST data - * that may be in the request. - */ - private function forceTrailingSlashes(ServerRequestInterface $request):void { - if(str_ends_with($request->getUri()->getPath(), "/")) { - return; - } - - $this->response = $this->response - ->withHeader( - "Location", - $request->getUri()->withPath( - $request->getUri()->getPath() . "/" - ) - ) - ->withStatus(307); - } - - /** @return array */ - private function getAttributesFromFile(string $file):array { - $attrArray = []; - - $firstHash = strpos($file, "#"); - if($firstHash === false) { - return $attrArray; - } - - $file = substr($file, $firstHash + 1); - - foreach(explode("#", $file) as $attrString) { - array_push($attrArray, strtok($attrString, "(")); - } - return $attrArray; - } -} diff --git a/src/Redirection/DelimitedRedirectLoader.php b/src/Redirection/DelimitedRedirectLoader.php new file mode 100644 index 00000000..a2ace02a --- /dev/null +++ b/src/Redirection/DelimitedRedirectLoader.php @@ -0,0 +1,32 @@ +delimiter, escape: '')) !== false) { + if(count($row) < 2) { + continue; + } + + $statusCodeValidator = new StatusCodeValidator(); + $old = trim((string)$row[0]); + $new = trim((string)$row[1]); + $code = isset($row[2]) ? $statusCodeValidator->validate($row[2]) : StatusCodeValidator::DEFAULT_CODE; + $map->addRule($code, $old, $new); + } + fclose($fileHandle); + } +} diff --git a/src/Redirection/IniRedirectLoader.php b/src/Redirection/IniRedirectLoader.php new file mode 100644 index 00000000..df6861dd --- /dev/null +++ b/src/Redirection/IniRedirectLoader.php @@ -0,0 +1,58 @@ +isSkippableIniLine($line)) { + continue; + } + // Section header? + if($line !== '' && $line[0] === '[' && substr($line, -1) === ']') { + $section = trim($line, '[] '); + $statusCodeValidator = new StatusCodeValidator(); + $currentCode = $statusCodeValidator->validate($section); + continue; + } + + if($keyValue = $this->splitIniKeyValue($line)) { + [$old, $new] = $keyValue; + $map->addRule($currentCode, $old, $new); + } + } + + fclose($fileHandle); + } + + private function isSkippableIniLine(string $line):bool { + $line = trim($line); + return $line === '' || str_starts_with($line, ';') || str_starts_with($line, '#'); + } + + /** @return array{0:string,1:string}|null */ + private function splitIniKeyValue(string $line):?array { + $eqPos = strpos($line, '='); + if($eqPos === false) { + return null; + } + $old = trim(substr($line, 0, $eqPos)); + $new = trim(substr($line, $eqPos + 1)); + if($old === '' || $new === '') { + return null; + } + return [$old, $new]; + } +} diff --git a/src/Redirection/Redirect.php b/src/Redirection/Redirect.php new file mode 100644 index 00000000..0a3810c1 --- /dev/null +++ b/src/Redirection/Redirect.php @@ -0,0 +1,96 @@ +expandBraceGlob($glob); + if (count($matches) > 1) { + throw new RedirectException("Multiple redirect files in project root"); + } + + $this->redirectFile = $matches[0] ?? null; + $this->redirectHandler = $redirectHandler ?? + fn(string $uri, int $code) + => header("Location: $uri", true, $code); + + $this->map = new RedirectMap(); + if($this->redirectFile) { + $extension = strtolower(pathinfo($this->redirectFile, PATHINFO_EXTENSION)); + $loader = $this->createLoader($extension); + $loader?->load($this->redirectFile, $this->map); + } + } + + private function createLoader(string $extension):?RedirectLoader { + return match($extension) { + 'ini' => new IniRedirectLoader(), + 'csv' => new DelimitedRedirectLoader(','), + 'tsv' => new DelimitedRedirectLoader("\t"), + default => null, + }; + } + + /** + * Cross-platform brace expansion for glob patterns. + * Supports a single {a,b,c} segment. Falls back to plain glob when no braces. + * Returns a sorted, unique list of matches. + * + * @return array + */ + private function expandBraceGlob(string $pattern): array { + /** @noinspection RegExpRedundantEscape */ + if(preg_match('/\{([^}]+)\}/', $pattern, $braceMatch)) { + $options = array_map('trim', explode(',', $braceMatch[1])); + $all = []; + foreach($options as $option) { + $subPattern = str_replace($braceMatch[0], $option, $pattern); + $subMatches = glob($subPattern) ?: []; + if(!empty($subMatches)) { + $all = array_merge($all, $subMatches); + } + } + $all = array_values(array_unique($all)); + sort($all); + return $all; + } + return glob($pattern) ?: []; + } + + public function execute(string $uri = "/"):void { + $redirect = $this->getRedirectUri($uri); + if($redirect && $redirect->code > 0 && $redirect->uri !== $uri) { + ($this->redirectHandler)($redirect->uri, $redirect->code); + } + } + + public function getRedirectUri(string $oldUri):?RedirectUri { + if($this->map->isEmpty()) { + return null; + } + return $this->map->match($oldUri); + } +} diff --git a/src/Redirection/RedirectException.php b/src/Redirection/RedirectException.php new file mode 100644 index 00000000..654b52ef --- /dev/null +++ b/src/Redirection/RedirectException.php @@ -0,0 +1,6 @@ +> */ + private array $literal = []; + /** @var array> */ + private array $regex = []; + + public function addRule(int $code, string $old, string $new):void { + if($old === '' || $new === '') { + return; + } + if(str_starts_with($old, '~')) { + $this->regex[$code][] = [ + 'pattern' => substr($old, 1), + 'replacement' => $new, + ]; + } + else { + $this->literal[$code][$old] = $new; + } + } + + public function isEmpty():bool { + return empty($this->literal) && empty($this->regex); + } + + public function match(string $oldUri):?RedirectUri { + if($result = $this->matchLiteral($oldUri)) { + return $result; + } + return $this->matchRegex($oldUri); + } + + private function matchLiteral(string $oldUri):?RedirectUri { + if(empty($this->literal)) { + return null; + } + foreach($this->literal as $code => $pairs) { + $newUri = $pairs[$oldUri] ?? null; + if($newUri !== null) { + return new RedirectUri($newUri, (int)$code); + } + } + return null; + } + + private function matchRegex(string $oldUri):?RedirectUri { + if(empty($this->regex)) { + return null; + } + foreach($this->regex as $code => $rules) { + foreach($rules as $rule) { + $pattern = $rule['pattern']; + $replacement = $rule['replacement']; + $matchResult = preg_match("~$pattern~", $oldUri); + if($matchResult === false) { + throw new RedirectException("Invalid regex pattern in redirect file: $pattern"); + } + if($matchResult === 1) { + $newUri = preg_replace("~$pattern~", $replacement, $oldUri, 1); + if(is_string($newUri) && $newUri !== $oldUri) { + return new RedirectUri($newUri, (int)$code); + } + } + } + } + return null; + } +} diff --git a/src/Redirection/RedirectUri.php b/src/Redirection/RedirectUri.php new file mode 100644 index 00000000..e044011d --- /dev/null +++ b/src/Redirection/RedirectUri.php @@ -0,0 +1,9 @@ + self::MAX_CODE) { + throw new RedirectException("Invalid HTTP status code in redirect file: $raw"); + } + + return (int)$raw; + } +} diff --git a/src/Service/ContainerFactory.php b/src/Service/ContainerFactory.php new file mode 100644 index 00000000..5a9436d4 --- /dev/null +++ b/src/Service/ContainerFactory.php @@ -0,0 +1,32 @@ +get("app.namespace"), "\\"); + $serviceLoaderClass = trim((string)$config->get("app.service_loader"), "\\"); + $customLoaderClass = ""; + if($appNamespace !== "" && $serviceLoaderClass !== "") { + $customLoaderClass = implode("\\", [ + $appNamespace, + $serviceLoaderClass, + ]); + } + + if($customLoaderClass !== "" && class_exists($customLoaderClass)) { + $container->addLoaderClass(new $customLoaderClass($config, $container)); + } + else { + // Always register the DefaultServiceLoader for core WebEngine services. + $container->addLoaderClass(new DefaultServiceLoader($config, $container)); + } + + return $container; + } +} diff --git a/src/Middleware/DefaultServiceLoader.php b/src/Service/DefaultServiceLoader.php similarity index 68% rename from src/Middleware/DefaultServiceLoader.php rename to src/Service/DefaultServiceLoader.php index 6020d19f..2de3b4cc 100644 --- a/src/Middleware/DefaultServiceLoader.php +++ b/src/Service/DefaultServiceLoader.php @@ -1,32 +1,40 @@ -uniqid = uniqid(more_entropy: true); + } + + public function loadConfig():Config { + return $this->config; + } public function loadResponseHeaders():ResponseHeaders { $response = $this->container->get(Response::class); @@ -88,7 +96,7 @@ public function loadBinder():Binder { return new DocumentBinder($document); } - public function loadRequestUri():Uri { + public function loadRequestUri():UriInterface { return $this->container->get(Request::class)->getUri(); } } diff --git a/src/View/BaseView.php b/src/View/BaseView.php index 18f75032..4d6a9a29 100644 --- a/src/View/BaseView.php +++ b/src/View/BaseView.php @@ -1,6 +1,7 @@ viewFileArray, $fileName); } - public function stream(mixed $viewModel):void { + public function stream(HTMLDocument $viewModel):void { $this->outputStream->write((string)$viewModel); } } diff --git a/src/View/HTMLView.php b/src/View/HTMLView.php index a4bf6db3..5d5b2e38 100644 --- a/src/View/HTMLView.php +++ b/src/View/HTMLView.php @@ -1,7 +1,7 @@ stream($viewModel); + } + +} diff --git a/src/WebEngineException.php b/src/WebEngineException.php index 163474f4..8795a090 100644 --- a/src/WebEngineException.php +++ b/src/WebEngineException.php @@ -1,4 +1,4 @@ true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => true, + ]; + + if($json !== null) { + $body = json_encode($json, JSON_UNESCAPED_SLASHES); + $headers[] = "Content-Type: application/json"; + $opts[CURLOPT_HTTPHEADER] = $headers; + $opts[CURLOPT_POSTFIELDS] = $body; + } + + curl_setopt_array($ch, $opts); + $fullResponse = curl_exec($ch); + if($fullResponse === false) { + die("curl error: " . curl_error($ch) . "\n"); + } + + $responseCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $rawHeaders = substr($fullResponse, 0, $headerSize); + $body = substr($fullResponse, $headerSize); + curl_close($ch); + + $headersOut = []; + foreach(explode("\r\n", $rawHeaders) as $h) { + $p = strpos($h, ":"); + if($p !== false) { + $headersOut[trim(substr($h, 0, $p))] = trim(substr($h, $p + 1)); + } + } + + if($responseCode >= 400) { + fwrite(STDERR, "HTTP $responseCode $method $url: $body\n"); + + if($responseCode == 422) { + return [[], $headersOut]; + } // continue on validation errors + } + + $decoded = json_decode($body, true); + return [$decoded ?? [], $headersOut]; +} + +function paginate(string $path):array { + global $uriBase; + $items = []; + $url = $uriBase . $path; + + do { + [$page] = httpRequest("GET", $url, null, $headers); + if(isset($page[0]) || empty($page)) { + $items = array_merge($items, $page); + } + else { + $items[] = $page; + } + + $next = null; + if(!empty($headers["Link"])) { + foreach(explode(",", $headers["Link"]) as $link) { + if(preg_match('/<([^>]+)>;\s*rel="next"/', $link, $matches)) { + $next = $matches[1]; + break; + } + } + } + + $url = $next ?? ""; + } + while($url); + + return $items; +} + +function normaliseDesc(?string $d):string { + return $d === null ? "" : $d; +} + +function uri(string $uri):string { + return rawurlencode($uri); +} + +fwrite(STDERR, "Fetching template labels from $githubOrganisation/$githubRepoSource\n"); + +$template = paginate("/repos/$githubOrganisation/$githubRepoSource/labels?per_page=100"); +$tpl = []; +foreach($template as $l) { + $tpl[$l["name"]] = [ + "color" => $l["color"], + "description" => normaliseDesc($l["description"] ?? "") + ]; +} + +fwrite(STDERR, "Fetching org repositories for $githubOrganisation\n"); +$repos = paginate("/orgs/$githubOrganisation/repos?per_page=100&type=public"); +$names = array_values(array_filter(array_map(fn($r) => $r['name'], $repos), fn($n) => $n !== $githubRepoSource)); + +foreach($names as $repo) { + fwrite(STDERR, "==> $repo\n"); + $existing = paginate("/repos/$githubOrganisation/$repo/labels?per_page=100"); + $have = []; + foreach($existing as $l) { + $have[$l["name"]] = [ + "color" => $l["color"], + "description" => normaliseDesc($l["description"] ?? "") + ]; + } + +// Create or update. + foreach($tpl as $name => $data) { + if(isset($have[$name])) { + if($have[$name]["color"] !== $data['color'] || $have[$name]['description'] !== $data['description']) { + fwrite(STDERR, " PATCH $name\n"); + if(!$optDryRun) { + httpRequest("PATCH", "$uriBase/repos/$githubOrganisation/$repo/labels/" . uri($name), [ + 'name' => $name, + 'color' => $data['color'], + 'description' => $data['description'] + ]); + } + } + } + else { + fwrite(STDERR, " CREATE $name\n"); + if(!$optDryRun) { + httpRequest("POST", "$uriBase/repos/$githubOrganisation/$repo/labels", [ + 'name' => $name, + 'color' => $data['color'], + 'description' => $data['description'] + ]); + } + } + } + +// Delete extras. + if($optDeleteExtra) { + foreach($have as $ename => $_) { + if(!isset($tpl[$ename])) { + fwrite(STDERR, " DELETE $ename\n"); + if(!$optDryRun) { + httpRequest("DELETE", "$uriBase/repos/$githubOrganisation/$repo/labels/" . uri($ename)); + } + } + } + } +} +fwrite(STDERR, "Done\n"); \ No newline at end of file diff --git a/test/phpunit/ApplicationTest.php b/test/phpunit/ApplicationTest.php new file mode 100644 index 00000000..557eee2e --- /dev/null +++ b/test/phpunit/ApplicationTest.php @@ -0,0 +1,797 @@ +resetApplicationLoggerState(); + parent::tearDown(); + } + + public function testStart_callsRedirectExecute():void { + $redirect = self::createMock(Redirect::class); + $redirect->expects(self::once()) + ->method("execute"); + + $globalProtection = self::createStub(Protection::class); + $serverRequest = self::createStub(ServerRequest::class); + $serverRequest->method("getUri") + ->willReturn(self::createStub(Uri::class)); + $serverRequest->method("getHeaderLine") + ->willReturnCallback( + fn(string $name):string => strtolower($name) === "accept" ? "*/*" : "" + ); + $serverRequest->method("getMethod") + ->willReturn("GET"); + $requestFactory = self::createStub(RequestFactory::class); + $requestFactory->method("createServerRequestFromGlobalState") + ->willReturn($serverRequest); + $dispatcher = self::createStub(Dispatcher::class); + + $response = self::createStub(Response::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getHeaders')->willReturn(['Content-Type' => ['text/html']]); + $response->method('getBody')->willReturn(new \GT\Http\Stream()); + $dispatcher->method('generateResponse')->willReturn($response); + + $dispatcherFactory = self::createStub(DispatcherFactory::class); + $dispatcherFactory->method('create')->willReturn($dispatcher); + + // Avoid warnings by ensuring server params contain REMOTE_ADDR + $serverRequest->method('getServerParams')->willReturn(['REMOTE_ADDR' => '127.0.0.1']); + + $sut = new Application( + redirect: $redirect, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + globalProtection: $globalProtection, + ); + $sut->start(); + } + + public function testStart_callsTimerFunctions():void { + $timer = self::createMock(Timer::class); + $timer->expects(self::once()) + ->method("start"); + $timer->expects(self::once()) + ->method("stop"); + $timer->expects(self::once()) + ->method("logDelta"); + + $dispatcher = self::createStub(Dispatcher::class); + $response = self::createStub(Response::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getHeaders')->willReturn(['Content-Type' => ['text/html']]); + $response->method('getBody')->willReturn(new \GT\Http\Stream()); + $dispatcher->method('generateResponse')->willReturn($response); + $dispatcherFactory = self::createStub(DispatcherFactory::class); + $dispatcherFactory->method('create')->willReturn($dispatcher); + + $requestFactory = self::createStub(RequestFactory::class); + $serverRequest = self::createStub(ServerRequest::class); + $serverRequest->method('getServerParams')->willReturn(['REMOTE_ADDR' => '127.0.0.1']); + $serverRequest->method('getUri')->willReturn(self::createStub(Uri::class)); + $serverRequest->method('getHeaderLine') + ->willReturnCallback( + fn(string $name):string => strtolower($name) === "accept" ? "*/*" : "" + ); + $serverRequest->method('getMethod')->willReturn('GET'); + $requestFactory->method('createServerRequestFromGlobalState')->willReturn($serverRequest); + + $sut = new Application( + timer: $timer, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + ); + $sut->start(); + } + + public function testStart_callsOutputBufferFunctions():void { + $outputBuffer = self::createMock(OutputBuffer::class); + $outputBuffer->expects(self::once()) + ->method("start"); + $outputBuffer->expects(self::once()) + ->method("debugOutput"); + + $globalProtection = self::createMock(Protection::class); + + $dispatcher = self::createMock(Dispatcher::class); + $response = self::createMock(Response::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getHeaders')->willReturn(['Content-Type' => ['text/html']]); + $response->method('getBody')->willReturn(new \GT\Http\Stream()); + $dispatcher->method('generateResponse')->willReturn($response); + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->method('create')->willReturn($dispatcher); + + $requestFactory = self::createMock(RequestFactory::class); + $serverRequest = self::createMock(ServerRequest::class); + $serverRequest->method('getServerParams')->willReturn(['REMOTE_ADDR' => '127.0.0.1']); + $serverRequest->method('getUri')->willReturn(self::createMock(Uri::class)); + $serverRequest->method('getHeaderLine')->with('accept')->willReturn('*/*'); + $serverRequest->method('getMethod')->willReturn('GET'); + $requestFactory->method('createServerRequestFromGlobalState')->willReturn($serverRequest); + + $sut = new Application( + outputBuffer: $outputBuffer, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + globalProtection: $globalProtection, + ); + $sut->start(); + self::addToAssertionCount(1); + } + + public function testStart_callsRequestFactoryFunctions():void { + $requestFactory = self::createMock(RequestFactory::class); + $requestFactory->expects(self::once()) + ->method("createServerRequestFromGlobalState"); + + $dispatcher = self::createMock(Dispatcher::class); + $response = self::createMock(Response::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('getHeaders')->willReturn(['Content-Type' => ['text/html']]); + $response->method('getBody')->willReturn(new \GT\Http\Stream()); + $dispatcher->method('generateResponse')->willReturn($response); + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->method('create')->willReturn($dispatcher); + + $serverRequest = self::createMock(ServerRequest::class); + $serverRequest->method('getServerParams')->willReturn(['REMOTE_ADDR' => '127.0.0.1']); + $serverRequest->method('getUri')->willReturn(self::createMock(Uri::class)); + $serverRequest->method('getHeaderLine')->with('accept')->willReturn('*/*'); + $serverRequest->method('getMethod')->willReturn('GET'); + $requestFactory->method('createServerRequestFromGlobalState')->willReturn($serverRequest); + + $sut = new Application( + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + ); + $sut->start(); + self::addToAssertionCount(1); + } + + /** + * This test is really important because it shows that all of the + * components that make up the request/response can be injected into the + * application, so everything can be meticulously tested in detail. + */ + public function testStart_callsDispatcherFactoryFunctions():void { + $response = self::createMock(Response::class); + $dispatcher = self::createMock(Dispatcher::class); + + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->expects(self::once()) + ->method("create") + ->willReturn($dispatcher); + + $dispatcher->expects(self::once()) + ->method("generateResponse") + ->willReturn($response); + + $response->expects(self::once()) + ->method("getStatusCode") + ->willReturn(200); + $response->expects(self::once()) + ->method("getHeaders") + ->willReturn(['Content-Type' => ['text/html']]); + $response->expects(self::once()) + ->method("getBody") + ->willReturn(new \GT\Http\Stream()); + + $globalProtection = self::createMock(Protection::class); + + $sut = new Application( + dispatcherFactory: $dispatcherFactory, + globalProtection: $globalProtection, + ); + $sut->start(); + self::addToAssertionCount(1); + } + + public function testStart_callErrorScriptOnThrowable():void { + $php = <<getMessage(); + PHP; + + $tmpFile = tempnam(sys_get_temp_dir(), "webengine-test-"); + file_put_contents($tmpFile, $php); + + $config = $this->createTestConfig([ + "app.error_script" => $tmpFile, + ]); + + $dispatcher = self::createMock(Dispatcher::class); + + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->expects(self::once()) + ->method("create") + ->willReturn($dispatcher); + + $sut = new Application( + config: $config, + dispatcherFactory: $dispatcherFactory, + ); + + $dispatcher->expects(self::once()) + ->method("generateResponse") + ->willThrowException(new Exception("testing")); + + ob_start(); + $sut->start(); + $output = ob_get_clean(); + ob_end_clean(); + + self::assertStringContainsString("exception message is testing", $output); + } + + public function testStart_protectsGlobalsUsingConfiguredWhitelists():void { + $config = $this->createTestConfig([ + "app.globals_whitelist_env" => "ENV_OK", + "app.globals_whitelist_server" => "SERVER_OK,SERVER_OK_2", + "app.globals_whitelist_get" => "GET_OK", + "app.globals_whitelist_post" => "POST_OK", + "app.globals_whitelist_files" => "FILES_OK", + "app.globals_whitelist_cookies" => "COOKIE_OK", + ]); + $globals = [ + "_SERVER" => ["SERVER_OK" => "a", "SERVER_OK_2" => "b"], + "_FILES" => ["FILES_OK" => ["name" => "file.txt"]], + "_GET" => ["GET_OK" => "search"], + "_POST" => ["POST_OK" => "token"], + "_ENV" => ["ENV_OK" => "1"], + "_COOKIE" => ["COOKIE_OK" => "cookie"], + ]; + + $globalProtection = self::createMock(Protection::class); + $globalProtection->expects(self::once()) + ->method("removeGlobals") + ->with( + [ + "server" => $globals["_SERVER"], + "files" => $globals["_FILES"], + "get" => $globals["_GET"], + "post" => $globals["_POST"], + "env" => $globals["_ENV"], + "cookie" => $globals["_COOKIE"], + ], + [ + "_ENV" => ["ENV_OK"], + "_SERVER" => ["SERVER_OK", "SERVER_OK_2"], + "_GET" => ["GET_OK"], + "_POST" => ["POST_OK"], + "_FILES" => ["FILES_OK"], + "_COOKIE" => ["COOKIE_OK"], + ] + ) + ->willReturn(["server" => "protected"]); + $globalProtection->expects(self::once()) + ->method("overrideInternals") + ->with(["server" => "protected"]); + + $requestFactory = self::createMock(RequestFactory::class); + $requestFactory->expects(self::once()) + ->method("createServerRequestFromGlobalState") + ->with( + $globals["_SERVER"], + $globals["_FILES"], + $globals["_GET"], + $globals["_POST"], + ) + ->willReturn($this->createServerRequest()); + + $dispatcher = self::createMock(Dispatcher::class); + $dispatcher->expects(self::once()) + ->method("generateResponse") + ->willReturn($this->createResponse()); + + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->expects(self::once()) + ->method("create") + ->willReturn($dispatcher); + + $sut = new Application( + config: $config, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + globals: $globals, + globalProtection: $globalProtection, + ); + + $sut->start(); + } + + public function testStart_rebuildsDispatcherWithErrorStatusAndSessionInit():void { + $config = $this->createTestConfig(["app.error_script" => ""]); + $request = $this->createServerRequest("/missing-page"); + $requestFactory = self::createStub(RequestFactory::class); + $requestFactory->method("createServerRequestFromGlobalState") + ->willReturn($request); + + $sessionInit = self::createMock(SessionInit::class); + $firstDispatcher = self::createMock(Dispatcher::class); + $firstDispatcher->expects(self::once()) + ->method("generateResponse") + ->willThrowException(new HttpNotFound()); + $firstDispatcher->expects(self::once()) + ->method("getSessionInit") + ->willReturn($sessionInit); + + $errorResponse = $this->createResponse(404, "not found"); + $secondDispatcher = self::createMock(Dispatcher::class); + $secondDispatcher->expects(self::once()) + ->method("generateErrorResponse") + ->with(self::isInstanceOf(HttpNotFound::class)) + ->willReturn($errorResponse); + + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $createCalls = []; + $dispatcherFactory->expects(self::exactly(2)) + ->method("create") + ->willReturnCallback(function( + Config $passedConfig, + ServerRequest $passedRequest, + array $passedGlobals, + Closure $finishCallback, + ?int $errorStatus = null, + ?SessionInit $passedSessionInit = null, + )use($config, $request, $sessionInit, &$createCalls, $firstDispatcher, $secondDispatcher) { + $createCalls[] = [ + "config" => $passedConfig, + "request" => $passedRequest, + "errorStatus" => $errorStatus, + "sessionInit" => $passedSessionInit, + "globalsKeys" => array_keys($passedGlobals), + "finishCallback" => $finishCallback, + ]; + + return count($createCalls) === 1 + ? $firstDispatcher + : $secondDispatcher; + }); + + $sut = new Application( + config: $config, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + globalProtection: self::createStub(Protection::class), + ); + + $sut->start(); + + self::assertSame($config, $createCalls[0]["config"]); + self::assertSame($request, $createCalls[0]["request"]); + self::assertNull($createCalls[0]["errorStatus"]); + self::assertNull($createCalls[0]["sessionInit"]); + self::assertSame(["_SERVER", "_FILES", "_GET", "_POST", "_ENV", "_COOKIE"], $createCalls[0]["globalsKeys"]); + self::assertSame($config, $createCalls[1]["config"]); + self::assertSame($request, $createCalls[1]["request"]); + self::assertSame(404, $createCalls[1]["errorStatus"]); + self::assertSame($sessionInit, $createCalls[1]["sessionInit"]); + } + + public function testStart_fallsBackToBasicErrorResponseWhenErrorPageRenderingFails():void { + $config = $this->createTestConfig(["app.error_script" => ""]); + $requestFactory = self::createStub(RequestFactory::class); + $requestFactory->method("createServerRequestFromGlobalState") + ->willReturn($this->createServerRequest("/broken-error")); + + $originalThrowable = new Exception("page failed"); + $innerThrowable = new Exception("error page failed"); + $fallbackResponse = $this->createResponse(500, "fallback"); + + $firstDispatcher = self::createMock(Dispatcher::class); + $firstDispatcher->expects(self::once()) + ->method("generateResponse") + ->willThrowException($originalThrowable); + $firstDispatcher->method("getSessionInit") + ->willReturn(null); + + $secondDispatcher = self::createMock(Dispatcher::class); + $secondDispatcher->expects(self::once()) + ->method("generateErrorResponse") + ->with(self::identicalTo($originalThrowable)) + ->willThrowException($innerThrowable); + $secondDispatcher->expects(self::once()) + ->method("generateBasicErrorResponse") + ->with( + self::identicalTo($originalThrowable), + self::identicalTo($innerThrowable), + ) + ->willReturn($fallbackResponse); + + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->expects(self::exactly(2)) + ->method("create") + ->willReturnOnConsecutiveCalls($firstDispatcher, $secondDispatcher); + + $sut = new Application( + config: $config, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + globalProtection: self::createStub(Protection::class), + ); + + $sut->start(); + } + + public function testStart_restoresGlobalsBeforeExecutingErrorScript():void { + $php = <<<'PHP' + createTestConfig([ + "app.error_script" => $tmpFile, + ]); + $globals = [ + "_SERVER" => ["RESTORED" => "server"], + "_FILES" => [], + "_GET" => ["restored" => "query"], + "_POST" => [], + "_ENV" => [], + "_COOKIE" => [], + ]; + + $_GET = ["restored" => "wrong"]; + $_SERVER["RESTORED"] = "wrong"; + $GLOBALS["GET"] = ["restored" => "wrong"]; + + $dispatcher = self::createMock(Dispatcher::class); + $dispatcher->expects(self::once()) + ->method("generateResponse") + ->willThrowException(new Exception("testing")); + + $dispatcherFactory = self::createMock(DispatcherFactory::class); + $dispatcherFactory->expects(self::once()) + ->method("create") + ->willReturn($dispatcher); + + $sut = new Application( + config: $config, + dispatcherFactory: $dispatcherFactory, + requestFactory: $this->createRequestFactory(), + globals: $globals, + ); + + ob_start(); + $sut->start(); + $output = ob_get_clean(); + + self::assertSame("query|server|query", $output); + } + + public function testRestoreGlobals_restoresSuperglobalAliases():void { + $globals = [ + "_SERVER" => ["RESTORED" => "server"], + "_FILES" => ["upload" => ["name" => "file.txt"]], + "_GET" => ["query" => "value"], + "_POST" => ["token" => "abc"], + "_ENV" => ["APP_ENV" => "test"], + "_COOKIE" => ["session" => "cookie"], + ]; + + $sut = new Application( + config: $this->createTestConfig([]), + requestFactory: $this->createRequestFactory(), + dispatcherFactory: self::createStub(DispatcherFactory::class), + globals: $globals, + globalProtection: self::createStub(Protection::class), + ); + + $_GET = []; + $_POST = []; + $_SERVER = []; + $_COOKIE = []; + $_FILES = []; + $_ENV = []; + $GLOBALS["GET"] = []; + $GLOBALS["POST"] = []; + $GLOBALS["SERVER"] = []; + $GLOBALS["COOKIE"] = []; + $GLOBALS["FILES"] = []; + $GLOBALS["ENV"] = []; + + $sut->restoreGlobals(); + + self::assertSame($globals["_GET"], $_GET); + self::assertSame($globals["_POST"], $_POST); + self::assertSame($globals["_SERVER"], $_SERVER); + self::assertSame($globals["_COOKIE"], $_COOKIE); + self::assertSame($globals["_FILES"], $_FILES); + self::assertSame($globals["_ENV"], $_ENV); + self::assertSame($globals["_GET"], $GLOBALS["GET"]); + self::assertSame($globals["_POST"], $GLOBALS["POST"]); + self::assertSame($globals["_SERVER"], $GLOBALS["SERVER"]); + self::assertSame($globals["_COOKIE"], $GLOBALS["COOKIE"]); + self::assertSame($globals["_FILES"], $GLOBALS["FILES"]); + self::assertSame($globals["_ENV"], $GLOBALS["ENV"]); + } + + public function testFinish_injectsDebugScriptAndRunsOnlyOnce():void { + $config = $this->createTestConfig([ + "logger.log_all_requests" => false, + "app.render_buffer_size" => 8192, + ]); + $timer = self::createMock(Timer::class); + $timer->expects(self::once())->method("stop"); + $timer->expects(self::once())->method("logDelta"); + $outputBuffer = self::createMock(OutputBuffer::class); + $outputBuffer->expects(self::once()) + ->method("debugOutput") + ->willReturn(""); + + $stream = new Stream(); + $stream->write("Hello"); + + $response = self::createMock(Response::class); + $response->expects(self::once())->method("getStatusCode")->willReturn(200); + $response->expects(self::once())->method("getHeaders")->willReturn([]); + $response->expects(self::once())->method("getBody")->willReturn($stream); + + $sut = new Application( + config: $config, + timer: $timer, + outputBuffer: $outputBuffer, + requestFactory: $this->createRequestFactory(), + dispatcherFactory: self::createStub(DispatcherFactory::class), + globalProtection: self::createStub(Protection::class), + ); + + ob_start(); + $this->invokePrivateMethod($sut, "finish", $response); + $this->invokePrivateMethod($sut, "finish", $response); + $output = ob_get_clean(); + + self::assertSame("Hello", $output); + } + + public function testStart_logsAllRequestsWithRequestContext():void { + $this->resetApplicationLoggerState(); + TestLogHandler::$records = []; + LogConfig::addHandler(new TestLogHandler()); + $this->setApplicationLoggerConfigured(true); + + $config = $this->createTestConfig([ + "logger.log_all_requests" => true, + ]); + $request = $this->createServerRequest( + "/search", + ["q" => "php"], + ["token" => "abc"], + ["REMOTE_ADDR" => "127.0.0.1"], + ); + $requestFactory = self::createStub(RequestFactory::class); + $requestFactory->method("createServerRequestFromGlobalState") + ->willReturn($request); + + $dispatcher = self::createMock(Dispatcher::class); + $dispatcher->expects(self::once()) + ->method("generateResponse") + ->willReturn($this->createResponse(204)); + + $dispatcherFactory = self::createStub(DispatcherFactory::class); + $dispatcherFactory->method("create") + ->willReturn($dispatcher); + + $sut = new Application( + config: $config, + requestFactory: $requestFactory, + dispatcherFactory: $dispatcherFactory, + globalProtection: self::createStub(Protection::class), + ); + + $sut->start(); + + self::assertCount(1, TestLogHandler::$records); + self::assertSame("INFO", TestLogHandler::$records[0]["level"]); + self::assertSame("HTTP 204", TestLogHandler::$records[0]["message"]); + self::assertSame("/search", TestLogHandler::$records[0]["context"]["uri"]); + self::assertSame(["q" => "php"], TestLogHandler::$records[0]["context"]["query"]); + self::assertSame(["token" => "abc"], TestLogHandler::$records[0]["context"]["post"]); + self::assertSame("127.0.0.1:", TestLogHandler::$records[0]["context"]["id"]); + } + + public function testLogError_doesNotLogClientErrors():void { + $this->resetApplicationLoggerState(); + LogConfig::addHandler(new TestLogHandler()); + $this->setApplicationLoggerConfigured(true); + + $sut = new Application( + config: $this->createTestConfig([]), + requestFactory: $this->createRequestFactory(), + dispatcherFactory: self::createStub(DispatcherFactory::class), + globalProtection: self::createStub(Protection::class), + ); + + $this->invokePrivateMethod($sut, "logError", new HttpNotFound()); + + self::assertSame([], TestLogHandler::$records); + } + + public function testLogError_usesConfiguredLoggerHandlers():void { + $this->resetApplicationLoggerState(); + LogConfig::addHandler(new TestLogHandler()); + $this->setApplicationLoggerConfigured(true); + + $sut = new Application( + config: $this->createTestConfig([]), + requestFactory: $this->createRequestFactory(), + dispatcherFactory: self::createStub(DispatcherFactory::class), + globalProtection: self::createStub(Protection::class), + ); + + $this->invokePrivateMethod($sut, "logError", new Exception("boom")); + + self::assertCount(1, TestLogHandler::$records); + self::assertSame("ERROR", TestLogHandler::$records[0]["level"]); + self::assertStringContainsString("boom", TestLogHandler::$records[0]["message"]); + } + + public function testGetStderrMinimumLogLevel_fallsBackToErrorForInvalidConfig():void { + $sut = new Application( + config: $this->createTestConfig([ + "logger.stderr_min_level" => "banana", + ]), + requestFactory: $this->createRequestFactory(), + dispatcherFactory: self::createStub(DispatcherFactory::class), + globalProtection: self::createStub(Protection::class), + ); + + $level = $this->invokePrivateMethod($sut, "getStderrMinimumLogLevel"); + + self::assertSame("ERROR", $level); + } + + public function testConfigureLoggerStreams_registersSplitStdoutAndStderrHandlers():void { + $this->resetApplicationLoggerState(); + + new Application( + config: $this->createTestConfig([ + "logger.stderr_min_level" => "WARNING", + ]), + requestFactory: $this->createRequestFactory(), + dispatcherFactory: self::createStub(DispatcherFactory::class), + globalProtection: self::createStub(Protection::class), + ); + + $handlers = $this->getStaticProperty(LogConfig::class, "handlers"); + $minLevels = $this->getStaticProperty(LogConfig::class, "handlerMinLevels"); + $maxLevels = $this->getStaticProperty(LogConfig::class, "handlerMaxLevels"); + + self::assertCount(2, $handlers); + self::assertSame(["DEBUG", "WARNING"], $minLevels); + self::assertSame(["NOTICE", "EMERGENCY"], $maxLevels); + self::assertTrue($this->getStaticProperty(Application::class, "loggerConfigured")); + } + + private function createTestConfig(array $mockedValues):Config { + $config = self::createStub(Config::class); + + $configFile = "config.default.ini"; + $configContents = parse_ini_file($configFile, true); + + $map = []; + foreach ($configContents as $section => $kvp) { + foreach ($kvp as $key => $value) { + $map["$section.$key"] = $value; + } + } + + $map = array_merge($map, $mockedValues); + + $config->method(self::anything())->willReturnCallback(function ($key) use ($map) { + $type = null; + $bt = debug_backtrace(); + if(isset($bt[1]) && ($bt[1]["args"][0] ?? null) instanceof Invocation) { + $type = strtolower(substr($bt[1]["args"][0]->methodName(), 3)); + } + + return match($type) { + "bool" => (bool)$map[$key], + "int" => (int)$map[$key], + "float" => (float)$map[$key], + default => $map[$key], + }; + }); + + return $config; + } + + private function createRequestFactory(?ServerRequest $request = null):RequestFactory { + $requestFactory = self::createStub(RequestFactory::class); + $requestFactory->method("createServerRequestFromGlobalState") + ->willReturn($request ?? $this->createServerRequest()); + return $requestFactory; + } + + private function createServerRequest( + string $path = "/", + array $query = [], + array $post = [], + array $serverParams = ["REMOTE_ADDR" => "127.0.0.1"], + ):ServerRequest { + $request = self::createStub(ServerRequest::class); + $request->method("getUri") + ->willReturn(new Uri("https://example.test" . $path)); + $request->method("getHeaderLine") + ->willReturnCallback( + fn(string $name):string => strtolower($name) === "accept" ? "*/*" : "" + ); + $request->method("getMethod") + ->willReturn("GET"); + $request->method("getServerParams") + ->willReturn($serverParams); + $request->method("getQueryParams") + ->willReturn($query); + $request->method("getParsedBody") + ->willReturn($post); + return $request; + } + + private function createResponse(int $statusCode = 200, string $body = ""):Response { + $response = self::createStub(Response::class); + $stream = new Stream(); + $stream->write($body); + $response->method("getStatusCode")->willReturn($statusCode); + $response->method("getHeaders")->willReturn([]); + $response->method("getBody")->willReturn($stream); + return $response; + } + + private function invokePrivateMethod(object $object, string $method, mixed ...$args):mixed { + $reflectionMethod = new \ReflectionMethod($object, $method); + return $reflectionMethod->invokeArgs($object, $args); + } + + private function resetApplicationLoggerState():void { + $this->setStaticProperty(Application::class, "loggerConfigured", false); + $this->setStaticProperty(LogConfig::class, "handlers", []); + $this->setStaticProperty(LogConfig::class, "handlerMinLevels", []); + $this->setStaticProperty(LogConfig::class, "handlerMaxLevels", []); + TestLogHandler::$records = []; + } + + private function setApplicationLoggerConfigured(bool $configured):void { + $this->setStaticProperty(Application::class, "loggerConfigured", $configured); + } + + private function setStaticProperty(string $className, string $propertyName, mixed $value):void { + $property = new \ReflectionProperty($className, $propertyName); + $property->setValue(null, $value); + } + + private function getStaticProperty(string $className, string $propertyName):mixed { + $property = new \ReflectionProperty($className, $propertyName); + return $property->getValue(); + } +} diff --git a/test/phpunit/Debug/OutputBufferTest.php b/test/phpunit/Debug/OutputBufferTest.php new file mode 100644 index 00000000..a16e8bea --- /dev/null +++ b/test/phpunit/Debug/OutputBufferTest.php @@ -0,0 +1,77 @@ +start(); + self::assertTrue($called, 'Expected injected ob_start handler to be invoked'); + } + + public function testDebugOutput_emptyBuffer_returnsNull(): void { + $obStart = function() { /* no-op */ }; + $obGetCleanValues = [""]; // empty string simulates no output + $obGetClean = function() use (&$obGetCleanValues) { return array_shift($obGetCleanValues); }; + $sut = new OutputBuffer(true, $obStart, $obGetClean); + $sut->start(); + self::assertNull($sut->debugOutput()); + } + + public function testDebugOutput_whenDebugToJavaScriptFalse_returnsNullDespiteBuffer(): void { + $obStart = function() {}; + $obGetCleanValues = ["Some output"]; + $obGetClean = function() use (&$obGetCleanValues) { return array_shift($obGetCleanValues); }; + $sut = new OutputBuffer(false, $obStart, $obGetClean); + $sut->start(); + self::assertNull($sut->debugOutput()); + } + + public function testDebugOutput_returnsScriptWithEscapedBackticks_andTrimApplied(): void { + $obStart = function() {}; + $raw = " Hello `there` \n"; // leading/trailing whitespace and a backtick + $obGetCleanValues = [$raw]; + $obGetClean = function() use (&$obGetCleanValues) { return array_shift($obGetCleanValues); }; + $sut = new OutputBuffer(true, $obStart, $obGetClean); + $sut->start(); + $html = $sut->debugOutput(); + self::assertNotNull($html); + self::assertStringContainsString('