├── .editorconfig ├── .phpspec └── specification.tpl ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── UPGRADE-v2.md ├── appveyor.yml ├── bin └── grumphp ├── composer.json ├── composer.lock ├── doc ├── commands.md ├── conventions.md ├── extensions.md ├── faq.md ├── installation │ ├── exotic.md │ └── global.md ├── parameters.md ├── runner.md ├── tasks.md ├── tasks │ ├── ant.md │ ├── atoum.md │ ├── behat.md │ ├── brunch.md │ ├── clover_coverage.md │ ├── codeception.md │ ├── composer.md │ ├── composer_normalize.md │ ├── composer_require_checker.md │ ├── composer_script.md │ ├── deptrac.md │ ├── doctrine_orm.md │ ├── ecs.md │ ├── eslint.md │ ├── file_size.md │ ├── gherkin.md │ ├── git_blacklist.md │ ├── git_branch_name.md │ ├── git_commit_message.md │ ├── grunt.md │ ├── gulp.md │ ├── infection.md │ ├── jsonlint.md │ ├── kahlan.md │ ├── make.md │ ├── npm_script.md │ ├── paratest.md │ ├── pest.md │ ├── phan.md │ ├── phing.md │ ├── php7cc.md │ ├── phparkitect.md │ ├── phpcpd.md │ ├── phpcs.md │ ├── phpcsfixer.md │ ├── phplint.md │ ├── phpmd.md │ ├── phpmnd.md │ ├── phpparser.md │ ├── phpspec.md │ ├── phpstan.md │ ├── phpunit.md │ ├── phpunitbridge.md │ ├── phpversion.md │ ├── progpilot.md │ ├── psalm.md │ ├── rector.md │ ├── robo.md │ ├── securitychecker.md │ ├── securitychecker │ │ ├── composeraudit.md │ │ ├── enlightn.md │ │ ├── local.md │ │ ├── roave.md │ │ └── symfony.md │ ├── shell.md │ ├── stylelint.md │ ├── symfony_console.md │ ├── tester.md │ ├── twigcs.md │ ├── twigcsfixer.md │ ├── xmllint.md │ └── yamllint.md └── testsuites.md ├── grumphp.yml.dist ├── phpspec.yml ├── phpunit.xml.dist ├── psalm.xml ├── resources ├── ascii │ ├── failed.txt │ ├── grumphp-grumpy.txt │ ├── grumphp-happy.txt │ ├── me-gusta.txt │ ├── nopecat.txt │ └── succeeded.txt ├── config │ ├── config.yml │ ├── console.yml │ ├── fixer.yml │ ├── formatter.yml │ ├── linters.yml │ ├── locators.yml │ ├── parsers.yml │ ├── runner.yml │ ├── services.yml │ ├── subscribers.yml │ ├── tasks.yml │ └── util.yml ├── hooks │ ├── local │ │ ├── commit-msg │ │ └── pre-commit │ └── vagrant │ │ ├── commit-msg │ │ └── pre-commit └── logo │ ├── grumphp-grumpy.png │ ├── grumphp-happy.png │ ├── simplified-grumpy.png │ └── simplified-happy.png └── src ├── Collection ├── FilesCollection.php ├── LintErrorsCollection.php ├── ParseErrorsCollection.php ├── ProcessArgumentsCollection.php ├── TaskResultCollection.php ├── TasksCollection.php └── TestSuiteCollection.php ├── Composer ├── DevelopmentIntegrator.php └── GrumPHPPlugin.php ├── Configuration ├── Compiler │ ├── ExtensionCompilerPass.php │ ├── PhpParserCompilerPass.php │ ├── RegisterListenersPass.php │ ├── TaskCompilerPass.php │ └── TestSuiteCompilerPass.php ├── Configuration.php ├── Configurator │ └── TaskConfigurator.php ├── ContainerBuilder.php ├── ContainerFactory.php ├── Environment │ ├── DotEnvRegistrar.php │ ├── DotEnvSerializer.php │ └── PathsRegistrar.php ├── GrumPHPExtension.php ├── GuessedPaths.php ├── Loader │ └── DistFileLoader.php ├── LoaderFactory.php ├── Model │ ├── AsciiConfig.php │ ├── EnvConfig.php │ ├── FixerConfig.php │ ├── GitStashConfig.php │ ├── HooksConfig.php │ ├── ParallelConfig.php │ ├── ProcessConfig.php │ └── RunnerConfig.php └── Resolver │ └── TaskConfigResolver.php ├── Console ├── ApplicationConfigurator.php └── Command │ ├── ConfigureCommand.php │ ├── Git │ ├── CommitMsgCommand.php │ ├── DeInitCommand.php │ ├── InitCommand.php │ └── PreCommitCommand.php │ └── RunCommand.php ├── Event ├── Dispatcher │ ├── Bridge │ │ └── SymfonyEventDispatcher.php │ └── EventDispatcherInterface.php ├── Event.php ├── RunnerEvent.php ├── RunnerEvents.php ├── RunnerFailedEvent.php ├── Subscriber │ ├── StashUnstagedChangesSubscriber.php │ └── VerboseLoggerSubscriber.php ├── TaskEvent.php ├── TaskEvents.php └── TaskFailedEvent.php ├── Exception ├── ExceptionInterface.php ├── ExecutableNotFoundException.php ├── FileNotFoundException.php ├── FixerException.php ├── InvalidArgumentException.php ├── ParallelException.php ├── PlatformException.php ├── ProcessException.php ├── RuntimeException.php └── TaskConfigResolverException.php ├── Extension └── ExtensionInterface.php ├── Fixer ├── FixResult.php ├── FixerUpper.php └── Provider │ ├── FixableProcessProvider.php │ └── FixableProcessResultProvider.php ├── Formatter ├── GitBlacklistFormatter.php ├── PhpCsFixerFormatter.php ├── PhpcsFormatter.php ├── ProcessFormatterInterface.php └── RawProcessFormatter.php ├── Git └── GitRepository.php ├── IO ├── ConsoleIO.php ├── GitHubActionsIO.php ├── IOFactory.php └── IOInterface.php ├── Linter ├── Json │ ├── JsonLintError.php │ └── JsonLinter.php ├── LintError.php ├── LinterInterface.php ├── Xml │ ├── XmlLintError.php │ └── XmlLinter.php └── Yaml │ ├── YamlLintError.php │ └── YamlLinter.php ├── Locator ├── AsciiLocator.php ├── ChangedFiles.php ├── EnrichedGuessedPathsFromDotEnvLocator.php ├── ExternalCommand.php ├── GitRepositoryDirLocator.php ├── GitRepositoryLocator.php ├── GitWorkingDirLocator.php ├── GuessedPathsLocator.php ├── ListedFiles.php ├── RegisteredFiles.php └── StdInFiles.php ├── Parser ├── ParseError.php ├── ParserInterface.php └── Php │ ├── Configurator │ └── TraverserConfigurator.php │ ├── Container │ └── VisitorContainer.php │ ├── Context │ └── ParserContext.php │ ├── Factory │ ├── ParserFactory.php │ └── TraverserFactory.php │ ├── PhpParser.php │ ├── PhpParserError.php │ └── Visitor │ ├── AbstractVisitor.php │ ├── ConfigurableVisitorInterface.php │ ├── ContextAwareVisitorInterface.php │ ├── DeclareStrictTypesVisitor.php │ ├── ForbiddenClassMethodCallsVisitor.php │ ├── ForbiddenFunctionCallsVisitor.php │ ├── ForbiddenStaticMethodCallsVisitor.php │ ├── NeverUseElseVisitor.php │ └── NoExitStatementsVisitor.php ├── Process ├── InputWritingProcessRunner.php ├── ProcessBuilder.php ├── ProcessFactory.php └── TmpFileUsingProcessRunner.php ├── Runner ├── Ci │ └── CiDetector.php ├── FixableTaskResult.php ├── MemoizedTaskResultMap.php ├── Middleware │ ├── EventDispatchingRunnerMiddleware.php │ ├── FixCodeMiddleware.php │ ├── GroupByPriorityMiddleware.php │ ├── HandleRunnerMiddleware.php │ ├── ReportingRunnerMiddleware.php │ ├── ReportingTasksSectionRunnerMiddleware.php │ ├── RunnerMiddlewareInterface.php │ └── TasksFilteringRunnerMiddleware.php ├── MiddlewareStack.php ├── Parallel │ ├── PoolFactory.php │ └── SerializedClosureTask.php ├── Reporting │ ├── RunnerReporter.php │ └── TaskResultsReporter.php ├── StopOnFailure.php ├── TaskHandler │ ├── Middleware │ │ ├── ErrorHandlingTaskHandlerMiddleware.php │ │ ├── EventDispatchingTaskHandlerMiddleware.php │ │ ├── MemoizedResultsTaskHandlerMiddleware.php │ │ ├── NonBlockingTaskHandlerMiddleware.php │ │ ├── ParallelProcessingMiddleware.php │ │ ├── ReportingTaskHandlerMiddleware.php │ │ ├── StopOnFailureTaskHandlerMiddleware.php │ │ └── TaskHandlerMiddlewareInterface.php │ └── TaskHandler.php ├── TaskResult.php ├── TaskResultInterface.php ├── TaskRunner.php └── TaskRunnerContext.php ├── Task ├── AbstractExternalTask.php ├── AbstractLinterTask.php ├── AbstractParserTask.php ├── Ant.php ├── Atoum.php ├── Behat.php ├── Brunch.php ├── CloverCoverage.php ├── Codeception.php ├── Composer.php ├── ComposerNormalize.php ├── ComposerRequireChecker.php ├── ComposerScript.php ├── Config │ ├── ConfigOptionsResolver.php │ ├── EmptyTaskConfig.php │ ├── LazyTaskConfig.php │ ├── Metadata.php │ ├── TaskConfig.php │ └── TaskConfigInterface.php ├── Context │ ├── ContextInterface.php │ ├── GitCommitMsgContext.php │ ├── GitPreCommitContext.php │ └── RunContext.php ├── Deptrac.php ├── DoctrineOrm.php ├── ESLint.php ├── Ecs.php ├── FileSize.php ├── Gherkin.php ├── Git │ ├── Blacklist.php │ ├── BranchName.php │ └── CommitMessage.php ├── Grunt.php ├── Gulp.php ├── Infection.php ├── JsonLint.php ├── Kahlan.php ├── Make.php ├── NpmScript.php ├── Paratest.php ├── Pest.php ├── Phan.php ├── Phing.php ├── Php7cc.php ├── PhpArkitect.php ├── PhpCpd.php ├── PhpCsFixer.php ├── PhpLint.php ├── PhpMd.php ├── PhpMnd.php ├── PhpParser.php ├── PhpStan.php ├── PhpVersion.php ├── Phpcs.php ├── Phpspec.php ├── Phpunit.php ├── PhpunitBridge.php ├── Progpilot.php ├── Psalm.php ├── Rector.php ├── Robo.php ├── SecurityChecker.php ├── SecurityCheckerComposeraudit.php ├── SecurityCheckerEnlightn.php ├── SecurityCheckerLocal.php ├── SecurityCheckerRoave.php ├── SecurityCheckerSymfony.php ├── Shell.php ├── Stylelint.php ├── SymfonyConsole.php ├── TaskInterface.php ├── Tester.php ├── TwigCs.php ├── TwigCsFixer.php ├── XmlLint.php └── YamlLint.php ├── Test ├── Runner │ ├── AbstractMiddlewareTestCase.php │ ├── AbstractRunnerMiddlewareTestCase.php │ └── AbstractTaskHandlerMiddlewareTestCase.php └── Task │ ├── AbstractExternalTaskTestCase.php │ └── AbstractTaskTestCase.php ├── TestSuite ├── TestSuite.php └── TestSuiteInterface.php └── Util ├── ComposerFile.php ├── Filesystem.php ├── Paths.php ├── PhpVersion.php ├── Platform.php ├── Regex.php └── Str.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # 4 space indentation 12 | [*.{php,md}] 13 | indent_style = space 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /.phpspec/specification.tpl: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace %namespace%; 4 | 5 | use PhpSpec\ObjectBehavior; 6 | use Prophecy\Argument; 7 | use %subject%; 8 | 9 | class %name% extends ObjectBehavior 10 | { 11 | function it_is_initializable() 12 | { 13 | $this->shouldHaveType(%subject_class%::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | GrumPHP is an open source, community-driven project. If you'd like to contribute, 4 | feel free to do this, but remember to follow this few simple rules: 5 | 6 | ## Branching strategy 7 | 8 | - __Always__ base your changes on the latest version `v<version>.x` branch (all new development happens here), 9 | - When you create Pull Request, always select `v<version>.x` branch as target, otherwise it 10 | will be closed (this is selected by default). 11 | 12 | ## Coverage 13 | 14 | - All classes that interact solely with the core logic should be covered by Specs 15 | - Any infrastructure adaptors should be covered by integration tests using PHPUnit 16 | - All features should be covered with .feature descriptions automated with Behat 17 | 18 | ## Code style / Formatting 19 | 20 | - All new classes must carry the standard copyright notice docblock 21 | - All code in the `src` folder must follow the PSR-2 standard 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Phpro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "Please use \`make <target>' where <target> is one of" 3 | @echo " run to perform GrumPHP tests" 4 | @echo " tag to modify the version and tag" 5 | 6 | run: 7 | ./vendor/bin/grumphp run 8 | 9 | tag: 10 | $(if $(TAG),,$(error TAG is not defined. Pass via "make tag TAG=4.2.1")) 11 | @echo Tagging $(TAG) 12 | sed -i '' -e "s/APP_VERSION = '.*'/APP_VERSION = '$(TAG)'/" src/Console/ApplicationConfigurator.php 13 | php -l src/Console/ApplicationConfigurator.php 14 | git add -A 15 | git commit -m '$(TAG) release' 16 | git tag -s 'v$(TAG)' -m'Version $(TAG)' 17 | 18 | lock: 19 | $(if $(PHP),,$(error PHP is not defined. Pass via "make lock PHP=8.1")) 20 | composer self-update 21 | composer config platform.php '$(PHP)' 22 | composer update --no-scripts --no-plugins --no-interaction --optimize-autoloader 23 | composer validate 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Following versions are supported and will receive security updates depending on the vulnerability: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | > 2.x | :white_check_mark: | 10 | | < 2.0 | :x: | 11 | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | DO NOT PUBLISH SECURITY REPORTS PUBLICLY. 16 | 17 | (Since no-one is waiting for a zero-day vulnerability!) 18 | 19 | If you found any issues that might have security implications, 20 | please send a report through the security advisories form https://github.com/phpro/grumphp/security/advisories. 21 | This form will report a security vulnerability that is visible for the owners only. 22 | 23 | From there on, we can triage the issue and start fixing it. 24 | 25 | 26 | ## Security Bug Bounties 27 | 28 | GrumPHP is an Open-Source project where most of the work is done by volunteers. We appreciate that developers are trying to find security issues in GrumPHP and report them responsibly, but we are currently unable to pay bug bounties. 29 | 30 | 31 | -------------------------------------------------------------------------------- /bin/grumphp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | <?php 3 | 4 | use GrumPHP\Configuration\ContainerFactory; 5 | use Symfony\Component\Console\Input\ArgvInput; 6 | use Symfony\Component\Console\Output\ConsoleOutput; 7 | 8 | (function () { 9 | // First load current projects autoloader for smarter global / phar installs .... 10 | // A bit opinionated at the moment though: 11 | $autoloaderInWorkingDirectory = getcwd() . '/vendor/autoload.php'; 12 | if (is_file($autoloaderInWorkingDirectory)) { 13 | require_once $autoloaderInWorkingDirectory; 14 | } 15 | 16 | // Next load the GrumPHP autoloader 17 | $loaded = array_reduce( 18 | [ 19 | __DIR__.'/../vendor/autoload.php', // (Normal bin dir) 20 | __DIR__.'/../../../autoload.php', // From location inside folder 21 | __DIR__.'/../../vendor/autoload.php' // (Development integration) 22 | ], 23 | static function (?string $loaded, string $file): ?string { 24 | if ( ! $loaded && is_file($file)) { 25 | require_once($file); 26 | 27 | return $file; 28 | } 29 | 30 | return $loaded; 31 | } 32 | ); 33 | 34 | if (!$loaded) { 35 | fwrite( 36 | STDERR, 37 | 'You must set up the project dependencies, run the following commands:'.PHP_EOL. 38 | 'curl -s http://getcomposer.org/installer | php'.PHP_EOL. 39 | 'php composer.phar install'.PHP_EOL 40 | ); 41 | exit(1); 42 | } 43 | 44 | $container = ContainerFactory::build( 45 | $input = new ArgvInput(), 46 | $output = new ConsoleOutput() 47 | ); 48 | 49 | $application = $container->get('GrumPHP\Console\Application'); 50 | $application->run($input, $output); 51 | })(); 52 | -------------------------------------------------------------------------------- /doc/installation/global.md: -------------------------------------------------------------------------------- 1 | # Global installation 2 | 3 | It is possible to install or update GrumPHP on your system with following commands: 4 | 5 | ```sh 6 | composer global require phpro/grumphp 7 | ``` 8 | 9 | This will install the `grumphp` executable in the `~/.composer/vendor/bin` folder. 10 | Make sure to add this folder to your system `$PATH` variable: 11 | 12 | ```sh 13 | # .zshrc or .bashrc 14 | export PATH="$HOME/.composer/vendor/bin:$PATH" 15 | ``` 16 | 17 | That's all! The `grumphp` command will be available on your CLI. 18 | When your project also has a grumphp executable, this on will be used in favour of the one globally installed. 19 | The same goes for other tools like e.g. `phpunit` you have installed both globally and locally. 20 | -------------------------------------------------------------------------------- /doc/tasks/ant.md: -------------------------------------------------------------------------------- 1 | # Ant 2 | 3 | The Ant task will run your automated Ant tasks. 4 | It lives under the `ant` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | ant: 11 | build_file: ~ 12 | task: ~ 13 | triggered_by: [php] 14 | ``` 15 | 16 | **build_file** 17 | 18 | *Default: null* 19 | 20 | If your build.xml file is located at an exotic location, you can specify your custom build file location with this option. 21 | This option is set to `null` by default. 22 | This means that `build.xml` is automatically loaded if the file exists in the current directory. 23 | 24 | 25 | **task** 26 | 27 | *Default: null* 28 | 29 | This option specifies which Ant task you want to run. 30 | This option is set to `null` by default. 31 | This means that ant will run the `default` task. 32 | Note that this task should be used to verify things. 33 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 34 | 35 | 36 | **triggered_by** 37 | 38 | *Default: [php]* 39 | 40 | This option will specify which file extensions will trigger the ant task. 41 | By default, Ant will be triggered by altering a PHP file. 42 | You can overwrite this option to whatever file you want to use! 43 | -------------------------------------------------------------------------------- /doc/tasks/behat.md: -------------------------------------------------------------------------------- 1 | # Behat 2 | 3 | The Behat task will run your Behat tests. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev behat/behat 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `behat` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | behat: 20 | config: ~ 21 | format: ~ 22 | suite: ~ 23 | profile: ~ 24 | stop_on_failure: false 25 | ``` 26 | 27 | **config** 28 | 29 | *Default: null* 30 | 31 | If you want to use a different config file than the default behat.yml, you can specify your custom config file location with this option. 32 | 33 | 34 | **format** 35 | 36 | *Default: null* 37 | 38 | If you want to use a different formatter than the default one, specify it with this option. 39 | 40 | 41 | **suite** 42 | 43 | *Default: null* 44 | 45 | If you want to run a particular suite only, specify it with this option. 46 | 47 | 48 | **profile** 49 | 50 | *Default: null* 51 | 52 | If you want to use a specific configuration profile other than the default one, specify it with this option. 53 | 54 | 55 | **stop_on_failure** 56 | 57 | *Default: false* 58 | 59 | When this option is enabled, behat will stop at the first error. This means that it will not run your full test suite when an error occurs. 60 | -------------------------------------------------------------------------------- /doc/tasks/brunch.md: -------------------------------------------------------------------------------- 1 | # Brunch 2 | 3 | The Brunch task will run your automated frontend tasks. 4 | It lives under the `brunch` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | brunch: 11 | task: build 12 | env: production 13 | jobs: 4 14 | debug: false 15 | triggered_by: [js, jsx, coffee, ts, less, sass, scss] 16 | ``` 17 | 18 | **task** 19 | 20 | *Default: build* 21 | 22 | This option specifies which Brunch task you want to run. 23 | This option is set to `build` by default. 24 | This means that brunch will run the `build` task. 25 | Note that this task should be used to compile your assets. 26 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 27 | 28 | **env** 29 | 30 | *Default: production* 31 | 32 | This option specifies in which format you want to compile your assets. 33 | E.g: `--env production`. You can specify the env you set up in your brunch config file. 34 | 35 | **jobs** 36 | 37 | *Default: 4* 38 | 39 | This option enables experimental multi-process support. May improve compilation speed of large projects. Try different WORKERS amount to see which one works best for your system. 40 | 41 | **debug** 42 | 43 | *Default: false* 44 | 45 | It enables verbose debug output. 46 | 47 | **triggered_by** 48 | 49 | *Default: [js, jsx, coffee, ts, less, sass, scss]* 50 | 51 | This option will specify which file extensions will trigger the brunch task. 52 | By default, Brunch will be triggered by altering a front-end file. 53 | You can overwrite this option to whatever file you want to use! 54 | -------------------------------------------------------------------------------- /doc/tasks/clover_coverage.md: -------------------------------------------------------------------------------- 1 | # Clover Coverage 2 | 3 | The Clover Coverage task will run your unit tests. 4 | 5 | Note that to make sure that there is always a clover file available, you might need to 6 | set `always_execute` to `true` in the `phpunit` task configuration. 7 | 8 | It lives under the `clover_coverage` namespace and has following configurable parameters: 9 | 10 | ```yaml 11 | # grumphp.yml 12 | grumphp: 13 | tasks: 14 | clover_coverage: 15 | clover_file: /tmp/clover.xml 16 | minimum_level: 100 17 | target_level: null 18 | ``` 19 | 20 | **clover_file** 21 | 22 | *Required* 23 | 24 | The location of the clover code coverage XML file. 25 | 26 | **minimum_level** 27 | 28 | *Default: 100* 29 | 30 | The minimum code coverage percentage required to pass. 31 | 32 | **target_level** 33 | 34 | *Default: null* 35 | 36 | Setting a minimum code coverage level is letting the task fail hard. 37 | When you are in the process of increasing your code coverage, you can set a target level. 38 | When the code coverage is below the target level, the task will fail in a non-blocking way. 39 | This gives you the opportunity to increase the code coverage step by step in a non-blocking way whilst keeping track of the progress. 40 | -------------------------------------------------------------------------------- /doc/tasks/codeception.md: -------------------------------------------------------------------------------- 1 | # Codeception 2 | The Codeception task will run your full-stack tests. 3 | 4 | ***Composer*** 5 | 6 | ``` 7 | composer require --dev codeception/codeception 8 | ``` 9 | 10 | ***Config*** 11 | 12 | The task lives under the `codeception` namespace and has the following configurable parameters: 13 | 14 | ```yaml 15 | # grumphp.yml 16 | grumphp: 17 | tasks: 18 | codeception: 19 | config_file: ~ 20 | fail_fast: false 21 | suite: ~ 22 | test: ~ 23 | xml: false 24 | html: false 25 | ``` 26 | 27 | 28 | **config_file** 29 | 30 | *Default: null* 31 | 32 | If your `codeception.yml` file is located in an exotic location, you can specify your custom config file location with this option. This option is set to `null` by default. This means that `codeception.yml` is automatically located if it exists in the current directory. 33 | 34 | **fail_fast** 35 | 36 | *Default: false* 37 | 38 | When this option is enabled, Codeception will stop at the first error. This means that it will not run your full test suite when an error occurs. 39 | 40 | **suite** 41 | 42 | *Default: null* 43 | 44 | When this option is specified it will only run tests for the given suite. If left `null` Codeception will run tests for your full test suite. 45 | 46 | **test** 47 | 48 | *Default: null* 49 | 50 | When this option is specified it will only run the given test. If left `null` Codeception will run all tests within the suite. 51 | This option can only be used in combination with a suite. 52 | 53 | **xml** 54 | 55 | *Default: false* 56 | 57 | When this option is enabled, Codeception will output an XML report for the test run. 58 | 59 | **html** 60 | 61 | *Default: false* 62 | 63 | When this option is enabled, Codeception will output an HTML report for the test run. 64 | -------------------------------------------------------------------------------- /doc/tasks/composer.md: -------------------------------------------------------------------------------- 1 | # Composer 2 | 3 | When the `composer.json` file has changed, the new file should be checked for issues. 4 | This task will execute [`composer validate`](https://getcomposer.org/doc/03-cli.md#validate) to make sure that everything is OK. 5 | The configuration looks like: 6 | 7 | ```yaml 8 | # grumphp.yml 9 | grumphp: 10 | tasks: 11 | composer: 12 | file: ./composer.json 13 | no_check_all: false 14 | no_check_lock: false 15 | no_check_publish: false 16 | no_local_repository: false 17 | with_dependencies: false 18 | strict: false 19 | ``` 20 | 21 | **file** 22 | 23 | *Default: ./composer.json* 24 | 25 | Specifies at which location the `composer.json` file can be found. 26 | 27 | 28 | **no_check_all** 29 | 30 | *Default: false* 31 | 32 | Do not emit a warning if requirements in composer.json use unbound version constraints. 33 | 34 | 35 | **no_check_lock** 36 | 37 | *Default: false* 38 | 39 | Do not emit an error if composer.lock exists and is not up to date. 40 | 41 | 42 | **no_check_publish** 43 | 44 | *Default: false* 45 | 46 | Do not emit an error if composer.json is unsuitable for publishing as a package on Packagist but is otherwise valid. 47 | 48 | 49 | **no_local_repository** 50 | 51 | *Default: false* 52 | 53 | Do emit an error if composer.json declares local repositories (see https://getcomposer.org/doc/05-repositories.md#path). 54 | 55 | 56 | **with_dependencies** 57 | 58 | *Default: false* 59 | 60 | Also validate the `composer.json` of all installed dependencies. 61 | 62 | 63 | **strict** 64 | 65 | *Default: false* 66 | 67 | Return a non-zero exit code for warnings as well as errors. 68 | -------------------------------------------------------------------------------- /doc/tasks/composer_normalize.md: -------------------------------------------------------------------------------- 1 | # Composer Normalize  2 | 3 | If you are using `composer`, you have probably modified the file `composer.json` at least once to keep things nice 4 | and tidy. 5 | 6 | ***Composer*** 7 | 8 | ``` 9 | composer require --dev ergebnis/composer-normalize 10 | ``` 11 | 12 | ***Config*** 13 | 14 | This task is a wrapper around a composer plugin for tidying up the file `composer.json`. 15 | 16 | The default configuration looks like: 17 | 18 | ```yaml 19 | # grumphp.yml 20 | grumphp: 21 | tasks: 22 | composer_normalize: 23 | indent_size: ~ 24 | indent_style: ~ 25 | no_check_lock: false 26 | no_update_lock: true 27 | verbose: false 28 | ``` 29 | 30 | **indent_size** 31 | 32 | *Default: null* 33 | 34 | Indent size (an integer greater than 0); must be used with the `indent_style` option 35 | 36 | **indent_style** 37 | 38 | *Default: null* 39 | 40 | Indent style (one of "space", "tab"); must be used with the `indent_size` option 41 | 42 | **no_check_lock** 43 | 44 | *Default: false* 45 | 46 | If `true`, do not check if lock file is up to date. 47 | 48 | **no_update_lock** 49 | 50 | *Default: true* 51 | 52 | If `false`, do not update lock file if it exists. 53 | 54 | **use_standalone** 55 | 56 | *Default: false* 57 | 58 | If `true`, use the standalone `composer-normalize` command instead of the Composer plugin. 59 | 60 | **verbose** 61 | 62 | *Default: false* 63 | 64 | Set this to true if you want to see the diff. 65 | -------------------------------------------------------------------------------- /doc/tasks/composer_require_checker.md: -------------------------------------------------------------------------------- 1 | # Composer Require Checker 2 | 3 | The Composer Require Checker task analyzes composer dependencies and verifies that no unknown symbols are used in the 4 | code. This will prevent you from using "soft" dependencies that are not defined within your composer.json. 5 | It lives under the `composer_require_checker` namespace and has following configurable parameters: 6 | 7 | ## Composer 8 | ```bash 9 | composer require --dev maglnet/composer-require-checker 10 | ``` 11 | 12 | ## Config 13 | ```yaml 14 | # grumphp.yml 15 | grumphp: 16 | tasks: 17 | composer_require_checker: 18 | composer_file: 'composer.json' 19 | config_file: ~ 20 | ignore_parse_errors: false 21 | triggered_by: ['composer.json', 'composer.lock', '*.php'] 22 | ``` 23 | 24 | **composer_file** 25 | 26 | *Default: null* 27 | 28 | The composer.json of your code base that should be checked. 29 | 30 | **config_file** 31 | 32 | *Default: null* 33 | 34 | Composer Require Checker is configured to whitelist some symbols by default. You can now override this configuration 35 | with your own and tell GrumPHP to use that configuration file instead. 36 | 37 | **ignore_parse_errors** 38 | 39 | *Default: false* 40 | 41 | This will cause Composer Require Checker to ignore errors when files cannot be parsed, otherwise errors will be thrown. 42 | 43 | This option is only available in version 0.2.0 of `maglnet/composer-require-checker` and above. 44 | 45 | **triggered_by** 46 | 47 | *Default: ['composer.json', 'composer.lock', '\*.php']* 48 | 49 | This is a list of file names that should trigger this task. 50 | -------------------------------------------------------------------------------- /doc/tasks/composer_script.md: -------------------------------------------------------------------------------- 1 | # Composer script 2 | 3 | The Composer script task will run your configured Composer script. 4 | It lives under the `composer_script` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | composer_script: 11 | script: ~ 12 | triggered_by: [php, phtml] 13 | working_directory: ~ 14 | ``` 15 | 16 | **script** 17 | 18 | *Default: null* 19 | 20 | This option specifies which Composer script you want to run. 21 | This option is set to null by default. 22 | This means that grumphp will stop immediately. 23 | Note that this script should be used to verify things. 24 | It is also possible to alter code during commit, 25 | but this is surely NOT recommended! 26 | 27 | 28 | **triggered_by** 29 | 30 | *Default: [php, phtml]* 31 | 32 | This option will specify which file extensions will trigger the Composer script. 33 | By default, Composer script will be triggered by altering any file. 34 | You can overwrite this option to whatever file you want to use! 35 | 36 | 37 | **working_directory** 38 | 39 | *Default: null* 40 | 41 | This option specifies in which directory the Composer script should be run. 42 | -------------------------------------------------------------------------------- /doc/tasks/deptrac.md: -------------------------------------------------------------------------------- 1 | # Deptrac 2 | 3 | Follow the [installation instructions](https://qossmic.github.io/deptrac/#installation) to add deptrac to your 4 | project. 5 | 6 | The Deptrac task will check for dependencies between the software layers of your project. It lives under the `deptrac` 7 | namespace and has following configurable parameters: 8 | 9 | 10 | ```yaml 11 | # grumphp.yml 12 | grumphp: 13 | tasks: 14 | deptrac: 15 | cache_file: ~ 16 | depfile: ~ 17 | formatter: ~ 18 | output: ~ 19 | ``` 20 | 21 | **cache_file** 22 | 23 | *Default: null* 24 | 25 | Set location where cache file will be stored. Example: `/var/www/src/.deptrac.cache` 26 | 27 | **depfile** 28 | 29 | *Default: null* 30 | 31 | Set path to deptrac configuration file. Example: `/var/www/src/deptrac.yaml` 32 | 33 | **formatter** 34 | 35 | *Default: null* 36 | 37 | Enable formatter with this option, e.g. `console`, `github-actions`, `graphviz-display`, `graphviz-image`, `graphviz-dot`, `graphviz-html`, `junit`, `table`, `xml`, `baseline`, `json`. 38 | 39 | **output** 40 | 41 | *Default: null* 42 | 43 | Set output file path for formatter (if applicable). 44 | -------------------------------------------------------------------------------- /doc/tasks/doctrine_orm.md: -------------------------------------------------------------------------------- 1 | # Doctrine ORM 2 | 3 | The Doctrine ORM task will validate that your Doctrine mapping files and check if the mapping is in sync with the database. 4 | It lives under the `doctrine_orm` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | doctrine_orm: 11 | skip_mapping: false 12 | skip_sync: false 13 | skip_property_types: false 14 | triggered_by: ['php', 'xml', 'yml'] 15 | ``` 16 | 17 | **skip_mapping** 18 | 19 | *Default: false* 20 | 21 | With this parameter you can skip the mapping validation check. 22 | 23 | **skip_sync** 24 | 25 | *Default: false* 26 | 27 | With this parameter you can skip checking if the mapping is in sync with the database. 28 | 29 | **skip_property_types** 30 | 31 | *Default: false* 32 | 33 | With this parameter you can skip checking if Entity field property types match the Doctrine types. 34 | 35 | **triggered_by** 36 | 37 | *Default: [php, xml, yml]* 38 | 39 | This is a list of extensions that should trigger the Doctrine task. 40 | -------------------------------------------------------------------------------- /doc/tasks/file_size.md: -------------------------------------------------------------------------------- 1 | # File size 2 | 3 | The file size task ensures a maximum size for a file to be added to git. 4 | 5 | ```yaml 6 | # grumphp.yml 7 | grumphp: 8 | tasks: 9 | file_size: 10 | max_size: 10M 11 | ignore_patterns: [] 12 | ``` 13 | 14 | **max_size** 15 | 16 | *Default: 10M* 17 | 18 | Defines the maximum size. The target value may use magnitudes of kilobytes (k, ki), 19 | megabytes (m, mi), or gigabytes (g, gi). Those suffixed with an i use the appropriate 2**n version 20 | in accordance with the IEC standard. 21 | 22 | 23 | **ignore_patterns** 24 | 25 | *Default: []* 26 | 27 | This is a list of patterns that will be ignored. With this option you can skip files. 28 | Leave this option blank to run analysis for all files. 29 | -------------------------------------------------------------------------------- /doc/tasks/gherkin.md: -------------------------------------------------------------------------------- 1 | # Gherkin 2 | 3 | The Gherkin task will lint your Gherkin feature files. 4 | It lives under the `gherkin` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | gherkin: 11 | directory: 'features' 12 | align: ~ 13 | ``` 14 | 15 | **directory** 16 | 17 | *Default: 'features'* 18 | 19 | This option will specify the location of your Gherkin feature files. 20 | By default, the Behat preferred `features` folder is chosen. 21 | 22 | **align** 23 | 24 | *Default: null* 25 | 26 | This option will specify the alignment of your file. 27 | Possible values can be `left` or `right`. 28 | -------------------------------------------------------------------------------- /doc/tasks/git_branch_name.md: -------------------------------------------------------------------------------- 1 | # Git branch name 2 | 3 | The Git branch name task ensures that the current branch name matches the specified patterns. 4 | For this task to succeed, **any** whitelist patterns and **none** of the blacklist patterns have to 5 | match the branch name. For example: if you are working with JIRA, it is possible to add a 6 | pattern for the JIRA issue number. 7 | 8 | ```yaml 9 | # grumphp.yml 10 | grumphp: 11 | tasks: 12 | git_branch_name: 13 | whitelist: 14 | - "/JIRA-\d+/" 15 | blacklist: 16 | - "develop" 17 | - "master" 18 | additional_modifiers: '' 19 | allow_detached_head: true 20 | ``` 21 | 22 | 23 | **whitelist** 24 | 25 | *Default: []* 26 | 27 | Use this parameter to specify one or multiple patterns. The value can be in regex or glob style. 28 | Here are some example matchers: 29 | 30 | - /JIRA-([0-9]*)/ 31 | - pre-fix* 32 | - *suffix 33 | 34 | **blacklist** 35 | 36 | *Default: []* 37 | 38 | Use this parameter to specify one or multiple patterns. The value can be in regex or glob style. 39 | Here are some example matchers: 40 | 41 | - /JIRA-([0-9]*)/ 42 | - pre-fix* 43 | - *suffix 44 | 45 | 46 | **additional_modifiers** 47 | 48 | *Default: ''* 49 | 50 | Add one or multiple additional modifiers like: 51 | 52 | ```yaml 53 | additional_modifiers: 'u' 54 | 55 | # or 56 | 57 | additional_modifiers: 'xu' 58 | ``` 59 | 60 | 61 | **allow_detached_head** 62 | 63 | *Default: true* 64 | 65 | Set this to `false` if you wish the task to fail when ran on a detached HEAD. If set to `true` the 66 | task will always pass. 67 | -------------------------------------------------------------------------------- /doc/tasks/grunt.md: -------------------------------------------------------------------------------- 1 | # Grunt 2 | 3 | The Grunt task will run your automated frontend tasks. 4 | It lives under the `grunt` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | grunt: 11 | grunt_file: ~ 12 | task: ~ 13 | triggered_by: [js, jsx, coffee, ts, less, sass, scss] 14 | ``` 15 | 16 | **grunt_file** 17 | 18 | *Default: null* 19 | 20 | If your Gruntfile.js file is located at an exotic location, you can specify your custom grunt file location with this option. 21 | This option is set to `null` by default. 22 | This means that `Gruntfile.js` is automatically loaded if the file exists in the current directory. 23 | 24 | 25 | **task** 26 | 27 | *Default: null* 28 | 29 | This option specifies which Grunt task you want to run. 30 | This option is set to `null` by default. 31 | This means that grunt will run the `default` task. 32 | Note that this task should be used to verify things. 33 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 34 | 35 | 36 | **triggered_by** 37 | 38 | *Default: [js, jsx, coffee, ts, less, sass, scss]* 39 | 40 | This option will specify which file extensions will trigger the grunt task. 41 | By default, Grunt will be triggered by altering a front-end file. 42 | You can overwrite this option to whatever file you want to use! 43 | -------------------------------------------------------------------------------- /doc/tasks/gulp.md: -------------------------------------------------------------------------------- 1 | # Gulp 2 | 3 | The Gulp task will run your automated frontend tasks. 4 | It lives under the `gulp` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | gulp: 11 | gulp_file: ~ 12 | task: ~ 13 | triggered_by: [js, jsx, coffee, ts, less, sass, scss] 14 | ``` 15 | 16 | **gulp_file** 17 | 18 | *Default: null* 19 | 20 | If your Gulpfile.js file is located at an exotic location, you can specify your custom gulp file location with this option. 21 | This option is set to `null` by default. 22 | This means that `gulpfile.js` is automatically loaded if the file exists in the current directory. 23 | 24 | 25 | **task** 26 | 27 | *Default: null* 28 | 29 | This option specifies which Gulp task you want to run. 30 | This option is set to `null` by default. 31 | This means that gulp will run the `default` task. 32 | Note that this task should be used to verify things. 33 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 34 | 35 | 36 | **triggered_by** 37 | 38 | *Default: [js, jsx, coffee, ts, less, sass, scss]* 39 | 40 | This option will specify which file extensions will trigger the gulp task. 41 | By default, Gulp will be triggered by altering a front-end file. 42 | You can overwrite this option to whatever file you want to use! 43 | -------------------------------------------------------------------------------- /doc/tasks/jsonlint.md: -------------------------------------------------------------------------------- 1 | # JsonLint 2 | 3 | The JsonLint task will lint all your json files. 4 | It lives under the `jsonlint` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | jsonlint: 11 | ignore_patterns: [] 12 | detect_key_conflicts: false 13 | ``` 14 | 15 | **ignore_patterns** 16 | 17 | *Default: []* 18 | 19 | This is a list of patterns that will be ignored by the linter. 20 | With this option you can skip files like test fixtures. Leave this option blank to run the linter for every json file. 21 | 22 | 23 | **detect_key_conflicts** 24 | 25 | *Default: false* 26 | 27 | This option will throw exceptions when duplicate keys are detected in the json file. 28 | -------------------------------------------------------------------------------- /doc/tasks/make.md: -------------------------------------------------------------------------------- 1 | # Make 2 | 3 | The Make task will run your automated make tasks. 4 | It lives under the `make` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | make: 11 | make_file: ~ 12 | task: ~ 13 | triggered_by: [php] 14 | ``` 15 | 16 | **make_file** 17 | 18 | *Default: null* 19 | 20 | If your Makefile file is located at an exotic location, you can specify your custom make file location with this option. 21 | This option is set to `null` by default. 22 | This means that `Makefile` is automatically loaded if the file exists in the current directory. 23 | 24 | 25 | **task** 26 | 27 | *Default: null* 28 | 29 | This option specifies which Make task you want to run. 30 | This option is set to `null` by default. 31 | This means that make will run the `default` task. 32 | Note that this task should be used to verify things. 33 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 34 | 35 | 36 | **triggered_by** 37 | 38 | *Default: [php]* 39 | 40 | This option will specify which file extensions will trigger the make task. 41 | By default, Make will be triggered by altering a PHP file. 42 | You can overwrite this option to whatever file you want to use! 43 | -------------------------------------------------------------------------------- /doc/tasks/npm_script.md: -------------------------------------------------------------------------------- 1 | # NPM script 2 | 3 | The NPM script task will run your configured npm script. 4 | It lives under the `npm_script` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | npm_script: 11 | script: ~ 12 | triggered_by: [js, jsx, coffee, ts, less, sass, scss] 13 | working_directory: "./" 14 | is_run_task: false 15 | silent: false 16 | ``` 17 | 18 | **script** 19 | 20 | *Default: null* 21 | 22 | This option specifies which NPM script you want to run. 23 | This option is set to null by default. 24 | This means that grumphp will stop immediately. 25 | Note that this script should be used to verify things. 26 | It is also possible to alter code during commit, 27 | but this is surely NOT recommended! 28 | 29 | 30 | **triggered_by** 31 | 32 | *Default: [js, jsx, coffee, ts, less, sass, scss]* 33 | 34 | This option will specify which file extensions will trigger the NPM script. 35 | By default, NPM script will be triggered by altering any file. 36 | You can overwrite this option to whatever file you want to use! 37 | 38 | 39 | **working_directory** 40 | 41 | *Default: "./"* 42 | 43 | This option specifies in which directory the NPM script should be run. 44 | 45 | **is_run_task** 46 | 47 | *Default: false* 48 | 49 | This option will append 'run' to the npm command to make it possible to run custom npm scripts. 50 | 51 | **silent** 52 | 53 | *Default: false* 54 | 55 | This option will append '--silent' to the npm script to suppress `npm ERR!` messages from showing. 56 | -------------------------------------------------------------------------------- /doc/tasks/pest.md: -------------------------------------------------------------------------------- 1 | # Pest 2 | 3 | The Pest task will run your unit tests. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev pestphp/pest 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `pest` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | pest: 20 | config_file: ~ 21 | testsuite: ~ 22 | group: [] 23 | always_execute: false 24 | ``` 25 | 26 | **config_file** 27 | 28 | *Default: null* 29 | 30 | If your phpunit.xml file is located at an exotic location, you can specify your custom config file location with this option. 31 | This option is set to `null` by default. 32 | This means that `phpunit.xml` or `phpunit.xml.dist` are automatically loaded if one of them exist in the current directory. 33 | 34 | 35 | **testsuite** 36 | 37 | *Default: null* 38 | 39 | If you wish to only run tests from a certain Suite. 40 | `testsuite: unit` 41 | 42 | 43 | **group** 44 | 45 | *Default: array()* 46 | 47 | If you wish to only run tests from a certain Group. 48 | `group: [fast,quick,small]` 49 | 50 | 51 | **always_execute** 52 | 53 | *Default: false* 54 | 55 | Always run the whole test suite, even if no PHP files were changed. 56 | -------------------------------------------------------------------------------- /doc/tasks/phan.md: -------------------------------------------------------------------------------- 1 | # Phan 2 | 3 | The Phan task will run your automated PHP tasks. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev phan/phan 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `phan` namespace and has following configurable parameters. 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | phan: 20 | config_file: .phan/config.php 21 | output_mode: text 22 | output: null 23 | triggered_by: [php] 24 | ``` 25 | 26 | **config_file** 27 | 28 | *Default: .phan/config.php* 29 | 30 | If your config.php file is located at an exotic location, you can specify your custom build file location with this option. 31 | This option is set to `.phan/config.php` by default. 32 | This means that `.phan/config.php` is automatically loaded if the file exists in the current directory. 33 | 34 | 35 | **output_mode** 36 | 37 | *Default: text* 38 | 39 | This option sets the output mode. Valid output modes are 'text', 'json', 'csv', 'codeclimate', 'checkstyle', or 'pylint'. 40 | This option is set to `text` by default. 41 | 42 | **output** 43 | 44 | *Default: null* 45 | 46 | It's possible to save the output to a file, you can specify the file name with this option. 47 | This option is set to `null` by default. 48 | 49 | **triggered_by** 50 | 51 | *Default: [php]* 52 | 53 | This option will specify which file extensions will trigger the phan task. 54 | By default, Phan will be triggered by altering a PHP file. 55 | You can overwrite this option to whatever file you want to use! 56 | -------------------------------------------------------------------------------- /doc/tasks/phing.md: -------------------------------------------------------------------------------- 1 | # Phing 2 | 3 | The Phing task will run your automated PHP tasks. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev phing/phing 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `phing` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | phing: 20 | build_file: ~ 21 | task: ~ 22 | triggered_by: [php] 23 | ``` 24 | 25 | **build_file** 26 | 27 | *Default: null* 28 | 29 | If your build.xml file is located at an exotic location, you can specify your custom build file location with this option. 30 | This option is set to `null` by default. 31 | This means that `build.xml` is automatically loaded if the file exists in the current directory. 32 | 33 | 34 | **task** 35 | 36 | *Default: null* 37 | 38 | This option specifies which Phing task you want to run. 39 | This option is set to `null` by default. 40 | This means that phing will run the `default` task. 41 | Note that this task should be used to verify things. 42 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 43 | 44 | 45 | **triggered_by** 46 | 47 | *Default: [php]* 48 | 49 | This option will specify which file extensions will trigger the phing task. 50 | By default, Phing will be triggered by altering a PHP file. 51 | You can overwrite this option to whatever file you want to use! 52 | -------------------------------------------------------------------------------- /doc/tasks/php7cc.md: -------------------------------------------------------------------------------- 1 | # Php7cc 2 | 3 | The Php7cc task will check PHP 5.3 - 5.6 code compatibility with PHP 7. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev sstalle/php7cc 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `php7cc` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | php7cc: 20 | exclude: [] 21 | level: ~ 22 | triggered_by: ['php'] 23 | ``` 24 | 25 | **exclude** 26 | 27 | *Default: []* 28 | 29 | This is a list of directories to be excluded 30 | 31 | **level** 32 | 33 | *Default: null* 34 | 35 | Minimum issue level. There are 3 issue levels: "info", "warning" and "error". "info" is reserved for future use and is the same as "warning". 36 | 37 | **triggered_by** 38 | 39 | *Default: [php]* 40 | 41 | This is a list of extensions to be sniffed. 42 | 43 | 44 | ## Known issues 45 | 46 | - Since this task is using an old version of phpparser, it currently cannot be used in combination with the `phpparser` task. 47 | [Click here for more information](https://github.com/sstalle/php7cc/issues/79) 48 | -------------------------------------------------------------------------------- /doc/tasks/phparkitect.md: -------------------------------------------------------------------------------- 1 | # PHPArkitect 2 | 3 | PHPArkitect helps you to keep your PHP codebase coherent and solid, by permitting to add some architectural constraint check to your workflow. 4 | It lives under the `phparkitect` namespace and has following configurable parameters: 5 | 6 | PhpArkitect doesn't support checking only the changed files. 7 | It will always run on the directory specified in your config file. 8 | 9 | ## Composer 10 | ```bash 11 | composer require --dev phparkitect/phparkitect 12 | ``` 13 | 14 | ## Config 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | phparkitect: 20 | config: ~ 21 | target_php_version: ~ 22 | stop_on_failure: ~ 23 | ``` 24 | 25 | **config** 26 | 27 | *Default: null* 28 | 29 | With this parameter you can specify the path your project's configuration file. 30 | By default PHPArkitect will search all rules in phparkitect.php located in the root of your project. 31 | 32 | **target_php_version** 33 | 34 | *Default: null* 35 | 36 | With this parameter, you can specify which PHP version should use the parser. 37 | This can be useful to debug problems and to understand if there are problems with a different PHP version. 38 | 39 | **stop_on_failure** 40 | 41 | *Default: false* 42 | 43 | With this option the process will end immediately after the first violation. 44 | -------------------------------------------------------------------------------- /doc/tasks/phpcpd.md: -------------------------------------------------------------------------------- 1 | # PhpCpd 2 | 3 | The PhpCpd task will sniff your code for duplicated lines. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev sebastian/phpcpd 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `phpcpd` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | phpcpd: 20 | directory: ['.'] 21 | exclude: ['vendor'] 22 | fuzzy: false 23 | min_lines: 5 24 | min_tokens: 70 25 | triggered_by: ['php'] 26 | ``` 27 | 28 | **directory** 29 | 30 | *Default: [.]* 31 | 32 | With this parameter you can define which directories you want to run `phpcpd` in (must be relative to cwd). 33 | 34 | **exclude** 35 | 36 | *Default: [vendor]* 37 | 38 | With this parameter you will be able to exclude one or multiple directories from code analysis (must be relative to `directory`). 39 | 40 | **fuzzy** 41 | 42 | *Default: false* 43 | 44 | With this parameter you will be able to fuzz variable names. 45 | 46 | **min_lines** 47 | 48 | *Default: 5* 49 | 50 | With this parameter you will be able to set minimum number of identical lines. 51 | 52 | **min_tokens** 53 | 54 | *Default: 70* 55 | 56 | With this parameter you will be able to set minimum number of identical tokens. 57 | 58 | **triggered_by** 59 | 60 | *Default: [php]* 61 | 62 | This is a list of extensions to be sniffed. 63 | -------------------------------------------------------------------------------- /doc/tasks/phplint.md: -------------------------------------------------------------------------------- 1 | # PHPLint 2 | 3 | The PHPLint task will check your source files for syntax errors. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev php-parallel-lint/php-parallel-lint 9 | ``` 10 | 11 | ***Config*** 12 | 13 | ```yaml 14 | # grumphp.yml 15 | grumphp: 16 | tasks: 17 | phplint: 18 | exclude: [] 19 | jobs: ~ 20 | short_open_tag: false 21 | ignore_patterns: [] 22 | triggered_by: ['php', 'phtml', 'php3', 'php4', 'php5'] 23 | ``` 24 | **exclude** 25 | 26 | *Default: []* 27 | 28 | Any directories to be excluded from linting. You can specify which 29 | directories you wish to exclude, such as the vendor directory. 30 | 31 | **jobs** 32 | 33 | *Default: null* 34 | 35 | The number of jobs you wish to use for parallel processing. If no number 36 | is given, it is left up to parallel-lint itself, which currently 37 | defaults to 10. 38 | 39 | **short_open_tag** 40 | 41 | *Default: false* 42 | 43 | This option can allow PHP short open tags. 44 | 45 | **ignore_patterns** 46 | 47 | *Default: []* 48 | 49 | This is a list of patterns that will be ignored by PHPLint. Leave this option blank to run PHPLint for every php file. 50 | 51 | **triggered_by** 52 | 53 | *Default: ['php', 'phtml', 'php3', 'php4', 'php5']* 54 | 55 | Any file extensions that you wish to be passed to the linter. 56 | -------------------------------------------------------------------------------- /doc/tasks/phpspec.md: -------------------------------------------------------------------------------- 1 | # Phpspec 2 | 3 | The Phpspec task will spec your code with Phpspec. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev phpspec/phpspec 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `phpspec` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | phpspec: 20 | config_file: ~ 21 | format: ~ 22 | stop_on_failure: false 23 | verbose: false 24 | ``` 25 | 26 | **config_file** 27 | 28 | *Default: null* 29 | 30 | If your phpspec.yml file is located at an exotic location, you can specify your custom config file location with this option. 31 | 32 | 33 | **format** 34 | 35 | *Default: null* 36 | 37 | You can overwrite the default `progress` format or the one specified in the `phpspec.yml` config file by configuring this option. 38 | 39 | [A list of all formatters](http://www.phpspec.net/en/stable/cookbook/configuration.html#formatter) 40 | 41 | 42 | **stop_on_failure** 43 | 44 | *Default: false* 45 | 46 | When this option is enabled, phpspec will stop at the first error. This means that it will not run your full test suite when an error occurs. 47 | 48 | 49 | **verbose** 50 | 51 | *Default: false* 52 | 53 | When this option is enabled, phpspec will display a verbose error message about the failed example. This way, it is easier to debug what went wrong in the specs. 54 | -------------------------------------------------------------------------------- /doc/tasks/phpunitbridge.md: -------------------------------------------------------------------------------- 1 | # Phpunit bridge 2 | 3 | The Phpunit Bridge task will run your unit tests thanks to the Symfony Phpunit Bridge. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev symfony/phpunit-bridge 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `phpunitbridge` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | phpunitbridge: 20 | config_file: ~ 21 | testsuite: ~ 22 | group: [] 23 | exclude_group: [] 24 | always_execute: false 25 | order: null 26 | ``` 27 | 28 | **config_file** 29 | 30 | *Default: null* 31 | 32 | If your phpunit.xml file is located at an exotic location, you can specify your custom config file location with this option. 33 | This option is set to `null` by default. 34 | This means that `phpunit.xml` or `phpunit.xml.dist` are automatically loaded if one of them exist in the current directory. 35 | 36 | 37 | **testsuite** 38 | 39 | *Default: null* 40 | 41 | If you wish to only run tests from a certain Suite. 42 | `testsuite: unit` 43 | 44 | 45 | **group** 46 | 47 | *Default: array()* 48 | 49 | If you wish to only run tests from a certain Group. 50 | `group: ['fast','quick','small']` 51 | 52 | 53 | **exclude_group** 54 | 55 | *Default: array()* 56 | 57 | If you wish to run tests excluding a certain Group. 58 | `group: ['big','risky']` 59 | 60 | 61 | **always_execute** 62 | 63 | *Default: false* 64 | 65 | Always run the whole test suite, even if no PHP files were changed. 66 | 67 | **order** 68 | 69 | *Default: null* 70 | 71 | If you wish to run tests in a specific order. `order: [default,defects,duration,no-depends,random,reverse,size]` 72 | -------------------------------------------------------------------------------- /doc/tasks/phpversion.md: -------------------------------------------------------------------------------- 1 | # PhpVersion 2 | 3 | The Phpversion task will check if your current php version is still supported. 4 | The date of the php version that is checked, is the end of the security updates that can be found [here](https://secure.php.net/supported-versions.php). 5 | It lives under the `phpversion` namespace and has following configurable parameters: 6 | 7 | ```yaml 8 | # grumphp.yml 9 | grumphp: 10 | tasks: 11 | phpversion: 12 | project: '7.2' 13 | ``` 14 | 15 | **project** 16 | 17 | *Default: null* 18 | 19 | Manually set a minimum version for your project. 20 | -------------------------------------------------------------------------------- /doc/tasks/progpilot.md: -------------------------------------------------------------------------------- 1 | # Progpilot 2 | 3 | The Progpilot task will run your automated PHP tasks. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer config minimum-stability dev 9 | composer require --dev designsecurity/progpilot:dev-master 10 | ``` 11 | 12 | ***Config*** 13 | 14 | The task lives under the `progpilot` namespace and has following configurable parameters. 15 | 16 | ```yaml 17 | # grumphp.yml 18 | grumphp: 19 | tasks: 20 | progpilot: 21 | config_file: .progpilot/configuration.yml 22 | triggered_by: [php] 23 | ``` 24 | 25 | **config_file** 26 | 27 | *Default: configuration.yml* 28 | 29 | You can specify your custom configuration file location with this option. 30 | By default `.progpilot/configuration.yml` is automatically loaded if the file exists in the current directory. 31 | If not or yml file format is invalid default configuration of progpilot will be used. 32 | 33 | 34 | **triggered_by** 35 | 36 | *Default: [php]* 37 | 38 | This option will specify which file extensions will trigger the Progpilot task. 39 | By default, Progpilot will be triggered by altering a PHP file. 40 | 41 | -------------------------------------------------------------------------------- /doc/tasks/psalm.md: -------------------------------------------------------------------------------- 1 | # Psalm 2 | 3 | Psalm is a static analysis tool for finding errors in PHP applications, built on top of PHP Parser. 4 | It lives under the `psalm` namespace and has following configurable parameters: 5 | 6 | ## Composer 7 | ```bash 8 | composer require --dev vimeo/psalm 9 | ``` 10 | 11 | If you'd like to use the Phar version 12 | ```bash 13 | composer require --dev psalm/phar 14 | ``` 15 | 16 | ## Config 17 | ```yaml 18 | # grumphp.yml 19 | grumphp: 20 | tasks: 21 | psalm: 22 | config: psalm.xml 23 | ignore_patterns: [] 24 | no_cache: false 25 | report: ~ 26 | output_format: null 27 | threads: 1 28 | triggered_by: ['php'] 29 | show_info: false 30 | ``` 31 | 32 | 33 | **config** 34 | 35 | *Default: null* 36 | 37 | With this parameter you can specify the path your project's configuration file. 38 | 39 | 40 | **ignore_patterns** 41 | 42 | *Default: []* 43 | 44 | This is a list of patterns that will be ignored by psalm. With this option you can skip files like 45 | tests. Leave this option blank to run psalm for every php file/directory specified in your 46 | configuration. 47 | 48 | 49 | **no_cache** 50 | 51 | *Default: false* 52 | 53 | With this parameter you can run Psalm without using the cache file. 54 | 55 | 56 | **report** 57 | 58 | *Default: null* 59 | 60 | With this path you can specify the path your psalm report file 61 | 62 | 63 | **output_format** 64 | 65 | *Default: null* 66 | 67 | Changes the output format. 68 | Available formats: compact, console, emacs, json, pylint, xml, checkstyle, junit, sonarqube 69 | 70 | **threads** 71 | 72 | *Default: null* 73 | 74 | This parameter defines on how many threads Psalm's analysis stage is ran. 75 | 76 | 77 | **triggered_by** 78 | 79 | *Default: [php]* 80 | 81 | This is a list of extensions to be sniffed. 82 | 83 | **show_info** 84 | 85 | *Default: false* 86 | 87 | Show non-exception parser findings 88 | -------------------------------------------------------------------------------- /doc/tasks/rector.md: -------------------------------------------------------------------------------- 1 | # Rector 2 | 3 | Rector is a tool to instantly upgrade and automatically refactor your PHP 5.3+ code. 4 | It lives under the `rector` namespace and has following configurable parameters: 5 | 6 | ## Composer 7 | ```bash 8 | composer require --dev rector/rector 9 | ``` 10 | 11 | ## Config 12 | ```yaml 13 | # grumphp.yml 14 | grumphp: 15 | tasks: 16 | rector: 17 | config: null 18 | triggered_by: ['php'] 19 | ignore_patterns: [] 20 | clear_cache: true 21 | no_diffs: false 22 | ``` 23 | 24 | **config** 25 | 26 | *Default: null* 27 | 28 | With this parameter you can specify the path your project's configuration file. When 'null' rector will run with the default file: rector.php 29 | 30 | **triggered_by** 31 | 32 | *Default: [php]* 33 | 34 | This is a list of extensions to be sniffed. 35 | 36 | 37 | **ignore_patterns** 38 | 39 | *Default: []* 40 | 41 | This is a list of patterns that will be ignored by Rector. With this option you can skip files like 42 | tests. Leave this option blank to run Rector for every php file/directory specified in your 43 | configuration. 44 | 45 | 46 | **clear_cache** 47 | 48 | *Default: true* 49 | 50 | With this parameter you can run Rector without using the cache. 51 | 52 | **no_diffs** 53 | 54 | *Default: false* 55 | 56 | With this parameter you can run Rector without showing file diffs. 57 | -------------------------------------------------------------------------------- /doc/tasks/robo.md: -------------------------------------------------------------------------------- 1 | # Robo 2 | 3 | The Robo task will run your automated PHP tasks. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev consolidation/robo 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `robo` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | robo: 20 | load_from: ~ 21 | task: ~ 22 | triggered_by: [php] 23 | ``` 24 | 25 | **load_from** 26 | 27 | *Default: null* 28 | 29 | If your Robofile.php file is located at an exotic location, you can specify the path to your custom location with this option. 30 | This option is set to `null` by default. 31 | This means that `Robofile.php` is automatically loaded if the file exists in the current directory. 32 | 33 | 34 | **task** 35 | 36 | *Default: null* 37 | 38 | This option specifies which Robo task you want to run. 39 | This option is set to `null` by default. 40 | This means that robo will run the `default` task. 41 | Note that this task should be used to verify things. 42 | It is also possible to alter code during commit, but this is surely **NOT** recommended! 43 | 44 | 45 | **triggered_by** 46 | 47 | *Default: [php]* 48 | 49 | This option will specify which file extensions will trigger the robo task. 50 | By default, Robo will be triggered by altering a PHP file. 51 | You can overwrite this option to whatever file you want to use! 52 | -------------------------------------------------------------------------------- /doc/tasks/securitychecker.md: -------------------------------------------------------------------------------- 1 | # Security Checker 2 | 3 | The SensioLabs Security Checker API is abandoned 4 | 5 | You can use one of following tasks as a replacement: 6 | 7 | - [securitychecker_composeraudit](securitychecker/composeraudit.md) 8 | - [securitychecker_enlightn](securitychecker/enlightn.md) 9 | - [securitychecker_local](securitychecker/local.md) 10 | - [securitychecker_roave](securitychecker/roave.md) 11 | - [securitychecker_symfony](securitychecker/symfony.md) 12 | -------------------------------------------------------------------------------- /doc/tasks/securitychecker/composeraudit.md: -------------------------------------------------------------------------------- 1 | # Composer Audit Security Checker 2 | 3 | The Security Checker will check your `composer.lock` file for known security vulnerabilities. 4 | 5 | ***Config*** 6 | 7 | The task lives under the `securitychecker_composeraudit` namespace and has the following configurable parameters: 8 | 9 | ```yaml 10 | # grumphp.yml 11 | grumphp: 12 | tasks: 13 | securitychecker_composeraudit: 14 | abandoned: null 15 | format: null 16 | locked: true 17 | no_dev: false 18 | run_always: false 19 | working_dir: null 20 | ``` 21 | 22 | **abandoned** 23 | 24 | *Default: null* 25 | 26 | You can choose the behavior on abandoned packages. The available options are `ignore`, `report` and `fail`. By default, grumphp will use the `fail` behavior. 27 | 28 | **format** 29 | 30 | *Default: null* 31 | 32 | You can choose the format of the output. The available options are `table`, `plain`, `json` and `summary`. By default, grumphp will use the format `table`. 33 | 34 | **locked** 35 | 36 | *Default: true* 37 | 38 | Audit packages from the lock file, regardless of what is currently in vendor dir. 39 | 40 | **no_dev** 41 | 42 | *Default: false* 43 | 44 | When this option is set to `true`, the task will skip packages under `require-dev`. 45 | 46 | **run_always** 47 | 48 | *Default: false* 49 | 50 | When this option is set to `false`, the task will only run when the `composer.lock` file has changed. If it is set to `true`, the `composer.lock` file will be checked on every commit. 51 | 52 | **working_dir** 53 | 54 | *Default: null 55 | 56 | If your `composer.lock` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.lock` file in the current directory. -------------------------------------------------------------------------------- /doc/tasks/securitychecker/enlightn.md: -------------------------------------------------------------------------------- 1 | # Enlightn Security Checker 2 | 3 | The Security Checker will check your `composer.lock` file for known security vulnerabilities. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev enlightn/security-checker 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `securitychecker_enlightn` namespace and has the following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | securitychecker_enlightn: 20 | lockfile: ./composer.lock 21 | run_always: false 22 | allow_list: 23 | - CVE-2018-15133 24 | - CVE-2024-51755 25 | - CVE-2024-45411 26 | ``` 27 | 28 | **lockfile** 29 | 30 | *Default: ./composer.lock* 31 | 32 | If your `composer.lock` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.lock` file in the current directory. 33 | 34 | **run_always** 35 | 36 | *Default: false* 37 | 38 | When this option is set to `false`, the task will only run when the `composer.lock` file has changed. If it is set to `true`, the `composer.lock` file will be checked on every commit. 39 | 40 | **allow_list** 41 | 42 | *Default: []* 43 | 44 | This option allows you to specify a list of CVE identifiers that should be ignored during the security check. This is useful if you have assessed certain vulnerabilities and determined that they do not pose a risk to your project. The CVE identifiers should be provided as an array of strings. For example: 45 | 46 | ```yaml 47 | allow_list: 48 | - CVE-2018-15133 49 | - CVE-2024-51755 50 | - CVE-2024-45411 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /doc/tasks/securitychecker/local.md: -------------------------------------------------------------------------------- 1 | # Local Security Checker 2 | 3 | The Security Checker will check your `composer.lock` file for known security vulnerabilities. 4 | 5 | ***Binary*** 6 | 7 | Download the latest binary from [fabpot/local-php-security-checker ](https://github.com/fabpot/local-php-security-checker/releases) and make sure it is part of your PATH or place it in one of the directories defined by environment.paths in your grumphp.yml file. 8 | 9 | ***Config*** 10 | 11 | The task lives under the `securitychecker_local` namespace and has the following configurable parameters: 12 | 13 | ```yaml 14 | # grumphp.yml 15 | grumphp: 16 | tasks: 17 | securitychecker_local: 18 | lockfile: ./composer.lock 19 | format: ~ 20 | run_always: false 21 | no_dev: false 22 | ``` 23 | 24 | **lockfile** 25 | 26 | *Default: ./composer.lock* 27 | 28 | If your `composer.lock` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.lock` file in the current directory. 29 | 30 | **format** 31 | 32 | *Default: null* 33 | 34 | You can choose the format of the output. The available options are `ansi`, `json`, `markdown` and `yaml`. By default, grumphp will use the format `ansi`. 35 | 36 | **run_always** 37 | 38 | *Default: false* 39 | 40 | When this option is set to `false`, the task will only run when the `composer.lock` file has changed. If it is set to `true`, the `composer.lock` file will be checked on every commit. 41 | 42 | **no_dev** 43 | 44 | *Default: false* 45 | 46 | When this option is set to `true`, the task will skip packages under `require-dev`. 47 | -------------------------------------------------------------------------------- /doc/tasks/securitychecker/roave.md: -------------------------------------------------------------------------------- 1 | # Roave Security Checker 2 | 3 | The Security Checker will check your `composer.lock` file for known security vulnerabilities. 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev roave/security-advisories:dev-latest 9 | ``` 10 | More information about the library can be found on [GitHub](https://github.com/Roave/SecurityAdvisories). 11 | 12 | ***Config*** 13 | 14 | The task lives under the `securitychecker_roave` namespace and has the following configurable parameters: 15 | 16 | ```yaml 17 | # grumphp.yml 18 | grumphp: 19 | tasks: 20 | securitychecker_roave: 21 | jsonfile: ./composer.json 22 | lockfile: ./composer.lock 23 | run_always: false 24 | ``` 25 | 26 | **jsonfile** 27 | 28 | *Default: ./composer.json* 29 | 30 | If your `composer.json` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.json` file in the current directory. 31 | 32 | **lockfile** 33 | 34 | *Default: ./composer.lock* 35 | 36 | If your `composer.lock` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.lock` file in the current directory. 37 | 38 | **run_always** 39 | 40 | *Default: false* 41 | 42 | When this option is set to `false`, the task will only run when the `composer.lock` file has changed. If it is set to `true`, the `composer.lock` file will be checked on every commit. 43 | -------------------------------------------------------------------------------- /doc/tasks/securitychecker/symfony.md: -------------------------------------------------------------------------------- 1 | # Symfony Security Checker 2 | 3 | The Security Checker will check your `composer.lock` file for known security vulnerabilities. 4 | 5 | ***Binary*** 6 | 7 | Download the latest binary from [Symfony CLI](https://symfony.com/download) and make sure it is part of your PATH or place it in one of the directories defined by environment.paths in your grumphp.yml file. 8 | 9 | ***Config*** 10 | 11 | The task lives under the `securitychecker_symfony` namespace and has the following configurable parameters: 12 | 13 | ```yaml 14 | # grumphp.yml 15 | grumphp: 16 | tasks: 17 | securitychecker_symfony: 18 | lockfile: ./composer.lock 19 | format: ~ 20 | run_always: false 21 | ``` 22 | 23 | **lockfile** 24 | 25 | *Default: ./composer.lock* 26 | 27 | If your `composer.lock` file is located in an exotic location, you can specify the location with this option. By default, the task will try to load a `composer.lock` file in the current directory. 28 | 29 | **format** 30 | 31 | *Default: null* 32 | 33 | You can choose the format of the output. The available options are `ansi`, `json`, `markdown` and `yaml`. By default, grumphp will use the format `ansi`. 34 | 35 | **run_always** 36 | 37 | *Default: false* 38 | 39 | When this option is set to `false`, the task will only run when the `composer.lock` file has changed. If it is set to `true`, the `composer.lock` file will be checked on every commit. 40 | -------------------------------------------------------------------------------- /doc/tasks/twigcs.md: -------------------------------------------------------------------------------- 1 | # TwigCs 2 | 3 | Check Twig coding standard based on [FriendsOfTwig/TwigCs](https://github.com/FriendsOfTwig/TwigCs) . 4 | 5 | ***Composer*** 6 | 7 | ``` 8 | composer require --dev "friendsoftwig/twigcs:>=4" 9 | ``` 10 | 11 | ***Config*** 12 | 13 | The task lives under the `twigcs` namespace and has following configurable parameters: 14 | 15 | ```yaml 16 | # grumphp.yml 17 | grumphp: 18 | tasks: 19 | twigcs: 20 | path: '.' 21 | severity: 'warning' 22 | display: 'all' 23 | ruleset: 'FriendsOfTwig\Twigcs\Ruleset\Official' 24 | triggered_by: ['twig'] 25 | exclude: [] 26 | ``` 27 | 28 | **path** 29 | 30 | *Default: null* 31 | 32 | By default `.` (current folder) will be used. 33 | On precommit the path will not be used, changed files will be passed as arguments instead. 34 | You can specify an alternate location by changing this option. If the path doesn't exist or is not accessible an exception will be thrown. 35 | 36 | **severity** 37 | 38 | *Default: 'warning'* 39 | 40 | Severity level of sniffing (possibles values are : 'IGNORE', 'INFO', 'WARNING', 'ERROR'). 41 | 42 | **display** 43 | 44 | *Default: 'all'* 45 | 46 | The violations to display (possibles values are : 'all', 'blocking'). 47 | 48 | **ruleset** 49 | 50 | *Default: 'FriendsOfTwig\Twigcs\Ruleset\Official'* 51 | 52 | Ruleset used, default ruleset is based on [official one from twig](https://twig.symfony.com/doc/2.x/coding_standards.html) 53 | 54 | **triggered_by** 55 | 56 | *Default: [twig]* 57 | 58 | This option will specify which file extensions will trigger this task. 59 | 60 | **exclude** 61 | 62 | *Default: []* 63 | 64 | This option will specify which relative subfolders or files will be excluded from this task. 65 | -------------------------------------------------------------------------------- /doc/tasks/xmllint.md: -------------------------------------------------------------------------------- 1 | # XmlLint 2 | 3 | The XmlLint task will lint all your XML files. 4 | It lives under the `xmllint` namespace and has following configurable parameters: 5 | 6 | ```yaml 7 | # grumphp.yml 8 | grumphp: 9 | tasks: 10 | xmllint: 11 | ignore_patterns: [] 12 | load_from_net: false 13 | x_include: false 14 | dtd_validation: false 15 | scheme_validation: false 16 | triggered_by: ['xml'] 17 | ``` 18 | 19 | **ignore_patterns** 20 | 21 | *Default: []* 22 | 23 | This is a list of patterns that will be ignored by the linter. 24 | With this option you can skip files like test fixtures. Leave this option blank to run the linter for every xml file. 25 | 26 | 27 | **load_from_net** 28 | 29 | *Default: false* 30 | 31 | This option can be used to tell the linter if external files can be loaded from the net. 32 | When enabled all online DTD and XSD resources will be loaded and validated if required. 33 | You can speed up the validation a lot by disabling this option. 34 | 35 | **x_include** 36 | 37 | *Default: false* 38 | 39 | By enabling this option, the xincluded resources you specified in the XMl are fetched. 40 | After fetching the resources, all additional validations are run on the complete XML resource. 41 | 42 | 43 | **dtd_validation** 44 | 45 | *Default: false* 46 | 47 | It is possible to validate XML against the specified DTD. 48 | Both internal, external as online resources are fetched and used for validation. 49 | 50 | 51 | **scheme_validation** 52 | 53 | *Default: false* 54 | 55 | It is possible to validate XML against the specified XSD schemes. 56 | Both internal, external as online resources are fetched and used for validation. 57 | 58 | **triggered_by** 59 | 60 | *Default: [xml]* 61 | 62 | This is a list of extensions to be sniffed. Extend it for including xsd, wsdl, and others. 63 | -------------------------------------------------------------------------------- /doc/testsuites.md: -------------------------------------------------------------------------------- 1 | # Test Suites 2 | Don't want to run all the tests during a commit? 3 | Do you want to specify some pre-defined tasks you want to run? 4 | It is easy to configure and run custom testsuites in GrumPHP 5 | Test suites live under their own namespace in the parameters part. 6 | 7 | 8 | ```yaml 9 | # grumphp.yml 10 | grumphp: 11 | testsuites: 12 | suitename: 13 | tasks: 14 | - phpcs 15 | - phpspec 16 | ``` 17 | 18 | It is possible to define multiple testsuites in the `grumphp.yml` file. 19 | Every test-suite has a unique name that can be used in the run command. 20 | A test-suite has the following parameters: 21 | 22 | 23 | **tasks** 24 | 25 | *Default: []* 26 | 27 | A test-suite consists of a list of tasks that should be executed. 28 | You can use any registered task name in the list of tasks. 29 | The list is validated against this list of registered tasks. 30 | When you enter an unknown task, an error will be thrown. 31 | 32 | 33 | ## Overriding git hook test-suites 34 | To make it possible to define which tests should run during a git hook, 35 | we made it possible to use one of following pre-defined test suites: 36 | 37 | ```yaml 38 | # grumphp.yml 39 | grumphp: 40 | testsuites: 41 | # Specify the test-suite for the git:commit-msg command: 42 | git_commit_msg: 43 | tasks: [] 44 | # Specify the test-suite for the git:pre-commit command: 45 | git_pre_commit: 46 | tasks: [] 47 | ``` 48 | -------------------------------------------------------------------------------- /grumphp.yml.dist: -------------------------------------------------------------------------------- 1 | grumphp: 2 | process_timeout: 480 3 | tasks: 4 | phpcs: 5 | standard: PSR2 6 | ignore_patterns: 7 | - "spec/*Spec.php" 8 | - "test/*.php" 9 | - "stubs/*.php" 10 | phpspec: 11 | format: progress 12 | verbose: true 13 | phpunit: 14 | testsuite: Unit 15 | composer: 16 | no_check_lock: true 17 | composer_normalize: 18 | use_standalone: true 19 | yamllint: 20 | parse_custom_tags: true 21 | ignore_patterns: 22 | - "#test/(.*).yml#" 23 | phplint: ~ 24 | phpparser: 25 | ignore_patterns: 26 | - '#src/Event/Event.php#' 27 | - '#test/Symfony/(.*)#' 28 | visitors: 29 | no_exit_statements: ~ 30 | never_use_else: ~ 31 | forbidden_function_calls: 32 | blacklist: [var_dump] 33 | paratest: 34 | testsuite: E2E 35 | verbose: true 36 | functional: true 37 | psalm: 38 | no_cache: true 39 | testsuites: 40 | git_pre_commit: 41 | tasks: [phpcs, phpspec, phpunit, composer, composer_normalize, yamllint, phplint, phpparser, psalm] 42 | # On CI, we run paratest separately. For some reason this currently fails in GitHub actions. 43 | ci: 44 | tasks: [phpcs, phpspec, phpunit, composer, composer_normalize, yamllint, phplint, phpparser, psalm] 45 | # Don't run psalm on Windows for now. There is a known issue with the Windows phar: 46 | # https://github.com/vimeo/psalm/issues/2858 47 | windows: 48 | tasks: [phpcs, phpspec, phpunit, composer, composer_normalize, yamllint, phplint, phpparser] 49 | environment: 50 | variables: 51 | BOX_REQUIREMENT_CHECKER: 0 52 | paths: 53 | - tools 54 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | default: 3 | namespace: GrumPHP 4 | psr4_prefix: GrumPHP 5 | 6 | formatter.name: pretty 7 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | <phpunit bootstrap="test/Bootstrap.php" colors="true"> 2 | <php> 3 | <ini name="error_reporting" value="-1" /> 4 | </php> 5 | <testsuites> 6 | <testsuite name="Unit"> 7 | <directory>test/Unit</directory> 8 | </testsuite> 9 | <testsuite name="E2E"> 10 | <directory>test/E2E</directory> 11 | </testsuite> 12 | </testsuites> 13 | </phpunit> 14 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | <psalm 3 | errorLevel="2" 4 | resolveFromConfigFile="true" 5 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 6 | xmlns="https://getpsalm.org/schema/config" 7 | xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" 8 | findUnusedBaselineEntry="false" 9 | findUnusedCode="false" 10 | > 11 | <projectFiles> 12 | <directory name="src" /> 13 | <ignoreFiles> 14 | <directory name="src/Test" /> 15 | <directory name="vendor" /> 16 | <directory name="test" /> 17 | <directory name="spec" /> 18 | <file name="src/Parser/Php/Visitor/AbstractVisitor.php" /> 19 | </ignoreFiles> 20 | </projectFiles> 21 | <issueHandlers> 22 | <RiskyTruthyFalsyComparison> 23 | <errorLevel type="suppress"> 24 | <directory name="src/" /> 25 | </errorLevel> 26 | </RiskyTruthyFalsyComparison> 27 | </issueHandlers> 28 | </psalm> 29 | -------------------------------------------------------------------------------- /resources/ascii/failed.txt: -------------------------------------------------------------------------------- 1 | ███████╗ █████╗ ██╗██╗ ███████╗██████╗ 2 | ██╔════╝██╔══██╗██║██║ ██╔════╝██╔══██╗ 3 | █████╗ ███████║██║██║ █████╗ ██║ ██║ 4 | ██╔══╝ ██╔══██║██║██║ ██╔══╝ ██║ ██║ 5 | ██║ ██║ ██║██║███████╗███████╗██████╔╝ 6 | ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═════╝ 7 | -------------------------------------------------------------------------------- /resources/ascii/grumphp-grumpy.txt: -------------------------------------------------------------------------------- 1 | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 2 | ▄▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 3 | ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▄ 4 | ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 5 | ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 6 | ▄███▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 7 | █▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 8 | ▐█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 9 | ▀█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 10 | ▀▀▓▓▓▓▓▓▓▓▓▓▓▓█▀▀▀▀▀▀▀▀▀▀▀▀▀▀████████████▄ 11 | ▄███████ ██████████ 12 | ███████▀ ▀▀▀▀▀▄ ▄▀▀▀▀▀ █████ ▀ 13 | ▐████ ▐██ ▐██ ████▌ 14 | ████▌ ███ 15 | ▌██▌ ▄▄ ▄▄ ▐███ 16 | ███ ▄▄▄▄▄▄▄▄▄▄▄▄ ▐███ 17 | ██▄ ▐███████████████████████████ 18 | █▀███████████▀ ▀▀███████████ 19 | ██████████▄███████▄███████████ 20 | ▐█████████████████████████████ 21 | █████████████████████████████ 22 | ██ █████████████████████▐██▀ 23 | ▀ ▐███████████████████▌ ▐▀ 24 | ████▀████████▀▐███ 25 | ▀█▌ ▐█████ ██▌ 26 | ██▀ ▐▀ 27 | 28 | ██████████████████████████████████ 29 | █░░░░░░▀█▀░░░░░░▀█░░░░░░▀█▀░░░░░▀█ 30 | █░░▐█▌░░█░░░██░░░█░░██░░░█░░░██░░█ 31 | █░░▐█▌░░█░░░██░░░█░░██░░░█░░░██░░█ 32 | █░░▐█▌░░█░░░██░░░█░░░░░░▄█░░▄▄▄▄▄█ 33 | █░░▐█▌░░█░░░██░░░█░░░░████░░░░░░░█ 34 | █░░░█░░░█▄░░░░░░▄█░░░░████▄░░░░░▄█ 35 | ██████████████████████████████████ 36 | -------------------------------------------------------------------------------- /resources/ascii/grumphp-happy.txt: -------------------------------------------------------------------------------- 1 | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ 2 | ▄▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 3 | ▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 4 | ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 5 | ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 6 | ▄▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 7 | ▐▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 8 | ▐█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 9 | ▀█▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▌ 10 | ▀▀▓▓▓▓▓▓▓▓▓▓▓▓█▀▀▀▀▀▀▀▀▀▀▀▀▀▀████████████▄ 11 | ▄████████▀▀▀▀▀ ██████████ 12 | ███████▀ ██████▀ 13 | ▐████ ██▌ ██ ████▌ 14 | ▐█▌ ███ 15 | █▌ ▄▄ ▄▄ ▐███ 16 | ███ ▄▄▄▄▄▄▄▄▄▄▄▄ ▐███ 17 | ██▄ ▐███████████████████████████ 18 | █▀█████████▌▀▀▀▀▀▀▀▀▀██████████▌▐ 19 | ███████████▄▄▄▄▄▄▄███████████▌ 20 | ▐█████████████████████████████ 21 | █████████████████████████████ 22 | ██ █████████████████████▐██▀ 23 | ▀ ▐███████████████████▌ ▐▀ 24 | ████▀████████▀▐███ 25 | ▀█▌ ▐█████ ▐█▌ 26 | ██▀ ▐▀ 27 | _ _ _ _ _ 28 | / \ | | | __ _ ___ ___ __| | | 29 | / _ \ | | | / _` |/ _ \ / _ \ / _` | | 30 | / ___ \| | | | (_| | (_) | (_) | (_| |_| 31 | /_/ \_\_|_| \__, |\___/ \___/ \__,_(_) 32 | |___/ 33 | -------------------------------------------------------------------------------- /resources/ascii/me-gusta.txt: -------------------------------------------------------------------------------- 1 | ░░░░░░░░▄▄▄███░░░░░░░░░░░░░░░░░░░░ 2 | ░░░▄▄██████████░░░░░░░░░░░░░░░░░░░ 3 | ░███████████████░░░░░░░░░░░░░░░░░░ 4 | ░▀███████████████░░░░░▄▄▄░░░░░░░░░ 5 | ░░░███████████████▄███▀▀▀░░░░░░░░░ 6 | ░░░░███████████████▄▄░░░░░░░░░░░░░ 7 | ░░░░▄████████▀▀▄▄▄▄▄░▀░░░░░░░░░░░░ 8 | ▄███████▀█▄▀█▄░░█░▀▀▀░█░░▄▄░░░░░░░ 9 | ▀▀░░░██▄█▄░░▀█░░▄███████▄█▀░░░▄░░░ 10 | ░░░░░█░█▀▄▄▀▄▀░█▀▀▀█▀▄▄▀░░░░░░▄░▄█ 11 | ░░░░░█░█░░▀▀▄▄█▀░█▀▀░░█░░░░░░░▀██░ 12 | ░░░░░▀█▄░░░░░░░░░░░░░▄▀░░░░░░▄██░░ 13 | ░░░░░░▀█▄▄░░░░░░░░▄▄█░░░░░░▄▀░░█░░ 14 | ░░░░░░░░░▀███▀▀████▄██▄▄░░▄▀░░░░░░ 15 | ░░░░░░░░░░░█▄▀██▀██▀▄█▄░▀▀░░░░░░░░ 16 | ░░░░░░░░░░░██░▀█▄█░█▀░▀▄░░░░░░░░░░ 17 | ░░░░░░░░░░█░█▄░░▀█▄▄▄░░█░░░░░░░░░░ 18 | ░░░░░░░░░░█▀██▀▀▀▀░█▄░░░░░░░░░░░░░ 19 | ░░░░░░░░░░░░▀░░░░░░░░░░░▀░░░░░░░░░ 20 | ░░░░░░▄░░░▄░▄▄▄▄░░░░░░░░░░░░░░░░░░ 21 | ░░░░░░█▀▄▀█░█▄▄░░░░░░░░░░░░░░░░░░░ 22 | ░░░░░░█░░░█░█▄▄▄░░░░░░░░░░░░░░░░░░ 23 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 24 | ░░░░░▄▄▄░▄░░░▄░░▄▄▄░▄▄▄▄▄░░▄▄▄░░░░ 25 | ░░░░█░░░░█░░░█░█░░░░░░█░░░█░░░█░░░ 26 | ░░░░█░▀█░█░░░█░░▀▀▄░░░█░░░█▀▀▀█░░░ 27 | ░░░░░▀▀▀░░▀▀▀░░▄▄▄▀░░░▀░░░▀░░░▀░░░ 28 | -------------------------------------------------------------------------------- /resources/ascii/nopecat.txt: -------------------------------------------------------------------------------- 1 | ▌─────────────────────────▐█─────▐ 2 | ▌────▄──────────────────▄█▓█▌────▐ 3 | ▌───▐██▄───────────────▄▓░░▓▓────▐ 4 | ▌───▐█░██▓────────────▓▓░░░▓▌────▐ 5 | ▌───▐█▌░▓██──────────█▓░░░░▓─────▐ 6 | ▌────▓█▌░░▓█▄███████▄███▓░▓█─────▐ 7 | ▌────▓██▌░▓██░░░░░░░░░░▓█░▓▌─────▐ 8 | ▌─────▓█████░░░░░░░░░░░░▓██──────▐ 9 | ▌─────▓██▓░░░░░░░░░░░░░░░▓█──────▐ 10 | ▌─────▐█▓░░░░░░█▓░░▓█░░░░▓█▌─────▐ 11 | ▌─────▓█▌░▓█▓▓██▓░█▓▓▓▓▓░▓█▌─────▐ 12 | ▌─────▓▓░▓██████▓░▓███▓▓▌░█▓─────▐ 13 | ▌────▐▓▓░█▄▐▓▌█▓░░▓█▐▓▌▄▓░██─────▐ 14 | ▌────▓█▓░▓█▄▄▄█▓░░▓█▄▄▄█▓░██▌────▐ 15 | ▌────▓█▌░▓█████▓░░░▓███▓▀░▓█▓────▐ 16 | ▌───▐▓█░░░▀▓██▀░░░░░─▀▓▀░░▓█▓────▐ 17 | ▌───▓██░░░░░░░░▀▄▄▄▄▀░░░░░░▓▓────▐ 18 | ▌───▓█▌░░░░░░░░░░▐▌░░░░░░░░▓▓▌───▐ 19 | ▌───▓█░░░░░░░░░▄▀▀▀▀▄░░░░░░░█▓───▐ 20 | ▌──▐█▌░░░░░░░░▀░░░░░░▀░░░░░░█▓▌──▐ 21 | ▌──▓█░░░░░░░░░░░░░░░░░░░░░░░██▓──▐ 22 | ▌──▓█░░░░░░░░░░░░░░░░░░░░░░░▓█▓──▐ 23 | ▌──██░░░░░░░░░░░░░░░░░░░░░░░░█▓──▐ 24 | ▌──█▌░░░░░░░░░░░░░░░░░░░░░░░░▐▓▌─▐ 25 | ▌─▐▓░░░░░░░░░░░░░░░░░░░░░░░░░░█▓─▐ 26 | ▌─█▓░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓─▐ 27 | ▌─█▓░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▌▐ 28 | ▌▐█▓░░░░░░░░░░░░░░░░░░░░░░░░░░░██▐ 29 | ▌█▓▌░░░░░░░░░░░░░░░░░░░░░░░░░░░▓█▐ 30 | ██████████████████████████████████ 31 | █░▀░░░░▀█▀░░░░░░▀█░░░░░░▀█▀░░░░░▀█ 32 | █░░▐█▌░░█░░░██░░░█░░██░░░█░░░██░░█ 33 | █░░▐█▌░░█░░░██░░░█░░██░░░█░░░██░░█ 34 | █░░▐█▌░░█░░░██░░░█░░░░░░▄█░░▄▄▄▄▄█ 35 | █░░▐█▌░░█░░░██░░░█░░░░████░░░░░░░█ 36 | █░░░█░░░█▄░░░░░░▄█░░░░████▄░░░░░▄█ 37 | ██████████████████████████████████ 38 | -------------------------------------------------------------------------------- /resources/ascii/succeeded.txt: -------------------------------------------------------------------------------- 1 | ███████╗██╗ ██╗ ██████╗ ██████╗███████╗███████╗██████╗ ███████╗██████╗ 2 | ██╔════╝██║ ██║██╔════╝██╔════╝██╔════╝██╔════╝██╔══██╗██╔════╝██╔══██╗ 3 | ███████╗██║ ██║██║ ██║ █████╗ █████╗ ██║ ██║█████╗ ██║ ██║ 4 | ╚════██║██║ ██║██║ ██║ ██╔══╝ ██╔══╝ ██║ ██║██╔══╝ ██║ ██║ 5 | ███████║╚██████╔╝╚██████╗╚██████╗███████╗███████╗██████╔╝███████╗██████╔╝ 6 | ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝╚══════╝╚═════╝ ╚══════╝╚═════╝ 7 | -------------------------------------------------------------------------------- /resources/config/config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # This entry makes sure to trigger the grumphp container extension. 3 | # It can be removed in the future if everybody migrated `parameters:` to `grumphp:` in the configuration file. 4 | grumphp: ~ 5 | 6 | # 7 | # Load config based on configured parameters 8 | # 9 | services: 10 | GrumPHP\Configuration\Model\AsciiConfig: 11 | arguments: 12 | - '%ascii%' 13 | GrumPHP\Configuration\Model\EnvConfig: 14 | public: true 15 | factory: ['GrumPHP\Configuration\Model\EnvConfig', 'fromArray'] 16 | arguments: 17 | - '%environment%' 18 | GrumPHP\Configuration\Model\HooksConfig: 19 | arguments: 20 | - '%hooks_dir%' 21 | - '%hooks_preset%' 22 | - '%git_hook_variables%' 23 | GrumPHP\Configuration\Model\ParallelConfig: 24 | factory: ['GrumPHP\Configuration\Model\ParallelConfig', 'fromArray'] 25 | arguments: 26 | - '%parallel%' 27 | GrumPHP\Configuration\Model\FixerConfig: 28 | factory: ['GrumPHP\Configuration\Model\FixerConfig', 'fromArray'] 29 | arguments: 30 | - '%fixer%' 31 | GrumPHP\Configuration\Model\ProcessConfig: 32 | arguments: 33 | - '%process_timeout%' 34 | GrumPHP\Configuration\Model\GitStashConfig: 35 | arguments: 36 | - '%ignore_unstaged_changes%' 37 | GrumPHP\Configuration\Model\RunnerConfig: 38 | arguments: 39 | - '%stop_on_failure%' 40 | - '%hide_circumvention_tip%' 41 | - '%additional_info%' 42 | -------------------------------------------------------------------------------- /resources/config/fixer.yml: -------------------------------------------------------------------------------- 1 | services: 2 | GrumPHP\Fixer\FixerUpper: 3 | arguments: 4 | - '@grumphp.io' 5 | - '@GrumPHP\Configuration\Model\FixerConfig' 6 | -------------------------------------------------------------------------------- /resources/config/formatter.yml: -------------------------------------------------------------------------------- 1 | services: 2 | formatter.raw_process: 3 | class: GrumPHP\Formatter\RawProcessFormatter 4 | formatter.phpcsfixer: 5 | class: GrumPHP\Formatter\PhpCsFixerFormatter 6 | formatter.phpcs: 7 | class: GrumPHP\Formatter\PhpcsFormatter 8 | formatter.git_blacklist: 9 | class: GrumPHP\Formatter\GitBlacklistFormatter 10 | arguments: 11 | - '@grumphp.io' 12 | -------------------------------------------------------------------------------- /resources/config/linters.yml: -------------------------------------------------------------------------------- 1 | services: 2 | linter.jsonlint: 3 | class: GrumPHP\Linter\Json\JsonLinter 4 | arguments: 5 | - '@grumphp.util.filesystem' 6 | - '@json.parser' 7 | 8 | linter.xmllint: 9 | class: GrumPHP\Linter\Xml\XmlLinter 10 | arguments: [] 11 | 12 | linter.yamllint: 13 | class: GrumPHP\Linter\Yaml\YamlLinter 14 | arguments: 15 | - '@grumphp.util.filesystem' 16 | -------------------------------------------------------------------------------- /resources/config/locators.yml: -------------------------------------------------------------------------------- 1 | services: 2 | GrumPHP\Locator\AsciiLocator: 3 | arguments: 4 | - '@GrumPHP\Configuration\Model\AsciiConfig' 5 | - '@filesystem' 6 | - '@GrumPHP\Util\Paths' 7 | 8 | GrumPHP\Locator\ExternalCommand: 9 | class: 10 | factory: ['GrumPHP\Locator\ExternalCommand', 'loadWithPaths'] 11 | arguments: 12 | - '@GrumPHP\Util\Paths' 13 | - '@executable_finder' 14 | 15 | GrumPHP\Locator\ChangedFiles: 16 | arguments: 17 | - '@GrumPHP\Git\GitRepository' 18 | - '@filesystem' 19 | - '@GrumPHP\Util\Paths' 20 | 21 | GrumPHP\Locator\ListedFiles: 22 | arguments: 23 | - '@GrumPHP\Util\Paths' 24 | 25 | GrumPHP\Locator\RegisteredFiles: 26 | arguments: 27 | - '@GrumPHP\Git\GitRepository' 28 | - '@GrumPHP\Util\Paths' 29 | - '@GrumPHP\Locator\ListedFiles' 30 | 31 | GrumPHP\Locator\StdInFiles: 32 | arguments: 33 | - '@GrumPHP\Locator\ChangedFiles' 34 | - '@GrumPHP\Locator\ListedFiles' 35 | 36 | GrumPHP\Locator\GitWorkingDirLocator: 37 | arguments: 38 | - '@executable_finder' 39 | GrumPHP\Locator\GitRepositoryDirLocator: 40 | arguments: 41 | - '@grumphp.util.filesystem' 42 | GrumPHP\Locator\GitRepositoryLocator: 43 | arguments: 44 | - '@GrumPHP\Util\Paths' 45 | GrumPHP\Locator\EnrichedGuessedPathsFromDotEnvLocator: 46 | public: true 47 | arguments: 48 | - '@grumphp.util.filesystem' 49 | -------------------------------------------------------------------------------- /resources/config/subscribers.yml: -------------------------------------------------------------------------------- 1 | services: 2 | subscriber.stash_unstaged_changes: 3 | class: GrumPHP\Event\Subscriber\StashUnstagedChangesSubscriber 4 | arguments: 5 | - '@GrumPHP\Configuration\Model\GitStashConfig' 6 | - '@GrumPHP\Git\GitRepository' 7 | - '@grumphp.io' 8 | tags: 9 | - { name: grumphp.event_subscriber } 10 | public: true 11 | 12 | GrumPHP\Event\Subscriber\VerboseLoggerSubscriber: 13 | arguments: 14 | - '@grumphp.logger' 15 | - '@GrumPHP\Configuration\GuessedPaths' 16 | tags: 17 | - { name: grumphp.event_subscriber } 18 | -------------------------------------------------------------------------------- /resources/config/util.yml: -------------------------------------------------------------------------------- 1 | services: 2 | grumphp.util.filesystem: 3 | class: GrumPHP\Util\Filesystem 4 | public: true 5 | 6 | GrumPHP\Util\Paths: 7 | public: true 8 | arguments: 9 | - '@grumphp.util.filesystem' 10 | - '@GrumPHP\Configuration\GuessedPaths' 11 | 12 | grumphp.util.phpversion: 13 | class: GrumPHP\Util\PhpVersion 14 | arguments: 15 | - 16 | '5.6': '2018-12-31 23:59:59' 17 | '7.0': '2018-12-03 23:59:59' 18 | '7.1': '2019-12-01 23:59:59' 19 | '7.2': '2020-11-30 23:59:59' 20 | '7.3': '2021-12-06 23:59:59' 21 | '7.4': '2022-11-28 23:59:59' 22 | '8.0': '2023-11-26 23:59:59' 23 | '8.1': '2025-12-31 23:59:59' 24 | '8.2': '2026-12-31 23:59:59' 25 | '8.3': '2027-12-31 23:59:59' 26 | '8.4': '2028-12-31 23:59:59' 27 | -------------------------------------------------------------------------------- /resources/hooks/local/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Run the hook command. 5 | # Note: this will be replaced by the real command during copy. 6 | # 7 | 8 | GIT_USER=$(git config user.name) 9 | GIT_EMAIL=$(git config user.email) 10 | COMMIT_MSG_FILE=$1 11 | 12 | # Fetch the GIT diff and format it as command input: 13 | DIFF=$(git -c diff.mnemonicprefix=false -c diff.noprefix=false --no-pager diff -r -p -m -M --full-index --no-color --staged | cat) 14 | 15 | # Grumphp env vars 16 | $(ENV) 17 | export GRUMPHP_GIT_WORKING_DIR="$(git rev-parse --show-toplevel)" 18 | 19 | # Run GrumPHP 20 | (cd "${HOOK_EXEC_PATH}" && printf "%s\n" "${DIFF}" | $(EXEC_GRUMPHP_COMMAND) $(HOOK_COMMAND) "--git-user='$GIT_USER'" "--git-email='$GIT_EMAIL'" "$COMMIT_MSG_FILE") 21 | -------------------------------------------------------------------------------- /resources/hooks/local/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Run the hook command. 5 | # Note: this will be replaced by the real command during copy. 6 | # 7 | 8 | # Fetch the GIT diff and format it as command input: 9 | DIFF=$(git -c diff.mnemonicprefix=false -c diff.noprefix=false --no-pager diff -r -p -m -M --full-index --no-color --staged | cat) 10 | 11 | # Grumphp env vars 12 | $(ENV) 13 | export GRUMPHP_GIT_WORKING_DIR="$(git rev-parse --show-toplevel)" 14 | 15 | # Run GrumPHP 16 | (cd "${HOOK_EXEC_PATH}" && printf "%s\n" "${DIFF}" | $(EXEC_GRUMPHP_COMMAND) $(HOOK_COMMAND) '--skip-success-output') 17 | -------------------------------------------------------------------------------- /resources/hooks/vagrant/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Run the hook command. 5 | # Note: this will be replaced by the real command during copy. 6 | # 7 | 8 | GIT_USER=$(git config user.name) 9 | GIT_EMAIL=$(git config user.email) 10 | 11 | # Fetch the commit message 12 | COMMIT_MSG_FILE=$1 13 | COMMIT_MSG=$(cat "${COMMIT_MSG_FILE}") 14 | 15 | # Fetch the GIT diff and format it as command input: 16 | DIFF=$(git -c diff.mnemonicprefix=false -c diff.noprefix=false --no-pager diff -r -p -m -M --full-index --no-color --staged | cat) 17 | 18 | # Copy the commit message and run GrumPHP 19 | cd $(VAGRANT_HOST_DIR) && vagrant ssh --command '$(which sh)' << COMMANDS 20 | 21 | cd $(VAGRANT_PROJECT_DIR) 22 | 23 | # Add grumphp envs: 24 | $(ENV) 25 | 26 | # Transfer the DIFF 27 | DIFF=\$(cat <<- '__GRUMPHP_DIFF_HEREDOC__' 28 | ${DIFF} 29 | __GRUMPHP_DIFF_HEREDOC__ 30 | ) 31 | 32 | # Transfer the commit message 33 | COMMIT_MSG=\$(cat <<- '__GRUMPHP_COMMIT_MSG_HEREDOC__' 34 | ${COMMIT_MSG} 35 | __GRUMPHP_COMMIT_MSG_HEREDOC__ 36 | ) 37 | 38 | VAGRANT_COMMIT_MSG_FILE=\$(mktemp -t "grumphp-commitmsg.XXXXXXXXXX") 39 | echo "\${COMMIT_MSG}" > \$VAGRANT_COMMIT_MSG_FILE 40 | printf "%s\n" "\${DIFF}" | $(EXEC_GRUMPHP_COMMAND) $(HOOK_COMMAND) '--ansi' "--git-user='$GIT_USER'" "--git-email='$GIT_EMAIL'" \$VAGRANT_COMMIT_MSG_FILE 41 | RC=\$? 42 | rm \$VAGRANT_COMMIT_MSG_FILE 43 | exit \$RC 44 | COMMANDS 45 | -------------------------------------------------------------------------------- /resources/hooks/vagrant/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Run the hook command. 5 | # Note: this will be replaced by the real command during copy. 6 | # 7 | 8 | # Fetch the GIT diff and format it as command input: 9 | DIFF=$(git -c diff.mnemonicprefix=false -c diff.noprefix=false --no-pager diff -r -p -m -M --full-index --no-color --staged | cat) 10 | 11 | # Run GrumPHP 12 | cd $(VAGRANT_HOST_DIR) && vagrant ssh --command '$(which sh)' << COMMANDS 13 | 14 | cd $(VAGRANT_PROJECT_DIR) 15 | 16 | # Add grumphp envs: 17 | $(ENV) 18 | 19 | # Transfer the DIFF 20 | DIFF=\$(cat <<- '__GRUMPHP_DIFF_HEREDOC__' 21 | ${DIFF} 22 | __GRUMPHP_DIFF_HEREDOC__ 23 | ) 24 | 25 | printf "%s\n" "\${DIFF}" | $(EXEC_GRUMPHP_COMMAND) $(HOOK_COMMAND) '--ansi' '--skip-success-output' 26 | COMMANDS 27 | -------------------------------------------------------------------------------- /resources/logo/grumphp-grumpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpro/grumphp/5f5d566f3aa15548884b6309e785effaad9a9ea5/resources/logo/grumphp-grumpy.png -------------------------------------------------------------------------------- /resources/logo/grumphp-happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpro/grumphp/5f5d566f3aa15548884b6309e785effaad9a9ea5/resources/logo/grumphp-happy.png -------------------------------------------------------------------------------- /resources/logo/simplified-grumpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpro/grumphp/5f5d566f3aa15548884b6309e785effaad9a9ea5/resources/logo/simplified-grumpy.png -------------------------------------------------------------------------------- /resources/logo/simplified-happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpro/grumphp/5f5d566f3aa15548884b6309e785effaad9a9ea5/resources/logo/simplified-happy.png -------------------------------------------------------------------------------- /src/Collection/LintErrorsCollection.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Collection; 6 | 7 | use Doctrine\Common\Collections\ArrayCollection; 8 | 9 | /** 10 | * @extends ArrayCollection<int, \GrumPHP\Linter\LintError> 11 | */ 12 | class LintErrorsCollection extends ArrayCollection 13 | { 14 | public function __toString(): string 15 | { 16 | $errors = []; 17 | foreach ($this->getIterator() as $error) { 18 | $errors[] = $error->__toString(); 19 | } 20 | 21 | return implode(PHP_EOL, $errors); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Collection/ParseErrorsCollection.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Collection; 6 | 7 | use Doctrine\Common\Collections\ArrayCollection; 8 | 9 | /** 10 | * @extends ArrayCollection<int, \GrumPHP\Parser\ParseError> 11 | */ 12 | class ParseErrorsCollection extends ArrayCollection 13 | { 14 | public function __toString(): string 15 | { 16 | $errors = []; 17 | foreach ($this->getIterator() as $error) { 18 | $errors[] = $error->__toString(); 19 | } 20 | 21 | return implode(PHP_EOL, $errors); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Collection/TaskResultCollection.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Collection; 6 | 7 | use Doctrine\Common\Collections\ArrayCollection; 8 | use GrumPHP\Runner\TaskResult; 9 | use GrumPHP\Runner\TaskResultInterface; 10 | 11 | /** 12 | * @extends ArrayCollection<int, TaskResultInterface> 13 | */ 14 | class TaskResultCollection extends ArrayCollection 15 | { 16 | const NO_TASKS = -100; 17 | 18 | public function isPassed(): bool 19 | { 20 | return TaskResult::PASSED === $this->getResultCode(); 21 | } 22 | 23 | public function isFailed(): bool 24 | { 25 | foreach ($this as $taskResult) { 26 | if (TaskResult::FAILED === $taskResult->getResultCode()) { 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | public function getResultCode(): int 35 | { 36 | $resultCode = static::NO_TASKS; 37 | foreach ($this as $taskResult) { 38 | $resultCode = (int) max($resultCode, $taskResult->getResultCode()); 39 | } 40 | 41 | return $resultCode; 42 | } 43 | 44 | public function filterByResultCode(int $resultCode): self 45 | { 46 | return $this->filter(function (TaskResultInterface $taskResult) use ($resultCode): bool { 47 | return $resultCode === $taskResult->getResultCode(); 48 | }); 49 | } 50 | 51 | /** 52 | * @return array<string, string> 53 | */ 54 | public function getAllMessages(): array 55 | { 56 | $messages = []; 57 | 58 | /** @var TaskResultInterface $taskResult */ 59 | foreach ($this as $taskResult) { 60 | $config = $taskResult->getTask()->getConfig(); 61 | $label = $config->getMetadata()->label() ?: $config->getName(); 62 | $messages[$label] = $taskResult->getMessage(); 63 | } 64 | 65 | return $messages; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Collection/TestSuiteCollection.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Collection; 6 | 7 | use Doctrine\Common\Collections\ArrayCollection; 8 | use GrumPHP\Exception\InvalidArgumentException; 9 | use GrumPHP\TestSuite\TestSuiteInterface; 10 | 11 | /** 12 | * @extends ArrayCollection<string, TestSuiteInterface> 13 | */ 14 | class TestSuiteCollection extends ArrayCollection 15 | { 16 | public function getRequired(string $name): TestSuiteInterface 17 | { 18 | if (!$result = $this->get($name)) { 19 | throw InvalidArgumentException::unknownTestSuite($name); 20 | } 21 | 22 | return $result; 23 | } 24 | 25 | public function getOptional(string $name): ?TestSuiteInterface 26 | { 27 | return $this->get($name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Configuration/Compiler/ExtensionCompilerPass.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Compiler; 6 | 7 | use GrumPHP\Configuration\LoaderFactory; 8 | use GrumPHP\Exception\RuntimeException; 9 | use GrumPHP\Extension\ExtensionInterface; 10 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 11 | use Symfony\Component\DependencyInjection\ContainerBuilder; 12 | 13 | class ExtensionCompilerPass implements CompilerPassInterface 14 | { 15 | public function process(ContainerBuilder $container): void 16 | { 17 | $loader = LoaderFactory::createLoader($container); 18 | $extensions = $container->getParameter('extensions'); 19 | $extensions = \is_array($extensions) ? $extensions : []; 20 | foreach ($extensions as $extensionClass) { 21 | if (!class_exists($extensionClass)) { 22 | throw new RuntimeException(sprintf('Invalid extension class specified: %s', $extensionClass)); 23 | } 24 | 25 | $extension = new $extensionClass(); 26 | if (!$extension instanceof ExtensionInterface) { 27 | throw new RuntimeException(sprintf( 28 | 'Extension class must implement ExtensionInterface. But `%s` is not.', 29 | $extensionClass 30 | )); 31 | } 32 | 33 | foreach ($extension->imports() as $import) { 34 | $loader->load($import); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Configuration/Compiler/RegisterListenersPass.php: -------------------------------------------------------------------------------- 1 | <?php 2 | declare(strict_types=1); 3 | 4 | namespace GrumPHP\Configuration\Compiler; 5 | 6 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 7 | use Symfony\Component\DependencyInjection\ContainerBuilder; 8 | use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass as SymfonyRegisterListenersPass; 9 | 10 | /** 11 | * @see https://github.com/symfony/symfony/pull/40468 12 | * Symfony removed the ability to add custom tags. So we need to take care of that! 13 | */ 14 | final class RegisterListenersPass implements CompilerPassInterface 15 | { 16 | private SymfonyRegisterListenersPass $pass; 17 | 18 | public function __construct(SymfonyRegisterListenersPass $pass) 19 | { 20 | $this->pass = $pass; 21 | } 22 | 23 | public static function create(): self 24 | { 25 | return new self(new SymfonyRegisterListenersPass()); 26 | } 27 | 28 | public function process(ContainerBuilder $container): void 29 | { 30 | $this->changeKey($container, 'grumphp.event_listener', 'kernel.event_listener'); 31 | $this->changeKey($container, 'grumphp.event_subscriber', 'kernel.event_subscriber'); 32 | 33 | $this->pass->process($container); 34 | } 35 | 36 | private function changeKey(ContainerBuilder $container, string $sourceKey, string $targetKey): void 37 | { 38 | foreach ($container->getDefinitions() as $definition) { 39 | if ($definition->hasTag($sourceKey)) { 40 | $attributes = $definition->getTag($sourceKey)[0]; 41 | $definition->addTag($targetKey, $attributes); 42 | $definition->clearTag($sourceKey); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Configuration/Configurator/TaskConfigurator.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Configurator; 6 | 7 | use GrumPHP\Task\Config\TaskConfigInterface; 8 | use GrumPHP\Task\TaskInterface; 9 | 10 | class TaskConfigurator 11 | { 12 | public function __invoke(TaskInterface $task, TaskConfigInterface $config): TaskInterface 13 | { 14 | return $task->withConfig($config); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Configuration/Environment/DotEnvRegistrar.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Environment; 6 | 7 | use GrumPHP\Configuration\Model\EnvConfig; 8 | use Symfony\Component\Dotenv\Dotenv; 9 | 10 | class DotEnvRegistrar 11 | { 12 | public static function register(EnvConfig $config): void 13 | { 14 | $env = new Dotenv(); 15 | 16 | if ($config->hasFiles()) { 17 | /** @psalm-suppress InvalidArgument - Psalm types in Dotenv class are not valid currently */ 18 | $env->overload(...$config->getFiles()); 19 | } 20 | 21 | if ($config->hasVariables()) { 22 | $env->populate($config->getVariables(), true); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Configuration/Environment/DotEnvSerializer.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Environment; 6 | 7 | class DotEnvSerializer 8 | { 9 | /** 10 | * @param array<string,string> $env 11 | * 12 | * @return string 13 | */ 14 | public static function serialize(array $env): string 15 | { 16 | return implode("\n", array_map( 17 | static function (string $key, string $value): string { 18 | return 'export '.$key.'='.$value; 19 | }, 20 | array_keys($env), 21 | $env 22 | )); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Configuration/GrumPHPExtension.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration; 6 | 7 | use Symfony\Component\Config\Definition\ConfigurationInterface; 8 | use Symfony\Component\DependencyInjection\ContainerBuilder; 9 | use Symfony\Component\DependencyInjection\Extension\Extension; 10 | 11 | class GrumPHPExtension extends Extension 12 | { 13 | public function load(array $configs, ContainerBuilder $container): void 14 | { 15 | $this->loadInternal( 16 | $this->processConfiguration( 17 | $this->getConfiguration($configs, $container), 18 | $configs 19 | ), 20 | $container 21 | ); 22 | } 23 | 24 | public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface 25 | { 26 | return new Configuration(); 27 | } 28 | 29 | public function getAlias(): string 30 | { 31 | return 'grumphp'; 32 | } 33 | 34 | private function loadInternal(array $config, ContainerBuilder $container): void 35 | { 36 | foreach ($config as $key => $value) { 37 | $container->setParameter($key, $value); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Configuration/Loader/DistFileLoader.php: -------------------------------------------------------------------------------- 1 | <?php 2 | declare(strict_types=1); 3 | 4 | namespace GrumPHP\Configuration\Loader; 5 | 6 | use Symfony\Component\Config\Loader\LoaderInterface; 7 | use Symfony\Component\Config\Loader\LoaderResolverInterface; 8 | 9 | /** 10 | * A decorating dist loader that supports **.dist files and defers loading. 11 | */ 12 | final class DistFileLoader implements LoaderInterface 13 | { 14 | private LoaderInterface $loader; 15 | 16 | public function __construct(LoaderInterface $loader) 17 | { 18 | $this->loader = $loader; 19 | } 20 | 21 | public function load(mixed $resource, ?string $type = null): mixed 22 | { 23 | return $this->loader->load($resource, $type); 24 | } 25 | 26 | public function supports(mixed $resource, ?string $type = null): bool 27 | { 28 | if (!\is_string($resource)) { 29 | return false; 30 | } 31 | 32 | if ($type !== null) { 33 | return $this->loader->supports($resource, $type); 34 | } 35 | 36 | $extension = pathinfo($resource, \PATHINFO_EXTENSION); 37 | if ($extension !== 'dist') { 38 | return false; 39 | } 40 | 41 | $distForFile = pathinfo($resource, \PATHINFO_FILENAME); 42 | 43 | return $this->loader->supports($distForFile); 44 | } 45 | 46 | public function getResolver(): LoaderResolverInterface 47 | { 48 | return $this->loader->getResolver(); 49 | } 50 | 51 | public function setResolver(LoaderResolverInterface $resolver): void 52 | { 53 | $this->loader->setResolver($resolver); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Configuration/LoaderFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | declare(strict_types=1); 3 | 4 | namespace GrumPHP\Configuration; 5 | 6 | use GrumPHP\Configuration\Loader\DistFileLoader; 7 | use Symfony\Component\Config\FileLocator; 8 | use Symfony\Component\Config\Loader\DelegatingLoader; 9 | use Symfony\Component\Config\Loader\LoaderResolver; 10 | use Symfony\Component\DependencyInjection\ContainerBuilder; 11 | use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; 12 | use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; 13 | use Symfony\Component\DependencyInjection\Loader\IniFileLoader; 14 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 15 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 16 | 17 | final class LoaderFactory 18 | { 19 | private const ENV = 'grumphp'; 20 | 21 | /** 22 | * @param list<string> $paths 23 | */ 24 | public static function createLoader(ContainerBuilder $container, array $paths = []): DelegatingLoader 25 | { 26 | $locator = new FileLocator($paths); 27 | $resolver = new LoaderResolver([ 28 | $xmlLoader = new XmlFileLoader($container, $locator, self::ENV), 29 | $yamlLoader = new YamlFileLoader($container, $locator, self::ENV), 30 | $iniLoader = new IniFileLoader($container, $locator, self::ENV), 31 | new GlobFileLoader($container, $locator, self::ENV), 32 | new DirectoryLoader($container, $locator, self::ENV), 33 | new DistFileLoader($xmlLoader), 34 | new DistFileLoader($yamlLoader), 35 | new DistFileLoader($iniLoader), 36 | ]); 37 | 38 | return new DelegatingLoader($resolver); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Configuration/Model/AsciiConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | class AsciiConfig 8 | { 9 | /** 10 | * @var array|null 11 | */ 12 | private $asciiConfig; 13 | 14 | public function __construct(?array $asciiConfig) 15 | { 16 | $this->asciiConfig = $asciiConfig; 17 | } 18 | 19 | public function fetchResource(string $resource): ?string 20 | { 21 | if (null === $this->asciiConfig) { 22 | return null; 23 | } 24 | 25 | $paths = $this->asciiConfig; 26 | if (!array_key_exists($resource, $paths)) { 27 | return null; 28 | } 29 | 30 | // Deal with multiple ascii files by returning one at random. 31 | if (\is_array($paths[$resource])) { 32 | shuffle($paths[$resource]); 33 | return reset($paths[$resource]); 34 | } 35 | 36 | return $paths[$resource]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Configuration/Model/FixerConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | /** 8 | * @psalm-immutable 9 | */ 10 | class FixerConfig 11 | { 12 | /** 13 | * @var bool 14 | */ 15 | private $enabled; 16 | 17 | /** 18 | * @var bool 19 | */ 20 | private $fixByDefault; 21 | 22 | public function __construct( 23 | bool $enabled, 24 | bool $fixByDefault 25 | ) { 26 | $this->enabled = $enabled; 27 | $this->fixByDefault = $fixByDefault; 28 | } 29 | 30 | /** 31 | * @param array{fix_by_default: bool, enabled: bool} $config 32 | */ 33 | public static function fromArray(array $config): self 34 | { 35 | return new self( 36 | ($config['enabled'] ?? false), 37 | ($config['fix_by_default'] ?? false) 38 | ); 39 | } 40 | 41 | public function isEnabled(): bool 42 | { 43 | return $this->enabled; 44 | } 45 | 46 | public function fixByDefault(): bool 47 | { 48 | return $this->fixByDefault; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Configuration/Model/GitStashConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | class GitStashConfig 8 | { 9 | /** 10 | * @var bool 11 | */ 12 | private $ignoreUnstagedChanges; 13 | 14 | public function __construct(bool $ignoreUnstagedChanges) 15 | { 16 | $this->ignoreUnstagedChanges = $ignoreUnstagedChanges; 17 | } 18 | 19 | public function ignoreUnstagedChanges(): bool 20 | { 21 | return $this->ignoreUnstagedChanges; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Configuration/Model/HooksConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | /** 8 | * @psalm-immutable 9 | */ 10 | class HooksConfig 11 | { 12 | /** 13 | * @var string|null 14 | */ 15 | private $dir; 16 | 17 | /** 18 | * @var string 19 | */ 20 | private $preset; 21 | 22 | /** 23 | * @var array 24 | */ 25 | private $variables; 26 | 27 | public function __construct( 28 | ?string $dir, 29 | string $preset, 30 | array $variables 31 | ) { 32 | $this->dir = $dir; 33 | $this->preset = $preset; 34 | $this->variables = $variables; 35 | } 36 | 37 | public function getDir(): ?string 38 | { 39 | return $this->dir; 40 | } 41 | 42 | public function getPreset(): string 43 | { 44 | return $this->preset; 45 | } 46 | 47 | public function getVariables(): array 48 | { 49 | return $this->variables; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Configuration/Model/ParallelConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | /** 8 | * @psalm-immutable 9 | */ 10 | class ParallelConfig 11 | { 12 | /** 13 | * @var bool 14 | */ 15 | private $enabled; 16 | 17 | /** 18 | * @var int 19 | */ 20 | private $maxWorkers; 21 | 22 | public function __construct( 23 | bool $enabled, 24 | int $maxWorkers 25 | ) { 26 | $this->enabled = $enabled; 27 | $this->maxWorkers = $maxWorkers; 28 | } 29 | 30 | /** 31 | * @param array{max_workers: int, enabled: bool} $config 32 | */ 33 | public static function fromArray(array $config): self 34 | { 35 | return new self( 36 | ($config['enabled'] ?? false), 37 | ($config['max_workers'] ?? 1) 38 | ); 39 | } 40 | 41 | public function isEnabled(): bool 42 | { 43 | return $this->enabled; 44 | } 45 | 46 | public function getMaxWorkers(): int 47 | { 48 | return $this->maxWorkers; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Configuration/Model/ProcessConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | /** 8 | * @psalm-immutable 9 | */ 10 | class ProcessConfig 11 | { 12 | /** 13 | * @var float|null 14 | */ 15 | private $timeout; 16 | 17 | public function __construct( 18 | ?float $timeout 19 | ) { 20 | $this->timeout = $timeout; 21 | } 22 | 23 | public function getTimeout(): ?float 24 | { 25 | return $this->timeout; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Configuration/Model/RunnerConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Model; 6 | 7 | class RunnerConfig 8 | { 9 | /** 10 | * @var bool 11 | */ 12 | private $stopOnFailure; 13 | 14 | /** 15 | * @var bool 16 | */ 17 | private $hideCircumventionTip; 18 | 19 | /** 20 | * @var string|null 21 | */ 22 | private $additionalInfo; 23 | 24 | public function __construct( 25 | bool $stopOnFailure, 26 | bool $hideCircumventionTip, 27 | ?string $additionalInfo 28 | ) { 29 | 30 | $this->stopOnFailure = $stopOnFailure; 31 | $this->hideCircumventionTip = $hideCircumventionTip; 32 | $this->additionalInfo = $additionalInfo; 33 | } 34 | 35 | public function stopOnFailure(): bool 36 | { 37 | return $this->stopOnFailure; 38 | } 39 | 40 | public function hideCircumventionTip(): bool 41 | { 42 | return $this->hideCircumventionTip; 43 | } 44 | 45 | public function getAdditionalInfo(): ?string 46 | { 47 | return $this->additionalInfo; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Configuration/Resolver/TaskConfigResolver.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Configuration\Resolver; 6 | 7 | use GrumPHP\Exception\TaskConfigResolverException; 8 | use GrumPHP\Task\Config\ConfigOptionsResolver; 9 | use GrumPHP\Task\TaskInterface; 10 | 11 | class TaskConfigResolver 12 | { 13 | /** 14 | * @var array<string, string> 15 | */ 16 | private $taskMap; 17 | 18 | public function __construct(array $taskMap) 19 | { 20 | $this->taskMap = $taskMap; 21 | } 22 | 23 | /** 24 | * @return array<string> 25 | */ 26 | public function listAvailableTaskNames(): array 27 | { 28 | return array_keys($this->taskMap); 29 | } 30 | 31 | public function resolve(string $taskName, array $config): array 32 | { 33 | $resolver = $this->fetchByName($taskName); 34 | 35 | // Make sure metadata is never a part of the task configuration 36 | unset($config['metadata']); 37 | 38 | return $resolver->resolve($config); 39 | } 40 | 41 | public function fetchByName(string $taskName): ConfigOptionsResolver 42 | { 43 | if (!array_key_exists($taskName, $this->taskMap)) { 44 | throw TaskConfigResolverException::unknownTask($taskName); 45 | } 46 | 47 | $class = $this->taskMap[$taskName]; 48 | if (!class_exists($class) || !is_subclass_of($class, TaskInterface::class)) { 49 | throw TaskConfigResolverException::unknownClass($class); 50 | } 51 | 52 | return $class::getConfigurableOptions(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Console/ApplicationConfigurator.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Console; 6 | 7 | use Symfony\Component\Console\Application; 8 | use Symfony\Component\Console\Input\InputOption; 9 | 10 | class ApplicationConfigurator 11 | { 12 | const APP_NAME = 'GrumPHP'; 13 | const APP_VERSION = '2.13.0'; 14 | 15 | public function configure(Application $application): void 16 | { 17 | $application->setVersion(self::APP_VERSION); 18 | $application->setName(self::APP_NAME); 19 | $this->registerInputDefinitions($application); 20 | } 21 | 22 | private function registerInputDefinitions(Application $application): void 23 | { 24 | $definition = $application->getDefinition(); 25 | $definition->addOption( 26 | new InputOption( 27 | 'config', 28 | 'c', 29 | InputOption::VALUE_REQUIRED, 30 | 'Path to config' 31 | ) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Event/Dispatcher/Bridge/SymfonyEventDispatcher.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event\Dispatcher\Bridge; 6 | 7 | use GrumPHP\Event\Dispatcher\EventDispatcherInterface; 8 | use GrumPHP\Event\Event; 9 | use Symfony\Component\EventDispatcher\EventDispatcherInterface as SymfonyLegacyEventDispatcher; 10 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherContract; 11 | 12 | class SymfonyEventDispatcher implements EventDispatcherInterface 13 | { 14 | /** 15 | * @var SymfonyLegacyEventDispatcher|SymfonyEventDispatcherContract 16 | */ 17 | private $dispatcher; 18 | 19 | /** 20 | * @param SymfonyLegacyEventDispatcher|SymfonyEventDispatcherContract $eventDispatcher 21 | */ 22 | public function __construct($eventDispatcher) 23 | { 24 | $this->dispatcher = $eventDispatcher; 25 | } 26 | 27 | public function dispatch(Event $event, ?string $name = null): void 28 | { 29 | $interfacesImplemented = class_implements($this->dispatcher); 30 | if (in_array(SymfonyEventDispatcherContract::class, $interfacesImplemented, true)) { 31 | /** 32 | * @psalm-suppress InvalidArgument 33 | * @psalm-suppress TooManyArguments 34 | */ 35 | $this->dispatcher->dispatch($event, $name); 36 | return; 37 | } 38 | 39 | /** 40 | * @psalm-suppress InvalidArgument 41 | * @psalm-suppress TooManyArguments 42 | */ 43 | $this->dispatcher->dispatch($name, $event); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Event/Dispatcher/EventDispatcherInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event\Dispatcher; 6 | 7 | use GrumPHP\Event\Event; 8 | 9 | interface EventDispatcherInterface 10 | { 11 | public function dispatch(Event $event, ?string $name = null): void; 12 | } 13 | -------------------------------------------------------------------------------- /src/Event/Event.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | use Symfony\Contracts\EventDispatcher\Event as SymfonyEventContract; 8 | use Symfony\Component\EventDispatcher\Event as SymfonyLegacyEvent; 9 | 10 | // @codingStandardsIgnoreStart 11 | if (class_exists(SymfonyEventContract::class)) { 12 | class Event extends SymfonyEventContract 13 | { 14 | } 15 | } else { 16 | class Event extends SymfonyLegacyEvent 17 | { 18 | } 19 | } 20 | // @codingStandardsIgnoreEnd 21 | -------------------------------------------------------------------------------- /src/Event/RunnerEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Collection\TasksCollection; 9 | use GrumPHP\Task\Context\ContextInterface; 10 | 11 | class RunnerEvent extends Event 12 | { 13 | /** 14 | * @var TasksCollection 15 | */ 16 | private $tasks; 17 | 18 | /** 19 | * @var ContextInterface 20 | */ 21 | private $context; 22 | 23 | /** 24 | * @var TaskResultCollection 25 | */ 26 | private $taskResults; 27 | 28 | public function __construct(TasksCollection $tasks, ContextInterface $context, TaskResultCollection $taskResults) 29 | { 30 | $this->tasks = $tasks; 31 | $this->context = $context; 32 | $this->taskResults = $taskResults; 33 | } 34 | 35 | public function getTasks(): TasksCollection 36 | { 37 | return $this->tasks; 38 | } 39 | 40 | public function getContext(): ContextInterface 41 | { 42 | return $this->context; 43 | } 44 | 45 | public function getTaskResults(): TaskResultCollection 46 | { 47 | return $this->taskResults; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Event/RunnerEvents.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | final class RunnerEvents 8 | { 9 | const RUNNER_RUN = 'grumphp.runner.run'; 10 | const RUNNER_COMPLETE = 'grumphp.runner.complete'; 11 | const RUNNER_FAILED = 'grumphp.runner.failed'; 12 | } 13 | -------------------------------------------------------------------------------- /src/Event/RunnerFailedEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | class RunnerFailedEvent extends RunnerEvent 8 | { 9 | public function getMessages(): array 10 | { 11 | $messages = []; 12 | 13 | foreach ($this->getTaskResults() as $taskResult) { 14 | if ('' !== $taskResult->getMessage()) { 15 | $messages[] = $taskResult->getMessage(); 16 | } 17 | } 18 | 19 | return $messages; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Event/TaskEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | use GrumPHP\Task\Context\ContextInterface; 8 | use GrumPHP\Task\TaskInterface; 9 | 10 | class TaskEvent extends Event 11 | { 12 | /** 13 | * @var TaskInterface 14 | */ 15 | private $task; 16 | 17 | /** 18 | * @var ContextInterface 19 | */ 20 | private $context; 21 | 22 | public function __construct(TaskInterface $task, ContextInterface $context) 23 | { 24 | $this->task = $task; 25 | $this->context = $context; 26 | } 27 | 28 | public function getTask(): TaskInterface 29 | { 30 | return $this->task; 31 | } 32 | 33 | public function getContext(): ContextInterface 34 | { 35 | return $this->context; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Event/TaskEvents.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | final class TaskEvents 8 | { 9 | const TASK_RUN = 'grumphp.task.run'; 10 | const TASK_COMPLETE = 'grumphp.task.complete'; 11 | const TASK_FAILED = 'grumphp.task.failed'; 12 | const TASK_SKIPPED = 'grumphp.task.skipped'; 13 | } 14 | -------------------------------------------------------------------------------- /src/Event/TaskFailedEvent.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Event; 6 | 7 | use Exception; 8 | use GrumPHP\Task\Context\ContextInterface; 9 | use GrumPHP\Task\TaskInterface; 10 | 11 | class TaskFailedEvent extends TaskEvent 12 | { 13 | /** 14 | * @var Exception 15 | */ 16 | private $exception; 17 | 18 | public function __construct(TaskInterface $task, ContextInterface $context, Exception $exception) 19 | { 20 | parent::__construct($task, $context); 21 | 22 | $this->exception = $exception; 23 | } 24 | 25 | public function getException(): Exception 26 | { 27 | return $this->exception; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | interface ExceptionInterface 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/Exception/ExecutableNotFoundException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | class ExecutableNotFoundException extends RuntimeException 8 | { 9 | public static function forCommand(string $command): self 10 | { 11 | return new self( 12 | sprintf('The executable for "%s" could not be found.', $command) 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exception/FileNotFoundException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | class FileNotFoundException extends RuntimeException 8 | { 9 | public function __construct(string $path) 10 | { 11 | parent::__construct(sprintf('File "%s" doesn\'t exists.', $path)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exception/FixerException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | use GrumPHP\Formatter\RawProcessFormatter; 8 | use Symfony\Component\Process\Process; 9 | 10 | class FixerException extends RuntimeException 11 | { 12 | public static function fromProcess(Process $process): self 13 | { 14 | return new self( 15 | 'Error while fixing: '. 16 | $process->getCommandLine() 17 | . PHP_EOL 18 | . (new RawProcessFormatter())->format($process) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | class InvalidArgumentException extends RuntimeException 8 | { 9 | public static function unknownTestSuite(string $testSuiteName): self 10 | { 11 | return new self(sprintf('Unknown testsuite specified: %s', $testSuiteName)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exception/ParallelException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | class ParallelException extends RuntimeException 8 | { 9 | public static function fromThrowable(\Throwable $error): self 10 | { 11 | return new self($error->getMessage(), (int)$error->getCode(), $error); 12 | } 13 | 14 | public static function fromVerboseThrowable(\Throwable $error): self 15 | { 16 | return new self( 17 | $error->getMessage() . PHP_EOL . $error->getTraceAsString() . PHP_EOL . (string) $error->getPrevious(), 18 | (int)$error->getCode(), 19 | $error 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/PlatformException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | use GrumPHP\Util\Platform; 8 | use Symfony\Component\Process\Process; 9 | 10 | class PlatformException extends RuntimeException 11 | { 12 | public static function commandLineStringLimit(Process $process): self 13 | { 14 | return new self(sprintf( 15 | 'The Windows maximum amount of %s input characters exceeded while running process: %s ...', 16 | Platform::WINDOWS_COMMANDLINE_STRING_LIMITATION, 17 | substr($process->getCommandLine(), 0, 75) 18 | )); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/ProcessException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | class ProcessException extends RuntimeException 8 | { 9 | public static function tmpFileCouldNotBeCreated(): self 10 | { 11 | return new self( 12 | 'The process requires a temporary file in order to run. We could not create one.' 13 | . 'Please check your ini setting!' 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | use Exception; 8 | use GrumPHP\Task\TaskInterface; 9 | use RuntimeException as BaseRuntimeException; 10 | 11 | class RuntimeException extends BaseRuntimeException implements ExceptionInterface 12 | { 13 | public static function fromAnyException(Exception $e): self 14 | { 15 | return new self($e->getMessage(), (int)$e->getCode(), $e); 16 | } 17 | 18 | public static function invalidTaskReturnType(TaskInterface $task): self 19 | { 20 | return new self(sprintf('The %s task did not return a TaskResult.', $task->getConfig()->getName())); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/TaskConfigResolverException.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Exception; 6 | 7 | use GrumPHP\Task\TaskInterface; 8 | 9 | class TaskConfigResolverException extends RuntimeException 10 | { 11 | public static function unknownTask(string $task): self 12 | { 13 | return new self('Could not load config resolver for task: "'.$task.'". The task is not known.'); 14 | } 15 | 16 | public static function unknownClass(string $class): self 17 | { 18 | return new self( 19 | sprintf( 20 | 'Could not load config resolver for class: "%s". Expected an instance of: "%s"', 21 | $class, 22 | TaskInterface::class 23 | ) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Extension/ExtensionInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Extension; 6 | 7 | /** 8 | * Registers your own GrumPHP. 9 | */ 10 | interface ExtensionInterface 11 | { 12 | /** 13 | * Return a list of additional symfony/conso:e service imports that 14 | * GrumPHP needs to perform after loading all internal configurations. 15 | * 16 | * We support following loaders: YAML, XML, INI, GLOB, DIR 17 | * 18 | * More info 19 | * @link https://symfony.com/doc/current/service_container.html 20 | * 21 | * @return iterable<string> 22 | */ 23 | public function imports(): iterable; 24 | } 25 | -------------------------------------------------------------------------------- /src/Fixer/FixResult.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Fixer; 6 | 7 | /** 8 | * @template TResult 9 | * @psalm-readonly 10 | */ 11 | class FixResult 12 | { 13 | /** 14 | * @var \Throwable|null 15 | */ 16 | private $error; 17 | 18 | /** 19 | * @var TResult|null 20 | */ 21 | private $result; 22 | 23 | /** 24 | * @param TResult|null $result 25 | */ 26 | private function __construct($result, ?\Throwable $error) 27 | { 28 | $this->error = $error; 29 | $this->result = $result; 30 | } 31 | 32 | public static function failed(\Throwable $error): self 33 | { 34 | return new self(null, $error); 35 | } 36 | 37 | /** 38 | * @param mixed $result 39 | */ 40 | public static function success($result): self 41 | { 42 | return new self($result, null); 43 | } 44 | 45 | public function ok(): bool 46 | { 47 | return null === $this->error; 48 | } 49 | 50 | /** 51 | * @return TResult|null 52 | */ 53 | public function result() 54 | { 55 | return $this->result; 56 | } 57 | 58 | public function error(): ?\Throwable 59 | { 60 | return $this->error; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Fixer/Provider/FixableProcessProvider.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Fixer\Provider; 6 | 7 | use GrumPHP\Exception\FixerException; 8 | use GrumPHP\Fixer\FixResult; 9 | use Laravel\SerializableClosure\SerializableClosure; 10 | use Symfony\Component\Process\Process; 11 | 12 | class FixableProcessProvider 13 | { 14 | /** 15 | * @param int[] $successExitCodes 16 | * 17 | * @return callable(): FixResult 18 | */ 19 | public static function provide(string $command, array $successExitCodes = [0]): callable 20 | { 21 | return new SerializableClosure( 22 | static function () use ($command, $successExitCodes): FixResult { 23 | $process = Process::fromShellCommandline($command); 24 | $process->run(); 25 | 26 | if (!in_array($process->getExitCode(), $successExitCodes, true)) { 27 | return FixResult::failed(FixerException::fromProcess($process)); 28 | } 29 | 30 | return FixResult::success($process->getOutput()); 31 | } 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Fixer/Provider/FixableProcessResultProvider.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Fixer\Provider; 6 | 7 | use GrumPHP\Runner\FixableTaskResult; 8 | use GrumPHP\Runner\TaskResultInterface; 9 | use Symfony\Component\Process\Process; 10 | 11 | class FixableProcessResultProvider 12 | { 13 | /** 14 | * @param callable(): Process $fixerProcessBuilder 15 | */ 16 | public static function provide( 17 | TaskResultInterface $taskResult, 18 | callable $fixerProcessBuilder, 19 | array $successExitCodes = [0] 20 | ): FixableTaskResult { 21 | $fixerProcess = $fixerProcessBuilder(); 22 | /** @psalm-suppress RedundantConditionGivenDocblockType */ 23 | assert($fixerProcess instanceof Process); 24 | 25 | $fixerCommand = $fixerProcess->getCommandLine(); 26 | $fixerMessage = sprintf( 27 | '%sYou can fix errors by running the following command:%s', 28 | PHP_EOL . PHP_EOL, 29 | PHP_EOL . $fixerCommand 30 | ); 31 | 32 | return new FixableTaskResult( 33 | $taskResult->withAppendedMessage($fixerMessage), 34 | FixableProcessProvider::provide($fixerCommand, $successExitCodes) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Formatter/PhpCsFixerFormatter.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Formatter; 6 | 7 | use Symfony\Component\Process\Process; 8 | 9 | class PhpCsFixerFormatter implements ProcessFormatterInterface 10 | { 11 | /** 12 | * @var int 13 | */ 14 | private $counter = 0; 15 | 16 | /** 17 | * Resets the internal counter. 18 | */ 19 | public function resetCounter(): void 20 | { 21 | $this->counter = 0; 22 | } 23 | 24 | public function format(Process $process): string 25 | { 26 | $output = $process->getOutput(); 27 | if (!$output) { 28 | return $process->getErrorOutput(); 29 | } 30 | 31 | if (!$json = json_decode($output, true)) { 32 | return $output; 33 | } 34 | 35 | return $this->formatJsonResponse($json); 36 | } 37 | 38 | private function formatJsonResponse(array $json): string 39 | { 40 | $formatted = []; 41 | foreach ($json['files'] as $file) { 42 | if (!\is_array($file) || !isset($file['name'])) { 43 | $formatted[] = 'Invalid file: '.print_r($file, true); 44 | continue; 45 | } 46 | 47 | $formatted[] = $this->formatFile($file); 48 | } 49 | 50 | return implode(PHP_EOL, $formatted); 51 | } 52 | 53 | private function formatFile(array $file): string 54 | { 55 | if (!isset($file['name'])) { 56 | return 'Invalid file: '.print_r($file, true); 57 | } 58 | 59 | $hasFixers = isset($file['appliedFixers']); 60 | $hasDiff = isset($file['diff']); 61 | 62 | return sprintf( 63 | '%s) %s%s%s', 64 | ++$this->counter, 65 | $file['name'], 66 | $hasFixers ? ' ('.implode(', ', $file['appliedFixers']).')' : '', 67 | $hasDiff ? PHP_EOL.PHP_EOL.$file['diff'] : '' 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Formatter/ProcessFormatterInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Formatter; 6 | 7 | use Symfony\Component\Process\Process; 8 | 9 | interface ProcessFormatterInterface 10 | { 11 | public function format(Process $process): string; 12 | } 13 | -------------------------------------------------------------------------------- /src/Formatter/RawProcessFormatter.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Formatter; 6 | 7 | use Symfony\Component\Process\Process; 8 | 9 | class RawProcessFormatter implements ProcessFormatterInterface 10 | { 11 | public function format(Process $process): string 12 | { 13 | $stdout = $process->getOutput(); 14 | $stderr = $process->getErrorOutput(); 15 | 16 | return trim($stdout.PHP_EOL.$stderr); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/IO/GitHubActionsIO.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace GrumPHP\IO; 4 | 5 | use Symfony\Component\Console\Output\ConsoleSectionOutput; 6 | 7 | class GitHubActionsIO extends ConsoleIO 8 | { 9 | public function startGroup(string $title): void 10 | { 11 | $this->write(['::group::' . $title]); 12 | parent::startGroup($title); 13 | } 14 | 15 | public function endGroup(): void 16 | { 17 | parent::endGroup(); 18 | $this->write(['::endgroup::']); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IO/IOFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace GrumPHP\IO; 4 | 5 | use OndraM\CiDetector\Ci\GitHubActions; 6 | use OndraM\CiDetector\CiDetector; 7 | use Symfony\Component\Console\Input\InputInterface; 8 | use Symfony\Component\Console\Output\OutputInterface; 9 | 10 | class IOFactory 11 | { 12 | public function __construct(private CiDetector $ciDetector) 13 | { 14 | } 15 | 16 | public function create(InputInterface $input, OutputInterface $output): IOInterface 17 | { 18 | if ($this->ciDetector->isCiDetected()) { 19 | $platform = $this->ciDetector->detect(); 20 | 21 | if ($platform instanceof GitHubActions) { 22 | return new GitHubActionsIO($input, $output); 23 | } 24 | } 25 | 26 | return new ConsoleIO($input, $output); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/IO/IOInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\IO; 6 | 7 | use Symfony\Component\Console\Output\ConsoleSectionOutput; 8 | use Symfony\Component\Console\Style\StyleInterface; 9 | 10 | interface IOInterface 11 | { 12 | public function isInteractive(): bool; 13 | 14 | public function isVerbose(): bool; 15 | 16 | public function isVeryVerbose(): bool; 17 | 18 | public function isDebug(): bool; 19 | 20 | public function isDecorated(): bool; 21 | 22 | /** 23 | * @return void 24 | */ 25 | public function write(array $messages, bool $newline = true); 26 | 27 | /** 28 | * @return void 29 | */ 30 | public function writeError(array $messages, bool $newline = true); 31 | 32 | public function style(): StyleInterface; 33 | 34 | public function section(): ConsoleSectionOutput; 35 | 36 | public function colorize(array $messages, string $color): array; 37 | 38 | public function startGroup(string $title): void; 39 | 40 | public function endGroup(): void; 41 | 42 | /** 43 | * @param resource $handle 44 | */ 45 | public function readCommandInput($handle): string; 46 | } 47 | -------------------------------------------------------------------------------- /src/Linter/Json/JsonLintError.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Linter\Json; 6 | 7 | use GrumPHP\Linter\LintError; 8 | use Seld\JsonLint\ParsingException; 9 | use SplFileInfo; 10 | 11 | class JsonLintError extends LintError 12 | { 13 | public static function fromParsingException(SplFileInfo $file, ParsingException $exception): self 14 | { 15 | return new self(LintError::TYPE_ERROR, $exception->getMessage(), $file->getPathname(), 0); 16 | } 17 | 18 | public function __toString(): string 19 | { 20 | return sprintf( 21 | '[%s] %s: %s', 22 | strtoupper($this->getType()), 23 | $this->getFile(), 24 | $this->getError() 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Linter/LintError.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Linter; 6 | 7 | class LintError 8 | { 9 | const TYPE_NONE = 'none'; 10 | const TYPE_WARNING = 'warning'; 11 | const TYPE_ERROR = 'error'; 12 | const TYPE_FATAL = 'fatal'; 13 | 14 | private $type; 15 | private $error; 16 | private $file; 17 | private $line; 18 | 19 | public function __construct(string $type, string $error, string $file, int $line) 20 | { 21 | $this->type = $type; 22 | $this->error = $error; 23 | $this->file = $file; 24 | $this->line = $line; 25 | } 26 | 27 | public function getType(): string 28 | { 29 | return $this->type; 30 | } 31 | 32 | public function getError(): string 33 | { 34 | return $this->error; 35 | } 36 | 37 | public function getFile(): string 38 | { 39 | return $this->file; 40 | } 41 | 42 | public function getLine(): int 43 | { 44 | return $this->line; 45 | } 46 | 47 | public function __toString(): string 48 | { 49 | return sprintf( 50 | '[%s] %s: %s on line %s', 51 | strtoupper($this->getType()), 52 | $this->getFile(), 53 | $this->getError(), 54 | $this->getLine() 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Linter/LinterInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Linter; 6 | 7 | use GrumPHP\Collection\LintErrorsCollection; 8 | use SplFileInfo; 9 | 10 | interface LinterInterface 11 | { 12 | public function lint(SplFileInfo $file): LintErrorsCollection; 13 | 14 | public function isInstalled(): bool; 15 | } 16 | -------------------------------------------------------------------------------- /src/Linter/Xml/XmlLintError.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Linter\Xml; 6 | 7 | use GrumPHP\Linter\LintError; 8 | use LibXMLError; 9 | 10 | class XmlLintError extends LintError 11 | { 12 | /** 13 | * @var int 14 | */ 15 | private $code; 16 | 17 | /** 18 | * @var int 19 | */ 20 | private $column; 21 | 22 | public function __construct( 23 | string $type, 24 | int $code, 25 | string $error, 26 | string $file, 27 | int $line, 28 | int $column 29 | ) { 30 | parent::__construct($type, $error, $file, $line); 31 | $this->code = $code; 32 | $this->column = $column; 33 | } 34 | 35 | public static function fromLibXmlError(LibXMLError $error): self 36 | { 37 | $type = LintError::TYPE_NONE; 38 | switch ($error->level) { 39 | case LIBXML_ERR_WARNING: 40 | $type = LintError::TYPE_WARNING; 41 | break; 42 | case LIBXML_ERR_FATAL: 43 | $type = LintError::TYPE_FATAL; 44 | break; 45 | case LIBXML_ERR_ERROR: 46 | $type = LintError::TYPE_ERROR; 47 | break; 48 | } 49 | 50 | return new self($type, $error->code, $error->message, $error->file, $error->line, $error->column); 51 | } 52 | 53 | public function getCode(): int 54 | { 55 | return $this->code; 56 | } 57 | 58 | public function getColumn(): int 59 | { 60 | return $this->column; 61 | } 62 | 63 | public function __toString(): string 64 | { 65 | return sprintf( 66 | '[%s] %s: %s (%s) on line %s,%s', 67 | strtoupper($this->getType()), 68 | $this->getFile(), 69 | $this->getError(), 70 | $this->getCode() ?: 0, 71 | $this->getLine(), 72 | $this->getColumn() ?: 0 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Linter/Yaml/YamlLintError.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Linter\Yaml; 6 | 7 | use GrumPHP\Linter\LintError; 8 | use Symfony\Component\Yaml\Exception\ParseException; 9 | 10 | class YamlLintError extends LintError 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private $snippet; 16 | 17 | public function __construct(string $type, string $error, string $file, int $line = -1, string $snippet = '') 18 | { 19 | parent::__construct($type, $error, $file, $line); 20 | $this->snippet = $snippet; 21 | } 22 | 23 | public static function fromParseException(ParseException $exception): self 24 | { 25 | return new self( 26 | LintError::TYPE_ERROR, 27 | $exception->getMessage(), 28 | $exception->getParsedFile(), 29 | $exception->getParsedLine(), 30 | $exception->getSnippet() 31 | ); 32 | } 33 | 34 | public function getSnippet(): string 35 | { 36 | return $this->snippet; 37 | } 38 | 39 | public function __toString(): string 40 | { 41 | return sprintf('[%s] %s', strtoupper($this->getType()), $this->getError()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Locator/AsciiLocator.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Configuration\Model\AsciiConfig; 8 | use GrumPHP\Util\Filesystem; 9 | use GrumPHP\Util\Paths; 10 | use SplFileInfo; 11 | 12 | class AsciiLocator 13 | { 14 | /** 15 | * @var AsciiConfig 16 | */ 17 | private $config; 18 | 19 | /** 20 | * @var Filesystem 21 | */ 22 | private $filesystem; 23 | 24 | /** 25 | * @var Paths 26 | */ 27 | private $paths; 28 | 29 | public function __construct(AsciiConfig $config, Filesystem $filesystem, Paths $paths) 30 | { 31 | $this->config = $config; 32 | $this->filesystem = $filesystem; 33 | $this->paths = $paths; 34 | } 35 | 36 | public function locate(string $resource): string 37 | { 38 | $file = $this->config->fetchResource($resource); 39 | 40 | // Disabled: 41 | if (null === $file) { 42 | return ''; 43 | } 44 | 45 | // Specified by user: 46 | if ($this->filesystem->exists($file)) { 47 | return $this->filesystem->readFromFileInfo(new SplFileInfo($file)); 48 | } 49 | 50 | // Embedded ASCII art: 51 | $embeddedFile = $this->filesystem->buildPath($this->paths->getInternalAsciiPath(), $file); 52 | if ($this->filesystem->exists($embeddedFile)) { 53 | return $this->filesystem->readFromFileInfo(new SplFileInfo($embeddedFile)); 54 | } 55 | 56 | // Error: 57 | return sprintf('ASCII file %s could not be found.', $file); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Locator/EnrichedGuessedPathsFromDotEnvLocator.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Configuration\GuessedPaths; 8 | use GrumPHP\Util\Filesystem; 9 | 10 | /** 11 | * @psalm-suppress RedundantCast - We don't want to blindly assume variables in $_SERVER are from the string type. 12 | */ 13 | class EnrichedGuessedPathsFromDotEnvLocator 14 | { 15 | /** 16 | * @var Filesystem 17 | */ 18 | private $filesystem; 19 | 20 | public function __construct(Filesystem $filesystem) 21 | { 22 | $this->filesystem = $filesystem; 23 | } 24 | 25 | public function locate(GuessedPaths $guessedPaths): GuessedPaths 26 | { 27 | $workingDir = $guessedPaths->getWorkingDir(); 28 | $projectDir = $this->filesystem->makePathAbsolute( 29 | (string) ($_SERVER['GRUMPHP_PROJECT_DIR'] ?? $guessedPaths->getProjectDir()), 30 | $workingDir 31 | ); 32 | $gitWorkingDir = $this->filesystem->makePathAbsolute( 33 | (string) ($_SERVER['GRUMPHP_GIT_WORKING_DIR'] ?? $guessedPaths->getGitWorkingDir()), 34 | $workingDir 35 | ); 36 | $gitRepositoryDir = $this->filesystem->makePathAbsolute( 37 | (string) ($_SERVER['GRUMPHP_GIT_REPOSITORY_DIR'] ?? $guessedPaths->getGitRepositoryDir()), 38 | $workingDir 39 | ); 40 | $binDir = $this->filesystem->makePathAbsolute( 41 | (string) ($_SERVER['GRUMPHP_BIN_DIR'] ?? $guessedPaths->getBinDir()), 42 | $workingDir 43 | ); 44 | 45 | return new GuessedPaths( 46 | $gitWorkingDir, 47 | $gitRepositoryDir, 48 | $workingDir, 49 | $projectDir, 50 | $binDir, 51 | $guessedPaths->getComposerFile(), 52 | $guessedPaths->getConfigFile() 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Locator/ExternalCommand.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Exception\ExecutableNotFoundException; 8 | use GrumPHP\Util\Paths; 9 | use GrumPHP\Util\Platform; 10 | use Symfony\Component\Process\ExecutableFinder; 11 | 12 | class ExternalCommand 13 | { 14 | /** 15 | * @var string 16 | */ 17 | protected $binDir; 18 | 19 | /** 20 | * @var ExecutableFinder 21 | */ 22 | protected $executableFinder; 23 | 24 | public function __construct(string $binDir, ExecutableFinder $executableFinder) 25 | { 26 | $this->binDir = rtrim($binDir, '/\\'); 27 | $this->executableFinder = $executableFinder; 28 | } 29 | 30 | public static function loadWithPaths(Paths $paths, ExecutableFinder $executableFinder): self 31 | { 32 | return new self( 33 | $paths->getBinDir(), 34 | $executableFinder 35 | ); 36 | } 37 | 38 | public function locate(string $command): string 39 | { 40 | $suffixes = Platform::isWindows() ? ['.bat', '', '.phar'] : ['', '.phar']; 41 | foreach ($suffixes as $suffix) { 42 | $cmdName = $command . $suffix; 43 | $executable = $this->executableFinder->find($cmdName, null, [$this->binDir]); 44 | 45 | if ($executable) { 46 | return $executable; 47 | } 48 | } 49 | 50 | throw ExecutableNotFoundException::forCommand($command); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Locator/GitRepositoryLocator.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use Gitonomy\Git\Repository; 8 | use GrumPHP\Util\Paths; 9 | 10 | class GitRepositoryLocator 11 | { 12 | /** 13 | * @var Paths 14 | */ 15 | private $paths; 16 | 17 | public function __construct(Paths $paths) 18 | { 19 | $this->paths = $paths; 20 | } 21 | 22 | public function locate(array $options): Repository 23 | { 24 | return new Repository( 25 | $this->paths->getGitRepositoryDir(), 26 | array_merge( 27 | [ 28 | 'working_dir' => $this->paths->getGitWorkingDir(), 29 | ], 30 | $options 31 | ) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Locator/GitWorkingDirLocator.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Collection\ProcessArgumentsCollection; 8 | use GrumPHP\Exception\RuntimeException; 9 | use GrumPHP\Process\ProcessFactory; 10 | use Symfony\Component\Process\ExecutableFinder; 11 | 12 | class GitWorkingDirLocator 13 | { 14 | /** 15 | * @var ExecutableFinder 16 | */ 17 | private $executableFinder; 18 | 19 | public function __construct(ExecutableFinder $executableFinder) 20 | { 21 | $this->executableFinder = $executableFinder; 22 | } 23 | 24 | public function locate(): string 25 | { 26 | $arguments = ProcessArgumentsCollection::forExecutable((string) $this->executableFinder->find('git', 'git')); 27 | $arguments->add('rev-parse'); 28 | $arguments->add('--show-toplevel'); 29 | 30 | $process = ProcessFactory::fromArguments($arguments); 31 | $process->run(); 32 | 33 | if (!$process->isSuccessful()) { 34 | throw new RuntimeException( 35 | 'The git directory could not be found. Did you initialize git? ('.$process->getErrorOutput().')' 36 | ); 37 | } 38 | 39 | return trim($process->getOutput()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Locator/ListedFiles.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | use GrumPHP\Util\Paths; 9 | use Symfony\Component\Finder\SplFileInfo; 10 | 11 | class ListedFiles 12 | { 13 | /** 14 | * @var Paths 15 | */ 16 | private $paths; 17 | 18 | public function __construct(Paths $paths) 19 | { 20 | $this->paths = $paths; 21 | } 22 | 23 | public function locate(string $fileList): FilesCollection 24 | { 25 | $filePaths = preg_split("/\r\n|\n|\r/", $fileList); 26 | 27 | $files = []; 28 | foreach (array_filter($filePaths) as $file) { 29 | $relativeFile = $this->paths->makePathRelativeToProjectDir($file); 30 | $files[] = new SplFileInfo($relativeFile, dirname($relativeFile), $relativeFile); 31 | } 32 | 33 | return new FilesCollection($files); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Locator/RegisteredFiles.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | use GrumPHP\Git\GitRepository; 9 | use GrumPHP\Util\Paths; 10 | 11 | class RegisteredFiles 12 | { 13 | /** 14 | * @var GitRepository 15 | */ 16 | private $repository; 17 | 18 | /** 19 | * @var Paths 20 | */ 21 | private $paths; 22 | 23 | /** 24 | * @var ListedFiles 25 | */ 26 | private $listedFiles; 27 | 28 | public function __construct(GitRepository $repository, Paths $paths, ListedFiles $listedFiles) 29 | { 30 | $this->repository = $repository; 31 | $this->paths = $paths; 32 | $this->listedFiles = $listedFiles; 33 | } 34 | 35 | public function locate(): FilesCollection 36 | { 37 | // Make sure to only return the files that are registered to GIT inside current project directory: 38 | $allFiles = trim((string) $this->repository->run('ls-files', [$this->paths->getProjectDir()])); 39 | 40 | return $this->listedFiles->locate($allFiles); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Locator/StdInFiles.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Locator; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | 9 | class StdInFiles 10 | { 11 | /** 12 | * @var ChangedFiles 13 | */ 14 | private $changedFilesLocator; 15 | 16 | /** 17 | * @var ListedFiles 18 | */ 19 | private $listedFiles; 20 | 21 | public function __construct( 22 | ChangedFiles $changedFilesLocator, 23 | ListedFiles $listedFiles 24 | ) { 25 | $this->changedFilesLocator = $changedFilesLocator; 26 | $this->listedFiles = $listedFiles; 27 | } 28 | 29 | public function locate(string $stdIn): FilesCollection 30 | { 31 | if (preg_match('/^diff --git/', $stdIn)) { 32 | return $this->changedFilesLocator->locateFromRawDiffInput($stdIn); 33 | } 34 | 35 | return $this->listedFiles->locate($stdIn); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Parser/ParseError.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser; 6 | 7 | class ParseError 8 | { 9 | const TYPE_NOTICE = 'notice'; 10 | const TYPE_WARNING = 'warning'; 11 | const TYPE_ERROR = 'error'; 12 | const TYPE_FATAL = 'fatal'; 13 | 14 | private $type; 15 | private $error; 16 | private $file; 17 | private $line; 18 | 19 | public function __construct(string $type, string $error, string $file, int $line = -1) 20 | { 21 | $this->type = $type; 22 | $this->error = $error; 23 | $this->file = $file; 24 | $this->line = $line; 25 | } 26 | 27 | public function getType(): string 28 | { 29 | return $this->type; 30 | } 31 | 32 | public function getError(): string 33 | { 34 | return $this->error; 35 | } 36 | 37 | public function getFile(): string 38 | { 39 | return $this->file; 40 | } 41 | 42 | public function getLine(): int 43 | { 44 | return $this->line; 45 | } 46 | 47 | public function __toString(): string 48 | { 49 | if ($this->getLine() < 0) { 50 | return sprintf( 51 | '[%s] %s: %s', 52 | strtoupper($this->getType()), 53 | $this->getFile(), 54 | $this->getError() 55 | ); 56 | } 57 | 58 | return sprintf( 59 | '[%s] %s: %s on line %d', 60 | strtoupper($this->getType()), 61 | $this->getFile(), 62 | $this->getError(), 63 | $this->getLine() 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Parser/ParserInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser; 6 | 7 | use GrumPHP\Collection\ParseErrorsCollection; 8 | use SplFileInfo; 9 | 10 | interface ParserInterface 11 | { 12 | public function parse(SplFileInfo $file): ParseErrorsCollection; 13 | 14 | public function isInstalled(): bool; 15 | } 16 | -------------------------------------------------------------------------------- /src/Parser/Php/Container/VisitorContainer.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Container; 6 | 7 | use PhpParser\NodeVisitor; 8 | use Psr\Container\ContainerInterface; 9 | use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; 10 | 11 | /** 12 | * This class is added to make sure the Symfony container doesn't get serialized for the phpparser task. 13 | * It will only contain an emty state of the visitor instances that are configured. 14 | * Every time a visitor is loaded, a cloned version will be provided instead. 15 | */ 16 | class VisitorContainer implements ContainerInterface 17 | { 18 | /** 19 | * @var array<string, NodeVisitor> 20 | */ 21 | private $instances; 22 | 23 | public function __construct(array $instances) 24 | { 25 | $this->instances = $instances; 26 | } 27 | 28 | /** 29 | * Always provide a cloned version of the visitor to make sure all properties get reset after a parser run. 30 | * @param string $id 31 | */ 32 | public function get($id): NodeVisitor 33 | { 34 | if (!$this->has($id)) { 35 | throw new ServiceNotFoundException($id); 36 | } 37 | 38 | return clone $this->instances[$id]; 39 | } 40 | 41 | /** 42 | * @param string $id 43 | */ 44 | public function has($id): bool 45 | { 46 | return array_key_exists($id, $this->instances); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Parser/Php/Context/ParserContext.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Context; 6 | 7 | use GrumPHP\Collection\ParseErrorsCollection; 8 | use SplFileInfo; 9 | 10 | class ParserContext 11 | { 12 | /** 13 | * @var SplFileInfo 14 | */ 15 | private $file; 16 | 17 | /** 18 | * @var ParseErrorsCollection 19 | */ 20 | private $errors; 21 | 22 | /** 23 | * ParserContext constructor. 24 | */ 25 | public function __construct(SplFileInfo $file, ParseErrorsCollection $errors) 26 | { 27 | $this->file = $file; 28 | $this->errors = $errors; 29 | } 30 | 31 | public function getFile(): SplFileInfo 32 | { 33 | return $this->file; 34 | } 35 | 36 | public function getErrors(): ParseErrorsCollection 37 | { 38 | return $this->errors; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Parser/Php/Factory/ParserFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Factory; 6 | 7 | use PhpParser\ParserFactory as PhpParserFactory; 8 | use PhpParser\PhpVersion; 9 | 10 | class ParserFactory 11 | { 12 | public function createFromOptions(array $options): \PhpParser\Parser 13 | { 14 | $version = $options['php_version'] ?? null; 15 | 16 | return (new PhpParserFactory())->createForVersion( 17 | match ($version) { 18 | null => PhpVersion::getHostVersion(), 19 | 'latest' => PhpVersion::getNewestSupported(), 20 | default => PhpVersion::fromString($version) 21 | } 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Parser/Php/Factory/TraverserFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Factory; 6 | 7 | use GrumPHP\Parser\Php\Configurator\TraverserConfigurator; 8 | use GrumPHP\Parser\Php\Context\ParserContext; 9 | use PhpParser\NodeTraverser; 10 | 11 | class TraverserFactory 12 | { 13 | /** 14 | * @var TraverserConfigurator 15 | */ 16 | private $configurator; 17 | 18 | /** 19 | * TraverserFactory constructor. 20 | */ 21 | public function __construct(TraverserConfigurator $configurator) 22 | { 23 | $this->configurator = $configurator; 24 | } 25 | 26 | /** 27 | * @throws \GrumPHP\Exception\RuntimeException 28 | */ 29 | public function createForTaskContext(array $parserOptions, ParserContext $context): NodeTraverser 30 | { 31 | $this->configurator->registerOptions($parserOptions); 32 | $this->configurator->registerContext($context); 33 | 34 | $traverser = new NodeTraverser(); 35 | $this->configurator->configure($traverser); 36 | 37 | return $traverser; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Parser/Php/PhpParserError.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php; 6 | 7 | use GrumPHP\Parser\ParseError; 8 | use PhpParser\Error; 9 | 10 | class PhpParserError extends ParseError 11 | { 12 | public static function fromParseException(Error $exception, string $filename): self 13 | { 14 | return new self( 15 | ParseError::TYPE_FATAL, 16 | $exception->getRawMessage(), 17 | $filename, 18 | $exception->getStartLine() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/AbstractVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use GrumPHP\Parser\ParseError; 8 | use GrumPHP\Parser\Php\Context\ParserContext; 9 | use GrumPHP\Parser\Php\PhpParserError; 10 | use PhpParser\NodeVisitorAbstract; 11 | 12 | /** 13 | * @psalm-suppress MissingConstructor 14 | */ 15 | class AbstractVisitor extends NodeVisitorAbstract implements ContextAwareVisitorInterface 16 | { 17 | /** 18 | * @var ParserContext 19 | */ 20 | protected $context; 21 | 22 | public function setContext(ParserContext $context): void 23 | { 24 | $this->context = $context; 25 | } 26 | 27 | protected function addError(string $message, int $line = -1, string $type = ParseError::TYPE_ERROR): void 28 | { 29 | $errors = $this->context->getErrors(); 30 | $fileName = $this->context->getFile()->getPathname(); 31 | $errors->add(new PhpParserError($type, $message, $fileName, $line)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/ConfigurableVisitorInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use PhpParser\NodeVisitor; 8 | 9 | interface ConfigurableVisitorInterface extends NodeVisitor 10 | { 11 | public function configure(array $options): void; 12 | } 13 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/ContextAwareVisitorInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use GrumPHP\Parser\Php\Context\ParserContext; 8 | use PhpParser\NodeVisitor; 9 | 10 | interface ContextAwareVisitorInterface extends NodeVisitor 11 | { 12 | public function setContext(ParserContext $context): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/DeclareStrictTypesVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use PhpParser\Node; 8 | 9 | class DeclareStrictTypesVisitor extends AbstractVisitor 10 | { 11 | /** 12 | * @var bool 13 | */ 14 | private $hasStrictType = false; 15 | 16 | public function leaveNode(Node $node): void 17 | { 18 | if (!$node instanceof Node\Stmt\Declare_) { 19 | return; 20 | } 21 | 22 | foreach ($node->declares as $id => $declare) { 23 | // In PhpParser 3 and lower the key used in a `declare()` statement 24 | // is represented as a string value. Starting with PhpParser 4 this 25 | // key is represented by a 'PhpParser\Node\Identifier' object. To 26 | // support backwards compatibility with older versions the object 27 | // can be cast to a string to get the original string value. 28 | if ((string) $declare->key !== 'strict_types') { 29 | continue; 30 | } 31 | 32 | /** @psalm-suppress UndefinedPropertyFetch */ 33 | $this->hasStrictType = $declare->value->value === 1; 34 | } 35 | } 36 | 37 | public function afterTraverse(array $nodes): void 38 | { 39 | if (!$this->hasStrictType) { 40 | $this->addError('No "declare(strict_types = 1)" found in file!'); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/ForbiddenClassMethodCallsVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use GrumPHP\Parser\ParseError; 8 | use PhpParser\Node; 9 | use Symfony\Component\OptionsResolver\OptionsResolver; 10 | 11 | class ForbiddenClassMethodCallsVisitor extends AbstractVisitor implements ConfigurableVisitorInterface 12 | { 13 | /** 14 | * @var array 15 | */ 16 | private $blacklist = []; 17 | 18 | public function configure(array $options): void 19 | { 20 | $resolver = new OptionsResolver(); 21 | 22 | $resolver->setDefaults([ 23 | 'blacklist' => [], 24 | ]); 25 | 26 | $resolver->setAllowedTypes('blacklist', ['array']); 27 | 28 | $config = $resolver->resolve($options); 29 | $this->blacklist = $config['blacklist']; 30 | } 31 | 32 | /** 33 | * @psalm-suppress UndefinedPropertyFetch 34 | * @psalm-suppress PossiblyInvalidCast 35 | * @psalm-suppress ImplicitToStringCast 36 | * @psalm-suppress InvalidArgument 37 | */ 38 | public function leaveNode(Node $node): void 39 | { 40 | if (!$node instanceof Node\Expr\MethodCall || !isset($node->var->name)) { 41 | return; 42 | } 43 | 44 | $variable = $node->var->name; 45 | $method = $node->name; 46 | $normalized = sprintf('$%s->%s', $variable, $method); 47 | if (!\in_array($normalized, $this->blacklist, true)) { 48 | return; 49 | } 50 | 51 | $this->addError( 52 | sprintf('Found blacklisted "%s" method call', $normalized), 53 | $node->getLine(), 54 | ParseError::TYPE_ERROR 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/ForbiddenFunctionCallsVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use PhpParser\Node; 8 | use Symfony\Component\OptionsResolver\OptionsResolver; 9 | 10 | class ForbiddenFunctionCallsVisitor extends AbstractVisitor implements ConfigurableVisitorInterface 11 | { 12 | /** 13 | * @var array 14 | */ 15 | private $blacklist = []; 16 | 17 | public function configure(array $options): void 18 | { 19 | $resolver = new OptionsResolver(); 20 | 21 | $resolver->setDefaults([ 22 | 'blacklist' => [], 23 | ]); 24 | 25 | $resolver->setAllowedTypes('blacklist', ['array']); 26 | 27 | $config = $resolver->resolve($options); 28 | $this->blacklist = $config['blacklist']; 29 | } 30 | 31 | /** 32 | * @psalm-suppress UndefinedPropertyFetch 33 | * @psalm-suppress PossiblyInvalidCast 34 | */ 35 | public function leaveNode(Node $node): void 36 | { 37 | 38 | if (!$node instanceof Node\Expr\FuncCall || !$node->name instanceof Node\Name) { 39 | return; 40 | } 41 | 42 | 43 | $function = (string) $node->name; 44 | if (!\in_array($function, $this->blacklist, false)) { 45 | return; 46 | } 47 | 48 | $this->addError( 49 | sprintf('Found blacklisted "%s" function call', $function), 50 | $node->getLine() 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/ForbiddenStaticMethodCallsVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use GrumPHP\Parser\ParseError; 8 | use PhpParser\Node; 9 | use Symfony\Component\OptionsResolver\OptionsResolver; 10 | 11 | class ForbiddenStaticMethodCallsVisitor extends AbstractVisitor implements ConfigurableVisitorInterface 12 | { 13 | /** 14 | * @var array 15 | */ 16 | private $blacklist = []; 17 | 18 | public function configure(array $options): void 19 | { 20 | $resolver = new OptionsResolver(); 21 | 22 | $resolver->setDefaults([ 23 | 'blacklist' => [], 24 | ]); 25 | 26 | $resolver->setAllowedTypes('blacklist', ['array']); 27 | 28 | $config = $resolver->resolve($options); 29 | $this->blacklist = $config['blacklist']; 30 | } 31 | 32 | /** 33 | * @psalm-suppress UndefinedPropertyFetch 34 | * @psalm-suppress PossiblyInvalidCast 35 | * @psalm-suppress ImplicitToStringCast 36 | * @psalm-suppress InvalidArgument 37 | */ 38 | public function leaveNode(Node $node): void 39 | { 40 | if (!$node instanceof Node\Expr\StaticCall || !$node->class instanceof Node\Name) { 41 | return; 42 | } 43 | 44 | $class = implode('\\', $node->class->getParts()); 45 | $method = $node->name; 46 | $normalized = sprintf('%s::%s', $class, $method); 47 | 48 | if (!\in_array($normalized, $this->blacklist, true)) { 49 | return; 50 | } 51 | 52 | $this->addError( 53 | sprintf('Found blacklisted "%s" static method call', $normalized), 54 | $node->getLine(), 55 | ParseError::TYPE_ERROR 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/NeverUseElseVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use GrumPHP\Parser\ParseError; 8 | use PhpParser\Node; 9 | 10 | class NeverUseElseVisitor extends AbstractVisitor 11 | { 12 | /** 13 | * @see http://www.slideshare.net/rdohms/your-code-sucks-lets-fix-it-15471808 14 | * @see http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php 15 | */ 16 | public function leaveNode(Node $node): void 17 | { 18 | if (!$node instanceof Node\Stmt\Else_ && !$node instanceof Node\Stmt\ElseIf_) { 19 | return; 20 | } 21 | 22 | $this->addError( 23 | sprintf( 24 | 'Object Calisthenics error: Do not use the "%s" keyword!', 25 | $node instanceof Node\Stmt\ElseIf_ ? 'elseif' : 'else' 26 | ), 27 | $node->getLine(), 28 | ParseError::TYPE_ERROR 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Parser/Php/Visitor/NoExitStatementsVisitor.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Parser\Php\Visitor; 6 | 7 | use GrumPHP\Parser\ParseError; 8 | use PhpParser\Node; 9 | 10 | class NoExitStatementsVisitor extends AbstractVisitor 11 | { 12 | public function leaveNode(Node $node): void 13 | { 14 | if (!$node instanceof Node\Expr\Exit_) { 15 | return; 16 | } 17 | 18 | $this->addError( 19 | 'Found a forbidden exit statement.', 20 | $node->getLine(), 21 | ParseError::TYPE_ERROR 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Process/InputWritingProcessRunner.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Process; 6 | 7 | use Symfony\Component\Process\InputStream; 8 | use Symfony\Component\Process\Process; 9 | 10 | /** 11 | * This runner can be used to run a process whilst writing data to STDIN 12 | */ 13 | class InputWritingProcessRunner 14 | { 15 | /** 16 | * @param callable(): Process $processBuilder 17 | * @param callable(): \Generator<array-key, string, mixed, void> $writer 18 | */ 19 | public static function run(callable $processBuilder, callable $writer): Process 20 | { 21 | $process = $processBuilder(); 22 | $inputStream = new InputStream(); 23 | $process->setInput($inputStream); 24 | $process->start(); 25 | foreach ($writer() as $input) { 26 | $inputStream->write($input); 27 | } 28 | 29 | $inputStream->close(); 30 | $process->wait(); 31 | 32 | return $process; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Process/ProcessFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Process; 6 | 7 | use GrumPHP\Collection\ProcessArgumentsCollection; 8 | use Symfony\Component\Process\Process; 9 | 10 | /** 11 | * @internal 12 | */ 13 | final class ProcessFactory 14 | { 15 | public static function fromArguments(ProcessArgumentsCollection $arguments): Process 16 | { 17 | return new Process($arguments->getValues()); 18 | } 19 | 20 | /** 21 | * @param array|string $arguments 22 | */ 23 | public static function fromScalar($arguments): Process 24 | { 25 | return is_array($arguments) ? new Process($arguments) : Process::fromShellCommandline($arguments); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Process/TmpFileUsingProcessRunner.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Process; 6 | 7 | use GrumPHP\Exception\ProcessException; 8 | use Symfony\Component\Process\Process; 9 | 10 | /** 11 | * This runner can be used to run a process whilst creating a file with temporary data. 12 | * Once the command has finished, the temporary file is removed. 13 | */ 14 | class TmpFileUsingProcessRunner 15 | { 16 | /** 17 | * @param callable(string): Process $processBuilder 18 | * @param callable(): \Generator<array-key, string, mixed, void> $writer 19 | */ 20 | public static function run(callable $processBuilder, callable $writer): Process 21 | { 22 | if (!$tmp = tmpfile()) { 23 | throw ProcessException::tmpFileCouldNotBeCreated(); 24 | } 25 | 26 | $path = stream_get_meta_data($tmp)['uri'] ?? null; 27 | if (!$path) { 28 | throw ProcessException::tmpFileCouldNotBeCreated(); 29 | } 30 | 31 | foreach ($writer() as $entry) { 32 | fwrite($tmp, $entry); 33 | } 34 | fseek($tmp, 0); 35 | 36 | try { 37 | $process = $processBuilder($path); 38 | $process->run(); 39 | } finally { 40 | fclose($tmp); 41 | } 42 | 43 | return $process; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Runner/Ci/CiDetector.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Ci; 6 | 7 | use OndraM\CiDetector\CiDetector as RealCiDetector; 8 | 9 | class CiDetector 10 | { 11 | /** 12 | * @var RealCiDetector 13 | */ 14 | private $ciDetector; 15 | 16 | /** 17 | * @var ?bool 18 | */ 19 | private $ciDetected; 20 | 21 | public function __construct(RealCiDetector $ciDetector) 22 | { 23 | $this->ciDetector = $ciDetector; 24 | } 25 | 26 | public function isCiDetected(): bool 27 | { 28 | if (null === $this->ciDetected) { 29 | $this->ciDetected = $this->ciDetector->isCiDetected(); 30 | } 31 | 32 | return $this->ciDetected; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Runner/MemoizedTaskResultMap.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner; 6 | 7 | final class MemoizedTaskResultMap 8 | { 9 | /** 10 | * @var array<string, TaskResultInterface> 11 | */ 12 | private $resultMap = []; 13 | 14 | public function onResult(TaskResultInterface $result): void 15 | { 16 | $this->resultMap[$result->getTask()->getConfig()->getName()] = $result; 17 | } 18 | 19 | public function contains(string $taskName): bool 20 | { 21 | return array_key_exists($taskName, $this->resultMap); 22 | } 23 | 24 | public function get(string $taskName): ?TaskResultInterface 25 | { 26 | return $this->resultMap[$taskName] ?? null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Runner/Middleware/EventDispatchingRunnerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Event\Dispatcher\EventDispatcherInterface; 9 | use GrumPHP\Event\RunnerEvent; 10 | use GrumPHP\Event\RunnerEvents; 11 | use GrumPHP\Event\RunnerFailedEvent; 12 | use GrumPHP\Runner\TaskRunnerContext; 13 | 14 | class EventDispatchingRunnerMiddleware implements RunnerMiddlewareInterface 15 | { 16 | /** 17 | * @var EventDispatcherInterface 18 | */ 19 | private $eventDispatcher; 20 | 21 | public function __construct(EventDispatcherInterface $eventDispatcher) 22 | { 23 | $this->eventDispatcher = $eventDispatcher; 24 | } 25 | 26 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection 27 | { 28 | $this->eventDispatcher->dispatch( 29 | new RunnerEvent($context->getTasks(), $context->getTaskContext(), new TaskResultCollection()), 30 | RunnerEvents::RUNNER_RUN 31 | ); 32 | 33 | /** @var TaskResultCollection $results */ 34 | $results = $next($context); 35 | 36 | if ($results->isFailed()) { 37 | $this->eventDispatcher->dispatch( 38 | new RunnerFailedEvent($context->getTasks(), $context->getTaskContext(), $results), 39 | RunnerEvents::RUNNER_FAILED 40 | ); 41 | 42 | return $results; 43 | } 44 | 45 | $this->eventDispatcher->dispatch( 46 | new RunnerEvent($context->getTasks(), $context->getTaskContext(), $results), 47 | RunnerEvents::RUNNER_COMPLETE 48 | ); 49 | 50 | return $results; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Runner/Middleware/FixCodeMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Fixer\FixerUpper; 9 | use GrumPHP\Runner\TaskRunnerContext; 10 | 11 | class FixCodeMiddleware implements RunnerMiddlewareInterface 12 | { 13 | /** 14 | * @var FixerUpper 15 | */ 16 | private $fixerUpper; 17 | 18 | public function __construct(FixerUpper $fixerUpper) 19 | { 20 | $this->fixerUpper = $fixerUpper; 21 | } 22 | 23 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection 24 | { 25 | /** @var TaskResultCollection $results */ 26 | $results = $next($context); 27 | 28 | $this->fixerUpper->fix($results); 29 | 30 | return $results; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Runner/Middleware/GroupByPriorityMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Configuration\Model\RunnerConfig; 9 | use GrumPHP\IO\IOInterface; 10 | use GrumPHP\Runner\TaskRunnerContext; 11 | 12 | class GroupByPriorityMiddleware implements RunnerMiddlewareInterface 13 | { 14 | /** 15 | * @var IOInterface 16 | */ 17 | private $IO; 18 | 19 | /** 20 | * @var RunnerConfig 21 | */ 22 | private $config; 23 | 24 | public function __construct(IOInterface $IO, RunnerConfig $config) 25 | { 26 | $this->IO = $IO; 27 | $this->config = $config; 28 | } 29 | 30 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection 31 | { 32 | $results = new TaskResultCollection(); 33 | $grouped = $context->getTasks() 34 | ->sortByPriority() 35 | ->groupByPriority(); 36 | 37 | foreach ($grouped as $priority => $tasks) { 38 | $this->IO->startGroup('Running tasks with priority '.$priority.'!'); 39 | $results = new TaskResultCollection(array_merge( 40 | $results->toArray(), 41 | $next($context->withTasks($tasks))->toArray() 42 | )); 43 | $this->IO->endGroup(); 44 | 45 | // Stop on failure: 46 | if ($this->config->stopOnFailure() && $results->isFailed()) { 47 | return $results; 48 | } 49 | } 50 | 51 | return $results; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Runner/Middleware/ReportingRunnerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Runner\Reporting\RunnerReporter; 9 | use GrumPHP\Runner\TaskRunnerContext; 10 | 11 | class ReportingRunnerMiddleware implements RunnerMiddlewareInterface 12 | { 13 | /** 14 | * @var RunnerReporter 15 | */ 16 | private $reporter; 17 | 18 | public function __construct(RunnerReporter $reporter) 19 | { 20 | $this->reporter = $reporter; 21 | } 22 | 23 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection 24 | { 25 | $this->reporter->start($context); 26 | $results = $next($context); 27 | $this->reporter->finish($context, $results); 28 | 29 | return $results; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Runner/Middleware/ReportingTasksSectionRunnerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Runner\Reporting\TaskResultsReporter; 9 | use GrumPHP\Runner\TaskRunnerContext; 10 | 11 | class ReportingTasksSectionRunnerMiddleware implements RunnerMiddlewareInterface 12 | { 13 | /** 14 | * @var TaskResultsReporter 15 | */ 16 | private $reporter; 17 | 18 | public function __construct(TaskResultsReporter $reporter) 19 | { 20 | $this->reporter = $reporter; 21 | } 22 | 23 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection 24 | { 25 | return $this->reporter->runInSection( 26 | /** 27 | * @return TaskResultCollection 28 | */ 29 | function () use ($context, $next): TaskResultCollection { 30 | $this->reporter->report($context); 31 | 32 | return $next($context); 33 | } 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Runner/Middleware/RunnerMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Runner\TaskRunnerContext; 9 | 10 | interface RunnerMiddlewareInterface 11 | { 12 | /** 13 | * @param callable(TaskRunnerContext $info): TaskResultCollection $next 14 | */ 15 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection; 16 | } 17 | -------------------------------------------------------------------------------- /src/Runner/Middleware/TasksFilteringRunnerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Middleware; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Collection\TasksCollection; 9 | use GrumPHP\Runner\TaskRunnerContext; 10 | 11 | class TasksFilteringRunnerMiddleware implements RunnerMiddlewareInterface 12 | { 13 | public function handle(TaskRunnerContext $context, callable $next): TaskResultCollection 14 | { 15 | return $next( 16 | $context->withTasks( 17 | (new TasksCollection($context->getTasks()->toArray())) 18 | ->filterByContext($context->getTaskContext()) 19 | ->filterByTestSuite($context->getTestSuite()) 20 | ->filterByTaskNames($context->getTaskNames()) 21 | ) 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Runner/Parallel/PoolFactory.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\Parallel; 6 | 7 | use Amp\Parallel\Worker\ContextWorkerPool; 8 | use Amp\Parallel\Worker\WorkerPool; 9 | use GrumPHP\Configuration\Model\ParallelConfig; 10 | 11 | class PoolFactory 12 | { 13 | private ParallelConfig $config; 14 | private ?WorkerPool $pool = null; 15 | 16 | public function __construct(ParallelConfig $config) 17 | { 18 | $this->config = $config; 19 | } 20 | 21 | public function createShared(): WorkerPool 22 | { 23 | if (!$this->pool) { 24 | $this->pool = new ContextWorkerPool( 25 | $this->config->getMaxWorkers() 26 | ); 27 | } 28 | 29 | return $this->pool; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Runner/Parallel/SerializedClosureTask.php: -------------------------------------------------------------------------------- 1 | <?php 2 | declare(strict_types=1); 3 | 4 | namespace GrumPHP\Runner\Parallel; 5 | 6 | use Amp\Cancellation; 7 | use Amp\Parallel\Worker\Task; 8 | use Amp\Sync\Channel; 9 | use Laravel\SerializableClosure\SerializableClosure; 10 | 11 | /** 12 | * @template-covariant TResult 13 | * @template TReceive 14 | * @template TSend 15 | * 16 | * @implements Task<TResult, TReceive, TSend> 17 | */ 18 | class SerializedClosureTask implements Task 19 | { 20 | /** 21 | * @param (\Closure(): TResult) $closure 22 | */ 23 | private function __construct( 24 | private string $serializedClosure 25 | ) { 26 | } 27 | 28 | /** 29 | * @template CResult 30 | * @template CReceive 31 | * @template CSend 32 | * 33 | * @param (\Closure(): CResult) $closure 34 | * @return self<CResult, CReceive, CSend> 35 | */ 36 | public static function fromClosure(\Closure $closure): self 37 | { 38 | return new self(serialize(new SerializableClosure($closure))); 39 | } 40 | 41 | /** 42 | * @return TResult 43 | */ 44 | public function run(Channel $channel, Cancellation $cancellation): mixed 45 | { 46 | $unserialized = \unserialize($this->serializedClosure, ['allowed_classes' => true]); 47 | 48 | if ($unserialized instanceof \__PHP_Incomplete_Class) { 49 | throw new \Error( 50 | 'When using a class instance as a callable, the class must be autoloadable' 51 | ); 52 | } 53 | 54 | if (!$unserialized instanceof SerializableClosure) { 55 | throw new \Error( 56 | 'This task can only deal with serialized closures. You passed '.get_debug_type($unserialized) 57 | ); 58 | } 59 | 60 | $closure = $unserialized->getClosure(); 61 | 62 | return $closure(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Runner/StopOnFailure.php: -------------------------------------------------------------------------------- 1 | <?php 2 | declare(strict_types=1); 3 | 4 | namespace GrumPHP\Runner; 5 | 6 | use Amp\Cancellation; 7 | use Amp\DeferredCancellation; 8 | use GrumPHP\Configuration\Model\RunnerConfig; 9 | 10 | final class StopOnFailure 11 | { 12 | private function __construct( 13 | private DeferredCancellation $cancellation, 14 | private bool $enabled 15 | ) { 16 | } 17 | 18 | public static function dummy(): self 19 | { 20 | return new self( 21 | new DeferredCancellation(), 22 | false 23 | ); 24 | } 25 | 26 | public static function createFromConfig(RunnerConfig $config): self 27 | { 28 | return new self( 29 | new DeferredCancellation(), 30 | $config->stopOnFailure() 31 | ); 32 | } 33 | 34 | public function cancellation(): Cancellation 35 | { 36 | return $this->cancellation->getCancellation(); 37 | } 38 | 39 | public function decideForResult(TaskResultInterface $result): void 40 | { 41 | if ($result->hasFailed() && $result->isBlocking()) { 42 | $this->stop(); 43 | } 44 | } 45 | 46 | public function stop(): void 47 | { 48 | if ($this->enabled) { 49 | $this->cancellation->cancel(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Runner/TaskHandler/Middleware/ErrorHandlingTaskHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\TaskHandler\Middleware; 6 | 7 | use GrumPHP\Runner\StopOnFailure; 8 | use function Amp\async; 9 | use Amp\Future; 10 | use GrumPHP\Exception\PlatformException; 11 | use GrumPHP\Runner\TaskResult; 12 | use GrumPHP\Runner\TaskResultInterface; 13 | use GrumPHP\Runner\TaskRunnerContext; 14 | use GrumPHP\Task\TaskInterface; 15 | 16 | class ErrorHandlingTaskHandlerMiddleware implements TaskHandlerMiddlewareInterface 17 | { 18 | public function handle( 19 | TaskInterface $task, 20 | TaskRunnerContext $runnerContext, 21 | StopOnFailure $stopOnFailure, 22 | callable $next 23 | ): Future { 24 | return async( 25 | static function () use ($task, $runnerContext): TaskResultInterface { 26 | $taskContext = $runnerContext->getTaskContext(); 27 | try { 28 | $result = $task->run($taskContext); 29 | } catch (PlatformException $e) { 30 | return TaskResult::createSkipped($task, $taskContext); 31 | } catch (\Throwable $e) { 32 | return TaskResult::createFailed($task, $taskContext, $e->getMessage()); 33 | } 34 | 35 | return $result; 36 | } 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Runner/TaskHandler/Middleware/MemoizedResultsTaskHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\TaskHandler\Middleware; 6 | 7 | use GrumPHP\Runner\StopOnFailure; 8 | use function Amp\async; 9 | use Amp\Future; 10 | use GrumPHP\Runner\MemoizedTaskResultMap; 11 | use GrumPHP\Runner\TaskResult; 12 | use GrumPHP\Runner\TaskResultInterface; 13 | use GrumPHP\Runner\TaskRunnerContext; 14 | use GrumPHP\Task\TaskInterface; 15 | 16 | class MemoizedResultsTaskHandlerMiddleware implements TaskHandlerMiddlewareInterface 17 | { 18 | /** 19 | * @var MemoizedTaskResultMap 20 | */ 21 | private $resultMap; 22 | 23 | public function __construct(MemoizedTaskResultMap $resultMap) 24 | { 25 | $this->resultMap = $resultMap; 26 | } 27 | 28 | public function handle( 29 | TaskInterface $task, 30 | TaskRunnerContext $runnerContext, 31 | StopOnFailure $stopOnFailure, 32 | callable $next 33 | ): Future { 34 | return async( 35 | function () use ($task, $runnerContext, $stopOnFailure, $next) : TaskResultInterface { 36 | try { 37 | $result = $next($task, $runnerContext, $stopOnFailure)->await(); 38 | } catch (\Throwable $error) { 39 | $result = TaskResult::createFailed($task, $runnerContext->getTaskContext(), $error->getMessage()); 40 | } 41 | 42 | $this->resultMap->onResult($result); 43 | 44 | return $result; 45 | } 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Runner/TaskHandler/Middleware/NonBlockingTaskHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\TaskHandler\Middleware; 6 | 7 | use GrumPHP\Runner\StopOnFailure; 8 | use function Amp\async; 9 | use Amp\Future; 10 | use GrumPHP\Runner\TaskResult; 11 | use GrumPHP\Runner\TaskResultInterface; 12 | use GrumPHP\Runner\TaskRunnerContext; 13 | use GrumPHP\Task\TaskInterface; 14 | 15 | class NonBlockingTaskHandlerMiddleware implements TaskHandlerMiddlewareInterface 16 | { 17 | public function handle( 18 | TaskInterface $task, 19 | TaskRunnerContext $runnerContext, 20 | StopOnFailure $stopOnFailure, 21 | callable $next 22 | ): Future { 23 | return async( 24 | static function () use ($task, $runnerContext, $next, $stopOnFailure): TaskResultInterface { 25 | $result = $next($task, $runnerContext, $stopOnFailure)->await(); 26 | 27 | if ($result->isPassed() || $result->isSkipped() || $task->getConfig()->getMetadata()->isBlocking()) { 28 | return $result; 29 | } 30 | 31 | return TaskResult::createNonBlockingFailed( 32 | $result->getTask(), 33 | $result->getContext(), 34 | $result->getMessage() 35 | ); 36 | } 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Runner/TaskHandler/Middleware/ReportingTaskHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\TaskHandler\Middleware; 6 | 7 | use GrumPHP\Runner\StopOnFailure; 8 | use function Amp\async; 9 | use Amp\Future; 10 | use GrumPHP\Runner\Reporting\TaskResultsReporter; 11 | use GrumPHP\Runner\TaskResultInterface; 12 | use GrumPHP\Runner\TaskRunnerContext; 13 | use GrumPHP\Task\TaskInterface; 14 | 15 | class ReportingTaskHandlerMiddleware implements TaskHandlerMiddlewareInterface 16 | { 17 | /** 18 | * @var TaskResultsReporter 19 | */ 20 | private $reporter; 21 | 22 | public function __construct(TaskResultsReporter $reporter) 23 | { 24 | $this->reporter = $reporter; 25 | } 26 | 27 | public function handle( 28 | TaskInterface $task, 29 | TaskRunnerContext $runnerContext, 30 | StopOnFailure $stopOnFailure, 31 | callable $next 32 | ): Future { 33 | return async( 34 | function () use ($task, $runnerContext, $stopOnFailure, $next): TaskResultInterface { 35 | $result = $next($task, $runnerContext, $stopOnFailure)->await(); 36 | 37 | $this->reporter->report($runnerContext); 38 | 39 | return $result; 40 | } 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Runner/TaskHandler/Middleware/StopOnFailureTaskHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\TaskHandler\Middleware; 6 | 7 | use GrumPHP\Runner\StopOnFailure; 8 | use function Amp\async; 9 | use Amp\Future; 10 | use GrumPHP\Runner\TaskResultInterface; 11 | use GrumPHP\Runner\TaskRunnerContext; 12 | use GrumPHP\Task\TaskInterface; 13 | 14 | class StopOnFailureTaskHandlerMiddleware implements TaskHandlerMiddlewareInterface 15 | { 16 | public function handle( 17 | TaskInterface $task, 18 | TaskRunnerContext $runnerContext, 19 | StopOnFailure $stopOnFailure, 20 | callable $next 21 | ): Future { 22 | return async( 23 | static function () use ($task, $runnerContext, $stopOnFailure, $next): TaskResultInterface { 24 | $result = $next($task, $runnerContext, $stopOnFailure)->await(); 25 | 26 | $stopOnFailure->decideForResult($result); 27 | 28 | return $result; 29 | } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Runner/TaskHandler/Middleware/TaskHandlerMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner\TaskHandler\Middleware; 6 | 7 | use Amp\Future; 8 | use GrumPHP\Runner\StopOnFailure; 9 | use GrumPHP\Runner\TaskResultInterface; 10 | use GrumPHP\Runner\TaskRunnerContext; 11 | use GrumPHP\Task\TaskInterface; 12 | 13 | interface TaskHandlerMiddlewareInterface 14 | { 15 | /** 16 | * @param callable(TaskInterface, TaskRunnerContext, StopOnFailure): Future<TaskResultInterface> $next 17 | * @return Future<TaskResultInterface> 18 | */ 19 | public function handle( 20 | TaskInterface $task, 21 | TaskRunnerContext $runnerContext, 22 | StopOnFailure $stopOnFailure, 23 | callable $next 24 | ): Future; 25 | } 26 | -------------------------------------------------------------------------------- /src/Runner/TaskResultInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner; 6 | 7 | use GrumPHP\Task\Context\ContextInterface; 8 | use GrumPHP\Task\TaskInterface; 9 | 10 | /** 11 | * @psalm-readonly 12 | */ 13 | interface TaskResultInterface 14 | { 15 | const SKIPPED = -100; 16 | const PASSED = 0; 17 | const NONBLOCKING_FAILED = 90; 18 | const FAILED = 99; 19 | 20 | public function getTask(): TaskInterface; 21 | 22 | public function getResultCode(): int; 23 | 24 | public function isPassed(): bool; 25 | 26 | public function hasFailed(): bool; 27 | 28 | public function isSkipped(): bool; 29 | 30 | public function isBlocking(): bool; 31 | 32 | public function getMessage(): string; 33 | 34 | public function getContext(): ContextInterface; 35 | 36 | public function withContext(ContextInterface $context): static; 37 | 38 | public function withAppendedMessage(string $message): self; 39 | } 40 | -------------------------------------------------------------------------------- /src/Runner/TaskRunner.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Runner; 6 | 7 | use GrumPHP\Collection\TaskResultCollection; 8 | use GrumPHP\Collection\TasksCollection; 9 | 10 | class TaskRunner 11 | { 12 | /** 13 | * @var TasksCollection 14 | */ 15 | private $tasks; 16 | 17 | /** 18 | * @var MiddlewareStack 19 | */ 20 | private $middleware; 21 | 22 | public function __construct(TasksCollection $tasks, MiddlewareStack $middleware) 23 | { 24 | $this->tasks = $tasks; 25 | $this->middleware = $middleware; 26 | } 27 | 28 | public function run(TaskRunnerContext $runnerContext): TaskResultCollection 29 | { 30 | return $this->middleware->handle( 31 | $runnerContext->withTasks($this->tasks) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Task/AbstractExternalTask.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task; 6 | 7 | use GrumPHP\Formatter\ProcessFormatterInterface; 8 | use GrumPHP\Process\ProcessBuilder; 9 | use GrumPHP\Task\Config\EmptyTaskConfig; 10 | use GrumPHP\Task\Config\TaskConfigInterface; 11 | 12 | /** 13 | * @template-covariant Formatter extends ProcessFormatterInterface 14 | */ 15 | abstract class AbstractExternalTask implements TaskInterface 16 | { 17 | /** 18 | * @var TaskConfigInterface 19 | */ 20 | protected $config; 21 | 22 | /** 23 | * @var ProcessBuilder 24 | */ 25 | protected $processBuilder; 26 | 27 | /** 28 | * @var Formatter 29 | */ 30 | protected $formatter; 31 | 32 | /** 33 | * @param Formatter $formatter 34 | */ 35 | public function __construct(ProcessBuilder $processBuilder, ProcessFormatterInterface $formatter) 36 | { 37 | $this->config = new EmptyTaskConfig(); 38 | $this->processBuilder = $processBuilder; 39 | $this->formatter = $formatter; 40 | } 41 | 42 | public function getConfig(): TaskConfigInterface 43 | { 44 | return $this->config; 45 | } 46 | 47 | public function withConfig(TaskConfigInterface $config): TaskInterface 48 | { 49 | $new = clone $this; 50 | $new->config = $config; 51 | 52 | return $new; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Task/Config/ConfigOptionsResolver.php: -------------------------------------------------------------------------------- 1 | <?php 2 | declare(strict_types=1); 3 | 4 | namespace GrumPHP\Task\Config; 5 | 6 | use Symfony\Component\OptionsResolver\OptionsResolver; 7 | 8 | final class ConfigOptionsResolver 9 | { 10 | /** 11 | * @var \Closure(array): array 12 | */ 13 | private \Closure $resolver; 14 | 15 | /** 16 | * @param \Closure(array): array $resolver 17 | */ 18 | private function __construct(\Closure $resolver) 19 | { 20 | $this->resolver = $resolver; 21 | } 22 | 23 | public static function fromOptionsResolver(OptionsResolver $optionsResolver): self 24 | { 25 | return self::fromClosure( 26 | static fn (array $options): array => $optionsResolver->resolve($options) 27 | ); 28 | } 29 | 30 | /** 31 | * @param \Closure(array): array $closure 32 | */ 33 | public static function fromClosure(\Closure $closure): self 34 | { 35 | return new self($closure); 36 | } 37 | 38 | public function resolve(array $data): array 39 | { 40 | return ($this->resolver)($data); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Task/Config/EmptyTaskConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Config; 6 | 7 | class EmptyTaskConfig implements TaskConfigInterface 8 | { 9 | public function getName(): string 10 | { 11 | return ''; 12 | } 13 | 14 | public function getOptions(): array 15 | { 16 | return []; 17 | } 18 | 19 | public function getMetadata(): Metadata 20 | { 21 | return new Metadata([]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Task/Config/LazyTaskConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Config; 6 | 7 | class LazyTaskConfig implements TaskConfigInterface 8 | { 9 | /** 10 | * @var callable() : TaskConfigInterface 11 | */ 12 | private $configFactory; 13 | 14 | /** 15 | * @var null|TaskConfigInterface 16 | */ 17 | private $config; 18 | 19 | /** 20 | * @param callable() : TaskConfigInterface $configFactory 21 | */ 22 | public function __construct(callable $configFactory) 23 | { 24 | $this->configFactory = $configFactory; 25 | } 26 | 27 | public function getName(): string 28 | { 29 | return $this->proxy()->getName(); 30 | } 31 | 32 | public function getOptions(): array 33 | { 34 | return $this->proxy()->getOptions(); 35 | } 36 | 37 | public function getMetadata(): Metadata 38 | { 39 | return $this->proxy()->getMetadata(); 40 | } 41 | 42 | private function proxy(): TaskConfigInterface 43 | { 44 | if (!$this->config) { 45 | $this->config = ($this->configFactory)(); 46 | } 47 | 48 | return $this->config; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Task/Config/Metadata.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Config; 6 | 7 | use Symfony\Component\OptionsResolver\OptionsResolver; 8 | 9 | class Metadata 10 | { 11 | /** 12 | * @var array 13 | */ 14 | private $metadata; 15 | 16 | public function __construct(array $metadata) 17 | { 18 | $this->metadata = self::getConfigurableOptions()->resolve($metadata); 19 | } 20 | 21 | public static function getConfigurableOptions(): \GrumPHP\Task\Config\ConfigOptionsResolver 22 | { 23 | static $resolver; 24 | if (null === $resolver) { 25 | $resolver = new OptionsResolver(); 26 | $resolver->setDefaults([ 27 | 'priority' => 0, 28 | 'blocking' => true, 29 | 'enabled' => true, 30 | 'task' => '', 31 | 'label' => '', 32 | ]); 33 | } 34 | 35 | return ConfigOptionsResolver::fromOptionsResolver($resolver); 36 | } 37 | 38 | public function priority(): int 39 | { 40 | return (int) $this->metadata['priority']; 41 | } 42 | 43 | public function isBlocking(): bool 44 | { 45 | return (bool) $this->metadata['blocking']; 46 | } 47 | 48 | public function isEnabled(): bool 49 | { 50 | return (bool) $this->metadata['enabled']; 51 | } 52 | 53 | public function task(): string 54 | { 55 | return (string) $this->metadata['task']; 56 | } 57 | 58 | public function label(): string 59 | { 60 | return (string) $this->metadata['label']; 61 | } 62 | 63 | public function toArray(): array 64 | { 65 | return $this->metadata; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Task/Config/TaskConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Config; 6 | 7 | class TaskConfig implements TaskConfigInterface 8 | { 9 | /** 10 | * @var string 11 | */ 12 | private $name; 13 | 14 | /** 15 | * @var array 16 | */ 17 | private $options; 18 | 19 | /** 20 | * @var Metadata 21 | */ 22 | private $metadata; 23 | 24 | public function __construct(string $name, array $options, Metadata $metadata) 25 | { 26 | $this->name = $name; 27 | $this->options = $options; 28 | $this->metadata = $metadata; 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return $this->name; 34 | } 35 | 36 | public function getOptions(): array 37 | { 38 | return $this->options; 39 | } 40 | 41 | public function getMetadata(): Metadata 42 | { 43 | return $this->metadata; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Task/Config/TaskConfigInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace GrumPHP\Task\Config; 4 | 5 | interface TaskConfigInterface 6 | { 7 | public function getName(): string; 8 | 9 | public function getOptions(): array; 10 | 11 | public function getMetadata(): Metadata; 12 | } 13 | -------------------------------------------------------------------------------- /src/Task/Context/ContextInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Context; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | 9 | interface ContextInterface 10 | { 11 | public function getFiles(): FilesCollection; 12 | } 13 | -------------------------------------------------------------------------------- /src/Task/Context/GitCommitMsgContext.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Context; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | 9 | class GitCommitMsgContext implements ContextInterface 10 | { 11 | private $files; 12 | private $commitMessage; 13 | private $userName; 14 | private $userEmail; 15 | 16 | public function __construct(FilesCollection $files, string $commitMessage, string $userName, string $userEmail) 17 | { 18 | $this->files = $files; 19 | $this->commitMessage = $commitMessage; 20 | $this->userName = $userName; 21 | $this->userEmail = $userEmail; 22 | } 23 | 24 | public function getCommitMessage(): string 25 | { 26 | return $this->commitMessage; 27 | } 28 | 29 | public function getUserName(): string 30 | { 31 | return $this->userName; 32 | } 33 | 34 | public function getUserEmail(): string 35 | { 36 | return $this->userEmail; 37 | } 38 | 39 | public function getFiles(): FilesCollection 40 | { 41 | return $this->files; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Task/Context/GitPreCommitContext.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Context; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | 9 | class GitPreCommitContext implements ContextInterface 10 | { 11 | private $files; 12 | 13 | public function __construct(FilesCollection $files) 14 | { 15 | $this->files = $files; 16 | } 17 | 18 | public function getFiles(): FilesCollection 19 | { 20 | return $this->files; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Task/Context/RunContext.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task\Context; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | 9 | class RunContext implements ContextInterface 10 | { 11 | private $files; 12 | 13 | public function __construct(FilesCollection $files) 14 | { 15 | $this->files = $files; 16 | } 17 | 18 | public function getFiles(): FilesCollection 19 | { 20 | return $this->files; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Task/TaskInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Task; 6 | 7 | use GrumPHP\Runner\TaskResultInterface; 8 | use GrumPHP\Task\Config\ConfigOptionsResolver; 9 | use GrumPHP\Task\Config\TaskConfigInterface; 10 | use GrumPHP\Task\Context\ContextInterface; 11 | 12 | interface TaskInterface 13 | { 14 | public static function getConfigurableOptions(): ConfigOptionsResolver; 15 | 16 | public function canRunInContext(ContextInterface $context): bool; 17 | 18 | public function run(ContextInterface $context): TaskResultInterface; 19 | 20 | public function getConfig(): TaskConfigInterface; 21 | 22 | public function withConfig(TaskConfigInterface $config): TaskInterface; 23 | } 24 | -------------------------------------------------------------------------------- /src/Test/Runner/AbstractMiddlewareTestCase.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Test\Runner; 6 | 7 | use GrumPHP\Collection\FilesCollection; 8 | use GrumPHP\IO\IOInterface; 9 | use GrumPHP\Runner\TaskRunnerContext; 10 | use GrumPHP\Task\Config\Metadata; 11 | use GrumPHP\Task\Config\TaskConfig; 12 | use GrumPHP\Task\Context\ContextInterface; 13 | use GrumPHP\Task\Context\RunContext; 14 | use GrumPHP\Task\TaskInterface; 15 | use PHPUnit\Framework\TestCase; 16 | use Prophecy\Argument; 17 | use Prophecy\Prophecy\ObjectProphecy; 18 | use Symfony\Component\Console\Style\StyleInterface; 19 | 20 | class AbstractMiddlewareTestCase extends TestCase 21 | { 22 | protected function createRunnerContext(): TaskRunnerContext 23 | { 24 | return new TaskRunnerContext( 25 | new RunContext(new FilesCollection()) 26 | ); 27 | } 28 | 29 | protected function createNextShouldNotBeCalledCallback(): callable 30 | { 31 | return static function () { 32 | throw new \RuntimeException('Expected next not to be called!'); 33 | }; 34 | } 35 | 36 | protected function mockIO(): IOInterface 37 | { 38 | /** @var ObjectProphecy|IOInterface $IO */ 39 | $IO = $this->prophesize(IOInterface::class); 40 | $IO->isVerbose()->willReturn(false); 41 | $IO->startGroup(Argument::any()); 42 | $IO->endGroup(); 43 | 44 | return $IO->reveal(); 45 | } 46 | 47 | protected function mockTask(string $name, array $meta = []): TaskInterface 48 | { 49 | /** @var ObjectProphecy|TaskInterface $task */ 50 | $task = $this->prophesize(TaskInterface::class); 51 | $task->getConfig()->willReturn(new TaskConfig($name, [], new Metadata($meta))); 52 | 53 | return $task->reveal(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Test/Runner/AbstractRunnerMiddlewareTestCase.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Test\Runner; 6 | 7 | use Symfony\Component\Messenger\Envelope; 8 | use Symfony\Component\Messenger\Middleware\MiddlewareInterface; 9 | use Symfony\Component\Messenger\Middleware\StackInterface; 10 | use Symfony\Component\Messenger\Middleware\StackMiddleware; 11 | 12 | abstract class AbstractRunnerMiddlewareTestCase extends AbstractMiddlewareTestCase 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Test/Runner/AbstractTaskHandlerMiddlewareTestCase.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Test\Runner; 6 | 7 | use Amp\Future; 8 | use GrumPHP\Runner\TaskResultInterface; 9 | use GrumPHP\Task\Config\Metadata; 10 | use GrumPHP\Task\Config\TaskConfig; 11 | use GrumPHP\Task\Context\ContextInterface; 12 | use GrumPHP\Task\TaskInterface; 13 | use Prophecy\Argument; 14 | use Prophecy\Prophecy\ObjectProphecy; 15 | 16 | abstract class AbstractTaskHandlerMiddlewareTestCase extends AbstractMiddlewareTestCase 17 | { 18 | protected function createNextResultCallback(TaskResultInterface $taskResult): callable 19 | { 20 | return static function () use ($taskResult) { 21 | return Future::complete($taskResult); 22 | }; 23 | } 24 | 25 | protected function createExceptionCallback(\Throwable $exception): callable 26 | { 27 | return static function () use ($exception) { 28 | return Future::error($exception); 29 | }; 30 | } 31 | 32 | protected function resolve(Future $promise): TaskResultInterface 33 | { 34 | return $promise->await(); 35 | } 36 | 37 | protected function mockTaskRun(string $name, callable $runWillDo): TaskInterface 38 | { 39 | /** @var ObjectProphecy|TaskInterface $task */ 40 | $task = $this->prophesize(TaskInterface::class); 41 | $task->getConfig()->willReturn(new TaskConfig($name, [], new Metadata([]))); 42 | $task->run(Argument::type(ContextInterface::class))->will($runWillDo); 43 | 44 | return $task->reveal(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/TestSuite/TestSuite.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\TestSuite; 6 | 7 | class TestSuite implements TestSuiteInterface 8 | { 9 | /** 10 | * @var string 11 | */ 12 | private $name; 13 | 14 | /** 15 | * @var array 16 | */ 17 | private $taskNames = []; 18 | 19 | /** 20 | * TestSuite constructor. 21 | */ 22 | public function __construct(string $name, array $taskNames) 23 | { 24 | $this->taskNames = $taskNames; 25 | $this->name = $name; 26 | } 27 | 28 | public function getName(): string 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function getTaskNames(): array 34 | { 35 | return $this->taskNames; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/TestSuite/TestSuiteInterface.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\TestSuite; 6 | 7 | interface TestSuiteInterface 8 | { 9 | public function getName(): string; 10 | 11 | public function getTaskNames(): array; 12 | } 13 | -------------------------------------------------------------------------------- /src/Util/ComposerFile.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Util; 6 | 7 | class ComposerFile 8 | { 9 | /** 10 | * @var array 11 | */ 12 | private $configuration; 13 | 14 | /** 15 | * @var string 16 | */ 17 | private $path; 18 | 19 | public function __construct(string $path, array $configuration) 20 | { 21 | $this->path = $path; 22 | $this->configuration = $configuration; 23 | } 24 | 25 | public function getBinDir(): string 26 | { 27 | $binDir = $this->configuration['config']['bin-dir'] ?? null; 28 | 29 | if (null !== $binDir) { 30 | return (string) $binDir; 31 | } 32 | 33 | $vendorDir = $this->configuration['config']['vendor-dir'] ?? null; 34 | 35 | if (null !== $vendorDir) { 36 | return $vendorDir . '/bin'; 37 | } 38 | 39 | return 'vendor/bin'; 40 | } 41 | 42 | public function getConfigDefaultPath(): ?string 43 | { 44 | return $this->configuration['extra']['grumphp']['config-default-path'] ?? null; 45 | } 46 | 47 | public function getProjectPath(): ?string 48 | { 49 | return $this->configuration['extra']['grumphp']['project-path'] ?? null; 50 | } 51 | 52 | public function getPath(): string 53 | { 54 | return $this->path; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Util/PhpVersion.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Util; 6 | 7 | class PhpVersion 8 | { 9 | private $versions; 10 | 11 | public function __construct(array $versions) 12 | { 13 | $this->versions = $versions; 14 | } 15 | 16 | /** 17 | * @see https://secure.php.net/supported-versions.php for a list of currently supported versions 18 | */ 19 | public function isSupportedVersion(string $currentVersion): bool 20 | { 21 | $now = new \DateTime(); 22 | foreach ($this->versions as $number => $eol) { 23 | $eol = new \DateTime($eol); 24 | if ($now < $eol && version_compare($currentVersion, $number) >= 0) { 25 | return true; 26 | } 27 | } 28 | 29 | return false; 30 | } 31 | 32 | public function isSupportedProjectVersion(string $currentVersion, string $projectVersion): bool 33 | { 34 | return version_compare($currentVersion, $projectVersion) >= 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Util/Platform.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Util; 6 | 7 | class Platform 8 | { 9 | /** 10 | * Windows has a limit on command line input strings. 11 | * This one is causing external commands to fail with exit code 1 without any error. 12 | * More information:. 13 | * 14 | * @see https://support.microsoft.com/en-us/kb/830473 15 | */ 16 | const WINDOWS_COMMANDLINE_STRING_LIMITATION = 8191; 17 | 18 | public static function isWindows(): bool 19 | { 20 | return \defined('PHP_WINDOWS_VERSION_BUILD'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Util/Str.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | declare(strict_types=1); 4 | 5 | namespace GrumPHP\Util; 6 | 7 | final class Str 8 | { 9 | /** 10 | * String contains one of the provided needles 11 | */ 12 | public static function containsOneOf(string $haystack, array $needles): bool 13 | { 14 | foreach ($needles as $needle) { 15 | if ($needle !== '' && mb_strpos($haystack, $needle) !== false) { 16 | return true; 17 | } 18 | } 19 | 20 | return false; 21 | } 22 | 23 | /** 24 | * Split $value on ",", trim the individual parts and 25 | * de-deduplicate the remaining values 26 | * 27 | * @param non-empty-string $delimiter 28 | */ 29 | public static function explodeWithCleanup(string $delimiter, string $value): array 30 | { 31 | return array_unique(array_map(function (string $value) { 32 | return trim($value); 33 | }, array_filter(explode($delimiter, $value)))); 34 | } 35 | } 36 | --------------------------------------------------------------------------------