Repository: jakzal/toolbox Branch: master Commit: 2ecc0003c306 Files: 131 Total size: 235.8 KB Directory structure: gitextract_t8u0t2r9/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── build.yml │ ├── publish-website.yml │ └── update-phars.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── .scrutinizer.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bin/ │ ├── devkit.php │ └── toolbox.php ├── box-devkit.json.dist ├── box.json.dist ├── composer.json ├── deptrac.yaml ├── infection.json.dist ├── phpunit.xml.dist ├── resources/ │ ├── architecture.json │ ├── checkstyle.json │ ├── compatibility.json │ ├── composer.json │ ├── deprecation.json │ ├── documentation.json │ ├── linting.json │ ├── metrics.json │ ├── phpcs.json │ ├── phpstan.json │ ├── pre-installation.json │ ├── psalm.json │ ├── refactoring.json │ ├── security.json │ ├── test.json │ └── tools.json ├── scoper.inc.php ├── src/ │ ├── Cli/ │ │ ├── Application.php │ │ ├── Command/ │ │ │ ├── DefaultTag.php │ │ │ ├── DefaultTargetDir.php │ │ │ ├── InstallCommand.php │ │ │ ├── ListCommand.php │ │ │ └── TestCommand.php │ │ ├── Runner/ │ │ │ └── DryRunner.php │ │ ├── ServiceContainer/ │ │ │ ├── LazyRunner.php │ │ │ └── RunnerFactory.php │ │ └── ServiceContainer.php │ ├── Json/ │ │ ├── Factory/ │ │ │ ├── Assert.php │ │ │ ├── BoxBuildCommandFactory.php │ │ │ ├── ComposerBinPluginCommandFactory.php │ │ │ ├── ComposerGlobalInstallCommandFactory.php │ │ │ ├── ComposerInstallCommandFactory.php │ │ │ ├── FileDownloadCommandFactory.php │ │ │ ├── PharDownloadCommandFactory.php │ │ │ ├── PhiveInstallCommandFactory.php │ │ │ ├── ShCommandFactory.php │ │ │ └── ToolFactory.php │ │ └── JsonTools.php │ ├── Runner/ │ │ ├── ParametrisedRunner.php │ │ ├── PassthruRunner.php │ │ └── Runner.php │ ├── Tool/ │ │ ├── Collection.php │ │ ├── Command/ │ │ │ ├── BoxBuildCommand.php │ │ │ ├── ComposerBinPluginCommand.php │ │ │ ├── ComposerBinPluginLinkCommand.php │ │ │ ├── ComposerGlobalInstallCommand.php │ │ │ ├── ComposerGlobalMultiInstallCommand.php │ │ │ ├── ComposerInstallCommand.php │ │ │ ├── FileDownloadCommand.php │ │ │ ├── MultiStepCommand.php │ │ │ ├── OptimisedComposerBinPluginCommand.php │ │ │ ├── PharDownloadCommand.php │ │ │ ├── PhiveInstallCommand.php │ │ │ ├── ShCommand.php │ │ │ └── TestCommand.php │ │ ├── Command.php │ │ ├── Filter.php │ │ ├── Tool.php │ │ └── Tools.php │ └── UseCase/ │ ├── InstallTools.php │ ├── ListTools.php │ └── TestTools.php ├── tests/ │ ├── Cli/ │ │ ├── ApplicationTest.php │ │ ├── Command/ │ │ │ ├── InstallCommandTest.php │ │ │ ├── ListCommandTest.php │ │ │ ├── TestCommandTest.php │ │ │ └── ToolboxCommandTestCase.php │ │ ├── Runner/ │ │ │ └── DryRunnerTest.php │ │ ├── ServiceContainer/ │ │ │ ├── LazyRunnerTest.php │ │ │ └── RunnerFactoryTest.php │ │ └── ServiceContainerTest.php │ ├── Json/ │ │ ├── Factory/ │ │ │ ├── AssertTest.php │ │ │ ├── BoxBuildCommandFactoryTest.php │ │ │ ├── ComposerBinPluginCommandFactoryTest.php │ │ │ ├── ComposerGlobalInstallCommandFactoryTest.php │ │ │ ├── ComposerInstallCommandFactoryTest.php │ │ │ ├── FileDownloadCommandFactoryTest.php │ │ │ ├── PharDownloadCommandFactoryTest.php │ │ │ ├── PhiveInstallCommandFactoryTest.php │ │ │ ├── ShCommandFactoryTest.php │ │ │ └── ToolFactoryTest.php │ │ └── JsonToolsTest.php │ ├── Runner/ │ │ ├── ParametrisedRunnerTest.php │ │ └── PassthruRunnerTest.php │ ├── Tool/ │ │ ├── CollectionTest.php │ │ ├── Command/ │ │ │ ├── BoxBuildCommandTest.php │ │ │ ├── ComposerBinPluginCommandTest.php │ │ │ ├── ComposerBinPluginLinkCommandTest.php │ │ │ ├── ComposerGlobalInstallCommandTest.php │ │ │ ├── ComposerGlobalMultiInstallCommandTest.php │ │ │ ├── ComposerInstallCommandTest.php │ │ │ ├── FileDownloadCommandTest.php │ │ │ ├── MultiStepCommandTest.php │ │ │ ├── OptimisedComposerBinPluginCommandTest.php │ │ │ ├── PharDownloadCommandTest.php │ │ │ ├── PhiveInstallCommandTest.php │ │ │ ├── ShCommandTest.php │ │ │ └── TestCommandTest.php │ │ ├── FilterTest.php │ │ └── ToolTest.php │ ├── UseCase/ │ │ ├── InstallToolsTest.php │ │ ├── ListToolsTest.php │ │ └── TestToolsTest.php │ └── resources/ │ ├── invalid-tools.json │ ├── invalid.json │ ├── no-tools.json │ ├── pre-installation.json │ └── tools.json └── tools/ └── .gitignore ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore /.php_cs export-ignore /.travis.yml export-ignore /CODE_OF_CONDUCT.md export-ignore /CONTRIBUTING.md export-ignore /Makefile export-ignore /box.json.dist export-ignore /composer.json export-ignore /depfile.yml export-ignore /infection.json.dist export-ignore /phpunit.xml.dist export-ignore /tests export-ignore /tools export-ignore /scoper.inc.php export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ github: [jakzal] ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: [master] pull_request: release: types: [created] schedule: - cron: '0 4 * * *' env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # for phive GITHUB_AUTH_TOKEN: ${{ secrets.ACCESS_TOKEN }} jobs: tests: runs-on: ubuntu-latest name: Build and test strategy: matrix: php: [8.2, 8.3, 8.4, 8.5] deps: [high] include: - php: 8.2 deps: low steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" tools: composer ini-values: "phar.readonly=0" coverage: pcov - if: matrix.deps == 'high' run: make update test package package-devkit - if: matrix.deps == 'low' run: make update-min test-min - uses: actions/upload-artifact@v4 if: matrix.php == '8.2' && matrix.deps == 'high' with: name: toolbox.phar path: build/toolbox.phar - uses: actions/upload-artifact@v4 if: matrix.php == '8.2' && matrix.deps == 'high' with: name: devkit.phar path: build/devkit.phar integration-tests: runs-on: ubuntu-latest name: Run integration tests needs: tests strategy: matrix: php: [8.2, 8.3, 8.4, 8.5] steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" tools: composer ini-values: "phar.readonly=0" coverage: none extensions: bz2, zip - uses: actions/download-artifact@v4 with: name: toolbox.phar path: build/ - run: make test-integration env: GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-phars: runs-on: ubuntu-latest name: Publish PHARs needs: tests if: github.event_name == 'release' steps: - uses: actions/download-artifact@v4 with: name: toolbox.phar path: . - uses: actions/download-artifact@v4 with: name: devkit.phar path: . - name: Upload toolbox.phar run: gh release upload ${{ github.event.release.tag_name }} toolbox.phar --clobber --repo github.com/jakzal/toolbox env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} - name: Upload devkit.phar run: gh release upload ${{ github.event.release.tag_name }} devkit.phar --clobber --repo github.com/jakzal/toolbox env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} ================================================ FILE: .github/workflows/publish-website.yml ================================================ name: Publish the website on: push: branches: [master] release: types: [created] jobs: publish-website: runs-on: ubuntu-latest name: Build and publish steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "8.2" ini-values: "phar.readonly=0" - name: Build the website run: make package-devkit website - name: Publish the website uses: JamesIves/github-pages-deploy-action@v4 with: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BRANCH: gh-pages FOLDER: build/website ================================================ FILE: .github/workflows/update-phars.yml ================================================ name: Update PHARs on: schedule: - cron: '30 3 * * *' jobs: update-phars: runs-on: ubuntu-latest name: Create a PR steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "8.2" ini-values: "phar.readonly=0" - name: Configure git run: git config user.email 'jakub@zalas.pl' && git config user.name 'Jakub Zalas' - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y hub - name: Update PHARs run: make package-devkit update-phars - name: Send a Pull Request run: "git diff --exit-code master -- resources/ || hub pull-request -h tools-update -a jakzal -m 'Update tools' -m '' -m ':robot: This pull request was automagically sent from Github'" env: GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} ================================================ FILE: .gitignore ================================================ /composer.lock /.deptrac.cache /.phpunit.result.cache /.php-cs-fixer.php /.php-cs-fixer.cache /build/ /vendor/ ================================================ FILE: .php-cs-fixer.dist.php ================================================ in(['src', 'tests']) ; return (new PhpCsFixer\Config()) ->setRules([ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'blank_line_before_statement' => true, 'concat_space' => ['spacing' => 'none'], 'declare_strict_types' => true, 'native_function_invocation' => ['include' => ['@internal']], 'no_empty_comment' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, 'no_extra_blank_lines' => true, 'no_leading_import_slash' => true, 'no_leading_namespace_whitespace' => true, 'no_unused_imports' => true, 'no_useless_else' => true, 'ordered_class_elements' => true, 'ordered_imports' => true, 'phpdoc_add_missing_param_annotation' => ['only_untyped' => true], 'protected_to_private' => true, 'strict_comparison' => true, 'ternary_operator_spaces' => true, 'ternary_to_null_coalescing' => true, 'yoda_style' => true, ]) ->setFinder($finder) ; ================================================ FILE: .scrutinizer.yml ================================================ inherit: true build: environment: php: version: 8.2 variables: XDEBUG_MODE: coverage tests: override: - make phpunit project_setup: nodes: coverage: tests: override: - command: make phpunit coverage: file: build/coverage.xml format: clover filter: paths: [src/*] build_failure_conditions: - 'elements.rating(<= B).new.exists' - 'issues.label("coding-style").new.exists' - 'issues.severity(>= MAJOR).new.exists' checks: php: true tools: php_code_sniffer: false php_cs_fixer: { config: { level: psr2 } } external_code_coverage: false php_code_coverage: true php_changetracking: true php_sim: true php_mess_detector: true php_pdepend: true php_analyzer: true sensiolabs_security_checker: true coding_style: php: spaces: within: brackets: false before_parentheses: closure_definition: true ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at oss@zalas.pl. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing When contributing to this repository send a new pull request. If your change is big or complex, or you simply want to suggest an improvement, please discuss the change you wish to make via an issue. Please note we have a [code of conduct](CODE_OF_CONDUCT.md). Please follow it in all your interactions with the project. ## Pull Request Process * Provide good commit messages describing what you've done. * Provide tests for any code you write. * Make sure all tests are passing. * Prefer `phive`, `phar` or `composer-bin-plugin` installation over `composer global` installations to avoid dependency conflicts. * Update the `resources/*.json` files with any new tools'd like to add. * Update `README.md` with any new tools you added (`php bin/devkit.php update:readme`). ## Adding a new tool To add support for a new tool, add it to the list in one of the `json` files in the `resources/` folder: ```json { "name": "behat", "summary": "Helps to test business expectations", "website": "http://behat.org/", "command": { "composer-bin-plugin": { "package": "behat/behat", "namespace": "behat" } }, "test": "behat --version", "tags": ["testing", "test", "bdd"] } ``` Each tool should have the following properties specified: * `name` - name of the tool, most of the time the name of executable; * `summary` - shortly stated purpose of the tool; * `website` - link to the tool's website; * `command` - the command to install the tool. See supported commands below; * `test` - the command to verify if the tool is installed. Most of the time it will be the command to show the version or help; Once you added a new tool to the list, update the list in `README.md` by running the following command: ```bash php bin/devkit.php update:readme ``` ### Commands There are several supported ways to install tools. All of them are listed below in order of preference. #### phive Downloads a phar for the given alias using phive and puts it into the specified location. ```json { "command": { "phive-install": { "alias": "dephpend", "bin": "%target-dir%/dephpend", "sig": "76835C9464877BDD" } } } ``` `sig` is optional, but it is recommended if the phar is signed. #### phar-download Downloads a phar from the given URL and puts it into the specified location. ```json { "command": { "phar-download": { "phar": "https://github.com/phpspec/phpspec/releases/download/4.3.0/phpspec.phar", "bin": "/usr/local/bin/phpspec" } } } ``` #### file-download Downloads a file from the given URL and puts it into the specified location. ```json { "command": { "file-download": { "url": "https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar.asc", "file": "/usr/local/bin/psalm.phar.asc" } } } ``` #### composer-bin-plugin The `composer-bin-plugin` method uses the [bamarni/composer-bin-plugin](https://github.com/bamarni/composer-bin-plugin) to install the package in isolated directory. Thanks to the isolation we're less likely to run into problem with conflicting dependencies between tools. ```json { "command": { "composer-bin-plugin": { "package": "behat/behat", "namespace": "behat", "links": {"/tools/behat": "behat"} } } } ``` The `links` attribute is optional, but it's recommended for packages that provide commands. #### box-build Uses [box](https://box-project.github.io/box2/) to build a phar and puts it into the specified location. It will clone the given repository and checkout the latest tag if available. ```json { "command": { "box-build": { "repository": "https://github.com/behat/behat.git", "phar": "behat.phar", "bin": "/usr/local/bin/behat" } } } ``` #### composer-global-install Uses the `composer global require` command to install a composer package globally. ```json { "command": { "composer-global-install": { "package": "bmitch/churn-php" } } } ``` #### composer-install Clones the specified repository, checkouts the latest tag (if available), and runs `composer install` inside. Mostly useful with applications. ```json { "command": { "composer-install": { "repository": "https://github.com/Qafoo/QualityAnalyzer.git" } } } ``` #### Executing multiple commands It's sometimes useful to run multiple installation commands i.e. when downloading a phar and its signature. ```json { "command": { "file-download": { "url": "https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar.asc", "file": "/usr/local/bin/psalm.phar.asc" }, "phar-download": { "phar": "https://github.com/vimeo/psalm/releases/download/2.0.0/psalm.phar", "bin": "/usr/local/bin/psalm" } } } ``` ================================================ FILE: LICENSE ================================================ Copyright (c) 2017 Jakub Zalas 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. ================================================ FILE: Makefile ================================================ default: build PHP_VERSION:=$(shell php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') TOOLBOX_VERSION?=dev build: install test .PHONY: build install: composer install .PHONY: install update: composer update .PHONY: update update-min: composer update --prefer-stable --prefer-lowest .PHONY: update-min update-no-dev: composer update --prefer-stable --no-dev .PHONY: update-no-dev test: vendor cs deptrac phpunit infection .PHONY: test test-min: update-min cs deptrac phpunit infection .PHONY: test-min test-integration: build/toolbox.phar rm -rf ./build/tools && \ export PATH="$(shell pwd)/build/tools:$(shell pwd)/build/tools/.composer/vendor/bin:$(shell pwd)/build/tools/QualityAnalyzer/bin:$$PATH" && \ export COMPOSER_HOME=$(shell pwd)/build/tools/.composer && \ chmod +x build/toolbox.phar && \ mkdir -p ./build/tools && \ build/toolbox.phar install --target-dir ./build/tools --exclude-tag exclude-php:$(PHP_VERSION) && \ build/toolbox.phar test --target-dir ./build/tools --exclude-tag exclude-php:$(PHP_VERSION) .PHONY: test-integration cs: tools/php-cs-fixer PHP_CS_FIXER_IGNORE_ENV=true tools/php-cs-fixer --dry-run --allow-risky=yes --no-interaction --ansi fix cs-fix: tools/php-cs-fixer PHP_CS_FIXER_IGNORE_ENV=true tools/php-cs-fixer --allow-risky=yes --no-interaction --ansi fix deptrac: tools/deptrac tools/deptrac --no-interaction --ansi .PHONY: deptrac infection: ./vendor/bin/infection --no-interaction --formatter=progress --min-msi=100 --min-covered-msi=100 --ansi .PHONY: infection phpunit: tools/phpunit tools/phpunit .PHONY: phpunit package: tools/box @rm -rf build/phar && mkdir -p build/phar build/phar/bin cp -r resources src LICENSE composer.json scoper.inc.php build/phar sed -e 's/Application('"'"'dev/Application('"'"'$(TOOLBOX_VERSION)/g' bin/toolbox.php > build/phar/bin/toolbox.php cd build/phar && \ composer config platform.php 8.2.0 && \ composer update --no-dev -o -a tools/box compile @rm -rf build/phar .PHONY: package package-devkit: tools/box @rm -rf build/devkit-phar && mkdir -p build/devkit-phar build/devkit-phar/bin build/devkit-phar/src cp -r resources LICENSE composer.json scoper.inc.php build/devkit-phar cp -r src/Json src/Runner src/Tool build/devkit-phar/src sed -e 's/\(Application(.*\)'"'"'dev/\1'"'"'$(TOOLBOX_VERSION)/g' bin/devkit.php > build/devkit-phar/bin/devkit.php cd build/devkit-phar && \ composer config platform.php 8.2.0 && \ composer update --no-dev -o -a tools/box compile -c box-devkit.json.dist @rm -rf build/devkit-phar .PHONY: package-devkit website: build/devkit.phar rm -rf build/website mkdir -p build/website php build/devkit.phar generate:html > build/website/index.html touch build/website/.nojekyll .PHONY: website publish-website: website cd build/website && \ git init . && \ git add . && \ git commit -m "Build the website" && \ git push --force --quiet "https://github.com/jakzal/toolbox.git" master:gh-pages .PHONY: publish-website update-phars: vendor php bin/devkit.php update:phars git diff --exit-code resources/ || \ ( \ git checkout -b tools-update && \ git add resources/*.json && \ git commit -m "Update tools" && \ git push origin tools-update \ ) .PHONY: update-phars tools: tools/php-cs-fixer tools/deptrac tools/box .PHONY: tools clean: rm -rf build rm -rf vendor find tools -not -path '*/\.*' -type f -delete .PHONY: clean vendor: install vendor/bin/phpunit: install tools/phpunit: vendor/bin/phpunit ln -sf ../vendor/bin/phpunit tools/phpunit tools/php-cs-fixer: curl -Ls https://cs.symfony.com/download/php-cs-fixer-v3.phar -o tools/php-cs-fixer && chmod +x tools/php-cs-fixer tools/deptrac: ln -sf ../vendor/bin/deptrac tools/deptrac tools/box: curl -Ls https://github.com/humbug/box/releases/download/4.2.0/box.phar -o tools/box && chmod +x tools/box ================================================ FILE: README.md ================================================ # Toolbox [![Build Status](https://github.com/jakzal/toolbox/workflows/Build/badge.svg)](https://github.com/jakzal/toolbox/actions) [![Build Status](https://scrutinizer-ci.com/g/jakzal/toolbox/badges/build.png?b=master)](https://scrutinizer-ci.com/g/jakzal/toolbox/build-status/master) Helps to discover and install tools. ## Use cases Toolbox [started its life](https://github.com/jakzal/phpqa/blob/49482ae447d4b6341cf77aac9d51390fe1176e8c/tools.php) as a simple script in the [phpqa docker image](https://github.com/jakzal/phpqa). Its purpose was to install set of tools while building the docker image and it's still its main goal. It has been extracted as a separate project to make maintenance easier and enable new use cases. ## Available tools | Name | Description | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 | | :--- | :---------- | :------ | :------ | :------ | :------ | | behat | [Helps to test business expectations](http://behat.org/) | ✅ | ✅ | ✅ | ✅ | | box | [Fast, zero config application bundler with PHARs](https://github.com/humbug/box) | ✅ | ✅ | ✅ | ✅ | | churn | [Discovers good candidates for refactoring](https://github.com/bmitch/churn-php) | ✅ | ✅ | ✅ | ✅ | | codeception | [Codeception is a BDD-styled PHP testing framework](https://codeception.com/) | ✅ | ✅ | ✅ | ❌ | | composer | [Dependency Manager for PHP](https://getcomposer.org/) | ✅ | ✅ | ✅ | ✅ | | composer-bin-plugin | [Composer plugin to install bin vendors in isolated locations](https://github.com/bamarni/composer-bin-plugin) | ✅ | ✅ | ✅ | ✅ | | composer-lock-diff | [Composer plugin to check what has changed after a composer update](https://github.com/davidrjonas/composer-lock-diff) | ✅ | ✅ | ✅ | ✅ | | composer-normalize | [Composer plugin to normalize composer.json files](https://github.com/ergebnis/composer-normalize) | ✅ | ✅ | ✅ | ✅ | | composer-require-checker | [Verify that no unknown symbols are used in the sources of a package.](https://github.com/maglnet/ComposerRequireChecker) | ❌ | ❌ | ✅ | ✅ | | composer-require-checker-3 | [Verify that no unknown symbols are used in the sources of a package.](https://github.com/maglnet/ComposerRequireChecker) | ✅ | ✅ | ✅ | ✅ | | composer-unused | [Show unused packages by scanning your code](https://github.com/icanhazstring/composer-unused) | ✅ | ✅ | ✅ | ✅ | | cyclonedx-php-composer | [Composer plugin to create Software-Bill-of-Materials (SBOM) in CycloneDX format](https://github.com/CycloneDX/cyclonedx-php-composer) | ✅ | ✅ | ✅ | ✅ | | dephpend | [Detect flaws in your architecture](https://dephpend.com/) | ✅ | ✅ | ✅ | ✅ | | deprecation-detector | [Finds usages of deprecated code](https://github.com/sensiolabs-de/deprecation-detector) | ✅ | ✅ | ✅ | ✅ | | deptrac | [Enforces dependency rules between software layers](https://github.com/deptrac/deptrac) | ✅ | ✅ | ✅ | ✅ | | diffFilter | [Applies QA tools to run on a single pull request](https://github.com/exussum12/coverageChecker) | ✅ | ✅ | ✅ | ✅ | | ecs | [Sets up and runs coding standard checks](https://github.com/Symplify/EasyCodingStandard) | ✅ | ✅ | ✅ | ✅ | | gherkin-lint-php | [Gherkin linter for PHP](https://github.com/dantleech/gherkin-lint-php) | ✅ | ✅ | ✅ | ✅ | | infection | [AST based PHP Mutation Testing Framework](https://infection.github.io/) | ✅ | ✅ | ✅ | ✅ | | jack | [Helps to upgrade outdated Composer dependencies incrementally](https://github.com/rectorphp/jack) | ✅ | ✅ | ✅ | ✅ | | kahlan | [Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec](https://kahlan.github.io/docs/) | ✅ | ✅ | ✅ | ✅ | | larastan | [PHPStan extension for Laravel](https://github.com/nunomaduro/larastan) | ✅ | ✅ | ✅ | ✅ | | lines | [CLI tool for quick metrics of PHP projects](https://github.com/tomasVotruba/lines) | ✅ | ✅ | ✅ | ✅ | | local-php-security-checker | [Checks composer dependencies for known security vulnerabilities](https://github.com/fabpot/local-php-security-checker) | ✅ | ✅ | ✅ | ✅ | | parallel-lint | [Checks PHP file syntax](https://github.com/php-parallel-lint/PHP-Parallel-Lint) | ✅ | ✅ | ✅ | ✅ | | paratest | [Parallel testing for PHPUnit](https://github.com/paratestphp/paratest) | ✅ | ✅ | ✅ | ✅ | | pdepend | [Static Analysis Tool](https://pdepend.org/) | ✅ | ✅ | ✅ | ✅ | | phan | [Static Analysis Tool](https://github.com/phan/phan) | ✅ | ✅ | ✅ | ✅ | | phive | [PHAR Installation and Verification Environment](https://phar.io/) | ✅ | ✅ | ✅ | ✅ | | php-cs-fixer | [PHP Coding Standards Fixer](http://cs.symfony.com/) | ✅ | ✅ | ✅ | ✅ | | php-fuzzer | [A fuzzer for PHP, which can be used to find bugs in libraries by feeding them 'random' inputs](https://github.com/nikic/PHP-Fuzzer) | ✅ | ✅ | ✅ | ✅ | | php-semver-checker | [Suggests a next version according to semantic versioning](https://github.com/tomzx/php-semver-checker) | ✅ | ✅ | ✅ | ✅ | | phpa | [Checks for weak assumptions](https://github.com/rskuipers/php-assumptions) | ✅ | ✅ | ✅ | ✅ | | phparkitect | [Helps to put architectural constraints in a PHP code base](https://github.com/phparkitect/arkitect) | ✅ | ✅ | ✅ | ✅ | | phpat | [Easy to use architecture testing tool](https://github.com/carlosas/phpat) | ✅ | ✅ | ✅ | ✅ | | phpbench | [PHP Benchmarking framework](https://github.com/phpbench/phpbench) | ✅ | ✅ | ✅ | ✅ | | phpca | [Finds usage of non-built-in extensions](https://github.com/wapmorgan/PhpCodeAnalyzer) | ✅ | ✅ | ✅ | ✅ | | phpcb | [PHP Code Browser](https://github.com/mayflower/PHP_CodeBrowser) | ✅ | ✅ | ✅ | ✅ | | phpcbf | [Automatically corrects coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer) | ✅ | ✅ | ✅ | ✅ | | phpcodesniffer-composer-install | [Easy installation of PHP_CodeSniffer coding standards (rulesets).](https://github.com/PHPCSStandards/composer-installer) | ✅ | ✅ | ✅ | ✅ | | phpcov | [a command-line frontend for the PHP_CodeCoverage library](https://github.com/sebastianbergmann/phpcov) | ❌ | ❌ | ✅ | ✅ | | phpcpd | [Copy/Paste Detector](https://github.com/sebastianbergmann/phpcpd) | ✅ | ✅ | ✅ | ✅ | | phpcs | [Detects coding standard violations](https://github.com/PHPCSStandards/PHP_CodeSniffer) | ✅ | ✅ | ✅ | ✅ | | phpcs-security-audit | [Finds vulnerabilities and weaknesses related to security in PHP code](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | ✅ | ✅ | ✅ | ✅ | | phpdd | [Finds usage of deprecated features](http://wapmorgan.github.io/PhpDeprecationDetector) | ✅ | ✅ | ✅ | ✅ | | phpDocumentor | [Documentation generator](https://www.phpdoc.org/) | ✅ | ✅ | ✅ | ✅ | | phpinsights | [Analyses code quality, style, architecture and complexity](https://phpinsights.com/) | ✅ | ✅ | ✅ | ✅ | | phplint | [Lints php files in parallel](https://github.com/overtrue/phplint) | ✅ | ✅ | ✅ | ✅ | | phploc | [A tool for quickly measuring the size of a PHP project](https://github.com/sebastianbergmann/phploc) | ✅ | ✅ | ✅ | ✅ | | phpmd | [A tool for finding problems in PHP code](https://phpmd.org/) | ✅ | ✅ | ✅ | ✅ | | phpmetrics | [Static Analysis Tool](http://www.phpmetrics.org/) | ✅ | ✅ | ✅ | ✅ | | phpmnd | [Helps to detect magic numbers](https://github.com/povils/phpmnd) | ✅ | ✅ | ✅ | ✅ | | phpspec | [SpecBDD Framework](http://www.phpspec.net/) | ✅ | ✅ | ✅ | ❌ | | phpstan | [Static Analysis Tool](https://github.com/phpstan/phpstan) | ✅ | ✅ | ✅ | ✅ | | phpstan-banned-code | [PHPStan rules for detecting calls to specific functions you don't want in your project](https://github.com/ekino/phpstan-banned-code) | ✅ | ✅ | ✅ | ✅ | | phpstan-beberlei-assert | [PHPStan extension for beberlei/assert](https://github.com/phpstan/phpstan-beberlei-assert) | ✅ | ✅ | ✅ | ✅ | | phpstan-deprecation-rules | [PHPStan rules for detecting deprecated code](https://github.com/phpstan/phpstan-deprecation-rules) | ✅ | ✅ | ✅ | ✅ | | phpstan-doctrine | [Doctrine extensions for PHPStan](https://github.com/phpstan/phpstan-doctrine) | ✅ | ✅ | ✅ | ✅ | | phpstan-ergebnis-rules | [Additional rules for PHPstan](https://github.com/ergebnis/phpstan-rules) | ✅ | ✅ | ✅ | ✅ | | phpstan-larastan | [Separate installation of phpstan for larastan](https://github.com/phpstan/phpstan) | ✅ | ✅ | ✅ | ✅ | | phpstan-phpunit | [PHPUnit extensions and rules for PHPStan](https://github.com/phpstan/phpstan-phpunit) | ✅ | ✅ | ✅ | ✅ | | phpstan-strict-rules | [Extra strict and opinionated rules for PHPStan](https://github.com/phpstan/phpstan-strict-rules) | ✅ | ✅ | ✅ | ✅ | | phpstan-symfony | [Symfony extension for PHPStan](https://github.com/phpstan/phpstan-symfony) | ✅ | ✅ | ✅ | ✅ | | phpstan-webmozart-assert | [PHPStan extension for webmozart/assert](https://github.com/phpstan/phpstan-webmozart-assert) | ✅ | ✅ | ✅ | ✅ | | phpunit | [The PHP testing framework](https://phpunit.de/) | ❌ | ❌ | ✅ | ✅ | | phpunit-10 | [The PHP testing framework (10.x version)](https://phpunit.de/) | ✅ | ✅ | ✅ | ✅ | | phpunit-11 | [The PHP testing framework (11.x version)](https://phpunit.de/) | ✅ | ✅ | ✅ | ✅ | | phpunit-12 | [The PHP testing framework (12.x version)](https://phpunit.de/) | ❌ | ✅ | ✅ | ✅ | | phpunit-8 | [The PHP testing framework (8.x version)](https://phpunit.de/) | ✅ | ✅ | ✅ | ✅ | | phpunit-9 | [The PHP testing framework (9.x version)](https://phpunit.de/) | ✅ | ✅ | ✅ | ✅ | | pint | [Opinionated PHP code style fixer for Laravel](https://github.com/laravel/pint) | ✅ | ✅ | ✅ | ✅ | | psalm | [Finds errors in PHP applications](https://psalm.dev/) | ✅ | ✅ | ✅ | ✅ | | psalm-plugin-doctrine | [Stubs to let Psalm understand Doctrine better](https://github.com/weirdan/doctrine-psalm-plugin) | ✅ | ✅ | ✅ | ✅ | | psalm-plugin-phpunit | [Psalm plugin for PHPUnit](https://github.com/psalm/psalm-plugin-phpunit) | ✅ | ✅ | ✅ | ✅ | | psalm-plugin-symfony | [Psalm Plugin for Symfony](https://github.com/psalm/psalm-plugin-symfony) | ✅ | ✅ | ✅ | ✅ | | psecio-parse | [Scans code for potential security-related issues](https://github.com/psecio/parse) | ✅ | ✅ | ✅ | ✅ | | rector | [Tool for instant code upgrades and refactoring](https://github.com/rectorphp/rector) | ✅ | ✅ | ✅ | ✅ | | roave-backward-compatibility-check | [Tool to compare two revisions of a class API to check for BC breaks](https://github.com/Roave/BackwardCompatibilityCheck) | ✅ | ✅ | ✅ | ✅ | | simple-phpunit | [Provides utilities to report legacy tests and usage of deprecated code](https://symfony.com/doc/current/components/phpunit_bridge.html) | ✅ | ✅ | ✅ | ✅ | | twig-cs-fixer | [Automatically corrects twig files following the official coding standard rules](https://github.com/VincentLanglet/Twig-CS-Fixer) | ✅ | ✅ | ✅ | ✅ | | twig-lint | [Standalone cli twig 1.X linter](https://github.com/asm89/twig-lint) | ✅ | ✅ | ✅ | ✅ | | twig-linter | [Standalone cli twig 3.X linter](https://github.com/sserbin/twig-linter) | ✅ | ✅ | ✅ | ✅ | | twigcs | [The missing checkstyle for twig!](https://github.com/friendsoftwig/twigcs) | ✅ | ✅ | ✅ | ❌ | | yaml-lint | [Compact command line utility for checking YAML file syntax](https://github.com/j13k/yaml-lint) | ✅ | ✅ | ✅ | ✅ | ### Removed tools | Name | Summary | | :--- | :------ | | analyze | [Visualizes metrics and source code](https://github.com/Qafoo/QualityAnalyzer) | | box-legacy | [Legacy version of box](https://box-project.github.io/box2/) | | design-pattern | [Detects design patterns](https://github.com/Halleck45/DesignPatternDetector) | | parallel-lint | [Checks PHP file syntax](https://github.com/JakubOnderka/PHP-Parallel-Lint) | | pest | [The elegant PHP Testing Framework](https://github.com/pestphp/pest) | | php-coupling-detector | [Detects code coupling issues](https://akeneo.github.io/php-coupling-detector/) | | php-formatter | [Custom coding standards fixer](https://github.com/mmoreram/php-formatter) | | phpcf | [Finds usage of deprecated features](http://wapmorgan.github.io/PhpCodeFixer/) | | phpda | [Generates dependency graphs](https://mamuz.github.io/PhpDependencyAnalysis/) | | phpdoc-to-typehint | [Automatically adds type hints and return types based on PHPDocs](https://github.com/dunglas/phpdoc-to-typehint) | | phpstan-exception-rules | [PHPStan rules for checked and unchecked exceptions](https://github.com/pepakriz/phpstan-exception-rules) | | phpstan-localheinz-rules | [Additional rules for PHPstan](https://github.com/localheinz/phpstan-rules) | | phpunit-5 | [The PHP testing framework (5.x version)](https://phpunit.de/) | | phpunit-7 | [The PHP testing framework (7.x version)](https://phpunit.de/) | | security-checker | [Checks composer dependencies for known security vulnerabilities](https://github.com/sensiolabs/security-checker) | | testability | [Analyses and reports testability issues of a php codebase](https://github.com/edsonmedina/php_testability) | ## Installation Get the `toolbox.phar` from the [latest release](https://github.com/jakzal/toolbox/releases/latest). The command below should do the job: ```bash curl -Ls https://github.com/jakzal/toolbox/releases/latest/download/toolbox.phar -o toolbox && chmod +x toolbox ``` ## Usage ### List available tools ``` ./toolbox list-tools ``` #### Filter tools by tags To exclude some tools from the listing multiple `--exclude-tag` options can be added. The `--tag` option can be used to filter tools by tags. ``` ./toolbox list-tools --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar ``` ### Install tools ``` ./toolbox install ``` #### Install tools in a custom directory By default tools are installed in the `/usr/local/bin` directory. To perform an installation in another location, pass the `--target-dir` option to the `install` command. Also, to change the location composer packages are installed in, export the `COMPOSER_HOME` environment variable. ``` mkdir /tools export COMPOSER_HOME=/tools/.composer export PATH="/tools:$COMPOSER_HOME/vendor/bin:$PATH" ./toolbox install --target-dir /tools ``` The target dir can also be configured with the `TOOLBOX_TARGET_DIR` environment variable. #### Dry run To only see what commands would be executed, use the dry run mode: ``` ./toolbox install --dry-run ``` #### Filter tools by tags To exclude some tools from the installation multiple `--exclude-tag` options can be added. The `--tag` option can be used to filter tools by tags. ``` ./toolbox install --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar ``` ### Test if installed tools are usable ``` ./toolbox test ``` #### Dry run To only see what commands would be executed, use the dry run mode: ``` ./toolbox test --dry-run ``` #### Filter tools by tags To exclude some tools from the generated test command multiple `--exclude-tag` options can be added. The `--tag` option can be used to filter tools by tags. ``` ./toolbox test --exclude-tag exclude-php:8.2 --exclude-tag foo --tag bar ``` ### Tools definitions By default the following files are used to load tool definitions: * `resources/pre-installation.json` * `resources/architecture.json` * `resources/checkstyle.json` * `resources/compatibility.json` * `resources/composer.json` * `resources/deprecation.json` * `resources/documentation.json` * `resources/linting.json` * `resources/metrics.json` * `resources/phpstan.json` * `resources/psalm.json` * `resources/refactoring.json` * `resources/security.json` * `resources/test.json` * `resources/tools.json` Definitions can be loaded from customised files by passing the `--tools` option(s): ``` ./toolbox list-tools --tools path/to/file1.json --tools path/to/file2.json ``` Tool definition location(s) can be also specified with the `TOOLBOX_JSON` environment variable: ``` TOOLBOX_JSON='path/to/file1.json,path/to/file2.json' ./toolbox list-tools ``` ### Tool tags Tools can be tagged in order to enable grouping and filtering them. The tags below have a special meaning: * `pre-installation` - these tools will be installed before any other tools. * `exclude-php:8.2`, `exclude-php:8.3` etc - used to exclude installation on the specified php version. ## Contributing Please read the [Contributing guide](CONTRIBUTING.md) to learn about contributing to this project. Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. ================================================ FILE: bin/devkit.php ================================================ #!/usr/bin/env php all($filter ?? new Filter([], [])); } } $application = new Application('Toolbox DevKit', 'dev'); $application->add( new class extends CliCommand { use Tools; protected function configure(): void { $this->setName('update:readme'); $this->setDescription('Updates README.md with latest list of available tools'); $this->addOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault()); $this->addOption('readme', null, InputOption::VALUE_REQUIRED, 'Path to the readme file', __DIR__ . '/../README.md'); } protected function execute(InputInterface $input, OutputInterface $output): int { $jsonPath = $input->getOption('tools'); $readmePath = $input->getOption('readme'); $tools = $this->loadTools($jsonPath); $versions = ['8.2', '8.3', '8.4', '8.5']; $toolsList = '| Name | Description | '. implode(' ', array_map(fn($v) => sprintf('PHP %s |', $v), $versions)) . PHP_EOL; $toolsList .= '| :--- | :---------- | '. implode(' ', array_fill(0, count($versions), ':------ |')) . PHP_EOL; $toolsList .= $tools->sort(function (Tool $left, Tool $right) { return strcasecmp($left->name(), $right->name()); })->reduce('', function ($acc, Tool $tool) use ($versions) { $args = [ $tool->name(), $tool->summary(), $tool->website(), ]; foreach ($versions as $version) { $args[] = in_array(sprintf('exclude-php:%s', $version), $tool->tags(), true) ? '❌' : '✅'; } return $acc . vsprintf('| %s | [%s](%s) | '. implode(' ', array_fill(0, count($versions), '%s |')), $args) . PHP_EOL; }); $readme = file_get_contents($readmePath); $readme = preg_replace('/(## Available tools\n\n).*?(\n#+ )/smi', '$1' . $toolsList . '$2', $readme); file_put_contents($readmePath, $readme); $output->writeln(sprintf('The %s was updated with latest tools found in %s.', $readmePath, implode(', ', $jsonPath))); return 0; } } ); $application->add( new class extends CliCommand { use Tools; protected function configure(): void { $this->setName('update:phars'); $this->setDescription('Attempts to update phar links to latest versions'); $this->addOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault()); } protected function execute(InputInterface $input, OutputInterface $output): int { foreach ($input->getOption('tools') as $jsonPath) { $result = $this->updatePhars($jsonPath, $output); if ($result !== 0) { return $result; } } return 0; } private function updatePhars(string $jsonPath, OutputInterface $output): int { $phars = $this->findLatestPhars($jsonPath); if (empty($phars)) { return 0; } $output->writeln('Found phars:'); foreach ($phars as $phar) { $output->writeln(sprintf('* %s', $phar)); } $output->writeln(sprintf('Updated %s.', $jsonPath)); return (new PassthruRunner())->run($this->updatePharsCommand($jsonPath, $phars)); } private function findLatestPharsCommand(string $jsonPath): Command { $command = <<<'CMD' grep -e 'github\.com.*releases.*\.phar"' %TOOLBOX_JSON% | grep -v -e '/latest/' | sed -e 's@.*github.com/\(.*\)/releases.*@\1@' | xargs -I"{}" sh -c "curl -s -XGET 'https://api.github.com/repos/{}/releases/latest' -H 'Accept:application/json' | grep browser_download_url | grep .phar | head -n 1" | sed -e 's/^[^:]*: "\([^"]*\)"/\1/' CMD; $command = strtr($command, ['%TOOLBOX_JSON%' => $jsonPath]); return new ShCommand($command); } private function findLatestPhars(string $jsonPath): array { $phars = []; exec((string)$this->findLatestPharsCommand($jsonPath), $phars); return $phars; } private function updatePharsCommand(string $jsonPath, array $phars): Command { $replacements = implode(' ', array_map( function (string $phar) { $project = preg_replace('@https://[^/]*/([^/]*/[^/]*).*@', '$1', $phar); return strtr( '-e "s@\"phar\": \"([^\"]*%PROJECT%[^\"]*)\"@\"phar\": \"%PHAR%\"@g"' . ' ' . '-e "s@\"url\": \"([^\"]*%PROJECT%[^\"]*\.phar(\.asc|\.pubkey))\"@\"url\": \"%PHAR%\\2\"@g"', ['%PROJECT%' => $project, '%PHAR%' => $phar] ); }, $phars )); return new ShCommand(sprintf('sed -i.bak -E %s %s', $replacements, $jsonPath)); } } ); $application->add( new class extends CliCommand { use Tools; protected function configure(): void { $this->setName('generate:html'); $this->setDescription('Generates an html list of available tools'); $this->addOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault()); } protected function execute(InputInterface $input, OutputInterface $output): int { $tools = $this->loadTools($input->getOption('tools'), new Filter(['pre-installation'], [])); $output->writeln($this->renderPage($tools->map($this->toolToHtml()))); return 0; } private function toolToHtml(): \Closure { $tagTemplate = '
  • %TAG%
  • '; $toolTemplate = <<<'TEMPLATE'
    %NAME%

    %SUMMARY%

    %WEBSITE_NAME%
    TEMPLATE; return function (Tool $tool) use ($toolTemplate, $tagTemplate) { return strtr( $toolTemplate, [ '%NAME%' => $tool->name(), '%SUMMARY%' => $tool->summary(), '%WEBSITE%' => $tool->website(), '%WEBSITE_NAME%' => preg_replace('#^(https?://(github.com/)?)(.*?)/?$#', '$3', $tool->website()), '%TAGS%' => \implode(\array_map(function (string $tag) use ($tagTemplate) { return strtr($tagTemplate, ['%TAG%' => $tag]); }, $tool->tags())) ]); }; } private function renderPage(Collection $toolsHtml): string { $template = <<<'TEMPLATE' Quality Assurance Tools for PHP | Toolbox | PHPQA

    Quality Assurance Tools for PHP

    The below list of tools is provided by the phpqa docker image. Toolbox is used to install them in the image.


    toolbox repository phpqa docker image phpqa repository
    %TOOLS%
    Generated on %GENERATED_ON%.
    TEMPLATE; return strtr($template, [ '%TOOLS%' => \implode(PHP_EOL, \array_map( function ($htmls) { return PHP_EOL . '
    ' . implode($htmls) . '
    ' . PHP_EOL; }, \array_chunk($toolsHtml->toArray(), 4) )), '%GENERATED_ON%' => (new \DateTime('now', new \DateTimeZone('UTC')))->format('r'), ]); } } ); $application->run(); ================================================ FILE: bin/toolbox.php ================================================ #!/usr/bin/env php run(); ================================================ FILE: box-devkit.json.dist ================================================ { "base-path": "build/devkit-phar", "output": "../devkit.phar", "compression": "GZ", "directories": ["."], "check-requirements": false, "main": "bin/devkit.php", "compactors": [ "KevinGH\\Box\\Compactor\\PhpScoper" ], "banner": [ "This file is part of the zalas/toolbox project.", "", "(c) Jakub Zalas " ] } ================================================ FILE: box.json.dist ================================================ { "base-path": "build/phar", "output": "../toolbox.phar", "compression": "GZ", "directories": ["."], "check-requirements": false, "main": "bin/toolbox.php", "compactors": [ "KevinGH\\Box\\Compactor\\PhpScoper" ], "banner": [ "This file is part of the zalas/toolbox project.", "", "(c) Jakub Zalas " ] } ================================================ FILE: composer.json ================================================ { "name": "zalas/toolbox", "description": "Helps to discover and install tools", "type": "project", "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "symfony/console": "^7.4 || ^8.0", "psr/container": "^2.0" }, "require-dev": { "phpunit/phpunit": "^11.5.9 || ^12.0 || ^13.0", "zalas/phpunit-globals": "^4.0", "infection/infection": "^0.31", "deptrac/deptrac": "^4.0" }, "autoload": { "psr-4": { "Zalas\\Toolbox\\": "src" } }, "autoload-dev": { "psr-4": { "Zalas\\Toolbox\\Tests\\": "tests" } }, "license": "MIT", "authors": [ { "name": "Jakub Zalas", "email": "jakub@zalas.pl" } ], "extra": { "branch-alias": { "dev-master": "1.x-dev" } }, "config": { "allow-plugins": { "infection/extension-installer": true } } } ================================================ FILE: deptrac.yaml ================================================ parameters: paths: - ./src exclude_files: [] layers: - name: Cli collectors: - type: classLike value: ^Zalas\\Toolbox\\Cli\\.* - name: Json collectors: - type: classLike value: ^Zalas\\Toolbox\\Json\\.* - name: Runner collectors: - type: classLike value: ^Zalas\\Toolbox\\Runner\\.* - name: Tool collectors: - type: classLike value: ^Zalas\\Toolbox\\Tool\\.* - name: UseCase collectors: - type: classLike value: ^Zalas\\Toolbox\\UseCase\\.* - name: Psr Container collectors: - type: classLike value: ^Psr\\Container\\.* - name: Symfony Console collectors: - type: classLike value: ^Symfony\\Component\\Console\\.* - name: Other Vendors collectors: - type: bool must: # must be outside of global namespace - type: classLike value: '[\\]+' must_not: # must not be one of the known vendors - type: classLike value: ^Zalas\\Toolbox\\(Cli|Json|Runner|Tool|UseCase)\\.* - type: classLike value: ^Psr\\Container\\.* - type: classLike value: ^Symfony\\Component\\Console\\.* ruleset: Cli: - Tool - Json - Runner - UseCase - Symfony Console - Psr Container Json: - Tool Runner: - Tool Tool: UseCase: - Tool ================================================ FILE: infection.json.dist ================================================ { "timeout": 2, "source": { "directories": [ "src" ] }, "logs": { "text": "build/infection-log.txt" }, "mutators": { "@default": true, "EqualIdentical": false, "NotIdenticalNotEqual": false, "Concat": false, "ConcatOperandRemoval": false, "ArrayItemRemoval": { "ignore": [ "Zalas\\Toolbox\\Cli\\Command\\ListCommand::execute" ] } } } ================================================ FILE: phpunit.xml.dist ================================================ tests src ================================================ FILE: resources/architecture.json ================================================ { "tools": [ { "name": "dephpend", "summary": "Detect flaws in your architecture", "website": "https://dephpend.com/", "command": { "phive-install": { "alias": "dephpend", "bin": "%target-dir%/dephpend", "sig": "76835C9464877BDD" } }, "test": "dephpend list", "tags": ["architecture"] }, { "name": "deptrac", "summary": "Enforces dependency rules between software layers", "website": "https://github.com/deptrac/deptrac", "command": { "composer-global-install": { "package": "deptrac/deptrac" } }, "test": "deptrac list", "tags": ["featured", "architecture"] }, { "name": "pdepend", "summary": "Static Analysis Tool", "website": "https://pdepend.org/", "command": { "phive-install": { "alias": "pdepend/pdepend", "bin": "%target-dir%/pdepend", "sig": "508003DAED98C308" } }, "test": "pdepend --version", "tags": ["featured", "architecture"] }, { "name": "phparkitect", "summary": "Helps to put architectural constraints in a PHP code base", "website": "https://github.com/phparkitect/arkitect", "command": { "phar-download": { "phar": "https://github.com/phparkitect/arkitect/releases/latest/download/phparkitect.phar", "bin": "%target-dir%/phparkitect" } }, "test": "phparkitect --version", "tags": ["architecture"] } ] } ================================================ FILE: resources/checkstyle.json ================================================ { "tools": [ { "name": "ecs", "summary": "Sets up and runs coding standard checks", "website": "https://github.com/Symplify/EasyCodingStandard", "command": { "composer-bin-plugin": { "package": "symplify/easy-coding-standard", "namespace": "ecs", "links": {"%target-dir%/ecs": "ecs"} } }, "test": "ecs -h", "tags": ["checkstyle"] }, { "name": "pint", "summary": "Opinionated PHP code style fixer for Laravel", "website": "https://github.com/laravel/pint", "command": { "composer-bin-plugin": { "package": "laravel/pint", "namespace": "pint", "links": {"%target-dir%/pint": "pint"} } }, "test": "pint --version", "tags": ["checkstyle"] }, { "name": "php-cs-fixer", "summary": "PHP Coding Standards Fixer", "website": "http://cs.symfony.com/", "command": { "phive-install": { "alias": "php-cs-fixer", "bin": "%target-dir%/php-cs-fixer", "sig": "E82B2FB314E9906E" } }, "test": "php-cs-fixer list", "tags": ["featured", "checkstyle"] }, { "name": "phpcbf", "summary": "Automatically corrects coding standard violations", "website": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "command": { "phive-install": { "alias": "phpcbf", "bin": "%target-dir%/phpcbf", "sig": "97B02DD8E5071466" } }, "test": "phpcbf --help", "tags": ["checkstyle"] }, { "name": "twigcs", "summary": "The missing checkstyle for twig!", "website": "https://github.com/friendsoftwig/twigcs", "command": { "phive-install": { "alias": "friendsoftwig/twigcs", "bin": "%target-dir%/twigcs", "sig": "C00543248C87FB13" } }, "test": "twigcs --help", "tags": ["checkstyle", "exclude-php:8.5"] }, { "name": "twig-cs-fixer", "summary": "Automatically corrects twig files following the official coding standard rules", "website": "https://github.com/VincentLanglet/Twig-CS-Fixer", "command": { "composer-bin-plugin": { "package": "vincentlanglet/twig-cs-fixer", "namespace": "twig-cs-fixer", "links": {"%target-dir%/twig-cs-fixer": "twig-cs-fixer"} } }, "test": "twig-cs-fixer --help", "tags": ["checkstyle"] } ] } ================================================ FILE: resources/compatibility.json ================================================ { "tools": [ { "name": "php-semver-checker", "summary": "Suggests a next version according to semantic versioning", "website": "https://github.com/tomzx/php-semver-checker", "command": { "phar-download": { "phar": "https://github.com/tomzx/php-semver-checker/releases/download/v0.17.0/php-semver-checker.phar", "bin": "%target-dir%/php-semver-checker" } }, "test": "php-semver-checker list", "tags": ["compatibility"] }, { "name": "roave-backward-compatibility-check", "summary": "Tool to compare two revisions of a class API to check for BC breaks", "website": "https://github.com/Roave/BackwardCompatibilityCheck", "command": { "sh": { "command": "composer global bin roavebackwardcompatibilitycheck config allow-plugins.ocramius/package-versions true" }, "composer-bin-plugin": { "package": "roave/backward-compatibility-check", "namespace": "roavebackwardcompatibilitycheck", "links": {"%target-dir%/roave-backward-compatibility-check": "roave-backward-compatibility-check"} } }, "test": "roave-backward-compatibility-check --version", "tags": ["compatibility"] } ] } ================================================ FILE: resources/composer.json ================================================ { "tools": [ { "name": "composer-normalize", "summary": "Composer plugin to normalize composer.json files", "website": "https://github.com/ergebnis/composer-normalize", "command": { "sh": { "command": "composer config --global --json allow-plugins.ergebnis/composer-normalize true" }, "composer-global-install": { "package": "ergebnis/composer-normalize" } }, "test": "composer global show ergebnis/composer-normalize", "tags": ["composer"] }, { "name": "composer-unused", "summary": "Show unused packages by scanning your code", "website": "https://github.com/icanhazstring/composer-unused", "command": { "phive-install": { "alias": "composer-unused", "bin": "%target-dir%/composer-unused", "sig": "3135AA4CB4F1AB0B" } }, "test": "composer-unused -V", "tags": ["composer"] }, { "name": "composer-require-checker", "summary": "Verify that no unknown symbols are used in the sources of a package.", "website": "https://github.com/maglnet/ComposerRequireChecker", "command": { "phive-install": { "alias": "composer-require-checker", "bin": "%target-dir%/composer-require-checker", "sig": "033E5F8D801A2F8D" } }, "test": "composer-require-checker -V", "tags": ["composer", "exclude-php:8.2", "exclude-php:8.3"] }, { "name": "composer-require-checker-3", "summary": "Verify that no unknown symbols are used in the sources of a package.", "website": "https://github.com/maglnet/ComposerRequireChecker", "command": { "phive-install": { "alias": "composer-require-checker@^3.8", "bin": "%target-dir%/composer-require-checker-3", "sig": "033E5F8D801A2F8D" } }, "test": "composer-require-checker-3 -V", "tags": ["composer"] }, { "name": "cyclonedx-php-composer", "summary": "Composer plugin to create Software-Bill-of-Materials (SBOM) in CycloneDX format", "website": "https://github.com/CycloneDX/cyclonedx-php-composer", "command": { "sh": { "command": "composer global config --no-plugins allow-plugins.cyclonedx/cyclonedx-php-composer true" }, "composer-global-install": { "package": "cyclonedx/cyclonedx-php-composer" } }, "test": "composer global show cyclonedx/cyclonedx-php-composer", "tags": ["composer"] }, { "name": "composer-lock-diff", "summary": "Composer plugin to check what has changed after a composer update", "website": "https://github.com/davidrjonas/composer-lock-diff", "command": { "composer-bin-plugin": { "package": "davidrjonas/composer-lock-diff", "namespace": "composer-lock-diff", "links": {"%target-dir%/composer-lock-diff": "composer-lock-diff"} } }, "test": "composer-lock-diff --help", "tags": ["composer"] }, { "name": "jack", "summary": "Helps to upgrade outdated Composer dependencies incrementally", "website": "https://github.com/rectorphp/jack", "command": { "composer-bin-plugin": { "package": "rector/jack:0.4.0", "namespace": "jack", "links": {"%target-dir%/jack": "jack"} } }, "test": "jack help", "tags": ["composer"] } ] } ================================================ FILE: resources/deprecation.json ================================================ { "tools": [ { "name": "deprecation-detector", "summary": "Finds usages of deprecated code", "website": "https://github.com/sensiolabs-de/deprecation-detector", "command": { "phive-install": { "alias": "sensiolabs-de/deprecation-detector", "bin": "%target-dir%/deprecation-detector" } }, "test": "deprecation-detector list", "tags": ["deprecation"] }, { "name": "phpdd", "summary": "Finds usage of deprecated features", "website": "http://wapmorgan.github.io/PhpDeprecationDetector", "command": { "phive-install": { "alias": "wapmorgan/phpdeprecationdetector", "bin": "%target-dir%/phpdd" } }, "test": "phpdd -h", "tags": ["deprecation"] } ] } ================================================ FILE: resources/documentation.json ================================================ { "tools": [ { "name": "phpDocumentor", "summary": "Documentation generator", "website": "https://www.phpdoc.org/", "command": { "phive-install": { "alias": "phpDocumentor", "bin": "%target-dir%/phpDocumentor", "sig": "6DA3ACC4991FFAE5" } }, "test": "phpDocumentor --help", "tags": ["featured", "documentation"] }, { "name": "phpcb", "summary": "PHP Code Browser", "website": "https://github.com/mayflower/PHP_CodeBrowser", "command": { "phar-download": { "phar": "https://github.com/bytepark/php-phar-qatools/raw/master/phpcb.phar", "bin": "%target-dir%/phpcb" } }, "test": "phpcb -V", "tags": ["documentation"] } ] } ================================================ FILE: resources/linting.json ================================================ { "tools": [ { "name": "parallel-lint", "summary": "Checks PHP file syntax", "website": "https://github.com/php-parallel-lint/PHP-Parallel-Lint", "command": { "phive-install": { "alias": "php-parallel-lint/PHP-Parallel-Lint", "bin": "%target-dir%/parallel-lint" } }, "test": "parallel-lint -h", "tags": ["linting"] }, { "name": "phplint", "summary": "Lints php files in parallel", "website": "https://github.com/overtrue/phplint", "command": { "composer-bin-plugin": { "package": "overtrue/phplint", "namespace": "phplint", "links": {"%target-dir%/phplint": "phplint"} } }, "test": "phplint -V", "tags": ["linting"] }, { "name": "twig-lint", "summary": "Standalone cli twig 1.X linter", "website": "https://github.com/asm89/twig-lint", "command": { "phar-download": { "phar": "https://asm89.github.io/d/twig-lint.phar", "bin": "%target-dir%/twig-lint" } }, "test": "twig-lint --version", "tags": ["linting"] }, { "name": "yaml-lint", "summary": "Compact command line utility for checking YAML file syntax", "website": "https://github.com/j13k/yaml-lint", "command": { "phive-install": { "alias": "j13k/yaml-lint", "bin": "%target-dir%/yaml-lint", "sig": "38A182AB413064D7" } }, "test": "yaml-lint --version", "tags": ["linting"] }, { "name": "twig-linter", "summary": "Standalone cli twig 3.X linter", "website": "https://github.com/sserbin/twig-linter", "command": { "composer-bin-plugin": { "package": "sserbin/twig-linter:@dev", "namespace": "twig-linter", "links": {"%target-dir%/twig-linter": "twig-linter"} } }, "test": "twig-linter --help", "tags": ["linting"] }, { "name": "gherkin-lint-php", "summary": "Gherkin linter for PHP", "website": "https://github.com/dantleech/gherkin-lint-php", "command": { "composer-bin-plugin": { "package": "dantleech/gherkin-lint", "namespace": "gherkin-lint-php", "links": {"%target-dir%/gherkinlint": "gherkinlint"} } }, "test": "gherkinlint --help", "tags": ["linting"] } ] } ================================================ FILE: resources/metrics.json ================================================ { "tools": [ { "name": "phpinsights", "summary": "Analyses code quality, style, architecture and complexity", "website": "https://phpinsights.com/", "command": { "sh": { "command": "composer global bin phpinsights config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true" }, "composer-bin-plugin": { "package": "nunomaduro/phpinsights", "namespace": "phpinsights", "links": {"%target-dir%/phpinsights": "phpinsights"} } }, "test": "phpinsights --version", "tags": ["metrics"] }, { "name": "phploc", "summary": "A tool for quickly measuring the size of a PHP project", "website": "https://github.com/sebastianbergmann/phploc", "command": { "phive-install": { "alias": "phploc", "bin": "%target-dir%/phploc", "sig": "4AA394086372C20A" } }, "test": "phploc -v", "tags": ["metrics"] }, { "name": "phpmetrics", "summary": "Static Analysis Tool", "website": "http://www.phpmetrics.org/", "command": { "phive-install": { "alias": "phpmetrics/PhpMetrics", "bin": "%target-dir%/phpmetrics" } }, "test": "phpmetrics --version", "tags": ["featured", "metrics"] }, { "name": "lines", "summary": "CLI tool for quick metrics of PHP projects", "website": "https://github.com/tomasVotruba/lines", "command": { "composer-bin-plugin": { "package": "tomasvotruba/lines", "namespace": "lines", "links": {"%target-dir%/lines": "lines"} } }, "test": "lines --version", "tags": ["metrics"] } ] } ================================================ FILE: resources/phpcs.json ================================================ { "tools": [ { "name": "phpcs", "summary": "Detects coding standard violations", "website": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "command": { "composer-bin-plugin": { "package": "squizlabs/php_codesniffer", "namespace": "phpcs", "links": {"%target-dir%/phpcs": "phpcs"} } }, "test": "phpcs --help", "tags": ["checkstyle"] }, { "name": "phpcodesniffer-composer-install", "summary": "Easy installation of PHP_CodeSniffer coding standards (rulesets).", "website": "https://github.com/PHPCSStandards/composer-installer", "command": { "sh": { "command": "composer global bin phpcs config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true" }, "composer-bin-plugin": { "package": "dealerdirect/phpcodesniffer-composer-installer", "namespace": "phpcs" } }, "test": "composer global bin phpcs show dealerdirect/phpcodesniffer-composer-installer", "tags": ["pre-installation"] }, { "name": "phpcs-security-audit", "summary": "Finds vulnerabilities and weaknesses related to security in PHP code", "website": "https://github.com/FloeDesignTechnologies/phpcs-security-audit", "command": { "composer-bin-plugin": { "package": "pheromone/phpcs-security-audit", "namespace": "phpcs" } }, "test": "phpcs -i | grep Security", "tags": ["security"] } ] } ================================================ FILE: resources/phpstan.json ================================================ { "tools": [ { "name": "phpstan", "summary": "Static Analysis Tool", "website": "https://github.com/phpstan/phpstan", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan", "namespace": "phpstan", "links": {"%target-dir%/phpstan": "phpstan"} } }, "test": "phpstan --version", "tags": ["featured", "phpstan"] }, { "name": "phpstan-deprecation-rules", "summary": "PHPStan rules for detecting deprecated code", "website": "https://github.com/phpstan/phpstan-deprecation-rules", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-deprecation-rules", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-deprecation-rules", "tags": ["phpstan"] }, { "name": "phpstan-ergebnis-rules", "summary": "Additional rules for PHPstan", "website": "https://github.com/ergebnis/phpstan-rules", "command": { "composer-bin-plugin": { "package": "ergebnis/phpstan-rules", "namespace": "phpstan" } }, "test": "composer global bin phpstan show ergebnis/phpstan-rules", "tags": ["phpstan"] }, { "name": "phpstan-strict-rules", "summary": "Extra strict and opinionated rules for PHPStan", "website": "https://github.com/phpstan/phpstan-strict-rules", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-strict-rules", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-strict-rules", "tags": ["phpstan"] }, { "name": "phpstan-doctrine", "summary": "Doctrine extensions for PHPStan", "website": "https://github.com/phpstan/phpstan-doctrine", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-doctrine", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-doctrine", "tags": ["phpstan"] }, { "name": "phpstan-phpunit", "summary": "PHPUnit extensions and rules for PHPStan", "website": "https://github.com/phpstan/phpstan-phpunit", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-phpunit", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-phpunit", "tags": ["phpstan"] }, { "name": "phpstan-symfony", "summary": "Symfony extension for PHPStan", "website": "https://github.com/phpstan/phpstan-symfony", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-symfony", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-symfony", "tags": ["phpstan"] }, { "name": "phpstan-beberlei-assert", "summary": "PHPStan extension for beberlei/assert", "website": "https://github.com/phpstan/phpstan-beberlei-assert", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-beberlei-assert", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-beberlei-assert", "tags": ["phpstan"] }, { "name": "phpstan-webmozart-assert", "summary": "PHPStan extension for webmozart/assert", "website": "https://github.com/phpstan/phpstan-webmozart-assert", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan-webmozart-assert", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpstan/phpstan-webmozart-assert", "tags": ["phpstan"] }, { "name": "phpat", "summary": "Easy to use architecture testing tool", "website": "https://github.com/carlosas/phpat", "command": { "composer-bin-plugin": { "package": "phpat/phpat", "namespace": "phpstan" } }, "test": "composer global bin phpstan show phpat/phpat", "tags": ["phpstan", "architecture"] }, { "name": "phpstan-larastan", "summary": "Separate installation of phpstan for larastan", "website": "https://github.com/phpstan/phpstan", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan", "namespace": "larastan", "links": {"%target-dir%/phpstan-larastan": "phpstan"} } }, "test": "phpstan-larastan --version", "tags": ["phpstan"] }, { "name": "larastan", "summary": "PHPStan extension for Laravel", "website": "https://github.com/nunomaduro/larastan", "command": { "composer-bin-plugin": { "package": "nunomaduro/larastan", "namespace": "larastan" } }, "test": "composer global bin phpstan show ergebnis/phpstan-rules", "tags": ["phpstan"] }, { "name": "phpstan-banned-code", "summary": "PHPStan rules for detecting calls to specific functions you don't want in your project", "website": "https://github.com/ekino/phpstan-banned-code", "command": { "composer-bin-plugin": { "package": "ekino/phpstan-banned-code", "namespace": "phpstan" } }, "test": "composer global bin phpstan show ekino/phpstan-banned-code", "tags": [ "phpstan" ] } ] } ================================================ FILE: resources/pre-installation.json ================================================ { "tools": [ { "name": "composer", "summary": "Dependency Manager for PHP", "website": "https://getcomposer.org/", "command": { "sh": { "command": "composer self-update" } }, "test": "composer list", "tags": ["pre-installation"] }, { "name": "phive", "summary": "PHAR Installation and Verification Environment", "website": "https://phar.io/", "command": { "file-download": { "url": "https://github.com/phar-io/phive/releases/download/0.16.0/phive-0.16.0.phar.asc", "file": "%target-dir%/phive.asc" }, "phar-download": { "phar": "https://github.com/phar-io/phive/releases/download/0.16.0/phive-0.16.0.phar", "bin": "%target-dir%/phive" }, "sh": { "command": "gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 && gpg --verify %target-dir%/phive.asc %target-dir%/phive" } }, "test": "phive --version", "tags": ["pre-installation"] }, { "name": "composer-bin-plugin", "summary": "Composer plugin to install bin vendors in isolated locations", "website": "https://github.com/bamarni/composer-bin-plugin", "command": { "sh": { "command": "composer global config --json extra.bamarni-bin.bin-links false && composer config --global --json allow-plugins.bamarni/composer-bin-plugin true" }, "composer-global-install": { "package": "bamarni/composer-bin-plugin" } }, "test": "composer global show bamarni/composer-bin-plugin", "tags": ["pre-installation"] }, { "name": "box", "summary": "Fast, zero config application bundler with PHARs", "website": "https://github.com/humbug/box", "command": { "phive-install": { "alias": "humbug/box", "bin": "%target-dir%/box", "sig": "2DF45277AEF09A2F" } }, "test": "box list", "tags": ["pre-installation"] } ] } ================================================ FILE: resources/psalm.json ================================================ { "tools": [ { "name": "psalm", "summary": "Finds errors in PHP applications", "website": "https://psalm.dev/", "command": { "composer-bin-plugin": { "package": "vimeo/psalm", "namespace": "psalm", "links": { "%target-dir%/psalm": "psalm", "%target-dir%/psalm-language-server": "psalm-language-server", "%target-dir%/psalm-plugin": "psalm-plugin", "%target-dir%/psalm-refactor": "psalm-refactor", "%target-dir%/psalter": "psalter" } } }, "test": "psalm -h", "tags": ["featured", "psalm"] }, { "name": "psalm-plugin-doctrine", "summary": "Stubs to let Psalm understand Doctrine better", "website": "https://github.com/weirdan/doctrine-psalm-plugin", "command": { "composer-bin-plugin": { "package": "weirdan/doctrine-psalm-plugin", "namespace": "psalm" } }, "test": "cd / && psalm-plugin show | grep weirdan/doctrine-psalm-plugin", "tags": ["psalm"] }, { "name": "psalm-plugin-phpunit", "summary": "Psalm plugin for PHPUnit", "website": "https://github.com/psalm/psalm-plugin-phpunit", "command": { "composer-bin-plugin": { "package": "psalm/plugin-phpunit", "namespace": "psalm" } }, "test": "cd / && psalm-plugin show | grep psalm/plugin-phpunit", "tags": ["psalm"] }, { "name": "psalm-plugin-symfony", "summary": "Psalm Plugin for Symfony", "website": "https://github.com/psalm/psalm-plugin-symfony", "command": { "composer-bin-plugin": { "package": "psalm/plugin-symfony", "namespace": "psalm" } }, "test": "cd / && psalm-plugin show | grep psalm/plugin-symfony", "tags": ["psalm"] } ] } ================================================ FILE: resources/refactoring.json ================================================ { "tools": [ { "name": "churn", "summary": "Discovers good candidates for refactoring", "website": "https://github.com/bmitch/churn-php", "command": { "phive-install": { "alias": "churn", "bin": "%target-dir%/churn", "sig": "96141E4421A9B0D5" } }, "test": "churn --version", "tags": ["featured", "refactoring"] }, { "name": "rector", "summary": "Tool for instant code upgrades and refactoring", "website": "https://github.com/rectorphp/rector", "command": { "composer-bin-plugin": { "package": "rector/rector", "namespace": "rector", "links": {"%target-dir%/rector": "rector"} } }, "test": "rector --version", "tags": ["refactoring"] } ] } ================================================ FILE: resources/security.json ================================================ { "tools": [ { "name": "psecio-parse", "summary": "Scans code for potential security-related issues", "website": "https://github.com/psecio/parse", "command": { "composer-bin-plugin": { "package": "psecio/parse:dev-master", "namespace": "legacy-php-parser", "links": {"%target-dir%/psecio-parse": "psecio-parse"} } }, "test": "psecio-parse --version", "tags": ["security"] }, { "name": "local-php-security-checker", "summary": "Checks composer dependencies for known security vulnerabilities", "website": "https://github.com/fabpot/local-php-security-checker", "command": { "file-download": { "url": "https://github.com/fabpot/local-php-security-checker/releases/download/v2.0.6/local-php-security-checker_2.0.6_linux_amd64", "file": "%target-dir%/local-php-security-checker" }, "sh": { "command": "chmod +x %target-dir%/local-php-security-checker" } }, "test": "local-php-security-checker --help", "tags": ["featured", "security"] } ] } ================================================ FILE: resources/test.json ================================================ { "tools": [ { "name": "behat", "summary": "Helps to test business expectations", "website": "http://behat.org/", "command": { "composer-bin-plugin": { "package": "behat/behat", "namespace": "behat", "links": {"%target-dir%/behat": "behat"} } }, "test": "behat --version", "tags": ["featured", "test"] }, { "name": "codeception", "summary": "Codeception is a BDD-styled PHP testing framework", "website": "https://codeception.com/", "command": { "phar-download": { "phar": "https://codeception.com/codecept.phar", "bin": "%target-dir%/codeception" } }, "test": "codeception --version", "tags": ["test", "exclude-php:8.5"] }, { "name": "infection", "summary": "AST based PHP Mutation Testing Framework", "website": "https://infection.github.io/", "command": { "phive-install": { "alias": "infection", "bin": "%target-dir%/infection", "sig": "C5095986493B4AA0" } }, "test": "infection --version", "tags": ["featured", "test"] }, { "name": "paratest", "summary": "Parallel testing for PHPUnit", "website": "https://github.com/paratestphp/paratest", "command": { "composer-bin-plugin": { "package": "brianium/paratest", "namespace": "paratest", "links": {"%target-dir%/paratest": "paratest"} } }, "test": "paratest --version", "tags": ["test"] }, { "name": "phpcov", "summary": "a command-line frontend for the PHP_CodeCoverage library", "website": "https://github.com/sebastianbergmann/phpcov", "command": { "phive-install": { "alias": "phpcov", "bin": "%target-dir%/phpcov", "sig": "4AA394086372C20A" } }, "test": "phpcov -v", "tags": ["test", "exclude-php:8.2", "exclude-php:8.3"] }, { "name": "php-fuzzer", "summary": "A fuzzer for PHP, which can be used to find bugs in libraries by feeding them 'random' inputs", "website": "https://github.com/nikic/PHP-Fuzzer", "command": { "phive-install": { "alias": "nikic/php-fuzzer", "bin": "%target-dir%/php-fuzzer" } }, "test": "php-fuzzer --help | grep 'Usage:'", "tags": ["test"] }, { "name": "phpspec", "summary": "SpecBDD Framework", "website": "http://www.phpspec.net/", "command": { "phive-install": { "alias": "phpspec/phpspec", "bin": "%target-dir%/phpspec" } }, "test": "phpspec --version", "tags": ["featured", "test", "exclude-php:8.5"] }, { "name": "phpunit", "summary": "The PHP testing framework", "website": "https://phpunit.de/", "command": { "phive-install": { "alias": "phpunit", "bin": "%target-dir%/phpunit", "sig": "4AA394086372C20A" } }, "test": "phpunit --version", "tags": ["featured", "test", "exclude-php:8.2", "exclude-php:8.3"] }, { "name": "phpunit-12", "summary": "The PHP testing framework (12.x version)", "website": "https://phpunit.de/", "command": { "phive-install": { "alias": "phpunit@^12.0", "bin": "%target-dir%/phpunit-12", "sig": "4AA394086372C20A" } }, "test": "phpunit-12 --version", "tags": ["test", "exclude-php:8.2"] }, { "name": "phpunit-11", "summary": "The PHP testing framework (11.x version)", "website": "https://phpunit.de/", "command": { "phive-install": { "alias": "phpunit@^11.0", "bin": "%target-dir%/phpunit-11", "sig": "4AA394086372C20A" } }, "test": "phpunit-11 --version", "tags": ["test"] }, { "name": "phpunit-10", "summary": "The PHP testing framework (10.x version)", "website": "https://phpunit.de/", "command": { "phive-install": { "alias": "phpunit@^10.0", "bin": "%target-dir%/phpunit-10", "sig": "4AA394086372C20A" } }, "test": "phpunit-10 --version", "tags": ["test"] }, { "name": "phpunit-9", "summary": "The PHP testing framework (9.x version)", "website": "https://phpunit.de/", "command": { "phive-install": { "alias": "phpunit@^9.0", "bin": "%target-dir%/phpunit-9", "sig": "4AA394086372C20A" } }, "test": "phpunit-9 --version", "tags": ["test"] }, { "name": "phpunit-8", "summary": "The PHP testing framework (8.x version)", "website": "https://phpunit.de/", "command": { "phive-install": { "alias": "phpunit@^8.0", "bin": "%target-dir%/phpunit-8", "sig": "4AA394086372C20A" } }, "test": "phpunit-8 --version", "tags": ["test"] }, { "name": "simple-phpunit", "summary": "Provides utilities to report legacy tests and usage of deprecated code", "website": "https://symfony.com/doc/current/components/phpunit_bridge.html", "command": { "composer-bin-plugin": { "package": "symfony/phpunit-bridge", "namespace": "symfony", "links": {"%target-dir%/simple-phpunit": "simple-phpunit"} }, "sh": { "command": "simple-phpunit install && SYMFONY_PHPUNIT_VERSION=9 simple-phpunit install" } }, "test": "simple-phpunit --version", "tags": ["test"] }, { "name": "kahlan", "summary": "Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec", "website": "https://kahlan.github.io/docs/", "command": { "composer-bin-plugin": { "package": "kahlan/kahlan", "namespace": "kahlan", "links": {"%target-dir%/kahlan": "kahlan"} } }, "test": "kahlan --version", "tags": ["test"] } ] } ================================================ FILE: resources/tools.json ================================================ { "tools": [ { "name": "diffFilter", "summary": "Applies QA tools to run on a single pull request", "website": "https://github.com/exussum12/coverageChecker", "command": { "composer-bin-plugin": { "package": "exussum12/coverage-checker", "namespace": "tools", "links": {"%target-dir%/diffFilter": "diffFilter"} } }, "test": "diffFilter -v", "tags": [] }, { "name": "phan", "summary": "Static Analysis Tool", "website": "https://github.com/phan/phan", "command": { "phar-download": { "phar": "https://github.com/phan/phan/releases/latest/download/phan.phar", "bin": "%target-dir%/phan" } }, "test": "phan -v", "tags": ["featured"] }, { "name": "phpbench", "summary": "PHP Benchmarking framework", "website": "https://github.com/phpbench/phpbench", "command": { "phive-install": { "alias": "phpbench", "bin": "%target-dir%/phpbench", "sig": "6FC579F5F0FCC966" } }, "test": "phpbench -V", "tags": [] }, { "name": "phpa", "summary": "Checks for weak assumptions", "website": "https://github.com/rskuipers/php-assumptions", "command": { "composer-bin-plugin": { "package": "rskuipers/php-assumptions", "namespace": "tools", "links": {"%target-dir%/phpa": "phpa"} } }, "test": "phpa --version", "tags": ["not-maintained"] }, { "name": "phpca", "summary": "Finds usage of non-built-in extensions", "website": "https://github.com/wapmorgan/PhpCodeAnalyzer", "command": { "composer-bin-plugin": { "package": "wapmorgan/php-code-analyzer", "namespace": "tools", "links": {"%target-dir%/phpca": "phpca"} } }, "test": "phpca -h" }, { "name": "phpcpd", "summary": "Copy/Paste Detector", "website": "https://github.com/sebastianbergmann/phpcpd", "command": { "phive-install": { "alias": "phpcpd", "bin": "%target-dir%/phpcpd", "sig": "4AA394086372C20A" } }, "test": "phpcpd -v", "tags": ["featured"] }, { "name": "phpmd", "summary": "A tool for finding problems in PHP code", "website": "https://phpmd.org/", "command": { "phive-install": { "alias": "phpmd", "bin": "%target-dir%/phpmd", "sig": "9093F8B32E4815AA" } }, "test": "phpmd --version" }, { "name": "phpmnd", "summary": "Helps to detect magic numbers", "website": "https://github.com/povils/phpmnd", "command": { "composer-bin-plugin": { "package": "povils/phpmnd", "namespace": "phpmnd", "links": {"%target-dir%/phpmnd": "phpmnd"} } }, "test": "phpmnd -V" } ] } ================================================ FILE: scoper.inc.php ================================================ true, 'expose-global-classes' => true, 'expose-global-functions' => true, 'exclude-files' => [ 'vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'vendor/symfony/polyfill-php80/bootstrap.php', 'vendor/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'vendor/symfony/polyfill-intl-normalizer/bootstrap.php', ], 'expose-namespaces' => [ 'Symfony\Polyfill\Php80', 'Symfony\Polyfill\Intl', ], ]; ================================================ FILE: src/Cli/Application.php ================================================ serviceContainer = $serviceContainer; $this->setCommandLoader($this->createCommandLoader($serviceContainer)); } /** * @throws \Throwable */ public function doRun(InputInterface $input, OutputInterface $output): int { $this->serviceContainer->set(InputInterface::class, $input); $this->serviceContainer->set(OutputInterface::class, $output); return parent::doRun($input, $output); } protected function getDefaultInputDefinition(): InputDefinition { $definition = parent::getDefaultInputDefinition(); $definition->addOption(new InputOption('tools', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Path(s) to the list of tools. Can also be set with TOOLBOX_JSON environment variable.', $this->toolsJsonDefault())); return $definition; } private function toolsJsonDefault(): array { return \getenv('TOOLBOX_JSON') ? \array_map('trim', \explode(',', \getenv('TOOLBOX_JSON'))) : [ __DIR__.'/../../resources/pre-installation.json', __DIR__.'/../../resources/architecture.json', __DIR__.'/../../resources/checkstyle.json', __DIR__.'/../../resources/compatibility.json', __DIR__.'/../../resources/composer.json', __DIR__.'/../../resources/deprecation.json', __DIR__.'/../../resources/documentation.json', __DIR__.'/../../resources/linting.json', __DIR__.'/../../resources/metrics.json', __DIR__.'/../../resources/phpcs.json', __DIR__.'/../../resources/phpstan.json', __DIR__.'/../../resources/psalm.json', __DIR__.'/../../resources/refactoring.json', __DIR__.'/../../resources/security.json', __DIR__.'/../../resources/test.json', __DIR__.'/../../resources/tools.json', ]; } private function createCommandLoader(ContainerInterface $container): CommandLoaderInterface { return new ContainerCommandLoader( $container, [ InstallCommand::NAME => InstallCommand::class, ListCommand::NAME => ListCommand::class, TestCommand::NAME => TestCommand::class, ] ); } } ================================================ FILE: src/Cli/Command/DefaultTag.php ================================================ useCase = $useCase; $this->runner = $runner; } protected function configure(): void { $this->setDescription('Installs tools'); $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it'); $this->addOption('target-dir', null, InputOption::VALUE_REQUIRED, 'The target installation directory', $this->defaultTargetDir()); $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag()); $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag()); } protected function execute(InputInterface $input, OutputInterface $output): int { return $this->runner->run(\call_user_func($this->useCase, new Filter($input->getOption('exclude-tag'), $input->getOption('tag')))); } } ================================================ FILE: src/Cli/Command/ListCommand.php ================================================ listTools = $listTools; } protected function configure(): void { $this->setDescription('Lists available tools'); $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag()); $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag()); } protected function execute(InputInterface $input, OutputInterface $output): int { $tools = \call_user_func($this->listTools, new Filter($input->getOption('exclude-tag'), $input->getOption('tag'))); $style = $this->createStyle($input, $output); $style->title('Available tools'); $style->table( ['Name', 'Summary'], $tools->map(function (Tool $tool) { return [\sprintf('%s', $tool->name()), $tool->summary().PHP_EOL.$tool->website().PHP_EOL]; })->toArray() ); return 0; } private function createStyle(InputInterface $input, OutputInterface $output): StyleInterface { return new SymfonyStyle($input, $output); } } ================================================ FILE: src/Cli/Command/TestCommand.php ================================================ useCase = $useCase; $this->runner = $runner; } protected function configure(): void { $this->setDescription('Runs basic tests to verify tools are installed'); $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the command without executing it'); $this->addOption('target-dir', null, InputOption::VALUE_REQUIRED, 'The target installation directory', $this->defaultTargetDir()); $this->addOption('exclude-tag', 'e', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to exclude', $this->defaultExcludeTag()); $this->addOption('tag', 't', InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, 'Tool tags to filter by', $this->defaultTag()); } protected function execute(InputInterface $input, OutputInterface $output): int { return $this->runner->run(\call_user_func($this->useCase, new Filter($input->getOption('exclude-tag'), $input->getOption('tag')))); } } ================================================ FILE: src/Cli/Runner/DryRunner.php ================================================ output = $output; } public function run(Command $command): int { $this->output->writeln((string) $command); return 0; } } ================================================ FILE: src/Cli/ServiceContainer/LazyRunner.php ================================================ factory = $factory; } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ public function run(Command $command): int { return $this->runner()->run($command); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function runner(): Runner { if (null === $this->runner) { $this->runner = $this->factory->createRunner(); } return $this->runner; } } ================================================ FILE: src/Cli/ServiceContainer/RunnerFactory.php ================================================ container = $container; } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ public function createRunner(): Runner { $runner = $this->createRealRunner(); if ($parameters = $this->parameters()) { return new ParametrisedRunner($runner, $parameters); } return $runner; } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createRealRunner(): DryRunner|PassthruRunner { if ($this->container->get(InputInterface::class)->getOption('dry-run')) { return new DryRunner($this->container->get(OutputInterface::class)); } return new PassthruRunner(); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function parameters(): array { if ($targetDir = $this->targetDir()) { return ['%target-dir%' => $targetDir]; } return []; } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function targetDir(): ?string { if (!$this->container->get(InputInterface::class)->hasOption('target-dir')) { return null; } $targetDir = $this->container->get(InputInterface::class)->getOption('target-dir'); if (!\is_dir($targetDir)) { throw new class(\sprintf('The target dir does not exist: "%s".', $targetDir)) extends \RuntimeException implements ContainerExceptionInterface { }; } return \realpath($targetDir); } } ================================================ FILE: src/Cli/ServiceContainer.php ================================================ 'createInstallCommand', ListCommand::class => 'createListCommand', TestCommand::class => 'createTestCommand', Runner::class => 'createRunner', InstallTools::class => 'createInstallToolsUseCase', ListTools::class => 'createListToolsUseCase', TestTools::class => 'createTestToolsUseCase', Tools::class => 'createTools', ]; private array $runtimeServices = [ InputInterface::class => null, OutputInterface::class => null, ]; public function set(string $id, /*object */$service): void { if (!\array_key_exists($id, $this->runtimeServices)) { throw new class(\sprintf('The "%s" runtime service is not expected.', $id)) extends RuntimeException implements ContainerExceptionInterface { }; } $this->runtimeServices[$id] = $service; } /** * {@inheritdoc} */ public function get(string $id) { if (isset($this->runtimeServices[$id])) { return $this->runtimeServices[$id]; } if (isset($this->services[$id])) { return \call_user_func([$this, $this->services[$id]]); } throw new class(\sprintf('The "%s" service is not registered in the service container.', $id)) extends RuntimeException implements NotFoundExceptionInterface { }; } /** * {@inheritdoc} */ public function has(string $id): bool { return isset($this->services[$id]) || isset($this->runtimeServices[$id]); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createInstallCommand(): InstallCommand { return new InstallCommand($this->get(InstallTools::class), $this->get(Runner::class)); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createListCommand(): ListCommand { return new ListCommand($this->get(ListTools::class)); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createTestCommand(): TestCommand { return new TestCommand($this->get(TestTools::class), $this->get(Runner::class)); } private function createRunner(): Runner { return new LazyRunner(new RunnerFactory($this)); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createInstallToolsUseCase(): InstallTools { return new InstallTools($this->get(Tools::class)); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createListToolsUseCase(): ListTools { return new ListTools($this->get(Tools::class)); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ private function createTestToolsUseCase(): TestTools { return new TestTools($this->get(Tools::class)); } private function createTools(): Tools { return new JsonTools(function (): array { return $this->get(InputInterface::class)->getOption('tools'); }); } } ================================================ FILE: src/Json/Factory/Assert.php ================================================ $command) { $commands = $commands->merge(self::createCommands($type, $command)); } if (0 === $commands->count()) { throw new \RuntimeException(\sprintf('No valid command defined for the tool: %s', \json_encode($tool))); } return 1 === $commands->count() ? $commands->toArray()[0] : new MultiStepCommand($commands); } private static function createCommands($type, $command): Collection { $factories = [ 'phar-download' => \sprintf('%s::import', PharDownloadCommandFactory::class), 'file-download' => \sprintf('%s::import', FileDownloadCommandFactory::class), 'box-build' => \sprintf('%s::import', BoxBuildCommandFactory::class), 'composer-install' => \sprintf('%s::import', ComposerInstallCommandFactory::class), 'phive-install' => \sprintf('%s::import', PhiveInstallCommandFactory::class), 'composer-global-install' => \sprintf('%s::import', ComposerGlobalInstallCommandFactory::class), 'composer-bin-plugin' => \sprintf('%s::import', ComposerBinPluginCommandFactory::class), 'sh' => \sprintf('%s::import', ShCommandFactory::class), ]; if (!isset($factories[$type])) { throw new \RuntimeException(\sprintf('Unrecognised command: "%s". Supported commands are: "%s".', $type, \implode(', ', \array_keys($factories)))); } $command = !\is_numeric(\key($command)) ? [$command] : $command; return Collection::create(\array_map(function ($c) use ($type, $factories) { return $factories[$type]($c); }, $command)); } } ================================================ FILE: src/Json/JsonTools.php ================================================ resourceLocator = $resourceLocator; } /** * @param Filter $filter * @return Collection */ public function all(Filter $filter): Collection { return $this->loadTools()->filter($filter); } private function loadTools(): Collection { return \array_reduce($this->resources(), function (Collection $tools, string $resource): Collection { return $tools->merge(Collection::create( \array_map(\sprintf('%s::import', ToolFactory::class), $this->loadJson($resource)) )); }, Collection::create([])); } private function loadJson(string $resource): array { $json = \json_decode(\file_get_contents($resource), true); if (!$json) { throw new RuntimeException(\sprintf('Failed to parse json: "%s"', $resource)); } if (!isset($json['tools']) || !\is_array($json['tools'])) { throw new RuntimeException(\sprintf('Failed to find any tools in: "%s".', $resource)); } return $json['tools']; } private function resources(): array { $resources = \call_user_func($this->resourceLocator); return \array_map(function (string $resource) { if (!\is_readable($resource)) { throw new InvalidArgumentException(\sprintf('Could not read the file: "%s".', $resource)); } return $resource; }, $resources); } } ================================================ FILE: src/Runner/ParametrisedRunner.php ================================================ decoratedRunner = $decoratedRunner; $this->parameters = $parameters; } public function run(Command $command): int { return $this->decoratedRunner->run(new class($command, $this->parameters) implements Command { private Command $command; private array $parameters; public function __construct(Command $command, array $parameters) { $this->command = $command; $this->parameters = $parameters; } public function __toString(): string { return \strtr((string) $this->command, $this->parameters); } }); } } ================================================ FILE: src/Runner/PassthruRunner.php ================================================ elements = $elements; } public static function create(array $elements): Collection { return new self($elements); } public function getIterator(): Traversable { yield from $this->elements; } public function merge(Collection $other): Collection { return self::create(\array_merge($this->elements, $other->elements)); } public function filter(callable $f): Collection { return self::create(\array_values(\array_filter($this->elements, $f))); } public function map(callable $f): Collection { return self::create(\array_map($f, $this->elements)); } public function reduce($initial, callable $param) { return \array_reduce($this->elements, $param, $initial); } public function sort(callable $f): Collection { $elements = $this->elements; \usort($elements, $f); return self::create($elements); } public function toArray(): array { return $this->elements; } public function count(): int { return \count($this->elements); } public function empty(): bool { return empty($this->elements); } } ================================================ FILE: src/Tool/Command/BoxBuildCommand.php ================================================ repository = $repository; $this->phar = $phar; $this->bin = $bin; $this->workDir = $workDir; $this->version = $version; } public function __toString(): string { return \sprintf( 'git clone %s %s&& cd %s && git checkout %s && composer install --no-dev --prefer-dist -n && box compile && mv %s %s && chmod +x %s && cd && rm -rf %s', $this->repository, $this->targetDir(), $this->targetDir(), $this->version ?? '$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null)', $this->phar, $this->bin, $this->bin, $this->targetDir() ); } private function targetDir(): string { $targetDir = \preg_replace('#^.*/(.*?)(.git)?$#', '$1', $this->repository); return \sprintf('%s/%s', $this->workDir, $targetDir !== $this->repository ? $targetDir : 'tmp'); } } ================================================ FILE: src/Tool/Command/ComposerBinPluginCommand.php ================================================ package = $package; $this->namespace = $namespace; $this->links = $links; } public function __toString(): string { return \sprintf('composer global bin %s require --prefer-dist --update-no-dev -n %s%s', $this->namespace, $this->package, $this->linkCommand()); } public function package(): string { return $this->package; } public function namespace(): string { return $this->namespace; } public function links(): Collection { return $this->links; } private function linkCommand(): string { return $this->links->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) { return $command.' && '.$link; }); } } ================================================ FILE: src/Tool/Command/ComposerBinPluginLinkCommand.php ================================================ source = $source; $this->target = $target; $this->namespace = $namespace; } public function __toString(): string { return \sprintf( 'ln -sf ${COMPOSER_HOME:-"~/.composer"}/vendor-bin/%s/vendor/bin/%s %s', $this->namespace, $this->source, $this->target ); } } ================================================ FILE: src/Tool/Command/ComposerGlobalInstallCommand.php ================================================ package = $package; } public function __toString(): string { return \sprintf('composer global require --prefer-dist --update-no-dev -n %s', $this->package); } public function package(): string { return $this->package; } } ================================================ FILE: src/Tool/Command/ComposerGlobalMultiInstallCommand.php ================================================ empty()) { throw new InvalidArgumentException('Collection of composer global install commands cannot be empty.'); } $this->commands = $commands->filter(function (ComposerGlobalInstallCommand $c) { return $c; }); } public function __toString(): string { $packages = \implode(' ', \array_map(function (ComposerGlobalInstallCommand $command) { return $command->package(); }, $this->commands->toArray())); return \sprintf('composer global require --prefer-dist --update-no-dev -n %s', $packages); } } ================================================ FILE: src/Tool/Command/ComposerInstallCommand.php ================================================ repository = $repository; $this->targetDir = $targetDir; $this->version = $version; } public function __toString(): string { return \sprintf( 'git clone %s %s && cd %s && git checkout %s && composer install --no-dev --prefer-dist -n', $this->repository, $this->targetDir, $this->targetDir, $this->version ?? '$(git describe --tags $(git rev-list --tags --max-count=1) 2>/dev/null)' ); } } ================================================ FILE: src/Tool/Command/FileDownloadCommand.php ================================================ url = $url; $this->file = $file; } public function __toString(): string { return \sprintf('curl -Ls -w %%{filename_effective}\'\n\' %s -o %s', $this->url, $this->file); } } ================================================ FILE: src/Tool/Command/MultiStepCommand.php ================================================ empty()) { throw new InvalidArgumentException('Collection of commands cannot be empty.'); } $this->commands = $commands->filter(function (Command $c) { return $c; }); $this->glue = $glue; } public function __toString(): string { return \implode($this->glue, $this->commands->toArray()); } } ================================================ FILE: src/Tool/Command/OptimisedComposerBinPluginCommand.php ================================================ empty()) { throw new InvalidArgumentException('Collection of composer bin plugin commands cannot be empty.'); } $this->commands = $commands->filter(function (ComposerBinPluginCommand $command) { return $command; }); } public function __toString(): string { return \implode(' && ', \array_merge($this->commandsToRun($this->packagesGroupedByNamespace()), $this->linksToCreate())); } private function packagesGroupedByNamespace(): array { return $this->commands->reduce([], function (array $packages, ComposerBinPluginCommand $command) { $packages[$command->namespace()][] = $command->package(); return $packages; }); } private function commandToRun(string $namespace, array $packages): string { return \sprintf('composer global bin %s require --prefer-dist --update-no-dev -n %s', $namespace, \implode(' ', $packages)); } private function commandsToRun(array $packagesGrouped): array { return \array_map([$this, 'commandToRun'], \array_keys($packagesGrouped), $packagesGrouped); } private function linksToCreate(): array { return $this->commands ->filter(function (ComposerBinPluginCommand $command) { return !$command->links()->empty(); }) ->map(function (ComposerBinPluginCommand $command) { return $command->links()->reduce('', function (string $command, ComposerBinPluginLinkCommand $link) { return !empty($command) ? $command.' && '.$link : $link; }); }) ->toArray(); } } ================================================ FILE: src/Tool/Command/PharDownloadCommand.php ================================================ phar = $phar; $this->bin = $bin; } public function __toString(): string { return \sprintf('curl -Ls -w %%{filename_effective}\'\n\' %s -o %s && chmod +x %s', $this->phar, $this->bin, $this->bin); } } ================================================ FILE: src/Tool/Command/PhiveInstallCommand.php ================================================ alias = $alias; $this->bin = $bin; $this->sig = $sig; } public function __toString(): string { $home = \sprintf('%s/.phive', \dirname($this->bin)); $tmp = \sprintf('%s/tmp/%s', $home, \md5($this->alias)); return \sprintf( 'phive --no-progress --home %s install %s %s -t %s && mv %s/* %s', $home, $this->sig ? '--trust-gpg-keys '.$this->sig : '--force-accept-unsigned', $this->alias, $tmp, $tmp, $this->bin ); } } ================================================ FILE: src/Tool/Command/ShCommand.php ================================================ command = $command; } public function __toString(): string { return $this->command; } } ================================================ FILE: src/Tool/Command/TestCommand.php ================================================ command = $command; $this->name = $name; } public function __toString(): string { return \sprintf('(output=$(%s 2>&1) && echo -e "\e[0;32m✔\e[0m︎%s" || (echo -e "\e[1;31m✘\e[0m%s\n$output" && false))', $this->command, $this->name, $this->name); } } ================================================ FILE: src/Tool/Command.php ================================================ excludedTags = $excludedTags; $this->tags = $tags; } public function __invoke(Tool $tool): bool { return $this->excludedTags === \array_diff($this->excludedTags, $tool->tags()) && (empty($this->tags) || \array_intersect($this->tags, $tool->tags())); } } ================================================ FILE: src/Tool/Tool.php ================================================ name = $name; $this->summary = $summary; $this->website = $website; $this->tags = \array_map(function (string $tag) { return $tag; }, $tags); $this->command = $command; $this->testCommand = $testCommand; } public function name(): string { return $this->name; } public function summary(): string { return $this->summary; } public function website(): string { return $this->website; } public function command(): Command { return $this->command; } public function testCommand(): Command { return $this->testCommand; } /** * @return array|string[] */ public function tags(): array { return $this->tags; } } ================================================ FILE: src/Tool/Tools.php ================================================ tools = $tools; } public function __invoke(Filter $filter): Command { $tools = $this->tools->all($filter); $installationCommands = $this->installationCommands($tools); $commandFilter = $this->commandFilter($this->toolCommands($tools)); return new MultiStepCommand( $installationCommands ->merge($commandFilter(ShCommand::class)) ->merge($commandFilter(FileDownloadCommand::class)) ->merge($commandFilter(PharDownloadCommand::class)) ->merge($commandFilter(PhiveInstallCommand::class)) ->merge($commandFilter(MultiStepCommand::class)) ->merge($this->groupComposerGlobalInstallCommands($commandFilter(ComposerGlobalInstallCommand::class))) ->merge($this->groupComposerBinPluginCommands($commandFilter(ComposerBinPluginCommand::class))) ->merge($commandFilter(ComposerInstallCommand::class)) ->merge($commandFilter(BoxBuildCommand::class)) ); } private function commandFilter(Collection $commands): Closure { return function ($type) use ($commands) { return $commands->filter(function (Command $command) use ($type) { return $command instanceof $type; }); }; } private function installationCommands(Collection $tools): Collection { return $tools->filter(function (Tool $tool) { return \in_array(self::PRE_INSTALLATION_TAG, $tool->tags()); })->map(function (Tool $tool) { return $tool->command(); }); } private function toolCommands(Collection $tools): Collection { return $tools->filter(function (Tool $tool) { return !\in_array(self::PRE_INSTALLATION_TAG, $tool->tags()); })->map(function (Tool $tool) { return $tool->command(); }); } private function groupComposerGlobalInstallCommands(Collection $commands): Collection { $commands = $commands->empty() ? [] : [new ComposerGlobalMultiInstallCommand($commands)]; return Collection::create($commands); } private function groupComposerBinPluginCommands(Collection $commands): Collection { $commands = $commands->empty() ? [] : [new OptimisedComposerBinPluginCommand($commands)]; return Collection::create($commands); } } ================================================ FILE: src/UseCase/ListTools.php ================================================ tools = $tools; } public function __invoke(Filter $filter): Collection { return $this->tools->all($filter); } } ================================================ FILE: src/UseCase/TestTools.php ================================================ tools = $tools; } public function __invoke(Filter $filter): Command { return new MultiStepCommand( $this->tools->all($filter)->map(function (Tool $tool) { return $tool->testCommand(); }) ); } } ================================================ FILE: tests/Cli/ApplicationTest.php ================================================ createStub(ServiceContainer::class); $this->app = new Application(self::VERSION, $container); } public function test_it_is_a_cli_application() { $this->assertInstanceOf(CliApplication::class, $this->app); } public function test_it_defines_the_app_name_and_version() { $this->assertSame('toolbox', $this->app->getName()); $this->assertSame(self::VERSION, $this->app->getVersion()); } public function test_it_defines_tools_option() { $this->assertTrue($this->app->getDefinition()->hasOption('tools')); $this->assertEquals( [ \realpath(__DIR__.'/../../src/Cli/').'/../../resources/pre-installation.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/architecture.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/checkstyle.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/compatibility.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/composer.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/deprecation.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/documentation.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/linting.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/metrics.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/phpcs.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/phpstan.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/psalm.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/refactoring.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/security.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/test.json', \realpath(__DIR__.'/../../src/Cli/').'/../../resources/tools.json' ], $this->app->getDefinition()->getOption('tools')->getDefault() ); } #[Putenv('TOOLBOX_JSON', 'resources/pre.json,resources/tools.json')] public function test_it_takes_the_tools_option_default_from_environment_if_present() { $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault()); } #[Putenv('TOOLBOX_JSON', 'resources/pre.json , resources/tools.json')] public function test_it_trims_the_tools_option() { $this->assertSame(['resources/pre.json', 'resources/tools.json'], $this->app->getDefinition()->getOption('tools')->getDefault()); } /** * @group integration */ public function test_it_allows_to_override_tools_location() { $app = new Application(self::VERSION, new ServiceContainer()); $result = $app->doRun( new ArrayInput([ 'command' => ListCommand::NAME, '--tools' => [__DIR__.'/../resources/tools.json'], '--no-interaction' => true, ]), new NullOutput() ); $this->assertSame(0, $result); } /** * @group integration */ public function test_it_runs_the_command_in_dry_run_mode() { $output = $this->givenOutputThatExpectsMessageWritten('composer global bin phpstan require'); $app = new Application(self::VERSION, new ServiceContainer()); $app->doRun( new ArrayInput([ 'command' => InstallCommand::NAME, '--dry-run' => true, '--tools' => [__DIR__.'/../resources/tools.json'], '--no-interaction' => true, ]), $output ); } public function givenOutputThatExpectsMessageWritten(string $message): OutputInterface { $output = $this->createMock(OutputInterface::class); $output->expects(self::once()) ->method('writeln') ->with(self::stringContains($message)); return $output; } } ================================================ FILE: tests/Cli/Command/InstallCommandTest.php ================================================ runner = $this->createStub(Runner::class); $this->useCase = $this->createStub(InstallTools::class); parent::setUp(); } public function test_it_runs_the_install_tools_use_case() { $command = $this->createCommand(); $this->useCase->method('__invoke')->willReturn($command); $this->runner->method('run')->with($command)->willReturn(0); $tester = $this->executeCliCommand(); $this->assertSame(0, $tester->getStatusCode()); } public function test_it_returns_the_status_code_of_the_run() { $this->useCase->method('__invoke')->willReturn($this->createCommand()); $this->runner->method('run')->willReturn(1); $tester = $this->executeCliCommand(); $this->assertSame(1, $tester->getStatusCode()); } public function test_it_filters_by_tags() { $this->useCase ->method('__invoke') ->with(new Filter(['foo'], ['bar'])) ->willReturn($this->createCommand()); $this->runner->method('run')->willReturn(0); $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]); $this->assertSame(0, $tester->getStatusCode()); } public function test_it_defines_dry_run_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run')); } public function test_it_defines_target_dir_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('target-dir')); $this->assertSame('/usr/local/bin', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault()); } #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')] public function test_it_takes_the_target_dir_option_default_from_environment_if_present() { $this->assertSame('/tmp', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault()); } public function test_it_defines_exclude_tag_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag')); $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault()); } #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')] public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present() { $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault()); } public function test_it_defines_tag_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag')); $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault()); } #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')] public function test_it_takes_the_tag_option_default_from_environment_if_present() { $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault()); } protected function getContainerTestDoubles(): array { return [ Runner::class => $this->runner, InstallTools::class => $this->useCase, ]; } private function createCommand(): Command { return new ShCommand('echo "foo"'); } } ================================================ FILE: tests/Cli/Command/ListCommandTest.php ================================================ useCase = $this->createStub(ListTools::class); parent::setUp(); } public function test_it_runs_the_list_tools_use_case() { $this->useCase->method('__invoke')->willReturn(Collection::create([ $this->createTool('Behat', 'Tests business expectations', 'http://behat.org'), ])); $tester = $this->executeCliCommand(); $this->assertSame(0, $tester->getStatusCode()); $this->assertMatchesRegularExpression('#Available tools#i', $tester->getDisplay()); $this->assertMatchesRegularExpression('#Behat.*?Tests business expectations.*?http://behat.org#smi', $tester->getDisplay()); } public function test_it_filters_by_tags() { $this->useCase->method('__invoke') ->with(new Filter(['foo'], ['bar'])) ->willReturn(Collection::create([ $this->createTool('Behat', 'Tests business expectations', 'http://behat.org'), ])); $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]); $this->assertSame(0, $tester->getStatusCode()); } public function test_it_defines_exclude_tag_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag')); $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault()); } #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')] public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present() { $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault()); } public function test_it_defines_tag_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag')); } #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')] public function test_it_takes_the_tag_option_default_from_environment_if_present() { $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault()); } protected function getContainerTestDoubles(): array { return [ ListTools::class => $this->useCase, ]; } private function createTool(string $name, string $summary, string $website): Tool { return new Tool( $name, $summary, $website, [], new ShCommand('any command'), new TestCommand('any test command', 'any') ); } } ================================================ FILE: tests/Cli/Command/TestCommandTest.php ================================================ runner = $this->createStub(Runner::class); $this->useCase = $this->createStub(TestTools::class); parent::setUp(); } public function test_it_runs_the_test_tools_use_case() { $command = $this->createCommand(); $this->useCase->method('__invoke')->willReturn($command); $this->runner->method('run')->with($command)->willReturn(0); $tester = $this->executeCliCommand(); $this->assertSame(0, $tester->getStatusCode()); } public function test_it_returns_the_status_code_of_the_run() { $this->useCase->method('__invoke')->willReturn($this->createCommand()); $this->runner->method('run')->willReturn(1); $tester = $this->executeCliCommand(); $this->assertSame(1, $tester->getStatusCode()); } public function test_it_filters_by_tags() { $this->useCase->method('__invoke')->with(new Filter(['foo'], ['bar']))->willReturn($this->createCommand()); $this->runner->method('run')->willReturn(0); $tester = $this->executeCliCommand(['--exclude-tag' => ['foo'], '--tag' => ['bar']]); $this->assertSame(0, $tester->getStatusCode()); } public function test_it_defines_dry_run_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('dry-run')); } public function test_it_defines_target_dir_option() { $this->assertSame('/usr/local/bin', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault()); $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('target-dir')); } #[Putenv('TOOLBOX_TARGET_DIR', '/tmp')] public function test_it_takes_the_target_dir_option_default_from_environment_if_present() { $this->assertSame('/tmp', $this->cliCommand()->getDefinition()->getOption('target-dir')->getDefault()); } public function test_it_defines_exclude_tag_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('exclude-tag')); $this->assertSame([], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault()); } #[Putenv('TOOLBOX_EXCLUDED_TAGS', 'foo,bar,baz')] public function test_it_takes_the_excluded_tag_option_default_from_environment_if_present() { $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('exclude-tag')->getDefault()); } public function test_it_defines_tag_option() { $this->assertTrue($this->cliCommand()->getDefinition()->hasOption('tag')); } #[Putenv('TOOLBOX_TAGS', 'foo,bar,baz')] public function test_it_takes_the_tag_option_default_from_environment_if_present() { $this->assertSame(['foo', 'bar', 'baz'], $this->cliCommand()->getDefinition()->getOption('tag')->getDefault()); } protected function getContainerTestDoubles(): array { return [ Runner::class => $this->runner, TestTools::class => $this->useCase, ]; } private function createCommand(): Command { return new ShCommand('true'); } } ================================================ FILE: tests/Cli/Command/ToolboxCommandTestCase.php ================================================ app = new Application('test', $this->createServiceContainer()); } public function test_it_provides_help() { $this->assertNotEmpty($this->cliCommand()->getDescription()); } protected function getContainerTestDoubles(): array { return []; } protected function executeCliCommand(array $input = []): CommandTester { $tester = new CommandTester($this->cliCommand()); $tester->execute($input); return $tester; } protected function cliCommand(): Command { return $this->app->find(static::CLI_COMMAND_NAME); } private function createServiceContainer(): ServiceContainer { return new class($this->getContainerTestDoubles()) extends ServiceContainer { private array $services; public function __construct(array $services) { $this->services = $services; } public function get($id) { if (isset($this->services[$id])) { return $this->services[$id]; } return parent::get($id); } }; } } ================================================ FILE: tests/Cli/Runner/DryRunnerTest.php ================================================ out = $this->createMock(OutputInterface::class); $this->runner = new DryRunner($this->out); } public function test_it_is_a_runner() { $this->assertInstanceOf(Runner::class, $this->runner); } public function test_it_sends_the_command_to_the_output() { $this->out->expects(self::once()) ->method('writeln') ->with('echo "Foo"'); $result = $this->runner->run(new class implements Command { public function __toString(): string { return 'echo "Foo"'; } }); $this->assertSame(0, $result); } } ================================================ FILE: tests/Cli/ServiceContainer/LazyRunnerTest.php ================================================ factory = $this->createMock(RunnerFactory::class); $this->lazyRunner = new LazyRunner($this->factory); } public function test_it_is_a_runner() { $this->assertInstanceOf(Runner::class, $this->lazyRunner); } public function test_it_returns_status_code_of_returned_by_the_created_runner() { $command = $this->command(); $runner = $this->givenRunner(command: $command, result: 1); $this->givenFactoryCreates($runner); $this->assertSame(1, $this->lazyRunner->run($command)); } public function test_it_only_initializes_the_runner_once() { $command = $this->command(); $runner = $this->givenRunner($command, 0); $this->factory ->expects(self::once()) ->method('createRunner') ->willReturn($runner); $this->lazyRunner->run($command); $this->lazyRunner->run($command); } public function givenRunner(Command $command, int $result): Runner { $runner = $this->createStub(Runner::class); $runner->method('run')->with($command)->willReturn($result); return $runner; } private function command(): Command { return new Command\ShCommand('any command'); } private function givenFactoryCreates(Runner $runner): void { $this->factory->method('createRunner')->willReturn($runner); } } ================================================ FILE: tests/Cli/ServiceContainer/RunnerFactoryTest.php ================================================ input = $this->givenInput([]); $this->output = $this->createMock(OutputInterface::class); $container = new class([ InputInterface::class => &$this->input, OutputInterface::class => &$this->output, ]) implements ContainerInterface { public function __construct(private readonly array $services) { } public function get(string $id) { return $this->services[$id]; } public function has(string $id): bool { return isset($this->services[$id]); } }; $this->runnerFactory = new RunnerFactory($container); } public function test_it_creates_the_passthru_runner_by_default() { $runner = $this->runnerFactory->createRunner(); $this->assertInstanceOf(PassthruRunner::class, $runner); } public function test_it_creates_the_dry_runner_if_dry_run_option_is_passed() { $this->givenInput(['--dry-run' => true]); $runner = $this->runnerFactory->createRunner(); $this->assertInstanceOf(DryRunner::class, $runner); } public function test_it_creates_the_parametrised_runner_if_target_dir_option_is_present() { $this->givenInput(['--target-dir' => '/usr/local/bin']); $runner = $this->runnerFactory->createRunner(); $this->assertInstanceOf(ParametrisedRunner::class, $runner); } public function test_the_parametrised_runner_includes_the_target_dir_parameter() { $this->givenInput(['--target-dir' => '/usr/local/bin', '--dry-run' => true]); $this->output->expects(self::once())->method('writeln')->with('ls /usr/local/bin'); $runner = $this->runnerFactory->createRunner(); $runner->run(new class implements Command { public function __toString(): string { return 'ls %target-dir%'; } }); } public function test_it_throws_an_exception_if_target_dir_does_not_exist() { $this->expectException(ContainerExceptionInterface::class); $this->givenInput(['--target-dir' => '/foo/bar/baz']); $this->runnerFactory->createRunner(); } public function test_it_uses_the_real_path_as_target_dir() { $this->givenInput(['--target-dir' => __DIR__.'/../../../bin', '--dry-run' => true]); $this->output->expects(self::once())->method('writeln')->with(\sprintf('ls %s', \realpath(__DIR__.'/../../../bin'))); $runner = $this->runnerFactory->createRunner(); $runner->run(new class implements Command { public function __toString(): string { return 'ls %target-dir%'; } }); } private function givenInput(array $parameters): InputInterface { $this->input = new ArrayInput($parameters, new InputDefinition(\array_filter([ new InputOption('dry-run', null, InputOption::VALUE_NONE), isset($parameters['--target-dir']) ? new InputOption('target-dir', null, InputOption::VALUE_REQUIRED) : null, ]))); return $this->input; } } ================================================ FILE: tests/Cli/ServiceContainerTest.php ================================================ container = new ServiceContainer(); $this->container->set(InputInterface::class, $this->createStub(InputInterface::class)); $this->container->set(OutputInterface::class, $this->createStub(OutputInterface::class)); } public function test_it_is_a_psr_container() { $this->assertInstanceOf(ContainerInterface::class, $this->container); } public function test_it_returns_false_if_service_is_not_registered() { $this->assertFalse($this->container->has('foo')); } #[DataProvider('provideApplicationServices')] public function test_it_creates_application_services(string $serviceId, string $expectedType) { $this->assertTrue($this->container->has($serviceId)); $this->assertInstanceOf($expectedType, $this->container->get($serviceId)); } public static function provideApplicationServices(): \Generator { yield [InstallCommand::class, InstallCommand::class]; yield [ListCommand::class, ListCommand::class]; yield [TestCommand::class, TestCommand::class]; yield [Runner::class, LazyRunner::class]; } public function test_it_throws_an_exception_if_unregistered_service_is_accessed() { $this->expectException(NotFoundExceptionInterface::class); $this->expectExceptionMessage('The "foo" service is not registered in the service container.'); $this->container->get('foo'); } public function test_it_registers_a_runtime_service() { $service = $this->createStub(InputInterface::class); $this->container->set(InputInterface::class, $service); $this->assertTrue($this->container->has(InputInterface::class)); $this->assertSame($service, $this->container->get(InputInterface::class)); } public function test_it_returns_false_if_runtime_service_has_not_been_defined() { $this->container = new ServiceContainer(); $this->assertFalse($this->container->has(InputInterface::class)); } public function test_it_throws_an_exception_if_missing_runtime_service_is_accessed() { $this->expectException(NotFoundExceptionInterface::class); $this->container = new ServiceContainer(); $this->container->get(InputInterface::class); } public function test_it_throws_an_exception_if_unknown_runtime_service_is_provided() { $this->expectException(ContainerExceptionInterface::class); $this->expectExceptionMessage('The "foo" runtime service is not expected.'); $this->container->set('foo', new \stdClass()); } } ================================================ FILE: tests/Json/Factory/AssertTest.php ================================================ expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Missing fields "b, d" in the Test: `{"a":"A","c":"C"}`.'); Assert::requireFields(['a', 'b', 'c', 'd'], ['a' => 'A', 'c' => 'C'], 'Test'); } } ================================================ FILE: tests/Json/Factory/BoxBuildCommandFactoryTest.php ================================================ self::REPOSITORY, 'phar' => self::PHAR, 'bin' => self::BIN, 'version' => self::VERSION, ]); $this->assertInstanceOf(BoxBuildCommand::class, $command); $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command); } public function test_the_version_is_not_required() { $command = BoxBuildCommandFactory::import([ 'repository' => self::REPOSITORY, 'phar' => self::PHAR, 'bin' => self::BIN, ]); $this->assertInstanceOf(BoxBuildCommand::class, $command); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_any_of_required_properties_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = [ 'repository' => self::REPOSITORY, 'phar' => self::PHAR, 'bin' => self::BIN, ]; unset($properties[$property]); BoxBuildCommandFactory::import($properties); } public static function provideRequiredProperties(): \Generator { yield ['repository']; yield ['phar']; yield ['bin']; } } ================================================ FILE: tests/Json/Factory/ComposerBinPluginCommandFactoryTest.php ================================================ self::PACKAGE, 'namespace' => self::NAMESPACE, ]); $this->assertInstanceOf(ComposerBinPluginCommand::class, $command); } public function test_it_creates_a_command_with_links_in_tools() { $command = ComposerBinPluginCommandFactory::import([ 'package' => self::PACKAGE, 'namespace' => self::NAMESPACE, 'links' => ['/tools/phpstan' => 'phpstan'], ]); $this->assertInstanceOf(ComposerBinPluginCommand::class, $command); $this->assertEquals( Collection::create([ new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE) ]), $command->links() ); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_any_of_required_properties_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = [ 'package' => self::PACKAGE, 'namespace' => self::NAMESPACE, ]; unset($properties[$property]); ComposerBinPluginCommandFactory::import($properties); } public static function provideRequiredProperties(): \Generator { yield ['package']; yield ['namespace']; } } ================================================ FILE: tests/Json/Factory/ComposerGlobalInstallCommandFactoryTest.php ================================================ self::PACKAGE, ]); $this->assertInstanceOf(ComposerGlobalInstallCommand::class, $command); } public function test_it_complains_if_package_is_missing() { $this->expectException(\InvalidArgumentException::class); ComposerGlobalInstallCommandFactory::import([]); } } ================================================ FILE: tests/Json/Factory/ComposerInstallCommandFactoryTest.php ================================================ self::REPOSITORY, 'target-dir' => self::LOCATION, 'version' => self::VERSION, ]); $this->assertInstanceOf(ComposerInstallCommand::class, $command); $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_a_required_property_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = [ 'repository' => self::REPOSITORY, 'target-dir' => self::LOCATION, 'version' => self::VERSION, ]; unset($properties[$property]); ComposerInstallCommandFactory::import($properties); } public static function provideRequiredProperties(): \Generator { yield ['repository']; yield ['target-dir']; } } ================================================ FILE: tests/Json/Factory/FileDownloadCommandFactoryTest.php ================================================ self::URL, 'file' => self::FILE, ]); $this->assertInstanceOf(FileDownloadCommand::class, $command); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_any_of_required_properties_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = [ 'url' => self::URL, 'file' => self::FILE, ]; unset($properties[$property]); FileDownloadCommandFactory::import($properties); } public static function provideRequiredProperties(): \Generator { yield ['url']; yield ['file']; } } ================================================ FILE: tests/Json/Factory/PharDownloadCommandFactoryTest.php ================================================ self::PHAR, 'bin' => self::BIN, ]); $this->assertInstanceOf(PharDownloadCommand::class, $command); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_any_of_required_properties_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = [ 'phar' => self::PHAR, 'bin' => self::BIN, ]; unset($properties[$property]); PharDownloadCommandFactory::import($properties); } public static function provideRequiredProperties(): \Generator { yield ['phar']; yield ['bin']; } } ================================================ FILE: tests/Json/Factory/PhiveInstallCommandFactoryTest.php ================================================ self::ALIAS, 'bin' => self::BIN, 'sig' => self::SIG ]); $this->assertInstanceOf(PhiveInstallCommand::class, $command); $this->assertStringNotContainsString('unsigned', (string)$command); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_any_of_required_properties_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = [ 'alias' => self::ALIAS, 'bin' => self::BIN, ]; unset($properties[$property]); $command = PhiveInstallCommandFactory::import($properties); $this->assertStringContainsString('unsigned', (string)$command); } public function test_it_accepts_unsigned_phars() { $properties = [ 'alias' => self::ALIAS, 'bin' => self::BIN ]; $command = PhiveInstallCommandFactory::import($properties); $this->assertStringContainsString('unsigned', (string)$command); } public static function provideRequiredProperties(): \Generator { yield ['alias']; yield ['bin']; } } ================================================ FILE: tests/Json/Factory/ShCommandFactoryTest.php ================================================ 'echo "42"', ]); $this->assertInstanceOf(ShCommand::class, $command); } public function test_it_complains_if_command_is_missing() { $this->expectException(\InvalidArgumentException::class); ShCommandFactory::import([]); } } ================================================ FILE: tests/Json/Factory/ToolFactoryTest.php ================================================ 'phpstan', 'summary' => 'Static analysis tool', 'website' => 'https://github.com/phpstan/phpstan', 'command' => [ 'composer-bin-plugin' => [ 'package' => 'phpstan/phpstan', 'namespace' => 'tools' ] ], 'test' => '/usr/bin/true', 'tags' => ['qa', 'static-analysis'], ]); $this->assertSame('phpstan', $tool->name()); $this->assertSame('Static analysis tool', $tool->summary()); $this->assertSame('https://github.com/phpstan/phpstan', $tool->website()); $this->assertSame(['qa', 'static-analysis'], $tool->tags()); $this->assertInstanceOf(Command::class, $tool->command()); $this->assertInstanceOf(TestCommand::class, $tool->testCommand()); } public function test_it_imports_the_composer_bin_plugin_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'composer-bin-plugin' => [ 'package' => 'phpstan/phpstan', 'namespace' => 'tools' ] ] ])); $this->assertInstanceOf(ComposerBinPluginCommand::class, $tool->command()); } public function test_it_imports_the_phar_download_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'phar-download' => [ 'phar' => 'phpstan/phpstan', 'bin' => 'tools' ] ] ])); $this->assertInstanceOf(PharDownloadCommand::class, $tool->command()); } public function test_it_imports_the_phive_install_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'phive-install' => [ 'alias' => 'phpstan/phpstan', 'bin' => 'tools' ] ] ])); $this->assertInstanceOf(PhiveInstallCommand::class, $tool->command()); } public function test_it_imports_the_file_download_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'file-download' => [ 'url' => 'http://example.com/file', 'file' => 'file' ] ] ])); $this->assertInstanceOf(FileDownloadCommand::class, $tool->command()); } public function test_it_imports_the_box_build_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'box-build' => [ 'repository' => 'https://github.com/behat/behat.git', 'phar' => 'behat.phar', 'bin' => '/usr/local/bin/behat', 'version' => 'v3.4.0', ] ] ])); $this->assertInstanceOf(BoxBuildCommand::class, $tool->command()); } public function test_it_imports_the_composer_install_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'composer-install' => [ 'repository' => 'https://github.com/behat/behat.git', 'target-dir' => '/usr/local/bin', 'version' => 'v3.4.0', ] ] ])); $this->assertInstanceOf(ComposerInstallCommand::class, $tool->command()); } public function test_it_imports_the_composer_global_install_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'composer-global-install' => [ 'package' => 'behat/behat', 'version' => 'v3.4.0', ] ] ])); $this->assertInstanceOf(ComposerGlobalInstallCommand::class, $tool->command()); } public function test_it_imports_the_sh_command() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'sh' => [ 'command' => 'echo "42"', ] ] ])); $this->assertInstanceOf(ShCommand::class, $tool->command()); } public function test_it_imports_multiple_commands() { $tool = ToolFactory::import($this->definition([ 'command' => [ 'phar-download' => [ 'phar' => 'phpstan/phpstan', 'bin' => 'tools' ], 'file-download' => [ [ 'url' => 'http://example.com/file1', 'file' => 'file1' ], [ 'url' => 'http://example.com/file2', 'file' => 'file2' ] ] ] ])); $this->assertInstanceOf(MultiStepCommand::class, $tool->command()); } public function test_it_complains_if_it_cannot_recognise_the_command() { $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches('/Unrecognised command: "foo". Supported commands are: "phar-download,.*?"/'); ToolFactory::import($this->definition(['command' => ['foo' => ['phar' => 'phpstan/phpstan']]])); } public function test_it_complains_if_the_command_is_empty() { $this->expectException(\RuntimeException::class); ToolFactory::import($this->definition(['command' => []])); } #[DataProvider('provideRequiredProperties')] public function test_it_complains_if_any_of_required_properties_is_missing(string $property) { $this->expectException(\InvalidArgumentException::class); $properties = $this->definition(); unset($properties[$property]); ToolFactory::import($properties); } public static function provideRequiredProperties(): \Generator { yield ['name']; yield ['summary']; yield ['website']; yield ['command']; yield ['test']; } private function definition(array $overrides = []): array { return \array_merge( [ 'name' => 'phpstan', 'summary' => 'Static analysis tool', 'website' => 'https://github.com/phpstan/phpstan', 'command' => [ 'composer-bin-plugin' => [ 'package' => 'phpstan/phpstan', 'namespace' => 'tools' ] ], 'test' => '/usr/bin/true', ], $overrides ); } } ================================================ FILE: tests/Json/JsonToolsTest.php ================================================ assertInstanceOf(Tools::class, new JsonTools($this->locator([__DIR__.'/../resources/tools.json']))); } public function test_it_throws_an_exception_if_resource_is_missing() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessageMatches('/Could not read the file/'); $tools = new JsonTools($this->locator(['/foo/tools.json'])); $tools->all($this->filter()); } public function test_it_throws_an_exception_if_resource_contains_invalid_json() { $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/Failed to parse json/'); $tools = new JsonTools($this->locator([__DIR__.'/../resources/invalid.json'])); $tools->all($this->filter()); } public function test_it_throws_an_exception_if_tools_are_not_present_in_the_resource() { $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/Failed to find any tools/'); $tools = new JsonTools($this->locator([__DIR__.'/../resources/no-tools.json'])); $tools->all($this->filter()); } public function test_it_throws_an_exception_if_tools_is_not_a_collection() { $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/Failed to find any tools/'); $tools = new JsonTools($this->locator([__DIR__.'/../resources/invalid-tools.json'])); $tools->all($this->filter()); } public function test_it_loads_tools_from_multiple_resources() { $tools = \iterator_to_array( (new JsonTools($this->locator([ __DIR__.'/../resources/pre-installation.json', __DIR__.'/../resources/tools.json', ])))->all($this->filter()) ); $this->assertCount(8, $tools); $this->assertSame('composer', $tools[0]->name()); $this->assertSame('composer-bin-plugin', $tools[1]->name()); $this->assertSame('box', $tools[2]->name()); $this->assertSame('analyze', $tools[3]->name()); $this->assertSame('behat', $tools[4]->name()); $this->assertSame('deptrac', $tools[5]->name()); $this->assertSame('infection', $tools[6]->name()); $this->assertSame('phpstan', $tools[7]->name()); } public function test_it_filters_out_tools() { $tools = \iterator_to_array( (new JsonTools($this->locator([__DIR__.'/../resources/tools.json'])))->all($this->filter(['static-analysis', 'testing'])) ); $this->assertCount(1, $tools); $this->assertSame('phpstan', $tools[0]->name()); } private function locator(array $resources): callable { return function () use ($resources): array { return $resources; }; } private function filter(array $excludedTags = []): Filter { return new Filter($excludedTags, []); } } ================================================ FILE: tests/Runner/ParametrisedRunnerTest.php ================================================ decoratedRunner = $this->createStub(Runner::class); $this->runner = new ParametrisedRunner($this->decoratedRunner, ['%foo%' => 'ABC']); } public function test_it_is_a_runner() { $this->assertInstanceOf(Runner::class, $this->runner); } public function test_it_replaces_parameter_holders_in_the_command_before_running_it() { $command = $this->command('echo "%foo%"'); $this->decoratedRunner->method('run') ->with(self::callback(function (Command $c) { if ('echo "ABC"' !== $c->__toString()) { throw new \RuntimeException(\sprintf('Expected `echo "ABC"`, but got `%s`.', $c->__toString())); } return true; })) ->willReturn(42); $exitCode = $this->runner->run($command); $this->assertSame(42, $exitCode); } private function command(string $commandString): Command { return new ShCommand($commandString); } } ================================================ FILE: tests/Runner/PassthruRunnerTest.php ================================================ assertInstanceOf(Runner::class, new PassthruRunner()); } public function test_it_returns_the_exit_code_of_the_run_command() { $runner = new PassthruRunner(); $this->assertSame(0, $runner->run(new ShCommand('true'))); $this->assertSame(1, $runner->run(new ShCommand('false'))); } public function test_it_outputs_commands_output() { $runner = new PassthruRunner(); \ob_start(); $runner->run(new ShCommand('echo "ABC"')); $this->assertSame('ABC'.PHP_EOL, \ob_get_clean()); } } ================================================ FILE: tests/Tool/CollectionTest.php ================================================ assertIterates($elements, $c); } public function test_it_is_cast_to_an_array() { $elements = [1, 2, 3]; $c = Collection::create($elements); $this->assertSame($elements, $c->toArray()); } public function test_it_merges_two_collections() { $c1 = Collection::create([1, 2, 3]); $c2 = Collection::create([4, 5]); $c = $c1->merge($c2); $this->assertNotSame($c1, $c, 'merge() creates a new collection'); $this->assertNotSame($c2, $c, 'merge() creates a new collection'); $this->assertIterates([1, 2, 3, 4, 5], $c); } public function test_it_filters_elements_in_the_collection() { $c = Collection::create([1, 2, 3, 4]); $filtered = $c->filter(function (int $e) { return 0 === $e % 2; }); $this->assertNotSame($c, $filtered, 'filter() creates a new collection'); $this->assertIterates([2, 4], $filtered); } public function test_it_maps_elements_in_the_collection() { $c = Collection::create([1, 2, 3, 4]); $mapped = $c->map(function (int $e) { return $e * 2; }); $this->assertNotSame($c, $mapped, 'map() creates a new collection'); $this->assertIterates([2, 4, 6, 8], $mapped); } public function test_it_folds_the_collection_left() { $c = Collection::create(['a', 'b', 'c']); $reduced = $c->reduce('d', function (string $a, string $b): string { return $a.$b; }); $this->assertNotSame($c, $reduced, 'reduce() creates a new collection'); $this->assertSame('dabc', $reduced); } public function test_it_counts_its_elements() { $this->assertSame(3, Collection::create(['a', 'b', 'c'])->count()); $this->assertSame(3, \count(Collection::create(['a', 'b', 'c']))); } public function test_it_checks_if_collection_is_empty() { $this->assertFalse(Collection::create(['a', 'b', 'c'])->empty()); $this->assertTrue(Collection::create([])->empty()); } public function test_it_sorts_the_collection() { $c = Collection::create(['ab', 'c', 'aa', 'aaa']); $sorted = $c->sort(function ($left, $right) { return \strcasecmp($left, $right); }); $this->assertIterates(['aa', 'aaa', 'ab', 'c'], $sorted); $this->assertIterates(['ab', 'c', 'aa', 'aaa'], $c, 'The original collection is not modified'); } private function assertIterates(array $elements, Collection $c, string $message = ''): void { $this->assertSame($elements, \iterator_to_array($c), $message); } } ================================================ FILE: tests/Tool/Command/BoxBuildCommandTest.php ================================================ assertInstanceOf(Command::class, $command); } public function test_it_generates_the_installation_command() { $command = new BoxBuildCommand( self::REPOSITORY, self::PHAR, self::BIN, self::TMP_DIR, self::VERSION ); $this->assertMatchesRegularExpression('#git clone '.self::REPOSITORY.'#', (string) $command); $this->assertMatchesRegularExpression('#cd /tools/behat#', (string) $command); $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command); $this->assertMatchesRegularExpression('#composer install --no-dev --prefer-dist -n#', (string) $command); $this->assertMatchesRegularExpression('#box compile#', (string) $command); } public function test_it_tries_to_guess_version_number_if_not_given_one() { $command = new BoxBuildCommand( self::REPOSITORY, self::PHAR, self::BIN, self::TMP_DIR ); $this->assertMatchesRegularExpression('#git checkout \$\(git describe --tags .*?\)#', (string) $command); } public function test_it_uses_a_generic_directory_if_name_cannot_be_guessed_from_the_repository() { $command = new BoxBuildCommand( 'example.com:foo.git', self::PHAR, self::BIN, self::TMP_DIR ); $this->assertMatchesRegularExpression('#cd /tools/tmp#', (string) $command); } } ================================================ FILE: tests/Tool/Command/ComposerBinPluginCommandTest.php ================================================ command = new ComposerBinPluginCommand( self::PACKAGE, self::NAMESPACE, Collection::create([]) ); } public function test_it_is_a_command() { $this->assertInstanceOf(Command::class, $this->command); } public function test_it_generates_the_installation_command() { $this->assertMatchesRegularExpression('#composer global bin tools require .*? phpstan/phpstan#', (string) $this->command); } public function test_it_exposes_the_package_and_namespace() { $this->assertSame(self::PACKAGE, $this->command->package()); $this->assertSame(self::NAMESPACE, $this->command->namespace()); } public function test_it_optionally_creates_a_symlink() { $links = Collection::create([ new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', self::NAMESPACE) ]); $this->command = new ComposerBinPluginCommand(self::PACKAGE, self::NAMESPACE, $links); $this->assertSame($links, $this->command->links()); $this->assertMatchesRegularExpression('#composer global bin tools require .*? phpstan/phpstan#', (string) $this->command); $this->assertMatchesRegularExpression('# && ln -sf.*?phpstan /tools/phpstan#', (string) $this->command); } public function test_it_does_not_create_a_symlink_if_links_option_was_not_given() { $this->assertDoesNotMatchRegularExpression('#ln -s#', (string) $this->command); } } ================================================ FILE: tests/Tool/Command/ComposerBinPluginLinkCommandTest.php ================================================ command = new ComposerBinPluginLinkCommand(self::SOURCE, self::TARGET, self::NAMESPACE); } public function test_it_is_a_command() { $this->assertInstanceOf(Command::class, $this->command); } public function test_it_generates_a_symlink_command() { $this->assertMatchesRegularExpression('#ln -sf \$\{COMPOSER_HOME:-"~/.composer"\}/vendor-bin/tools/vendor/bin/churn /tools/churn#', (string) $this->command); } } ================================================ FILE: tests/Tool/Command/ComposerGlobalInstallCommandTest.php ================================================ command = new ComposerGlobalInstallCommand(self::PACKAGE); } public function test_it_is_a_command() { $this->assertInstanceOf(Command::class, $this->command); } public function test_it_exposes_the_package_name() { $this->assertSame(self::PACKAGE, $this->command->package()); } public function test_it_generates_the_installation_command() { $this->assertMatchesRegularExpression('#composer global require .*? phan/phan#', (string) $this->command); } } ================================================ FILE: tests/Tool/Command/ComposerGlobalMultiInstallCommandTest.php ================================================ assertInstanceOf(Command::class, $command); } public function test_it_generates_a_single_installation_command() { $command = new ComposerGlobalMultiInstallCommand(Collection::create([ new ComposerGlobalInstallCommand('phan/phan'), new ComposerGlobalInstallCommand('phpstan/phpstan'), ])); $this->assertMatchesRegularExpression('#composer global require .*? phan/phan phpstan/phpstan#', (string) $command); } public function test_it_throws_an_exception_if_there_is_no_commands() { $this->expectException(InvalidArgumentException::class); new ComposerGlobalMultiInstallCommand(Collection::create([])); } } ================================================ FILE: tests/Tool/Command/ComposerInstallCommandTest.php ================================================ assertInstanceOf(Command::class, $command); } public function test_it_generates_the_installation_command() { $command = new ComposerInstallCommand(self::REPOSITORY, self::TARGET_DIR, self::VERSION); $this->assertMatchesRegularExpression('#git clone '.self::REPOSITORY.'#', (string) $command); $this->assertMatchesRegularExpression('#git checkout '.self::VERSION.'#', (string) $command); $this->assertMatchesRegularExpression('#composer install --no-dev --prefer-dist -n#', (string) $command); } public function test_it_tries_to_guess_version_number_if_not_given_one() { $command = new ComposerInstallCommand(self::REPOSITORY, self::TARGET_DIR); $this->assertMatchesRegularExpression('#git checkout \$\(git describe --tags .*?\)#', (string) $command); } } ================================================ FILE: tests/Tool/Command/FileDownloadCommandTest.php ================================================ command = new FileDownloadCommand(self::URL, self::FILE); } public function test_it_is_a_command() { $this->assertInstanceOf(Command::class, $this->command); } public function test_it_generates_the_installation_command() { $this->assertMatchesRegularExpression(\sprintf('#curl .*? %s -o %s#', self::URL, self::FILE), (string) $this->command); } } ================================================ FILE: tests/Tool/Command/MultiStepCommandTest.php ================================================ command('echo "A"')])); $this->assertInstanceOf(Command::class, $command); } public function test_it_glues_all_its_subcommands() { $command1 = $this->command('echo "A"'); $command2 = $this->command('echo "B"'); $command = new MultiStepCommand(Collection::create([$command1, $command2])); $this->assertSame('echo "A" && echo "B"', (string) $command); } public function test_it_glues_all_its_subcommands_with_a_custom_glue() { $command1 = $this->command('echo "A"'); $command2 = $this->command('echo "B"'); $command = new MultiStepCommand(Collection::create([$command1, $command2]), ' ; '); $this->assertSame('echo "A" ; echo "B"', (string) $command); } public function test_it_throws_an_exception_if_there_is_no_steps() { $this->expectException(InvalidArgumentException::class); new MultiStepCommand(Collection::create([])); } private function command(string $commandString): Command { return new ShCommand($commandString); } } ================================================ FILE: tests/Tool/Command/OptimisedComposerBinPluginCommandTest.php ================================================ assertInstanceOf(Command::class, new OptimisedComposerBinPluginCommand(Collection::create([new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([]))]))); } public function test_it_groups_composer_bin_command_by_namespace() { $commands = [ new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([])), new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([])), new ComposerBinPluginCommand('behat/behat', 'tools', Collection::create([])), ]; $command = new OptimisedComposerBinPluginCommand(Collection::create($commands)); $this->assertMatchesRegularExpression('#composer global bin phpstan require .*? phpstan/phpstan && composer global bin tools require .*? phan/phan behat/behat#', (string) $command); } public function test_it_throws_an_exception_if_there_is_no_commands() { $this->expectException(InvalidArgumentException::class); new OptimisedComposerBinPluginCommand(Collection::create([])); } public function test_it_creates_links_to_composer_bin_commands() { $commands = [ new ComposerBinPluginCommand( 'phpstan/phpstan', 'phpstan', Collection::create([ new ComposerBinPluginLinkCommand('phpstan', '/tools/phpstan', 'phpstan'), new ComposerBinPluginLinkCommand('phpstan', '/other/path/phpstan', 'phpstan'), ]) ), new ComposerBinPluginCommand( 'phan/phan', 'tools', Collection::create([ new ComposerBinPluginLinkCommand('phan', '/tools/phan', 'tools'), ]) ), new ComposerBinPluginCommand( 'behat/behat', 'tools', Collection::create([ new ComposerBinPluginLinkCommand('behat', '/tools/behat', 'tools'), ]) ), ]; $command = new OptimisedComposerBinPluginCommand(Collection::create($commands)); $this->assertMatchesRegularExpression('#composer global bin phpstan require .*? phpstan/phpstan && composer global bin tools require .*? phan/phan behat/behat#', (string) $command); $this->assertMatchesRegularExpression('# && ln -sf.*?phpstan /tools/phpstan#', (string) $command); $this->assertMatchesRegularExpression('# && ln -sf.*?phpstan /other/path/phpstan#', (string) $command); $this->assertMatchesRegularExpression('# && ln -sf.*?phan /tools/phan#', (string) $command); $this->assertMatchesRegularExpression('# && ln -sf.*?behat /tools/behat#', (string) $command); $this->assertDoesNotMatchRegularExpression('#&&\s*&&#', (string) $command, 'It does not generate empty commands'); } public function test_it_does_not_create_links_if_commands_have_no_links_defined() { $commands = [ new ComposerBinPluginCommand('phpstan/phpstan', 'phpstan', Collection::create([])), new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([])), new ComposerBinPluginCommand('behat/behat', 'tools', Collection::create([])), ]; $command = new OptimisedComposerBinPluginCommand(Collection::create($commands)); $this->assertDoesNotMatchRegularExpression('#ln -s#', (string) $command); $this->assertDoesNotMatchRegularExpression('#&&\s*&&#', (string) $command, 'It does not generate empty commands'); } } ================================================ FILE: tests/Tool/Command/PharDownloadCommandTest.php ================================================ command = new PharDownloadCommand(self::PHAR, self::BIN); } public function test_it_is_a_command() { $this->assertInstanceOf(Command::class, $this->command); } public function test_it_generates_the_installation_command() { $this->assertMatchesRegularExpression(\sprintf('#curl .*? %s -o %s#', self::PHAR, self::BIN), (string) $this->command); } } ================================================ FILE: tests/Tool/Command/PhiveInstallCommandTest.php ================================================ command = new PhiveInstallCommand(self::ALIAS, self::BIN, self::SIG); } public function test_it_is_a_command() { $this->assertInstanceOf(Command::class, $this->command); } public function test_it_generates_the_installation_command() { $this->assertMatchesRegularExpression(\sprintf('#phive --no-progress --home [^\s]*? install --trust-gpg-keys %s %s -t [^\s]++ && mv [^\s]+? %s#', self::SIG, self::ALIAS, self::BIN), (string) $this->command); } public function test_it_accepts_unsigned_phar_command() { $command = new PhiveInstallCommand(self::ALIAS, self::BIN); $this->assertMatchesRegularExpression(\sprintf('#phive --no-progress --home [^\s]*? install --force-accept-unsigned %s -t [^\s]++ && mv [^\s]+?#', self::ALIAS, self::BIN), (string) $command); } } ================================================ FILE: tests/Tool/Command/ShCommandTest.php ================================================ assertInstanceOf(Command::class, new ShCommand('echo')); } public function test_it_returns_the_command() { $this->assertSame('echo "A"', (string) new ShCommand('echo "A"')); } } ================================================ FILE: tests/Tool/Command/TestCommandTest.php ================================================ assertInstanceOf(Command::class, new TestCommand('/usr/bin/true', 'true')); } public function test_it_generates_the_command() { $this->assertMatchesRegularExpression( '#\(\(/usr/bin/true > /dev/null && echo -e .*?✔\.*?\) || \(echo -e .*?✘.*?" && false\)\)#', (string) new TestCommand('/usr/bin/true', 'true') ); } } ================================================ FILE: tests/Tool/FilterTest.php ================================================ assertTrue($filter($this->tool(['phpspec', 'phpstan']))); } public function test_it_returns_true_if_no_excluded_tags_match() { $filter = new Filter(['exclude-php:7.3'], []); $this->assertTrue($filter($this->tool(['phpspec', 'phpstan']))); } public function test_it_returns_true_if_tool_has_no_tags() { $filter = new Filter(['exclude-php:7.3'], []); $this->assertTrue($filter($this->tool([]))); } public function test_it_returns_true_if_neither_tool_nor_excluded_tags_were_defined() { $filter = new Filter([], []); $this->assertTrue($filter($this->tool([]))); } public function test_it_returns_false_if_one_excluded_tag_matches() { $filter = new Filter(['exclude-php:7.3'], []); $this->assertFalse($filter($this->tool(['phpspec', 'phpstan', 'exclude-php:7.3']))); } public function test_it_returns_false_if_multiple_excluded_tags_match() { $filter = new Filter(['exclude-php:7.3', 'phpstan'], []); $this->assertFalse($filter($this->tool(['phpspec', 'phpstan', 'exclude-php:7.3']))); } public function test_it_returns_false_if_all_excluded_tags_match() { $filter = new Filter(['exclude-php:7.3', 'phpspec', 'phpstan'], []); $this->assertFalse($filter($this->tool(['phpspec', 'phpstan', 'exclude-php:7.3']))); } public function test_it_returns_true_if_a_tag_matches() { $filter = new Filter([], ['phpspec']); $this->assertTrue($filter($this->tool(['phpspec', 'phpstan']))); } public function test_it_returns_true_if_all_tags_match_exactly() { $filter = new Filter([], ['phpspec', 'phpstan']); $this->assertTrue($filter($this->tool(['phpspec', 'phpstan']))); } public function test_it_returns_true_if_all_tags_match() { $filter = new Filter([], ['phpspec', 'phpstan', 'foo', 'bar']); $this->assertTrue($filter($this->tool(['phpspec', 'phpstan']))); } public function test_it_returns_false_if_the_tool_has_no_tags_to_match() { $filter = new Filter([], ['phpspec']); $this->assertFalse($filter($this->tool(['phpstan']))); } public function test_it_returns_false_if_a_tag_is_both_included_and_excluded() { $filter = new Filter(['phpstan'], ['phpspec', 'phpstan']); $this->assertFalse($filter($this->tool(['phpspec', 'phpstan']))); } private function tool(array $tags): Tool { return new Tool( 'any name', 'any summary', 'https://example.com', $tags, new ShCommand('any command'), new TestCommand('any test command', 'any') ); } } ================================================ FILE: tests/Tool/ToolTest.php ================================================ anyCommand(); $testCommand = $this->anyCommand(); $tool = new Tool('phpstan', 'Static analysis tool', 'https://github.com/phpstan/phpstan', ['qa', 'static-analysis'], $command, $testCommand); $this->assertSame('phpstan', $tool->name()); $this->assertSame('Static analysis tool', $tool->summary()); $this->assertSame('https://github.com/phpstan/phpstan', $tool->website()); $this->assertSame(['qa', 'static-analysis'], $tool->tags()); $this->assertSame($command, $tool->command()); $this->assertSame($testCommand, $tool->testCommand()); } public function test_tags_can_only_be_strings() { $this->expectException(TypeError::class); $command = $this->anyCommand(); $testCommand = $this->anyCommand(); new Tool('phpstan', 'Static analysis tool', 'https://github.com/phpstan/phpstan', [['qa'], ['static-analysis']], $command, $testCommand); } /** * @return object */ public function anyCommand(): object { return new ShCommand('any command'); } } ================================================ FILE: tests/UseCase/InstallToolsTest.php ================================================ tools = $this->createStub(Tools::class); $this->useCase = new InstallTools($this->tools); } public function test_it_returns_a_multi_step_command() { $filter = $this->filter(); $this->givenToolsFor($filter, Collection::create([$this->tool(new ShCommand('echo "Foo"'))])); $command = $this->useCase->__invoke($filter); $this->assertInstanceOf(MultiStepCommand::class, $command); } public function test_it_groups_composer_global_install_commands() { $this->givenTools(Collection::create([ $this->tool(new ComposerGlobalInstallCommand('phpstan/phpstan')), $this->tool(new ComposerGlobalInstallCommand('phan/phan')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#composer global require .* phpstan/phpstan phan/phan#', (string)$command); } public function test_it_does_not_include_empty_commands() { $this->givenTools(Collection::create([ $this->tool(new ShCommand('echo "Foo"')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertDoesNotMatchRegularExpression('#composer global require#', (string)$command, 'Composer commands are not grouped if there is none.'); $this->assertDoesNotMatchRegularExpression('#&&\s*$#', (string)$command, 'Empty commands are not included.'); } public function test_it_groups_composer_bin_plugin_commands() { $this->givenTools(Collection::create([ $this->tool(new ComposerBinPluginCommand('phpstan/phpstan', 'tools', Collection::create([]))), $this->tool(new ComposerBinPluginCommand('phan/phan', 'tools', Collection::create([]))), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#composer global bin tools require .* phpstan/phpstan phan/phan#', (string)$command); } public function test_it_includes_installation_tagged_commands_before_other_ones() { $this->givenTools(Collection::create([ $this->tool(new ShCommand('echo "Foo"')), $this->tool(new ShCommand('echo "Installation"'), [InstallTools::PRE_INSTALLATION_TAG]), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#echo "Installation".*echo "Foo"#smi', (string)$command, 'Installation commands are included before other ones.'); $this->assertDoesNotMatchRegularExpression('#echo "Installation".*echo "Installation"#smi', (string)$command, 'Installation commands are not duplicated.'); } public function test_it_includes_shell_commands() { $this->givenTools(Collection::create([ $this->tool(new ShCommand('echo "Foo"')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#echo "Foo"#', (string)$command); } public function test_it_includes_multi_step_commands() { $this->givenTools(Collection::create([ $this->tool(new MultiStepCommand(Collection::create([ new ShCommand('echo "Foo"'), new ShCommand('echo "Bar"') ]))), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#echo "Foo" && echo "Bar"#', (string)$command); } public function test_it_includes_composer_install_commands() { $this->givenTools(Collection::create([ $this->tool(new ComposerInstallCommand('git@github.com:phpspec/phpspec.git', '/usr/local/bin')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#git clone git@github.com:phpspec/phpspec.git#', (string)$command); } public function test_it_includes_box_build_commands() { $this->givenTools(Collection::create([ $this->tool(new BoxBuildCommand('https://github.com/behat/behat.git', 'behat.phar', '/tools/behat', '/tmp')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#box compile#', (string)$command); } public function test_it_includes_phar_download_commands() { $this->givenTools(Collection::create([ $this->tool(new PharDownloadCommand('https://github.com/sensiolabs-de/deptrac/releases/download/0.2.0/deptrac-0.2.0.phar', '/tools/phar')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#curl[^&]*?deptrac-0.2.0.phar#', (string)$command); } public function test_it_includes_phive_install_commands() { $this->givenTools(Collection::create([ $this->tool(new PhiveInstallCommand('phpunit', '/tools/phpunit')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#phive --no-progress --home /tools/.phive install[^&]*?phpunit[^&]*? [^\s]++ && mv [^\s]++ /tools/phpunit#', (string)$command); } public function test_it_includes_file_download_commands() { $this->givenTools(Collection::create([ $this->tool(new FileDownloadCommand('https://github.com/fabpot/local-php-security-checker/releases/download/v1.0.0/local-php-security-checker_1.0.0_linux_amd64', '/tools/security-checker')), ])); $command = $this->useCase->__invoke($this->filter()); $this->assertMatchesRegularExpression('#curl[^&]*?local-php-security-checker_1.0.0_linux_amd64#', (string)$command); } private function filter(): Filter { return new Filter([], []); } private function tool(Command $command, array $tags = []): Tool { return new Tool( "any name", "any summary", "https://example.com", $tags, $command, new TestCommand("any test command", "any test name") ); } private function givenToolsFor(Filter $filter, Collection $tools): void { $this->tools->method('all') ->with($filter) ->willReturn($tools); } private function givenTools(Collection $tools): void { $this->tools->method('all') ->willReturn($tools); } } ================================================ FILE: tests/UseCase/ListToolsTest.php ================================================ anyTool(), $this->anyTool()]); $filter = $this->filter(); $repository = $this->givenToolsFor($filter, $tools); $useCase = new ListTools($repository); $this->assertSame($tools, $useCase($filter)); } private function filter(): Filter { return new Filter([], []); } private function anyTool(): Tool { return new Tool( "any name", "any summary", "https://example.com", [], new Command\ShCommand("any command"), new TestCommand("any test command", "any test name") ); } private function givenToolsFor(Filter $filter, Collection $tools): Tools { $repository = $this->createStub(Tools::class); $repository->method('all')->with($filter)->willReturn($tools); return $repository; } } ================================================ FILE: tests/UseCase/TestToolsTest.php ================================================ command('echo "a"'), $this->command('echo "b"'), ]; $filter = $this->filter(); $tools = $this->tools($testCommands, $filter); $useCase = new TestTools($tools); $this->assertSame('echo "a" && echo "b"', (string) $useCase($filter)); } private function tool(Command $testCommand): Tool { return new Tool( "any name", "any summary", "https://example.com", [], new Command\ShCommand("any command"), $testCommand ); } private function command(string $command): Command { return new ShCommand($command); } private function tools(array $testCommands, Filter $filter): Tools { $tools = $this->createStub(Tools::class); $tools->method('all')->with($filter)->willReturn(Collection::create( \array_map(fn ($command) => $this->tool($command), $testCommands) )); return $tools; } private function filter(): Filter { return new Filter([], []); } } ================================================ FILE: tests/resources/invalid-tools.json ================================================ {"tools": "bar"} ================================================ FILE: tests/resources/invalid.json ================================================ {"tools":} ================================================ FILE: tests/resources/no-tools.json ================================================ {"foo": "bar"} ================================================ FILE: tests/resources/pre-installation.json ================================================ { "tools": [ { "name": "composer", "summary": "Dependency Manager for PHP", "website": "https://getcomposer.org/", "command": { "sh": { "command": "composer self-update" } }, "test": "composer list" }, { "name": "composer-bin-plugin", "summary": "Composer plugin to install bin vendors in isolated locations", "website": "https://github.com/bamarni/composer-bin-plugin", "command": { "sh": { "command": "composer global require bamarni/composer-bin-plugin" } }, "test": "composer global show bamarni/composer-bin-plugin" }, { "name": "box", "summary": "An application for building and managing Phars", "website": "https://box-project.github.io/box2/", "command": { "sh": { "command": "curl -Ls https://box-project.github.io/box2/installer.php | php && mv box.phar /usr/local/bin/box && chmod +x /usr/local/bin/box" } }, "test": "box list" } ] } ================================================ FILE: tests/resources/tools.json ================================================ { "tools": [ { "name": "analyze", "summary": "Visualizes metrics and source code", "website": "https://github.com/Qafoo/QualityAnalyzer", "command": { "composer-install": { "repository": "https://github.com/Qafoo/QualityAnalyzer.git", "target-dir": "/usr/local/bin/QualityAnalyzer" } }, "test": "analyze list", "tags": ["static-analysis"] }, { "name": "behat", "summary": "Helps to test business expectations", "website": "http://behat.org/", "command": { "box-build": { "repository": "https://github.com/behat/behat.git", "phar": "behat.phar", "bin": "/usr/local/bin/behat" } }, "test": "behat --version", "tags": ["testing"] }, { "name": "deptrac", "summary": "Enforces dependency rules", "website": "https://github.com/sensiolabs-de/deptrac", "command": { "phar-download": { "phar": "http://get.sensiolabs.de/deptrac.phar", "bin": "/usr/local/bin/deptrac" } }, "test": "deptrac list", "tags": ["static-analysis"] }, { "name": "infection", "summary": "AST based PHP Mutation Testing Framework", "website": "https://infection.github.io/", "command": { "file-download": { "url": "https://github.com/infection/infection/releases/download/0.10.0/infection.phar.asc", "file": "/usr/local/bin/infection.phar.asc" }, "phar-download": { "phar": "https://github.com/infection/infection/releases/download/0.10.0/infection.phar", "bin": "/usr/local/bin/infection" } }, "test": "infection --version", "tags": ["testing"] }, { "name": "phpstan", "summary": "Static Analysis Tool", "website": "https://github.com/phpstan/phpstan", "command": { "composer-bin-plugin": { "package": "phpstan/phpstan", "namespace": "phpstan" } }, "test": "phpstan --version", "tags": ["phpstan"] } ] } ================================================ FILE: tools/.gitignore ================================================ * !.gitignore