From 84d399072d4968179212dbe6477d670898949471 Mon Sep 17 00:00:00 2001 From: David Galet Date: Wed, 15 Jul 2026 20:02:15 +0200 Subject: [PATCH 1/5] feat: implement query collection and streaming execution for PHP code --- .github/workflows/code-style.yml | 9 +- .github/workflows/release.yml | 38 +- .github/workflows/tests.yml | 32 + .gitignore | 6 +- LICENSE.md | 21 - Makefile | 22 + README.md | 156 +- box.json | 4 - composer.json | 28 +- composer.lock | 2248 ++++++++++++++++- index.php | 99 +- src/Cli.php | 19 - src/Database/LaravelQueryProvider.php | 47 + src/Database/QueryCollector.php | 69 + src/Database/QueryProviderInterface.php | 11 + src/Database/SymfonyDoctrineQueryProvider.php | 117 + src/Loader.php | 68 - src/Loaders/BaseLoader.php | 4 +- src/Loaders/ComposerLoader.php | 23 +- src/Loaders/LaravelLoader.php | 4 + src/Loaders/LoaderInterface.php | 2 +- src/Loaders/PimcoreLoader.php | 35 - src/Loaders/PlainPhpLoader.php | 33 +- src/Loaders/SymfonyLoader.php | 27 +- src/Loaders/WordPressLoader.php | 1 - src/Output/StreamingOutput.php | 34 + src/OutputModifiers/CustomOutputModifier.php | 8 - src/OutputModifiers/OutputModifier.php | 8 - src/Psy/Configuration.php | 17 - src/Psy/Presenter.php | 54 - src/Tinker.php | 181 +- tests/CliTest.php | 40 + tests/LoaderTest.php | 264 ++ tests/OutputModifierTest.php | 26 + tests/QueryCollectorTest.php | 258 ++ tests/TinkerTest.php | 267 ++ 36 files changed, 3823 insertions(+), 457 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 LICENSE.md create mode 100644 Makefile delete mode 100644 box.json delete mode 100644 src/Cli.php create mode 100644 src/Database/LaravelQueryProvider.php create mode 100644 src/Database/QueryCollector.php create mode 100644 src/Database/QueryProviderInterface.php create mode 100644 src/Database/SymfonyDoctrineQueryProvider.php delete mode 100644 src/Loader.php delete mode 100644 src/Loaders/PimcoreLoader.php create mode 100644 src/Output/StreamingOutput.php delete mode 100644 src/OutputModifiers/OutputModifier.php delete mode 100644 src/Psy/Configuration.php delete mode 100644 src/Psy/Presenter.php create mode 100644 tests/CliTest.php create mode 100644 tests/LoaderTest.php create mode 100644 tests/OutputModifierTest.php create mode 100644 tests/QueryCollectorTest.php create mode 100644 tests/TinkerTest.php diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index e279458..62c3cf1 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -33,8 +33,11 @@ jobs: restore-keys: | ${{ runner.os }}-php- - - name: Install Pint - run: composer global require laravel/pint + - name: Install Composer dependencies + run: composer install --prefer-dist --no-interaction --no-progress - name: Run Pint - run: pint --test + run: vendor/bin/pint --test src tests + + - name: Run tests + run: composer test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8512e17..32c4fe5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,15 +14,13 @@ permissions: jobs: release: runs-on: ubuntu-latest - outputs: - upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create GitHub Release id: create_release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.version }} release_name: ${{ github.event.inputs.version }} @@ -49,12 +47,10 @@ jobs: php-version: ${{ matrix.php-version }} tools: composer - - name: Remove Vendor and Composer Lock + - name: Capture Runtime PHP run: | - rm -rf vendor composer.lock - - - name: Install Dependencies - run: composer install --no-dev + echo "RUNTIME_PHP=$(command -v php)" >> "$GITHUB_ENV" + echo "COMPOSER_BIN=$(command -v composer)" >> "$GITHUB_ENV" - name: Setup PHP for Box uses: shivammathur/setup-php@v2 @@ -62,20 +58,8 @@ jobs: php-version: "8.2" tools: composer - - name: Install Box Globally - run: | - composer global require humbug/box - env: - COMPOSER_HOME: ${{ runner.temp }}/composer - - - name: Add Composer Global Bin to PATH - run: echo "$COMPOSER_HOME/vendor/bin" >> $GITHUB_PATH - env: - COMPOSER_HOME: ${{ runner.temp }}/composer - - - name: Compile with Box - run: | - box compile + - name: Build PHAR + run: make build UPDATE_LOCK=true PHP_BIN="$RUNTIME_PHP" BOX_PHP="$(command -v php)" COMPOSER_BIN="$COMPOSER_BIN" env: COMPOSER_HOME: ${{ runner.temp }}/composer @@ -84,11 +68,9 @@ jobs: mv client.phar client-${{ matrix.php-version }}.phar - name: Upload Release Asset - uses: actions/upload-release-asset@v1 + uses: softprops/action-gh-release@v2 with: - upload_url: ${{ needs.release.outputs.upload_url }} - asset_path: ./client-${{ matrix.php-version }}.phar - asset_name: client-${{ matrix.php-version }}.phar - asset_content_type: application/octet-stream + tag_name: ${{ github.event.inputs.version }} + files: ./client-${{ matrix.php-version }}.phar env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2667238 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: tests + +on: + push: + branches: + - main + pull_request: + +jobs: + tests: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + php: [ "7.4", "8.0", "8.1", "8.2", "8.3", "8.4" ] + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json + tools: composer + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-interaction --no-progress --ignore-platform-req=php + + - name: Run tests + run: composer test diff --git a/.gitignore b/.gitignore index 1243a03..e528987 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea .vscode - vendor client.phar - -.DS_Store \ No newline at end of file +.DS_Store +.phpunit.result.cache +e2e_tests/* \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 9021060..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 TweakPHP - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d5c62e5 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: build + +PHP_BIN ?= php +BOX_PHP ?= $(PHP_BIN) +COMPOSER_BIN ?= $(shell command -v composer) +BOX_VERSION ?= 4.6.2 +BOX_COMPOSER_HOME := $(if $(strip $(COMPOSER_HOME)),$(COMPOSER_HOME),$(shell mktemp -d)) +BOX_BIN := $(shell COMPOSER_HOME="$(BOX_COMPOSER_HOME)" $(COMPOSER_BIN) global config bin-dir --absolute 2>/dev/null)/box + +ifeq ($(UPDATE_LOCK),true) +COMPOSER_ACTION := update +COMPOSER_ACTION_FLAGS := --with-all-dependencies +else +COMPOSER_ACTION := install +COMPOSER_ACTION_FLAGS := +endif + +build: + rm -rf vendor + $(PHP_BIN) $(COMPOSER_BIN) $(COMPOSER_ACTION) --no-dev --prefer-dist --optimize-autoloader --no-interaction --no-progress $(COMPOSER_ACTION_FLAGS) + COMPOSER_HOME="$(BOX_COMPOSER_HOME)" $(BOX_PHP) $(COMPOSER_BIN) global require humbug/box:$(BOX_VERSION) --with-all-dependencies --no-interaction + $(BOX_PHP) $(BOX_BIN) compile \ No newline at end of file diff --git a/README.md b/README.md index 45e9ac6..42ff76f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,157 @@ # TweakPHP Client -TweakPHP Client is a packaged Phar file that [TweakPHP](https://github.com/tweakphp/tweakphp) uses under the hood to run your tweaks! +TweakPHP Client is a PHAR used by [TweakPHP](https://github.com/tweakphp/tweakphp) +to execute PHP code inside a project. -(Documentation in progress...) +## Requirements + +- PHP 7.4 or higher + +## Build + +Build the PHAR with production dependencies only: + +```bash +make build +``` +The build removes `vendor`, installs the project dependencies with `--no-dev`, +installs the pinned Box version in a temporary Composer home, and creates +`client.phar`. Development dependencies are not included in the PHAR. + +Release builds resolve the dependencies against the PHP runtime that will +execute each PHAR. PHP 7.4 and 8.0 use Symfony 5.4, while newer PHP versions +can use the compatible Symfony 6.4, 7.x, or 8.x release. To do the same for a +local build, run: + +```bash +make build UPDATE_LOCK=true +``` + +## Command Syntax + +The project directory is always required and must come before the command: + +```bash +php client.phar [base64-code] +``` + +For the current directory, use `.` or `$PWD`: + +```bash +php client.phar . info +php client.phar "$PWD" info +``` + +Available commands: + +```bash +php client.phar info +php client.phar execute +php client.phar execute-stream +``` + +## Check Project Info + +`info` reports the detected project type, project version, and PHP version: + +```bash +php client.phar /path/to/project info +``` + +Example response: + +```json +{"name":"Laravel","version":"11.x","php_version":"8.4.0"} +``` + +## Execute Code + +The PHP code must be Base64-encoded before it is passed to the client. + +```bash +code=$(printf '%s' 'echo "Hello";' | base64) +php client.phar /path/to/project execute "$code" +``` + +The command waits until the complete script has finished and returns one JSON +result prefixed with `TWEAKPHP_RESULT:`: + +```text +TWEAKPHP_RESULT:{"output":[{"line":2,"code":"echo \"Hello\";","output":"Hello","queries":[]}],"queries":[]} +``` + +If the command fails, it returns `TWEAKPHP_ERROR:` and exits with status `1`: + +```text +TWEAKPHP_ERROR:{"class":"InvalidArgumentException","message":"Invalid Base64-encoded PHP code."} +``` + +User code calling `exit()` or `die()` preserves the requested exit status. +Instrumentation failures are reported in the optional `query_errors` field rather +than being silently discarded. + +Multiple statements are supported: + +```bash +code=$(printf '%s' 'echo "First"; sleep(1); echo "Second";' | base64) +php client.phar "$PWD" execute "$code" +``` + +## Execute As A Stream + +Use `execute-stream` when the caller needs output while the script is running. +The project directory is still required: + +```bash +code=$(printf '%s' 'echo "First"; sleep(1); echo "Second";' | base64) +php client.phar "$PWD" execute-stream "$code" +``` + +The command writes one JSON event per line, each prefixed with +`TWEAKPHP_STREAM:`: + +```text +TWEAKPHP_STREAM:{"type":"statement.started","index":0,"line":2,"code":"echo \"First\";"} +TWEAKPHP_STREAM:{"type":"output","index":0,"data":"First"} +TWEAKPHP_STREAM:{"type":"statement.completed","index":0,"queries":[]} +TWEAKPHP_STREAM:{"type":"completed"} +``` + +Event types: + +- `statement.started`: a statement started +- `output`: the statement produced output +- `statement.completed`: a statement finished, with collected queries +- `error`: a statement or the client failed; the process exits with status `1`, + or with the status requested by `exit()`/`die()` +- `completed`: all statements finished + +An `exit()` or `die()` call emits an `error` event with its exit status and the +process exits with that status. + +## Supported Projects + +The client detects the project type automatically: + +- Laravel: `vendor/autoload.php` and `bootstrap/app.php` +- Symfony: `vendor/autoload.php`, `symfony.lock`, and `src/Kernel.php` +- WordPress: `wp-load.php` +- Pimcore: `vendor/pimcore/pimcore` +- Composer: `vendor/autoload.php` +- Plain PHP: any directory not matching a more specific project type + +Examples: + +```bash +# Laravel +code=$(printf '%s' 'return App\Models\User::query()->latest()->first();' | base64) +php client.phar /path/to/laravel execute "$code" + +# WordPress +code=$(printf '%s' 'return get_option("blogname");' | base64) +php client.phar /path/to/wordpress execute "$code" + +# Plain PHP or Composer +code=$(printf '%s' 'return PHP_VERSION;' | base64) +php client.phar /path/to/project execute "$code" +``` diff --git a/box.json b/box.json deleted file mode 100644 index 71fa29b..0000000 --- a/box.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "compression": "GZ", - "output": "client.phar" -} diff --git a/composer.json b/composer.json index cde727d..a1d3c18 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,8 @@ { "name": "tweakphp/client", "type": "library", + "license": "MIT", + "description": "PHAR client for executing PHP code inside a project", "autoload": { "psr-4": { "TweakPHP\\Client\\": "src/" @@ -10,16 +12,32 @@ { "name": "Saeed Vaziry", "email": "mr.saeedvaziry@gmail.com" + }, + { + "name": "David Galet", + "email": "davidgalet@gmail.com" } ], "require": { "php": ">=7.4", "ext-json": "*", - "psy/psysh": "*", - "nikic/php-parser": "*", - "symfony/var-dumper": "*", - "symfony/console": "*" + "psy/psysh": "^0.12", + "nikic/php-parser": "^5.0", + "symfony/var-dumper": "^5.4 || ^6.4 || ^7.0 || ^8.0", + "symfony/console": "^5.4 || ^6.4 || ^7.0 || ^8.0" }, "minimum-stability": "stable", - "prefer-stable": true + "prefer-stable": true, + "require-dev": { + "phpunit/phpunit": "^9.6", + "laravel/pint": "^1.29" + }, + "autoload-dev": { + "psr-4": { + "TweakPHP\\Client\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit tests/" + } } diff --git a/composer.lock b/composer.lock index 138cbdb..6574184 100644 --- a/composer.lock +++ b/composer.lock @@ -4,24 +4,23 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "206f824071dc8fdf6bc7dcd684dd5aab", + "content-hash": "0b05177dfb78b8c648658e7a28a22f3e", "packages": [ { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -36,7 +35,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -60,33 +59,28 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "psr/container", - "version": "2.0.2", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -113,22 +107,22 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psy/psysh", - "version": "v0.12.7", + "version": "v0.12.24", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", "shasum": "" }, "require": { @@ -136,18 +130,19 @@ "ext-tokenizer": "*", "nikic/php-parser": "^5.0 || ^4.0", "php": "^8.0 || ^7.4", - "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" }, "conflict": { "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2" + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" }, "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-pdo-sqlite": "The doc command requires SQLite to work.", "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, "bin": [ @@ -178,12 +173,11 @@ "authors": [ { "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" + "email": "justin@justinhileman.info" } ], "description": "An interactive shell for modern PHP.", - "homepage": "http://psysh.org", + "homepage": "https://psysh.org", "keywords": [ "REPL", "console", @@ -192,52 +186,58 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" }, - "time": "2024-12-10T01:58:33+00:00" + "time": "2026-06-29T15:41:09+00:00" }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" }, "type": "library", "autoload": { @@ -271,7 +271,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -287,24 +287,24 @@ "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.1" }, "type": "library", "extra": { @@ -313,7 +313,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "2.5-dev" } }, "autoload": { @@ -338,7 +338,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" }, "funding": [ { @@ -354,20 +354,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -417,7 +417,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -428,25 +428,29 @@ "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" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -495,7 +499,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -506,25 +510,29 @@ "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" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -576,7 +584,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -587,28 +595,33 @@ "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" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -656,7 +669,87 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "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-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "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 backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.37.0" }, "funding": [ { @@ -667,6 +760,10 @@ "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" @@ -674,28 +771,115 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.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": "2026-04-10T16:19:22+00:00" + }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f37b419f7aea2e9abf10abd261832cace12e3300", + "reference": "f37b419f7aea2e9abf10abd261832cace12e3300", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, + "suggest": { + "symfony/service-implementation": "" + }, "type": "library", "extra": { "thanks": { @@ -703,16 +887,13 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "2.5-dev" } }, "autoload": { "psr-4": { "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -739,7 +920,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.4" }, "funding": [ { @@ -755,39 +936,38 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=7.2.5", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" }, "conflict": { - "symfony/translation-contracts": "<2.5" + "symfony/translation-contracts": ">=3.0" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" }, "type": "library", "autoload": { @@ -826,7 +1006,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, "funding": [ { @@ -842,36 +1022,42 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2024-11-10T20:33:58+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.2.0", + "version": "v5.4.48", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c" + "reference": "42f18f170aa86d612c3559cfb3bd11a375df32c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c6a22929407dec8765d6e2b6ff85b800b245879c", - "reference": "c6a22929407dec8765d6e2b6ff85b800b245879c", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/42f18f170aa86d612c3559cfb3bd11a375df32c8", + "reference": "42f18f170aa86d612c3559cfb3bd11a375df32c8", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.16" }, "conflict": { - "symfony/console": "<6.4" + "symfony/console": "<4.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.12" + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/uid": "^5.1|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" }, "bin": [ "Resources/bin/var-dump-server" @@ -909,7 +1095,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.0" + "source": "https://github.com/symfony/var-dumper/tree/v5.4.48" }, "funding": [ { @@ -925,10 +1111,1802 @@ "type": "tidelift" } ], - "time": "2024-11-08T15:48:14+00:00" + "time": "2024-11-08T15:21:10+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "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" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", + "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-06-16T15:34:04+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+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" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "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" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "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", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.35", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0edba2f3a0c48df3553cb9b640810b30df60302b", + "reference": "0edba2f3a0c48df3553cb9b640810b30df60302b", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.10", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.35" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-06T14:48:07+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "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://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" + }, + "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/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:22:56+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "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", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "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/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://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" + }, + "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/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "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": "Allows reflection of object attributes, including inherited and non-public ones", + "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" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "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" + }, + "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/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "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/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "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" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, @@ -939,5 +2917,5 @@ "ext-json": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/index.php b/index.php index aa7656b..476bf81 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,6 @@ init(); - +$command = $arguments[2]; $supportedCommands = [ 'info', 'execute', + 'execute-stream', ]; -if (! in_array($arguments[2], $supportedCommands)) { +if (! in_array($command, $supportedCommands, true)) { echo 'Invalid command'.PHP_EOL; exit(1); } -switch ($arguments[2]) { - case 'info': - $info = json_encode([ - 'name' => $loader->name(), - 'version' => $loader->version(), - 'php_version' => phpversion(), +$writeStreamEvent = static function (array $event): void { + fwrite(STDOUT, 'TWEAKPHP_STREAM:'.json_encode($event, JSON_INVALID_UTF8_SUBSTITUTE).PHP_EOL); + fflush(STDOUT); +}; + +$writeError = static function (Throwable $exception) use ($command, $writeStreamEvent): void { + $error = [ + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + ]; + + if ($command === 'execute-stream') { + $writeStreamEvent([ + 'type' => 'error', + 'error' => $error, ]); - echo $info.PHP_EOL; - break; - case 'execute': + + return; + } + + echo 'TWEAKPHP_ERROR:'.json_encode($error, JSON_INVALID_UTF8_SUBSTITUTE).PHP_EOL; +}; + +$customLoader = Cli::getArgument('loader'); +try { + $loader = Loader::load($arguments[1], $customLoader ?: null); + + if ($loader === null) { + throw new RuntimeException('No supported project found. Make sure the path contains a Composer project (vendor/autoload.php).'); + } + + $loader->init(); + + if (in_array($command, ['execute', 'execute-stream'], true)) { if (count($arguments) < 4) { - echo 'Invalid arguments'.PHP_EOL; - exit(1); + throw new InvalidArgumentException('Missing Base64-encoded PHP code.'); } - $output = json_encode($loader->execute(base64_decode($arguments[3]))); - echo 'TWEAKPHP_RESULT:'.$output.PHP_EOL; - break; + + $code = base64_decode($arguments[3], true); + + if ($code === false) { + throw new InvalidArgumentException('Invalid Base64-encoded PHP code.'); + } + } + + switch ($command) { + case 'info': + echo json_encode([ + 'name' => $loader->name(), + 'version' => $loader->version(), + 'php_version' => phpversion(), + ], JSON_INVALID_UTF8_SUBSTITUTE).PHP_EOL; + break; + case 'execute': + echo 'TWEAKPHP_RESULT:'.json_encode($loader->execute($code), JSON_INVALID_UTF8_SUBSTITUTE).PHP_EOL; + break; + case 'execute-stream': + $streamFailed = false; + $streamExitCode = 1; + $loader->executeStreaming($code, static function (array $event) use (&$streamFailed, &$streamExitCode, $writeStreamEvent): void { + $streamFailed = $streamFailed || ($event['type'] ?? null) === 'error'; + $streamExitCode = $event['error']['exit_code'] ?? $streamExitCode; + $writeStreamEvent($event); + }); + + if ($streamFailed) { + exit($streamExitCode); + } + break; + } +} catch (Throwable $exception) { + $writeError($exception); + exit($exception instanceof BreakException ? $exception->getCode() : 1); } diff --git a/src/Cli.php b/src/Cli.php deleted file mode 100644 index 0454722..0000000 --- a/src/Cli.php +++ /dev/null @@ -1,19 +0,0 @@ -> */ + private array $queries = []; + + private bool $isLogging = false; + + public function start(): void + { + $this->queries = []; + $this->isLogging = true; + + if ($this->listenerRegistered || ! class_exists(DB::class)) { + return; + } + + DB::listen(function ($query): void { + if (! $this->isLogging) { + return; + } + + $this->queries[] = [ + 'sql' => $query->sql, + 'bindings' => $query->bindings, + 'time' => $query->time, + 'connection' => $query->connectionName, + ]; + }); + + $this->listenerRegistered = true; + } + + public function stop(): array + { + $this->isLogging = false; + + return $this->queries; + } +} diff --git a/src/Database/QueryCollector.php b/src/Database/QueryCollector.php new file mode 100644 index 0000000..23fb841 --- /dev/null +++ b/src/Database/QueryCollector.php @@ -0,0 +1,69 @@ +start(); + } catch (\Throwable $exception) { + self::recordError($exception); + } + } + } + + public static function stop(): array + { + foreach (self::$providers as $provider) { + try { + self::$queries = array_merge(self::$queries, $provider->stop()); + } catch (\Throwable $exception) { + self::recordError($exception); + } + } + + return self::$queries; + } + + public static function errors(): array + { + return self::$errors; + } + + private static function recordError(\Throwable $exception): void + { + $error = [ + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + ]; + + if (! in_array($error, self::$errors, true)) { + self::$errors[] = $error; + } + } +} diff --git a/src/Database/QueryProviderInterface.php b/src/Database/QueryProviderInterface.php new file mode 100644 index 0000000..88ac573 --- /dev/null +++ b/src/Database/QueryProviderInterface.php @@ -0,0 +1,11 @@ +> */ + public function stop(): array; +} diff --git a/src/Database/SymfonyDoctrineQueryProvider.php b/src/Database/SymfonyDoctrineQueryProvider.php new file mode 100644 index 0000000..a511794 --- /dev/null +++ b/src/Database/SymfonyDoctrineQueryProvider.php @@ -0,0 +1,117 @@ +container = $container; + } + + public function start(): void + { + $this->initialCounts = []; + + if ($this->container->has('doctrine.debug_data_holder')) { + $holder = $this->container->get('doctrine.debug_data_holder'); + foreach ($holder->getData() as $name => $queries) { + $this->initialCounts[$name] = count($queries); + } + + return; + } + + if (! $this->container->has('doctrine')) { + return; + } + + foreach ($this->container->get('doctrine')->getConnections() as $name => $connection) { + $queries = $this->getQueries($connection, $name); + if ($queries !== null) { + $this->initialCounts[$name] = count($queries); + } + } + } + + public function stop(): array + { + $queries = []; + + if ($this->container->has('doctrine.debug_data_holder')) { + $data = $this->container->get('doctrine.debug_data_holder')->getData(); + foreach ($data as $name => $connectionQueries) { + $queries = array_merge($queries, $this->normalize( + array_slice($connectionQueries, $this->initialCounts[$name] ?? 0), + $name + )); + } + + return $queries; + } + + if (! $this->container->has('doctrine')) { + return $queries; + } + + foreach ($this->container->get('doctrine')->getConnections() as $name => $connection) { + $connectionQueries = $this->getQueries($connection, $name); + if ($connectionQueries !== null) { + $queries = array_merge($queries, $this->normalize( + array_slice($connectionQueries, $this->initialCounts[$name] ?? 0), + $name + )); + } + } + + return $queries; + } + + private function getQueries($connection, string $connectionName): ?array + { + $configuration = $connection->getConfiguration(); + + if (method_exists($configuration, 'getSQLLogger')) { + $logger = $configuration->getSQLLogger(); + if ($logger instanceof DebugStack) { + return $logger->queries; + } + } + + if (! method_exists($configuration, 'getMiddlewares')) { + return null; + } + + foreach ($configuration->getMiddlewares() as $middleware) { + if (! property_exists($middleware, 'debugDataHolder')) { + continue; + } + + $property = new \ReflectionProperty($middleware, 'debugDataHolder'); + $property->setAccessible(true); + $data = $property->getValue($middleware)->getData(); + + return $data[$connectionName] ?? []; + } + + return null; + } + + private function normalize(array $queries, string $connection): array + { + return array_map(static function (array $query) use ($connection): array { + return [ + 'sql' => $query['sql'] ?? '', + 'bindings' => $query['params'] ?? [], + 'time' => $query['executionMS'] ?? 0, + 'connection' => $connection, + ]; + }, $queries); + } +} diff --git a/src/Loader.php b/src/Loader.php deleted file mode 100644 index 6eab317..0000000 --- a/src/Loader.php +++ /dev/null @@ -1,68 +0,0 @@ -tinker->execute($code); } - public function executeStreaming(string $code): void + public function executeStreaming(string $code, callable $onEvent): void { - $this->tinker->executeStreaming($code); + $this->tinker->executeStreaming($code, $onEvent); } public function casters(): array diff --git a/src/Loaders/ComposerLoader.php b/src/Loaders/ComposerLoader.php index 5be2fa4..70031e8 100644 --- a/src/Loaders/ComposerLoader.php +++ b/src/Loaders/ComposerLoader.php @@ -11,7 +11,28 @@ public static function supports(string $path): bool public function __construct(string $path) { - require $path.'/vendor/autoload.php'; + $autoloadPath = $path.'/vendor/autoload.php'; + + if (! $this->isAutoloaderLoaded($autoloadPath)) { + require_once $autoloadPath; + } + } + + private function isAutoloaderLoaded(string $autoloadPath): bool + { + $autoloadRealPath = dirname($autoloadPath).'/composer/autoload_real.php'; + + if (! file_exists($autoloadRealPath)) { + return false; + } + + $contents = file_get_contents($autoloadRealPath); + + if ($contents === false || preg_match('/class ([A-Za-z0-9_]+)\s*\{/', $contents, $matches) !== 1) { + return false; + } + + return class_exists($matches[1], false); } public function name(): string diff --git a/src/Loaders/LaravelLoader.php b/src/Loaders/LaravelLoader.php index 793ce3c..87274ad 100644 --- a/src/Loaders/LaravelLoader.php +++ b/src/Loaders/LaravelLoader.php @@ -4,6 +4,8 @@ use Illuminate\Support\Env; use Laravel\Tinker\ClassAliasAutoloader; +use TweakPHP\Client\Database\LaravelQueryProvider; +use TweakPHP\Client\Database\QueryCollector; class LaravelLoader extends ComposerLoader { @@ -19,6 +21,8 @@ public function __construct(string $path) parent::__construct($path); $this->app = require_once $path.'/bootstrap/app.php'; $this->app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + + QueryCollector::register(new LaravelQueryProvider); } public function init(): void diff --git a/src/Loaders/LoaderInterface.php b/src/Loaders/LoaderInterface.php index e08b040..5f180db 100644 --- a/src/Loaders/LoaderInterface.php +++ b/src/Loaders/LoaderInterface.php @@ -14,7 +14,7 @@ public function init(): void; public function execute(string $code): array; - public function executeStreaming(string $code): void; + public function executeStreaming(string $code, callable $onEvent): void; public function casters(): array; } diff --git a/src/Loaders/PimcoreLoader.php b/src/Loaders/PimcoreLoader.php deleted file mode 100644 index a19563b..0000000 --- a/src/Loaders/PimcoreLoader.php +++ /dev/null @@ -1,35 +0,0 @@ -path = $path; + } + + public function execute(string $code): array + { + return $this->executeInProjectDirectory(fn (): array => parent::execute($code)); + } + + public function executeStreaming(string $code, callable $onEvent): void + { + $this->executeInProjectDirectory(function () use ($code, $onEvent): void { + parent::executeStreaming($code, $onEvent); + }); + } + + private function executeInProjectDirectory(callable $callback) + { + $previousWorkingDirectory = getcwd(); + + if (! chdir($this->path)) { + throw new \RuntimeException("Unable to change to project directory: {$this->path}"); + } + + try { + return $callback(); + } finally { + if ($previousWorkingDirectory !== false) { + chdir($previousWorkingDirectory); + } + } } public function name(): string diff --git a/src/Loaders/SymfonyLoader.php b/src/Loaders/SymfonyLoader.php index c409474..0a72fbe 100644 --- a/src/Loaders/SymfonyLoader.php +++ b/src/Loaders/SymfonyLoader.php @@ -3,6 +3,8 @@ namespace TweakPHP\Client\Loaders; use Symfony\Component\HttpKernel\Kernel; +use TweakPHP\Client\Database\QueryCollector; +use TweakPHP\Client\Database\SymfonyDoctrineQueryProvider; class SymfonyLoader extends ComposerLoader { @@ -19,23 +21,40 @@ public function __construct(string $path) { parent::__construct($path); - // Include the Composer autoloader require_once $path.'/vendor/autoload.php'; - // Initialize the Symfony Kernel $env = $_SERVER['APP_ENV'] ?? 'dev'; $debug = ($_SERVER['APP_DEBUG'] ?? '1') === '1'; $kernelClass = $this->findKernelClass($path); $this->kernel = new $kernelClass($env, $debug); $this->kernel->boot(); + + QueryCollector::register( + new SymfonyDoctrineQueryProvider($this->kernel->getContainer()) + ); } private function findKernelClass(string $path): string { - require_once $path.'/src/Kernel.php'; + $kernelFile = $path.'/src/Kernel.php'; + require_once $kernelFile; + + foreach (get_declared_classes() as $class) { + try { + $reflection = new \ReflectionClass($class); + } catch (\ReflectionException $exception) { + continue; + } + + if ($reflection->getFileName() === realpath($kernelFile) && + is_a($class, Kernel::class, true) && + $class !== Kernel::class) { + return $class; + } + } - return 'App\\Kernel'; + throw new \RuntimeException('Unable to find a Symfony kernel class in src/Kernel.php.'); } public function name(): string diff --git a/src/Loaders/WordPressLoader.php b/src/Loaders/WordPressLoader.php index 87f9883..7602c1e 100644 --- a/src/Loaders/WordPressLoader.php +++ b/src/Loaders/WordPressLoader.php @@ -30,7 +30,6 @@ public function version(): string return get_bloginfo('version'); } } catch (Throwable $e) { - // } return ''; diff --git a/src/Output/StreamingOutput.php b/src/Output/StreamingOutput.php new file mode 100644 index 0000000..75eec4f --- /dev/null +++ b/src/Output/StreamingOutput.php @@ -0,0 +1,34 @@ +onWrite = $onWrite; + + parent::__construct(self::VERBOSITY_NORMAL, false); + } + + protected function doWrite(string $message, bool $newline): void + { + if ($message === '' && $newline) { + return; + } + + // PsySH uses this marker for the newline it adds after non-newline output. + if ($newline && preg_match('/^(?:\x{23ce}|\\\\n)<\/whisper>$/u', $message) === 1) { + return; + } + + ($this->onWrite)($message.($newline ? PHP_EOL : '')); + } +} diff --git a/src/OutputModifiers/CustomOutputModifier.php b/src/OutputModifiers/CustomOutputModifier.php index 9fa2a54..296a4e4 100644 --- a/src/OutputModifiers/CustomOutputModifier.php +++ b/src/OutputModifiers/CustomOutputModifier.php @@ -6,14 +6,6 @@ class CustomOutputModifier implements OutputModifier { public function modify(string $output = ''): string { - $endMarker = 'TWEAKPHP_END'; - $position = strpos($output, $endMarker); - - if ($position !== false) { - $output = substr($output, 0, $position); - } - - // remove only the first tab from each line return preg_replace('/^ {2}/m', '', $output); } } diff --git a/src/OutputModifiers/OutputModifier.php b/src/OutputModifiers/OutputModifier.php deleted file mode 100644 index d2beadb..0000000 --- a/src/OutputModifiers/OutputModifier.php +++ /dev/null @@ -1,8 +0,0 @@ -presenter)) { - $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes()); - } - - return $this->presenter; - } -} diff --git a/src/Psy/Presenter.php b/src/Psy/Presenter.php deleted file mode 100644 index 49a0611..0000000 --- a/src/Psy/Presenter.php +++ /dev/null @@ -1,54 +0,0 @@ -cloner = new Cloner; - } - - public function addCasters(array $casters) - { - parent::addCasters($casters); - - $this->cloner->addCasters($casters); - } - - public function present($value, ?int $depth = null, int $options = 0): string - { - $dumper = new HtmlDumper; - $dumper->setDumpHeader(''); - $data = $this->cloner->cloneVar($value, ! ($options & self::VERBOSE) ? Caster::EXCLUDE_VERBOSE : 0); - if ($depth !== null) { - $data = $data->withMaxDepth($depth); - } - - $output = ''; - $dumper->dump($data, function ($line, $depth) use (&$output) { - if ($depth >= 0) { - if ($output !== '') { - $output .= \PHP_EOL; - } - $output .= \str_repeat(' ', $depth).$line; - } - }); - - if (isset(Tinker::$statements[Tinker::$current])) { - Tinker::$statements[Tinker::$current]['html'] = $output; - } - - return parent::present($value, $depth, $options); - } -} diff --git a/src/Tinker.php b/src/Tinker.php index ccc4d07..2fb14ad 100644 --- a/src/Tinker.php +++ b/src/Tinker.php @@ -5,14 +5,17 @@ use PhpParser\ParserFactory; use PhpParser\PrettyPrinter\Standard; use Psy\Configuration; -use Psy\ExecutionLoopClosure; +use Psy\Exception\BreakException; use Psy\Shell; use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Console\Output\OutputInterface; +use TweakPHP\Client\Database\QueryCollector; +use TweakPHP\Client\Output\StreamingOutput; use TweakPHP\Client\OutputModifiers\OutputModifier; class Tinker { - protected BufferedOutput $output; + protected OutputInterface $output; protected Shell $shell; @@ -33,9 +36,9 @@ public function __construct(OutputModifier $outputModifier, Configuration $confi public function execute(string $rawPHPCode): array { - if (strpos($rawPHPCode, 'normalizePHPCode($rawPHPCode); $parser = (new ParserFactory)->createForHostVersion(); $prettyPrinter = new Standard; @@ -46,29 +49,174 @@ public function execute(string $rawPHPCode): array 'line' => $stmt->getStartLine(), 'code' => $code, ]; - $output = $this->doExecute($code); + + QueryCollector::start(); + try { + $output = $this->doExecute($code); + } finally { + $queries = QueryCollector::stop(); + } + self::$statements[$key]['output'] = $output; + self::$statements[$key]['queries'] = $queries; + + $queryErrors = QueryCollector::errors(); + if ($queryErrors !== []) { + self::$statements[$key]['query_errors'] = $queryErrors; + } + } + + $allQueries = []; + $allQueryErrors = []; + foreach (self::$statements as $stmt) { + if (isset($stmt['queries'])) { + $allQueries = array_merge($allQueries, $stmt['queries']); + } + if (isset($stmt['query_errors'])) { + $allQueryErrors = array_merge($allQueryErrors, $stmt['query_errors']); + } } - return [ + $result = [ 'output' => self::$statements, + 'queries' => $allQueries, ]; + + if ($allQueryErrors !== []) { + $result['query_errors'] = $allQueryErrors; + } + + return $result; + } + + /** + * @param callable(array): void $onEvent + */ + public function executeStreaming(string $rawPHPCode, callable $onEvent): void + { + self::$statements = []; + + $rawPHPCode = $this->normalizePHPCode($rawPHPCode); + + $parser = (new ParserFactory)->createForHostVersion(); + $prettyPrinter = new Standard; + + foreach ($parser->parse($rawPHPCode) as $key => $stmt) { + $code = $prettyPrinter->prettyPrint([$stmt]); + self::$current = $key; + self::$statements[] = [ + 'line' => $stmt->getStartLine(), + 'code' => $code, + ]; + + $onEvent([ + 'type' => 'statement.started', + 'index' => $key, + 'line' => $stmt->getStartLine(), + 'code' => $code, + ]); + + if (! $this->executeStreamingStatement($code, $key, $onEvent)) { + return; + } + } + + $onEvent(['type' => 'completed']); + } + + protected function executeStreamingStatement(string $code, int $key, callable $onEvent): bool + { + try { + QueryCollector::start(); + $this->doExecuteStreaming($code, $key, $onEvent); + } catch (BreakException $exception) { + $event = [ + 'type' => 'error', + 'index' => $key, + 'error' => [ + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + 'exit_code' => $exception->getCode(), + ], + ]; + $queryErrors = QueryCollector::errors(); + if ($queryErrors !== []) { + $event['query_errors'] = $queryErrors; + } + $onEvent($event); + + return false; + } catch (\Throwable $exception) { + $event = [ + 'type' => 'error', + 'index' => $key, + 'error' => [ + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + ], + ]; + $queryErrors = QueryCollector::errors(); + if ($queryErrors !== []) { + $event['query_errors'] = $queryErrors; + } + $onEvent($event); + + return false; + } finally { + $queries = QueryCollector::stop(); + } + + self::$statements[$key]['queries'] = $queries; + $queryErrors = QueryCollector::errors(); + if ($queryErrors !== []) { + self::$statements[$key]['query_errors'] = $queryErrors; + } + + $event = [ + 'type' => 'statement.completed', + 'index' => $key, + 'queries' => $queries, + ]; + if ($queryErrors !== []) { + $event['query_errors'] = $queryErrors; + } + + $onEvent($event); + + return true; } protected function doExecute(string $code): string { - $this->shell->addInput($code); - $this->shell->addInput("\necho('TWEAKPHP_END'); exit();"); $this->output = new BufferedOutput; $this->shell->setOutput($this->output); - $closure = new ExecutionLoopClosure($this->shell); - $closure->execute(); + $this->shell->execute($code, true); $result = $this->outputModifier->modify($this->cleanOutput($this->output->fetch())); return trim($result); } - protected function createShell(BufferedOutput $output, Configuration $config): Shell + /** + * @param callable(array): void $onEvent + */ + protected function doExecuteStreaming(string $code, int $index, callable $onEvent): void + { + $this->output = new StreamingOutput(function (string $chunk) use ($index, $onEvent): void { + if ($chunk === '') { + return; + } + + $onEvent([ + 'type' => 'output', + 'index' => $index, + 'data' => $chunk, + ]); + }); + $this->shell->setOutput($this->output); + $this->shell->execute($code, true); + } + + protected function createShell(OutputInterface $output, Configuration $config): Shell { $shell = new Shell($config); @@ -86,6 +234,15 @@ protected function cleanOutput(string $output): string return trim($output); } + protected function normalizePHPCode(string $rawPHPCode): string + { + if (preg_match('/^\s*<\?php(?:\s|$)/', $rawPHPCode) !== 1) { + return "shell; diff --git a/tests/CliTest.php b/tests/CliTest.php new file mode 100644 index 0000000..9a5b912 --- /dev/null +++ b/tests/CliTest.php @@ -0,0 +1,40 @@ +assertEquals('LaravelLoader', Cli::getArgument('loader')); + $this->assertEquals('/some/path', Cli::getArgument('path')); + $this->assertEquals('', Cli::getArgument('nonexistent')); + + $_SERVER['argv'] = $oldArgv; + } + + public function test_execute_reports_runtime_errors(): void + { + $code = base64_encode('throw new RuntimeException("boom");'); + $command = sprintf( + '%s %s %s %s %s', + escapeshellarg(PHP_BINARY), + escapeshellarg(__DIR__.'/../index.php'), + escapeshellarg(__DIR__.'/..'), + 'execute', + escapeshellarg($code) + ); + + exec($command, $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringStartsWith('TWEAKPHP_ERROR:', implode(PHP_EOL, $output)); + } +} diff --git a/tests/LoaderTest.php b/tests/LoaderTest.php new file mode 100644 index 0000000..34c6f97 --- /dev/null +++ b/tests/LoaderTest.php @@ -0,0 +1,264 @@ +tempDir = sys_get_temp_dir().'/tweakphp_test_'.uniqid(); + mkdir($this->tempDir, 0777, true); + } + + protected function tearDown(): void + { + $this->removeDirectory($this->tempDir); + } + + private function removeDirectory(string $dir): void + { + if (! is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? $this->removeDirectory("$dir/$file") : unlink("$dir/$file"); + } + rmdir($dir); + } + + public function test_laravel_loader_detection_and_boot() + { + mkdir($this->tempDir.'/vendor', 0777, true); + mkdir($this->tempDir.'/bootstrap', 0777, true); + file_put_contents($this->tempDir.'/vendor/autoload.php', 'tempDir).'"; + } + };'; + file_put_contents($this->tempDir.'/bootstrap/app.php', $appMock); + + $this->assertTrue(LaravelLoader::supports($this->tempDir)); + + $loader = Loader::load($this->tempDir); + $this->assertInstanceOf(LaravelLoader::class, $loader); + $this->assertEquals('Laravel', $loader->name()); + $this->assertEquals('10.11.12', $loader->version()); + + $casters = $loader->casters(); + $this->assertArrayHasKey('Illuminate\Support\Collection', $casters); + } + + public function test_symfony_loader_detection_and_boot() + { + mkdir($this->tempDir.'/vendor', 0777, true); + mkdir($this->tempDir.'/src', 0777, true); + file_put_contents($this->tempDir.'/vendor/autoload.php', 'tempDir.'/symfony.lock', '{}'); + + $kernelMock = 'tempDir.'/src/Kernel.php', $kernelMock); + + $this->assertTrue(SymfonyLoader::supports($this->tempDir)); + + $loader = Loader::load($this->tempDir); + $this->assertInstanceOf(SymfonyLoader::class, $loader); + $this->assertEquals('Symfony', $loader->name()); + $this->assertEquals('6.4.0', $loader->version()); + } + + public function test_symfony_loader_uses_a_nonstandard_kernel_class(): void + { + mkdir($this->tempDir.'/vendor', 0777, true); + mkdir($this->tempDir.'/src', 0777, true); + file_put_contents($this->tempDir.'/vendor/autoload.php', 'tempDir.'/symfony.lock', '{}'); + + $kernelMock = 'tempDir.'/src/Kernel.php', $kernelMock); + + $loader = Loader::load($this->tempDir); + + $this->assertInstanceOf(SymfonyLoader::class, $loader); + } + + public function test_word_press_loader_detection_and_boot() + { + mkdir($this->tempDir.'/wp-admin/includes', 0777, true); + mkdir($this->tempDir.'/wp-includes', 0777, true); + file_put_contents($this->tempDir.'/wp-load.php', 'tempDir.'/wp-admin/includes/admin.php', 'tempDir.'/wp-includes/pluggable.php', 'assertTrue(WordPressLoader::supports($this->tempDir)); + + $loader = Loader::load($this->tempDir); + $this->assertInstanceOf(WordPressLoader::class, $loader); + $this->assertEquals('WordPress', $loader->name()); + $this->assertEquals('6.2.2', $loader->version()); + } + + public function test_pimcore_loader_detection_and_boot() + { + mkdir($this->tempDir.'/vendor/pimcore/pimcore', 0777, true); + file_put_contents($this->tempDir.'/vendor/autoload.php', 'assertTrue(PimcoreLoader::supports($this->tempDir)); + + $loader = Loader::load($this->tempDir); + $this->assertInstanceOf(PimcoreLoader::class, $loader); + $this->assertEquals('Pimcore', $loader->name()); + + $this->expectException(\OutOfBoundsException::class); + $loader->version(); + } + + public function test_composer_loader_detection() + { + mkdir($this->tempDir.'/vendor', 0777, true); + file_put_contents($this->tempDir.'/vendor/autoload.php', 'assertTrue(ComposerLoader::supports($this->tempDir)); + + $loader = Loader::load($this->tempDir); + $this->assertInstanceOf(ComposerLoader::class, $loader); + $this->assertEquals('Composer Project', $loader->name()); + $this->assertEquals('', $loader->version()); + } + + public function test_composer_loader_does_not_reload_the_client_autoloader() + { + mkdir($this->tempDir.'/vendor', 0777, true); + mkdir($this->tempDir.'/vendor/composer', 0777, true); + file_put_contents($this->tempDir.'/composer.json', json_encode([ + 'name' => 'tweakphp/client', + ])); + $className = 'ComposerAutoloaderInit'.str_replace('.', '', uniqid('', true)); + file_put_contents($this->tempDir.'/vendor/composer/autoload_real.php', 'tempDir.'/vendor/autoload.php', 'tempDir); + + $this->assertEquals('Composer Project', $loader->name()); + } + + public function test_plain_php_loader_detection() + { + $this->assertTrue(PlainPhpLoader::supports($this->tempDir)); + + $loader = Loader::load($this->tempDir); + $this->assertInstanceOf(PlainPhpLoader::class, $loader); + $this->assertEquals('PHP', $loader->name()); + $this->assertEquals('', $loader->version()); + } + + public function test_plain_php_loader_restores_working_directory(): void + { + $workingDirectory = getcwd(); + $loader = new PlainPhpLoader($this->tempDir); + + $this->assertSame($workingDirectory, getcwd()); + + $loader->init(); + $loader->execute('return getcwd();'); + + $this->assertSame($workingDirectory, getcwd()); + } + + public function test_custom_encoded_loader() + { + $customLoaderCode = 'tempDir, $encoded); + $this->assertInstanceOf(\CustomEncodedLoaderTestClass::class, $loader); + $this->assertEquals('CustomEncoded', $loader->name()); + } + } +} diff --git a/tests/OutputModifierTest.php b/tests/OutputModifierTest.php new file mode 100644 index 0000000..beeb4b5 --- /dev/null +++ b/tests/OutputModifierTest.php @@ -0,0 +1,26 @@ +assertEquals($expected, $modifier->modify($input)); + } + + public function test_custom_output_modifier_preserves_marker_text(): void + { + $modifier = new CustomOutputModifier; + + $this->assertSame('before TWEAKPHP_END after', $modifier->modify('before TWEAKPHP_END after')); + } +} diff --git a/tests/QueryCollectorTest.php b/tests/QueryCollectorTest.php new file mode 100644 index 0000000..ffba9fa --- /dev/null +++ b/tests/QueryCollectorTest.php @@ -0,0 +1,258 @@ +assertNotNull(DB::$listener); + + $queryObj = new \stdClass; + $queryObj->sql = 'SELECT * FROM users WHERE id = ?'; + $queryObj->bindings = [1]; + $queryObj->time = 12.34; + $queryObj->connectionName = 'mysql'; + + DB::triggerQuery($queryObj); + + $queries = QueryCollector::stop(); + + $this->assertCount(1, $queries); + $this->assertEquals('SELECT * FROM users WHERE id = ?', $queries[0]['sql']); + $this->assertEquals([1], $queries[0]['bindings']); + $this->assertEquals(12.34, $queries[0]['time']); + $this->assertEquals('mysql', $queries[0]['connection']); + } + + public function test_query_collector_captures_queries_from_secondary_connections(): void + { + QueryCollector::register(new LaravelQueryProvider); + QueryCollector::start(); + + $queryObj = new \stdClass; + $queryObj->sql = 'SELECT * FROM reports'; + $queryObj->bindings = []; + $queryObj->time = 3.1; + $queryObj->connectionName = 'reporting'; + + DB::triggerQuery($queryObj); + + $queries = QueryCollector::stop(); + + $this->assertCount(1, $queries); + $this->assertSame('reporting', $queries[0]['connection']); + } + + public function test_query_collector_with_symfony_doctrine3() + { + $containerMock = $this->createMock(ContainerInterface::class); + $debugDataHolderMock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getData']) + ->getMock(); + + $debugDataHolderMock->method('getData')->willReturnOnConsecutiveCalls( + [], + [ + 'default' => [ + [ + 'sql' => 'SELECT * FROM posts', + 'params' => [], + 'executionMS' => 5.2, + ], + ], + ] + ); + + $containerMock->method('has') + ->willReturnMap([ + ['doctrine.debug_data_holder', true], + ['doctrine', false], + ]); + + $containerMock->method('get') + ->willReturnMap([ + ['doctrine.debug_data_holder', $debugDataHolderMock], + ]); + + QueryCollector::register(new SymfonyDoctrineQueryProvider($containerMock)); + QueryCollector::start(); + $queries = QueryCollector::stop(); + + $this->assertCount(1, $queries); + $this->assertEquals('SELECT * FROM posts', $queries[0]['sql']); + $this->assertEquals([], $queries[0]['bindings']); + $this->assertEquals(5.2, $queries[0]['time']); + $this->assertEquals('default', $queries[0]['connection']); + + } + + public function test_query_collector_with_symfony_doctrine2() + { + $containerMock = $this->createMock(ContainerInterface::class); + $doctrineMock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getConnections']) + ->getMock(); + + $connMock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getConfiguration']) + ->getMock(); + + $configMock = $this->getMockBuilder(\stdClass::class) + ->addMethods(['getSQLLogger']) + ->getMock(); + + $loggerMock = new DebugStack; + $loggerMock->queries = []; + + $doctrineMock->method('getConnections')->willReturn([ + 'default' => $connMock, + ]); + + $connMock->method('getConfiguration')->willReturn($configMock); + $configMock->method('getSQLLogger')->willReturn($loggerMock); + + $containerMock->method('has') + ->willReturnMap([ + ['doctrine.debug_data_holder', false], + ['doctrine', true], + ]); + + $containerMock->method('get') + ->willReturnMap([ + ['doctrine', $doctrineMock], + ]); + + QueryCollector::register(new SymfonyDoctrineQueryProvider($containerMock)); + QueryCollector::start(); + + $loggerMock->queries[] = [ + 'sql' => 'SELECT * FROM comments', + 'params' => [1], + 'executionMS' => 1.5, + ]; + + $queries = QueryCollector::stop(); + + $this->assertCount(1, $queries); + $this->assertEquals('SELECT * FROM comments', $queries[0]['sql']); + $this->assertEquals([1], $queries[0]['bindings']); + $this->assertEquals(1.5, $queries[0]['time']); + $this->assertEquals('default', $queries[0]['connection']); + + } + + public function test_query_collector_exposes_instrumentation_errors(): void + { + $containerMock = $this->createMock(ContainerInterface::class); + $containerMock->method('has')->willThrowException(new \RuntimeException('container failed')); + + QueryCollector::register(new SymfonyDoctrineQueryProvider($containerMock)); + QueryCollector::start(); + QueryCollector::stop(); + + $this->assertSame([ + [ + 'class' => 'RuntimeException', + 'message' => 'container failed', + ], + ], QueryCollector::errors()); + + } + + public function test_query_collector_orchestrates_multiple_providers(): void + { + $first = $this->createMock(QueryProviderInterface::class); + $second = $this->createMock(QueryProviderInterface::class); + + $first->expects($this->once())->method('start'); + $first->expects($this->once())->method('stop')->willReturn([ + ['sql' => 'SELECT 1'], + ]); + $second->expects($this->once())->method('start'); + $second->expects($this->once())->method('stop')->willReturn([ + ['sql' => 'SELECT 2'], + ]); + + QueryCollector::register($first); + QueryCollector::register($second); + + QueryCollector::start(); + + $this->assertSame([ + ['sql' => 'SELECT 1'], + ['sql' => 'SELECT 2'], + ], QueryCollector::stop()); + } + } +} diff --git a/tests/TinkerTest.php b/tests/TinkerTest.php new file mode 100644 index 0000000..27b5165 --- /dev/null +++ b/tests/TinkerTest.php @@ -0,0 +1,267 @@ + null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + $tinker = new Tinker(new CustomOutputModifier, $config); + + $result = $tinker->execute("echo 'Hello World';"); + + $this->assertArrayHasKey('output', $result); + $this->assertArrayHasKey('queries', $result); + $this->assertNotEmpty($result['output']); + $this->assertEquals('Hello World', $result['output'][0]['output']); + } + + public function test_execute_resets_statements_between_calls() + { + $config = new Configuration([ + 'configFile' => null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + $tinker = new Tinker(new CustomOutputModifier, $config); + + $tinker->execute("echo 'first';"); + $result = $tinker->execute("echo 'second';"); + + $this->assertCount(1, $result['output']); + $this->assertEquals('second', $result['output'][0]['output']); + } + + public function test_execute_propagates_runtime_exceptions(): void + { + $tinker = $this->createTinker(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('boom'); + + $tinker->execute('throw new RuntimeException("boom");'); + } + + public function test_execute_preserves_marker_text_and_php_tag_strings(): void + { + $tinker = $this->createTinker(); + + $result = $tinker->execute('echo "before TWEAKPHP_END after"; echo " assertSame('before TWEAKPHP_END after', $result['output'][0]['output']); + $this->assertSame(' null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + $tinker = new class(new CustomOutputModifier, $config) extends Tinker + { + protected function doExecute(string $code): string + { + throw new \RuntimeException('Execution failed'); + } + }; + + try { + $tinker->execute("echo 'failure';"); + $this->fail('Expected execution to throw.'); + } catch (\RuntimeException $exception) { + $this->assertSame('Execution failed', $exception->getMessage()); + } + + $this->assertSame([], QueryCollector::errors()); + } + + public function test_execute_streaming_emits_output_for_each_statement() + { + $config = new Configuration([ + 'configFile' => null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + $tinker = new Tinker(new CustomOutputModifier, $config); + $events = []; + + $tinker->executeStreaming("echo 'first'; echo 'second';", function (array $event) use (&$events): void { + $events[] = $event; + }); + + $output = array_column( + array_filter($events, fn (array $event): bool => $event['type'] === 'output'), + 'data' + ); + $output = implode('', $output); + + $this->assertStringContainsString('first', $output); + $this->assertStringContainsString('second', $output); + $this->assertLessThan(strpos($output, 'second'), strpos($output, 'first')); + $this->assertSame( + 'completed', + end($events)['type'] + ); + $this->assertStringNotContainsString('', $output); + } + + public function test_execute_streaming_emits_error_when_a_statement_fails() + { + $config = new Configuration([ + 'configFile' => null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + $tinker = new class(new CustomOutputModifier, $config) extends Tinker + { + protected function doExecuteStreaming(string $code, int $index, callable $onEvent): void + { + throw new \RuntimeException('Execution failed'); + } + }; + $events = []; + + $tinker->executeStreaming("echo 'failure';", function (array $event) use (&$events): void { + $events[] = $event; + }); + + $this->assertSame('error', end($events)['type']); + $this->assertSame('Execution failed', end($events)['error']['message']); + } + + public function test_execute_streaming_preserves_user_output_whitespace(): void + { + $tinker = $this->createTinker(); + $events = []; + + $tinker->executeStreaming("echo ' first\\nsecond ';", function (array $event) use (&$events): void { + $events[] = $event; + }); + + $output = implode('', array_column( + array_filter($events, fn (array $event): bool => $event['type'] === 'output'), + 'data' + )); + + $this->assertSame(' first\\nsecond ', $output); + } + + public function test_execute_streaming_emits_exit_code_for_exit(): void + { + $tinker = $this->createTinker(); + $events = []; + + $tinker->executeStreaming('exit(7);', function (array $event) use (&$events): void { + $events[] = $event; + }); + + $error = end($events); + $this->assertSame('error', $error['type']); + $this->assertSame(7, $error['error']['exit_code']); + } + + public function test_execute_streaming_propagates_parse_errors(): void + { + $this->expectException(Error::class); + + $this->createTinker()->executeStreaming('echo ;', static function (array $event): void {}); + } + + private function createTinker(): Tinker + { + $config = new Configuration([ + 'configFile' => null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + return new Tinker(new CustomOutputModifier, $config); + } +} From f537d3e4c17cc1c6b7474aa02b6bc9924eb39574 Mon Sep 17 00:00:00 2001 From: David Galet Date: Wed, 15 Jul 2026 20:10:43 +0200 Subject: [PATCH 2/5] feat: add loaders and output modifiers for TweakPHP client --- src/Cli.php | 19 +++++++ src/Loader.php | 68 ++++++++++++++++++++++++++ src/Loaders/PimcoreLoader.php | 35 +++++++++++++ src/OutputModifiers/OutputModifier.php | 8 +++ src/Psy/Configuration.php | 17 +++++++ src/Psy/Presenter.php | 54 ++++++++++++++++++++ 6 files changed, 201 insertions(+) create mode 100644 src/Cli.php create mode 100644 src/Loader.php create mode 100644 src/Loaders/PimcoreLoader.php create mode 100644 src/OutputModifiers/OutputModifier.php create mode 100644 src/Psy/Configuration.php create mode 100644 src/Psy/Presenter.php diff --git a/src/Cli.php b/src/Cli.php new file mode 100644 index 0000000..0454722 --- /dev/null +++ b/src/Cli.php @@ -0,0 +1,19 @@ +presenter)) { + $this->presenter = new Presenter($this->getOutput()->getFormatter(), $this->forceArrayIndexes()); + } + + return $this->presenter; + } +} diff --git a/src/Psy/Presenter.php b/src/Psy/Presenter.php new file mode 100644 index 0000000..49a0611 --- /dev/null +++ b/src/Psy/Presenter.php @@ -0,0 +1,54 @@ +cloner = new Cloner; + } + + public function addCasters(array $casters) + { + parent::addCasters($casters); + + $this->cloner->addCasters($casters); + } + + public function present($value, ?int $depth = null, int $options = 0): string + { + $dumper = new HtmlDumper; + $dumper->setDumpHeader(''); + $data = $this->cloner->cloneVar($value, ! ($options & self::VERBOSE) ? Caster::EXCLUDE_VERBOSE : 0); + if ($depth !== null) { + $data = $data->withMaxDepth($depth); + } + + $output = ''; + $dumper->dump($data, function ($line, $depth) use (&$output) { + if ($depth >= 0) { + if ($output !== '') { + $output .= \PHP_EOL; + } + $output .= \str_repeat(' ', $depth).$line; + } + }); + + if (isset(Tinker::$statements[Tinker::$current])) { + Tinker::$statements[Tinker::$current]['html'] = $output; + } + + return parent::present($value, $depth, $options); + } +} From 49dfda6230b4b2783fe9040f4b399a3b8bf4af0b Mon Sep 17 00:00:00 2001 From: David Galet Date: Wed, 15 Jul 2026 20:32:36 +0200 Subject: [PATCH 3/5] fix: restore MIT license file to the project --- LICENSE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9021060 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 TweakPHP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From dedbe6971b0d57c429b33b55b7fe714eaf264047 Mon Sep 17 00:00:00 2001 From: David Galet Date: Wed, 15 Jul 2026 20:35:06 +0200 Subject: [PATCH 4/5] fix: enhance error handling in executeStreaming method and include query collection errors --- src/Tinker.php | 29 +++++++++++---------- tests/TinkerTest.php | 60 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/src/Tinker.php b/src/Tinker.php index 2fb14ad..879818f 100644 --- a/src/Tinker.php +++ b/src/Tinker.php @@ -126,11 +126,13 @@ public function executeStreaming(string $rawPHPCode, callable $onEvent): void protected function executeStreamingStatement(string $code, int $key, callable $onEvent): bool { + $error = null; + try { QueryCollector::start(); $this->doExecuteStreaming($code, $key, $onEvent); } catch (BreakException $exception) { - $event = [ + $error = [ 'type' => 'error', 'index' => $key, 'error' => [ @@ -139,15 +141,8 @@ protected function executeStreamingStatement(string $code, int $key, callable $o 'exit_code' => $exception->getCode(), ], ]; - $queryErrors = QueryCollector::errors(); - if ($queryErrors !== []) { - $event['query_errors'] = $queryErrors; - } - $onEvent($event); - - return false; } catch (\Throwable $exception) { - $event = [ + $error = [ 'type' => 'error', 'index' => $key, 'error' => [ @@ -155,15 +150,23 @@ protected function executeStreamingStatement(string $code, int $key, callable $o 'message' => $exception->getMessage(), ], ]; + } finally { + $queries = QueryCollector::stop(); + } + + if ($error !== null) { + self::$statements[$key]['queries'] = $queries; + + $error['queries'] = $queries; $queryErrors = QueryCollector::errors(); if ($queryErrors !== []) { - $event['query_errors'] = $queryErrors; + self::$statements[$key]['query_errors'] = $queryErrors; + $error['query_errors'] = $queryErrors; } - $onEvent($event); + + $onEvent($error); return false; - } finally { - $queries = QueryCollector::stop(); } self::$statements[$key]['queries'] = $queries; diff --git a/tests/TinkerTest.php b/tests/TinkerTest.php index 27b5165..ac63bb5 100644 --- a/tests/TinkerTest.php +++ b/tests/TinkerTest.php @@ -7,6 +7,7 @@ use Psy\Configuration as ConfigurationAlias; use Psy\VersionUpdater\Checker; use TweakPHP\Client\Database\QueryCollector; +use TweakPHP\Client\Database\QueryProviderInterface; use TweakPHP\Client\OutputModifiers\CustomOutputModifier; use TweakPHP\Client\Psy\Configuration; use TweakPHP\Client\Tinker; @@ -197,14 +198,67 @@ protected function doExecuteStreaming(string $code, int $index, callable $onEven }; $events = []; - $tinker->executeStreaming("echo 'failure';", function (array $event) use (&$events): void { - $events[] = $event; - }); + try { + $tinker->executeStreaming("echo 'failure';", function (array $event) use (&$events): void { + $events[] = $event; + }); + } finally { + QueryCollector::reset(); + } $this->assertSame('error', end($events)['type']); $this->assertSame('Execution failed', end($events)['error']['message']); } + public function test_execute_streaming_includes_query_collection_errors_in_failure_event(): void + { + $config = new Configuration([ + 'configFile' => null, + ]); + $config->setUpdateCheck(Checker::NEVER); + if (method_exists($config, 'setInteractiveMode')) { + $config->setInteractiveMode(ConfigurationAlias::INTERACTIVE_MODE_DISABLED); + } + if (method_exists($config, 'setColorMode')) { + $config->setColorMode(ConfigurationAlias::COLOR_MODE_DISABLED); + } + $config->setRawOutput(false); + $config->setTheme([ + 'prompt' => '', + ]); + $config->setHistoryFile(defined('PHP_WINDOWS_VERSION_BUILD') ? 'null' : '/dev/null'); + $config->setUsePcntl(false); + + $tinker = new class(new CustomOutputModifier, $config) extends Tinker + { + protected function doExecuteStreaming(string $code, int $index, callable $onEvent): void + { + throw new \RuntimeException('Execution failed'); + } + }; + $provider = $this->createMock(QueryProviderInterface::class); + $provider->method('stop')->willThrowException(new \RuntimeException('query collection failed')); + QueryCollector::register($provider); + $events = []; + + try { + $tinker->executeStreaming("echo 'failure';", function (array $event) use (&$events): void { + $events[] = $event; + }); + } finally { + QueryCollector::reset(); + } + + $error = end($events); + $this->assertSame('error', $error['type']); + $this->assertSame([ + [ + 'class' => 'RuntimeException', + 'message' => 'query collection failed', + ], + ], $error['query_errors']); + } + public function test_execute_streaming_preserves_user_output_whitespace(): void { $tinker = $this->createTinker(); From 58e65a04feddd673568f1824bd99459ebc2273a2 Mon Sep 17 00:00:00 2001 From: David Galet Date: Wed, 15 Jul 2026 20:44:49 +0200 Subject: [PATCH 5/5] fix: restore configuration for box.json with compression and output settings --- box.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 box.json diff --git a/box.json b/box.json new file mode 100644 index 0000000..71fa29b --- /dev/null +++ b/box.json @@ -0,0 +1,4 @@ +{ + "compression": "GZ", + "output": "client.phar" +}