├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── config └── services.yaml ├── ecs.php ├── grumphp.yml ├── phpunit.xml.dist ├── src ├── Command │ ├── CookCommand.php │ └── CookUninstallCommand.php ├── Cook.php ├── Filter │ ├── ClassConstantFilter.php │ ├── Filter.php │ └── SingleLineArrayFilter.php ├── Merger │ ├── AbstractMerger.php │ ├── DockerComposeMerger.php │ ├── JsonMerger.php │ ├── Merger.php │ ├── PhpArrayMerger.php │ ├── TextMerger.php │ └── YamlMerger.php ├── Options.php ├── Oven.php ├── ServiceContainer.php ├── State.php └── StateInterface.php └── tests ├── Dummy ├── after │ ├── .env │ ├── bundles.php │ ├── composer.json │ ├── routes.yaml │ └── services.yaml ├── before │ ├── .env │ ├── bundles.php │ ├── composer.json │ └── services.yaml └── recipe │ ├── .env │ ├── routes.yaml │ └── services.yaml ├── Filter ├── ClassConstantFilterTest.php └── SingleLineArrayFilterTest.php ├── Merger ├── DockerComposeMergerTest.php ├── JsonMergerTest.php ├── MergerTestCase.php ├── PhpArrayMergerTest.php ├── TextMergerTest.php └── YamlMergerTest.php ├── OptionsTest.php ├── StateTest.php └── bootstrap.php /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | paths-ignore: 9 | - '**/README.md' 10 | pull_request: 11 | branches: 12 | - '**' 13 | paths-ignore: 14 | - '**/README.md' 15 | 16 | concurrency: 17 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | test: 22 | name: "Test PHP ${{ matrix.php }} with Symfony ${{ matrix.symfony_version }} and composer flags ${{ matrix.composer-flags }}" 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | php: ['8.0', '8.1', '8.2', '8.3'] 27 | symfony_version: ['5.4.*', '6.0.*', '6.1.*', '6.2.*' , '6.3.*', '6.4.*', '7.0.*'] 28 | composer-flags: ['--prefer-stable'] 29 | exclude: 30 | - php: '8.0' 31 | symfony_version: '6.1.*' # Requires PHP >= 8.1 for compatibility 32 | - php: '8.0' 33 | symfony_version: '6.2.*' # Requires PHP >= 8.1 for compatibility 34 | - php: '8.0' 35 | symfony_version: '6.3.*' # Requires PHP >= 8.1 for compatibility 36 | - php: '8.0' 37 | symfony_version: '6.4.*' # Requires PHP >= 8.1 for compatibility 38 | - php: '8.0' 39 | symfony_version: '7.0.*' # Requires PHP >= 8.2 for compatibility 40 | - php: '8.1' 41 | symfony_version: '7.0.*' # Requires PHP >= 8.2 for compatibility 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Setup PHP 46 | id: setup-php 47 | uses: shivammathur/setup-php@v2 48 | with: 49 | php-version: ${{ matrix.php }} 50 | extensions: dom, curl, libxml, mbstring, zip 51 | tools: composer:v2 52 | 53 | - name: Validate composer.json and composer.lock 54 | run: composer validate 55 | 56 | - name: Install Flex and set Symfony version 57 | if: ${{ matrix.symfony_version }} 58 | run: | 59 | composer global config --no-plugins allow-plugins.symfony/flex true 60 | composer global require symfony/flex 61 | composer config extra.symfony.require ${{ matrix.symfony_version }} 62 | 63 | - name: Get composer cache directory 64 | id: composer-cache 65 | run: echo "dir="$(composer config cache-files-dir)"" >> $GITHUB_OUTPUT 66 | 67 | - name: Cache dependencies 68 | uses: actions/cache@v4 69 | with: 70 | path: ${{ steps.composer-cache.outputs.dir }} 71 | key: "key-os-${{ runner.os }}-php-${{matrix.php}}-symfony-${{ matrix.symfony_version }}-composer-${{ hashFiles('composer.json') }}" 72 | restore-keys: "key-os-${{ runner.os }}-php-${{matrix.php}}-symfony-${{ matrix.symfony_version }}-composer-" 73 | 74 | - name: Remove ECS for PHP 8.0 75 | if: ${{ matrix.php == '8.0' }} 76 | run: jq 'del(."require-dev"."symplify/coding-standard", ."require-dev"."symplify/easy-coding-standard")' --indent 4 composer.json > composer.json.tmp && mv composer.json.tmp composer.json 77 | env: 78 | SYMFONY_REQUIRE: ${{ matrix.symfony_version }} 79 | 80 | - name: Install composer dependencies with nikic/php-parser:^4.0 81 | if: ${{ matrix.php == '8.0' }} 82 | run: composer require --dev nikic/php-parser:^4.0 ${{ matrix.composer-flags }} 83 | env: 84 | SYMFONY_REQUIRE: ${{ matrix.symfony_version }} 85 | 86 | - name: Install composer dependencies 87 | if: ${{ matrix.php != '8.0' }} 88 | run: composer update ${{ matrix.composer-flags }} 89 | env: 90 | SYMFONY_REQUIRE: ${{ matrix.symfony_version }} 91 | 92 | - name: Launch test suite 93 | run: make test 94 | 95 | - name: Launch ECS 96 | if: ${{ matrix.php != '8.0' }} 97 | run: make ecs 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | composer.lock 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023, William Arin 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 | test: 2 | @vendor/bin/grumphp run --testsuite=default --no-interaction 3 | 4 | ecs: 5 | @vendor/bin/grumphp run --testsuite=ecs --no-interaction 6 | 7 | fix: 8 | @vendor/bin/ecs check --fix 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cook 2 | 3 | Baking recipes for any PHP package. 4 | 5 | 6 | [![Github Workflow](https://github.com/williarin/cook/workflows/Test/badge.svg)](https://github.com/williarin/cook/actions) 7 | 8 | 9 | * [Cook](#cook) 10 | * [Introduction](#introduction) 11 | * [Features](#features) 12 | * [Installation](#installation) 13 | * [Documentation](#documentation) 14 | * [Creating a recipe](#creating-a-recipe) 15 | * [Files](#files) 16 | * [Directories](#directories) 17 | * [Post install output](#post-install-output) 18 | * [Mergers](#mergers) 19 | * [Text](#text) 20 | * [PHP array](#php-array) 21 | * [JSON](#json) 22 | * [YAML](#yaml) 23 | * [Docker Compose](#docker-compose) 24 | * [Placeholders](#placeholders) 25 | * [CLI](#cli) 26 | * [License](#license) 27 | 28 | 29 | ## Introduction 30 | 31 | Cook is a Composer plugin that executes recipes embedded in packages, in a similar fashion to [Symfony Flex](https://github.com/symfony/flex). 32 | It can be used alongside with Flex, or in any other PHP project, as long as Composer is installed. 33 | 34 | ### Features 35 | 36 | * Add new entries to arrays or export new arrays, filter how you want to output it 37 | * Add content to existing files or create them (.env, Makefile, or anything else) 38 | * Copy entire directories from your repository to the project 39 | * Keep existing data by default or overwrite it with a CLI command 40 | * Supports PHP arrays, JSON, YAML, text files 41 | * Output post install instructions 42 | * Process only required packages in the root project 43 | * Uninstall recipe when a package is removed 44 | * CLI commands to install or uninstall recipes 45 | 46 | ## Installation 47 | 48 | ``` 49 | composer require williarin/cook 50 | ``` 51 | 52 | Make sure to allow the plugin to run. If it's not added automatically, add this in your `composer.json` file: 53 | 54 | ```json 55 | "config": { 56 | "allow-plugins": { 57 | "williarin/cook": true 58 | } 59 | }, 60 | ``` 61 | 62 | ## Documentation 63 | 64 | ### Creating a recipe 65 | 66 | Take a look at [williarin/cook-example](https://github.com/williarin/cook-example) for a working example of a Cook recipe. 67 | 68 | To make your package Cook-compatible, you just have to create a valid `cook.yaml` or `cook.json` at the root directory. 69 | 70 | The recipe schema must follow this structure: 71 | 72 | | Top level parameter | Type | Comments | 73 | |-------------------------|--------|----------------------------------------------------------------------------| 74 | | **files** | array | Individual files to be created or merged. | 75 | | **directories** | array | List of directories to be entirely copied from the package to the project. | 76 | | **post_install_output** | string | A text to display after installation or update of a package. | 77 | 78 | #### Files 79 | 80 | Files are a described as key-value pairs. 81 | 82 | * Key is the path to the destination file 83 | * Value is either an array or a string 84 | 85 | If a string is given, it must be a path to the source file. 86 | 87 | | Parameter | Type | Comments | 88 | |------------------------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 89 | | **type** | string | Type of file.

**Choices:****Default:** `text`
**Optional** | 90 | | **destination** | string | Path of the destination file in the project that will be created or merged.

**Required** | 91 | | **source** | string | Path of the source file in the package which content will be used to create or merge in the destination file.

**Required** if **content** isn't defined | 92 | | **content** | string | Text to merge in the destination file.

**Required** if **source** isn't defined | 93 | | **entries** | array | Key-value pairs used to fill a PHP or JSON array.

**Required** if **type** is of type `php_array` or `json` | 94 | | **filters** | {keys: array\, values: array\} | Filters for **entries** when **type** is `php_array`.

**Choices:**
  • `keys`
    • `class_constant` Convert the given string to a class constant. As an example, `'Williarin\Cook'` becomes `Williarin\Cook::class`
  • `values`
    • `class_constant` See above
    • `single_line_array` If the value is an array, it will be exported on a single line
**Optional** | 95 | | **valid_sections** | array\ | Used if **type** is `yaml` or `json` in order to restrict which top-level parameters need to be merged.

Example: `[parameters, services]`

**Optional** | 96 | | **blank_line_after** | array\ | Used if **type** is `yaml` in order to add a blank line under the merged section.

Example: `[services]`

**Optional** | 97 | | **uninstall_empty_sections** | boolean | Used if **type** is `yaml` in order to remove an empty recipe section when uninstalling the recipe.

**Default:** `false`
**Optional** | 98 | 99 | #### Directories 100 | 101 | Directories are a described as key-value pairs. 102 | 103 | * Key is the path to the destination directory that will receive the files 104 | * Value is the path of the source directory in the package that contains the files 105 | 106 | #### Post install output 107 | 108 | Maybe you want to display some text to the user after installation. 109 | You can use colors using [Symfony Console](https://symfony.com/doc/current/console/coloring.html) syntax. 110 | 111 | ### Mergers 112 | 113 | #### Text 114 | 115 | The text merger can be used to extend any text-based file such as: 116 | * .gitignore 117 | * .env 118 | * Makefile 119 | 120 | As it's the default merger, you can simply use the `destination: source` format in the recipe. 121 | 122 | **Example 1:** merge or create a `.env` file with a given source file 123 | 124 | Given `yourrepo/recipe/.env` with this content: 125 | ```dotenv 126 | SOME_ENV_VARIABLE='hello' 127 | ANOTHER_ENV_VARIABLE='world' 128 | ``` 129 | With this recipe: 130 | ```yaml 131 | files: 132 | .env: recipe/.env 133 | ``` 134 | The created `.env` file will look like this: 135 | ```dotenv 136 | ###> yourname/yourrepo ### 137 | SOME_ENV_VARIABLE='hello' 138 | ANOTHER_ENV_VARIABLE='world' 139 | ###< yourname/yourrepo ### 140 | ``` 141 | 142 | The `###> yourname/yourrepo ###` opening comment and `###< yourname/yourrepo ###` closing comment are used by Cook to identify the recipe in the file. 143 | If you're familiar with Symfony Flex, the syntax is the same. 144 | 145 | **Example 2:** merge or create a `.env` file with a string input 146 | 147 | Alternatively, you can use `content` instead of `source`, to avoid creating a file in your repository. 148 | ```yaml 149 | files: 150 | .env: 151 | content: |- 152 | SOME_ENV_VARIABLE='hello' 153 | ANOTHER_ENV_VARIABLE='world' 154 | ``` 155 | 156 | #### PHP array 157 | 158 | The PHP array merger adds new entries to existing arrays or creates a file if it doesn't exist. 159 | 160 | **Example 1:** without filters 161 | 162 | This recipe will create or merge the file `config/bundles.php` in the project. 163 | ```yaml 164 | files: 165 | config/bundles.php: 166 | type: php_array 167 | entries: 168 | Williarin\CookExample\CookExampleBundle: 169 | dev: true 170 | test: true 171 | ``` 172 | The output will look like this: 173 | ```php 174 | [ 178 | 'dev' => true, 179 | 'test' => true, 180 | ], 181 | ]; 182 | ``` 183 | 184 | **Example 2:** with filters 185 | 186 | Let's add some filters to our entries. 187 | ```yaml 188 | files: 189 | config/bundles.php: 190 | # ... 191 | filters: 192 | keys: [class_constant] 193 | values: [single_line_array] 194 | ``` 195 | The output will look like this: 196 | ```php 197 | ['dev' => true, 'test' => true], 201 | ]; 202 | ``` 203 | 204 | #### JSON 205 | 206 | The JSON merger adds new entries to an existing JSON file or creates a file if needed. 207 | 208 | **Note:** Only top-level keys are merged. 209 | 210 | **Example:** 211 | 212 | This recipe will add a script in the `composer.json` file of the project. 213 | ```yaml 214 | files: 215 | composer.json: 216 | type: json 217 | entries: 218 | scripts: 219 | post-create-project-cmd: php -r "copy('config/local-example.php', 'config/local.php');" 220 | ``` 221 | The output will look like this: 222 | ```json5 223 | { 224 | // ... existing config 225 | "scripts": { 226 | // ... other scripts 227 | "post-create-project-cmd": "php -r \"copy('config/local-example.php', 'config/local.php');\"" 228 | } 229 | } 230 | ``` 231 | 232 | #### YAML 233 | 234 | The YAML merger adds new parameters to top-level parameters in an existing file or creates a file if needed. 235 | 236 | Although a YAML file represents arrays like JSON or PHP arrays, the specificity of this merger is to allow YAML comments. 237 | Therefore, instead of using `entries` which restricts content as key-value pairs, you need to describe the content to merge as a string, or a YAML file. 238 | 239 | **Example 1:** default config 240 | 241 | Given this existing file in `config/services.yaml`: 242 | ```yaml 243 | parameters: 244 | database_url: postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8 245 | 246 | services: 247 | _defaults: 248 | autowire: true 249 | autoconfigure: true 250 | ``` 251 | With this recipe: 252 | ```yaml 253 | files: 254 | config/services.yaml: 255 | type: yaml 256 | content: | 257 | parameters: 258 | locale: fr 259 | 260 | services: 261 | Some\Service: ~ 262 | ``` 263 | The output will look like this: 264 | ```yaml 265 | parameters: 266 | ###> williarin/cook-example ### 267 | locale: fr 268 | ###< williarin/cook-example ### 269 | database_url: postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8 270 | 271 | services: 272 | ###> williarin/cook-example ### 273 | Some\Service: ~ 274 | ###< williarin/cook-example ### 275 | _defaults: 276 | autowire: true 277 | autoconfigure: true 278 | ``` 279 | 280 | **Example 2:** with blank lines 281 | 282 | To make things a bit prettier, let's add a blank line below our `services` merge: 283 | ```yaml 284 | files: 285 | config/services.yaml: 286 | # ... 287 | blank_line_after: [services] 288 | ``` 289 | The output will look like this: 290 | ```yaml 291 | parameters: 292 | ###> williarin/cook-example ### 293 | locale: fr 294 | ###< williarin/cook-example ### 295 | database_url: postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8 296 | 297 | services: 298 | ###> williarin/cook-example ### 299 | Some\Service: ~ 300 | ###< williarin/cook-example ### 301 | 302 | _defaults: 303 | autowire: true 304 | autoconfigure: true 305 | ``` 306 | 307 | **Note:** the YAML merger is only able to prepend existing content, not append. 308 | 309 | **Uninstalling YAML recipe** 310 | 311 | When uninstalling a recipe, the YAML merger will not remove the entire section if it's empty, 312 | unless you set the `uninstall_empty_sections` parameter to `true`. 313 | 314 | ```yaml 315 | files: 316 | config/routes.yaml: 317 | type: yaml 318 | source: | 319 | other_routes: 320 | resource: . 321 | type: other_routes_loader 322 | uninstall_empty_sections: true 323 | ``` 324 | 325 | In this example, if the `other_routes` section is empty, it will be removed when uninstalling the recipe. 326 | 327 | #### Docker Compose 328 | 329 | The Docker Compose merger is similar to the YAML merger with only specific sections that would be merged. 330 | 331 | Only `services`, `volumes`, `configs`, `secrets` and `networks` top-level sections will be merged. 332 | 333 | ### Placeholders 334 | 335 | You can use several placeholders in your destination and source paths: 336 | * `%BIN_DIR%`: defaults to `bin` 337 | * `%CONFIG_DIR%`: defaults to `config` 338 | * `%SRC_DIR%`: defaults to `src` 339 | * `%VAR_DIR%`: defaults to `var` 340 | * `%PUBLIC_DIR%`: defaults to `public` 341 | * `%ROOT_DIR%`: defaults to `.` or, if defined, to `extra.symfony.root-dir` defined in `composer.json` 342 | 343 | You can override any of these placeholders by defining them in your `composer.json` file. 344 | 345 | ```json 346 | "extra": { 347 | "bin-dir": "bin", 348 | "config-dir": "config", 349 | "src-dir": "src", 350 | "var-dir": "var", 351 | "public-dir": "public" 352 | } 353 | ``` 354 | 355 | Any other variable defined in `extra` is available with `%YOUR_VARIABLE%` in your recipe. 356 | 357 | ```json 358 | "extra": { 359 | "your-variable": "..." 360 | } 361 | ``` 362 | 363 | ### CLI 364 | 365 | You may want to execute your recipes after installation. 366 | Cook provides you this command to execute all available recipes: 367 | 368 | ```bash 369 | composer cook 370 | ``` 371 | 372 | It won't overwrite your configuration if it already exists. To overwrite everything, run: 373 | 374 | ```bash 375 | composer cook --overwrite 376 | ``` 377 | 378 | Additionally, you can uninstall a recipe with this command: 379 | 380 | ```bash 381 | composer cook:uninstall [--all] 382 | ``` 383 | Use either `` for individual package uninstallation or `--all` for all packages. 384 | 385 | ## License 386 | 387 | [MIT](LICENSE) 388 | 389 | Copyright (c) 2023, William Arin 390 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "williarin/cook", 3 | "description": "Composer plugin to execute recipes embedded in packages", 4 | "license": "MIT", 5 | "type": "composer-plugin", 6 | "require": { 7 | "php": ">=8.0", 8 | "ext-json": "*", 9 | "composer-plugin-api": "^2.3", 10 | "colinodell/indentation": "^1.0", 11 | "symfony/config": "^5.4 || ^6.0 || ^7.0", 12 | "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", 13 | "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", 14 | "symfony/finder": "^5.4 || ^6.0 || ^7.0", 15 | "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", 16 | "symfony/validator": "^5.4 || ^6.0 || ^7.0", 17 | "symfony/var-exporter": "^5.4 || ^6.0 || ^7.0", 18 | "symfony/yaml": "^5.4 || ^6.0 || ^7.0" 19 | }, 20 | "require-dev": { 21 | "composer/composer": "^2.3", 22 | "ergebnis/composer-normalize": "^2.29", 23 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.11", 24 | "mockery/mockery": "^1.5", 25 | "nikic/php-parser": "^4.15 || ^5.0", 26 | "php-parallel-lint/php-parallel-lint": "^1.3", 27 | "phpro/grumphp": "^1.13 || ^2.4", 28 | "phpunit/phpunit": "^9.5.22", 29 | "roave/security-advisories": "dev-latest", 30 | "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", 31 | "symplify/coding-standard": "^12.0", 32 | "symplify/easy-coding-standard": "^12.0" 33 | }, 34 | "minimum-stability": "dev", 35 | "prefer-stable": true, 36 | "autoload": { 37 | "psr-4": { 38 | "Williarin\\Cook\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Williarin\\Cook\\Test\\": "tests/" 44 | } 45 | }, 46 | "config": { 47 | "allow-plugins": { 48 | "ergebnis/composer-normalize": true, 49 | "phpro/grumphp": true 50 | } 51 | }, 52 | "extra": { 53 | "class": "Williarin\\Cook\\Cook" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | services: 4 | _defaults: 5 | autowire: true 6 | autoconfigure: true 7 | 8 | Williarin\Cook\: 9 | resource: '../src/' 10 | exclude: 11 | - '../src/Command/' 12 | - '../src/Cook.php' 13 | 14 | Williarin\Cook\Oven: 15 | public: true 16 | 17 | Williarin\Cook\StateInterface: '@Williarin\Cook\State' 18 | 19 | Composer\Composer: 20 | synthetic: true 21 | 22 | Composer\IO\IOInterface: 23 | synthetic: true 24 | 25 | Symfony\Component\Filesystem\Filesystem: ~ 26 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | parallel(); 24 | 25 | $ecsConfig->paths([__DIR__ . '/src', __DIR__ . '/tests']); 26 | 27 | $ecsConfig->skip([__DIR__ . '/ecs.php', __DIR__ . '/tests/Dummy/']); 28 | 29 | $ecsConfig->sets([SetList::SYMPLIFY, SetList::PSR_12, SetList::DOCTRINE_ANNOTATIONS, SetList::CLEAN_CODE]); 30 | 31 | $ecsConfig->ruleWithConfiguration(YodaStyleFixer::class, [ 32 | 'equal' => false, 33 | 'identical' => false, 34 | 'less_and_greater' => false, 35 | ]); 36 | 37 | $ecsConfig->ruleWithConfiguration(PhpdocTypesOrderFixer::class, [ 38 | 'null_adjustment' => 'always_last', 39 | 'sort_algorithm' => 'none', 40 | ]); 41 | 42 | $ecsConfig->ruleWithConfiguration(OrderedImportsFixer::class, [ 43 | 'imports_order' => ['class', 'function', 'const'], 44 | ]); 45 | 46 | $ecsConfig->ruleWithConfiguration(ConcatSpaceFixer::class, [ 47 | 'spacing' => 'one', 48 | ]); 49 | 50 | $ecsConfig->ruleWithConfiguration(LineLengthFixer::class, [ 51 | LineLengthFixer::LINE_LENGTH => 120, 52 | 53 | ]); 54 | 55 | $ecsConfig->rule(NativeFunctionInvocationFixer::class); 56 | $ecsConfig->rule(NoTrailingCommaInSinglelineFixer::class); 57 | $ecsConfig->rule(TrailingCommaInMultilineFixer::class); 58 | $ecsConfig->rule(MethodChainingNewlineFixer::class); 59 | $ecsConfig->rule(MethodChainingIndentationFixer::class); 60 | $ecsConfig->rule(StandaloneLineInMultilineArrayFixer::class); 61 | $ecsConfig->rule(ArrayIndentationFixer::class); 62 | $ecsConfig->rule(NoUnusedImportsFixer::class); 63 | $ecsConfig->rule(NoDuplicatedImportsFixer::class); 64 | }; 65 | -------------------------------------------------------------------------------- /grumphp.yml: -------------------------------------------------------------------------------- 1 | grumphp: 2 | ascii: 3 | failed: ~ 4 | succeeded: ~ 5 | tasks: 6 | phpunit: ~ 7 | ecs: 8 | files_on_pre_commit: true 9 | no-progress-bar: true 10 | composer: 11 | no_check_lock: false 12 | no_local_repository: true 13 | composer_normalize: 14 | indent_size: 4 15 | indent_style: space 16 | phplint: ~ 17 | phpparser: 18 | visitors: 19 | forbidden_function_calls: 20 | blacklist: 21 | - die 22 | - dd 23 | - dump 24 | - var_dump 25 | - print_r 26 | git_commit_message: 27 | allow_empty_message: false 28 | enforce_capitalized_subject: false 29 | max_subject_width: 120 30 | max_body_width: 500 31 | type_scope_conventions: 32 | types: 33 | - build 34 | - ci 35 | - chore 36 | - docs 37 | - feat 38 | - fix 39 | - perf 40 | - refactor 41 | - style 42 | - test 43 | git_blacklist: 44 | keywords: 45 | - 'die(' 46 | - ' dd(' 47 | - 'dump(' 48 | - 'var_dump(' 49 | - 'print_r(' 50 | whitelist_patterns: 51 | - '->dump(' 52 | - 'function dump' 53 | triggered_by: ['php'] 54 | regexp_type: G 55 | testsuites: 56 | default: 57 | tasks: 58 | - phpunit 59 | - composer 60 | - composer_normalize 61 | - phplint 62 | - phpparser 63 | - git_commit_message 64 | - git_blacklist 65 | ecs: 66 | tasks: 67 | - ecs 68 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Command/CookCommand.php: -------------------------------------------------------------------------------- 1 | setName('cook') 21 | ->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite existing files or values') 22 | ; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | $overwrite = $input->getOption('overwrite'); 28 | (new ServiceContainer($this->requireComposer(), $this->getIO())) 29 | ->get(Oven::class) 30 | ?->cookRecipes(null, $overwrite); 31 | 32 | return Command::SUCCESS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Command/CookUninstallCommand.php: -------------------------------------------------------------------------------- 1 | setName('cook:uninstall') 22 | ->addArgument('package', InputArgument::OPTIONAL, 'Name of the package to uninstall recipe from') 23 | ->addOption('all', null, InputOption::VALUE_NONE, 'Uninstall all') 24 | ; 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output): int 28 | { 29 | $package = $input->getArgument('package'); 30 | $all = $input->getOption('all'); 31 | 32 | if (!$package && !$all) { 33 | $this->getIO() 34 | ->writeError('You must either specify a package name or --all to remove all recipes.'); 35 | 36 | return Command::FAILURE; 37 | } 38 | 39 | $packagesToRemove = $package ? [$package] : $this->getRequiredPackages(); 40 | 41 | (new ServiceContainer($this->requireComposer(), $this->getIO())) 42 | ->get(Oven::class) 43 | ?->uninstallRecipes($packagesToRemove); 44 | 45 | return Command::SUCCESS; 46 | } 47 | 48 | private function getRequiredPackages(): array 49 | { 50 | $rootPackage = $this->requireComposer() 51 | ->getPackage(); 52 | 53 | return array_keys($rootPackage->getRequires() + $rootPackage->getDevRequires()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Cook.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 29 | $this->io = $io; 30 | } 31 | 32 | public function deactivate(Composer $composer, IOInterface $io): void 33 | { 34 | } 35 | 36 | public function uninstall(Composer $composer, IOInterface $io): void 37 | { 38 | } 39 | 40 | public function getCapabilities(): array 41 | { 42 | return [ 43 | CommandProvider::class => self::class, 44 | ]; 45 | } 46 | 47 | public function getCommands(): array 48 | { 49 | return [new CookCommand(), new CookUninstallCommand()]; 50 | } 51 | 52 | public static function getSubscribedEvents(): array 53 | { 54 | return [ 55 | PackageEvents::POST_PACKAGE_INSTALL => ['addNewPackage'], 56 | PackageEvents::POST_PACKAGE_UPDATE => ['addNewPackage'], 57 | PackageEvents::PRE_PACKAGE_UNINSTALL => ['removePackage'], 58 | ScriptEvents::POST_INSTALL_CMD => ['postUpdate'], 59 | ScriptEvents::POST_UPDATE_CMD => ['postUpdate'], 60 | ]; 61 | } 62 | 63 | public function addNewPackage(PackageEvent $event): void 64 | { 65 | $package = method_exists($event->getOperation(), 'getPackage') 66 | ? $event->getOperation() 67 | ->getPackage() 68 | : $event->getOperation() 69 | ->getInitialPackage(); 70 | 71 | $this->newPackages[] = $package->getName(); 72 | } 73 | 74 | public function removePackage(PackageEvent $event): void 75 | { 76 | $package = $event->getOperation() 77 | ->getPackage() 78 | ->getName(); 79 | 80 | $this->uninstallRecipe($package); 81 | } 82 | 83 | public function postUpdate(): void 84 | { 85 | $this->executeRecipes(); 86 | $this->displayPostInstallOutput(); 87 | } 88 | 89 | private function getServiceContainer(): ServiceContainer 90 | { 91 | if ($this->serviceContainer === null) { 92 | $this->serviceContainer = new ServiceContainer($this->composer, $this->io); 93 | } 94 | 95 | return $this->serviceContainer; 96 | } 97 | 98 | private function executeRecipes(): void 99 | { 100 | $this->getServiceContainer() 101 | ->get(Oven::class) 102 | ?->cookRecipes($this->newPackages); 103 | } 104 | 105 | private function uninstallRecipe(string $package): void 106 | { 107 | $this->getServiceContainer() 108 | ->get(Oven::class) 109 | ?->uninstallRecipes([$package]); 110 | } 111 | 112 | private function displayPostInstallOutput(): void 113 | { 114 | $this->getServiceContainer() 115 | ->get(Oven::class) 116 | ?->displayPostInstallOutput($this->newPackages); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Filter/ClassConstantFilter.php: -------------------------------------------------------------------------------- 1 | filters->has($filter)) { 32 | $this->io->write(sprintf( 33 | 'Error found in %s recipe: filter "%s" unknown.', 34 | $this->state->getCurrentPackage(), 35 | $filter, 36 | )); 37 | } 38 | 39 | $value = $this->filters->get($filter) 40 | ->process($value, $originalValue); 41 | } 42 | 43 | return $value; 44 | } 45 | 46 | protected function getDestinationRealPathname(array $file): string 47 | { 48 | return sprintf( 49 | '%s/%s', 50 | $this->state->getProjectDirectory(), 51 | $this->state->replacePathPlaceholders($file['destination']), 52 | ); 53 | } 54 | 55 | protected function getSourceContent(array $file): ?string 56 | { 57 | if (\array_key_exists('source', $file)) { 58 | $sourcePathname = sprintf('%s/%s', $this->state->getCurrentPackageDirectory(), $file['source']); 59 | 60 | if (!$this->filesystem->exists($sourcePathname)) { 61 | $this->io->write(sprintf( 62 | 'Error found in %s recipe: file "%s" not found.', 63 | $this->state->getCurrentPackage(), 64 | $sourcePathname, 65 | )); 66 | 67 | return null; 68 | } 69 | 70 | return file_get_contents($sourcePathname); 71 | } 72 | 73 | if (!\array_key_exists('content', $file)) { 74 | $this->io->write(sprintf( 75 | 'Error found in %s recipe: "source" or "content" field is required for "%s" file type.', 76 | $this->state->getCurrentPackage(), 77 | static::getName(), 78 | )); 79 | 80 | return null; 81 | } 82 | 83 | return $file['content']; 84 | } 85 | 86 | protected function getRecipeIdOpeningComment(): string 87 | { 88 | return sprintf('###> %s ###', $this->state->getCurrentPackage()); 89 | } 90 | 91 | protected function getRecipeIdClosingComment(): string 92 | { 93 | return sprintf('###< %s ###', $this->state->getCurrentPackage()); 94 | } 95 | 96 | protected function wrapRecipeId(string $text, bool $trim = false): string 97 | { 98 | return sprintf( 99 | "%s\n%s\n%s\n", 100 | $this->getRecipeIdOpeningComment(), 101 | $trim ? trim($text) : $text, 102 | $this->getRecipeIdClosingComment(), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Merger/DockerComposeMerger.php: -------------------------------------------------------------------------------- 1 | io->write(sprintf( 22 | 'Error found in %s recipe: file of type "json" requires "entries" field.', 23 | $this->state->getCurrentPackage(), 24 | )); 25 | 26 | return; 27 | } 28 | 29 | if (\array_key_exists('valid_sections', $file)) { 30 | $this->validSections = $file['valid_sections']; 31 | } 32 | 33 | $file['entries'] = array_intersect_key($file['entries'], $this->validSections ?? $file['entries']); 34 | $destinationPathname = $this->getDestinationRealPathname($file); 35 | $output = []; 36 | 37 | if ($this->filesystem->exists($destinationPathname)) { 38 | try { 39 | $output = json_decode(trim(file_get_contents($destinationPathname)), true, 512, JSON_THROW_ON_ERROR); 40 | } catch (JsonException) { 41 | $this->io->write(sprintf( 42 | 'Error found in %s recipe: invalid JSON in file "%s". Unable to merge.', 43 | $this->state->getCurrentPackage(), 44 | $file['destination'], 45 | )); 46 | 47 | return; 48 | } 49 | } 50 | 51 | $changedCount = 0; 52 | 53 | foreach ($file['entries'] as $section => $value) { 54 | if (\array_key_exists($section, $output)) { 55 | if ($output[$section] !== $value) { 56 | if (\is_array($value)) { 57 | if (array_intersect_key($output[$section], $value) === [] || $this->state->getOverwrite()) { 58 | $output[$section] = array_merge($output[$section], $value); 59 | $changedCount++; 60 | } 61 | } else { 62 | $output[$section] = $value; 63 | $changedCount++; 64 | } 65 | } 66 | } else { 67 | $output[$section] = $value; 68 | $changedCount++; 69 | } 70 | } 71 | 72 | if ($changedCount === 0) { 73 | return; 74 | } 75 | 76 | $fileExists = $this->filesystem->exists($destinationPathname); 77 | $this->filesystem->mkdir(\dirname($destinationPathname), 0755); 78 | $this->filesystem->dumpFile( 79 | $destinationPathname, 80 | json_encode($output, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), 81 | ); 82 | 83 | $this->io->write(sprintf('%s file: %s', $fileExists ? 'Updated' : 'Created', $destinationPathname)); 84 | } 85 | 86 | public function uninstall(array $file): void 87 | { 88 | if (!\array_key_exists('entries', $file)) { 89 | $this->io->write(sprintf( 90 | 'Error found in %s recipe: file of type "json" requires "entries" field.', 91 | $this->state->getCurrentPackage(), 92 | )); 93 | 94 | return; 95 | } 96 | 97 | $destinationPathname = sprintf( 98 | '%s/%s', 99 | $this->state->getProjectDirectory(), 100 | $this->state->replacePathPlaceholders($file['destination']), 101 | ); 102 | 103 | if (!$this->filesystem->exists($destinationPathname)) { 104 | return; 105 | } 106 | 107 | try { 108 | $output = json_decode(trim(file_get_contents($destinationPathname)), true, 512, JSON_THROW_ON_ERROR); 109 | } catch (JsonException) { 110 | $this->io->write(sprintf( 111 | 'Error found in %s recipe: invalid JSON in file "%s". Unable to uninstall.', 112 | $this->state->getCurrentPackage(), 113 | $file['destination'], 114 | )); 115 | 116 | return; 117 | } 118 | 119 | foreach ($file['entries'] as $section => $value) { 120 | if (\array_key_exists($section, $output)) { 121 | if (\is_array($value)) { 122 | if (!empty($keysToRemove = array_intersect_key($output[$section], $value))) { 123 | foreach ($keysToRemove as $k => $v) { 124 | unset($output[$section][$k]); 125 | } 126 | 127 | if (empty($output[$section])) { 128 | unset($output[$section]); 129 | } 130 | } 131 | } else { 132 | unset($output[$section]); 133 | } 134 | } else { 135 | unset($output[$section]); 136 | } 137 | } 138 | 139 | $this->filesystem->dumpFile( 140 | $destinationPathname, 141 | json_encode($output, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), 142 | ); 143 | 144 | $this->io->write(sprintf('Updated file: %s', $destinationPathname)); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Merger/Merger.php: -------------------------------------------------------------------------------- 1 | io->write(sprintf( 21 | 'Error found in %s recipe: file of type "php_array" requires "entries" field.', 22 | $this->state->getCurrentPackage(), 23 | )); 24 | 25 | return; 26 | } 27 | 28 | $destinationPathname = sprintf( 29 | '%s/%s', 30 | $this->state->getProjectDirectory(), 31 | $this->state->replacePathPlaceholders($file['destination']), 32 | ); 33 | $output = $this->filesystem->exists($destinationPathname) ? require ($destinationPathname) : []; 34 | $changedCount = 0; 35 | 36 | foreach ($file['entries'] as $key => $value) { 37 | if (\array_key_exists($key, $output)) { 38 | if ($output[$key] !== $value && $this->state->getOverwrite()) { 39 | $output[$key] = $value; 40 | $changedCount++; 41 | } 42 | } else { 43 | $output[$key] = $value; 44 | $changedCount++; 45 | } 46 | } 47 | 48 | if ($changedCount === 0) { 49 | return; 50 | } 51 | 52 | $fileExists = $this->filesystem->exists($destinationPathname); 53 | $this->filesystem->mkdir(\dirname($destinationPathname), 0755); 54 | $this->filesystem->dumpFile($destinationPathname, $this->dump($output, $file['filters'] ?? [])); 55 | 56 | $this->io->write(sprintf('%s file: %s', $fileExists ? 'Updated' : 'Created', $destinationPathname)); 57 | } 58 | 59 | public function uninstall(array $file): void 60 | { 61 | if (!\array_key_exists('entries', $file)) { 62 | $this->io->write(sprintf( 63 | 'Error found in %s recipe: file of type "php_array" requires "entries" field.', 64 | $this->state->getCurrentPackage(), 65 | )); 66 | 67 | return; 68 | } 69 | 70 | $destinationPathname = sprintf( 71 | '%s/%s', 72 | $this->state->getProjectDirectory(), 73 | $this->state->replacePathPlaceholders($file['destination']), 74 | ); 75 | 76 | if (!$this->filesystem->exists($destinationPathname)) { 77 | return; 78 | } 79 | 80 | $output = require($destinationPathname); 81 | 82 | foreach ($file['entries'] as $key => $value) { 83 | unset($output[$key]); 84 | } 85 | 86 | $this->filesystem->dumpFile($destinationPathname, $this->dump($output, $file['filters'] ?? [])); 87 | 88 | $this->io->write(sprintf('Updated file: %s', $destinationPathname)); 89 | } 90 | 91 | protected function dump(array $array, array $filters = []): string 92 | { 93 | $output = " $value) { 96 | $output .= ' ' . $this->applyFilters($filters['keys'] ?? [], "'" . $key . "'", $key) . ' => '; 97 | 98 | if (\is_array($value)) { 99 | $output .= $this->applyFilters( 100 | $filters['values'] ?? [], 101 | ltrim(Indentation::indent( 102 | VarExporter::export($value), 103 | new Indentation(4, Indentation::TYPE_SPACE), 104 | )), 105 | $value, 106 | ); 107 | } else { 108 | $output .= $this->applyFilters($filters['values'] ?? [], VarExporter::export($value), $value); 109 | } 110 | 111 | $output .= ",\n"; 112 | } 113 | 114 | $output .= "];\n"; 115 | 116 | return $output; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Merger/TextMerger.php: -------------------------------------------------------------------------------- 1 | getSourceContent($file)) === null) { 17 | return; 18 | } 19 | 20 | $input = $this->wrapRecipeId(rtrim($input, "\n")); 21 | $destinationPathname = $this->getDestinationRealPathname($file); 22 | $output = $this->filesystem->exists($destinationPathname) ? file_get_contents($destinationPathname) : ''; 23 | $updated = false; 24 | 25 | if ( 26 | preg_match(sprintf( 27 | '/(%s.*%s)/smU', 28 | preg_quote($this->getRecipeIdOpeningComment(), '/'), 29 | preg_quote($this->getRecipeIdClosingComment(), '/'), 30 | ), $output, $match) 31 | ) { 32 | if ($this->state->getOverwrite() && $match[1] !== trim($input)) { 33 | $output = str_replace($match[1], trim($input), $output); 34 | $updated = true; 35 | } 36 | } else { 37 | if ($output !== '') { 38 | $output .= "\n"; 39 | } 40 | 41 | $output .= $input; 42 | $updated = true; 43 | } 44 | 45 | if (!$updated) { 46 | return; 47 | } 48 | 49 | $fileExists = $this->filesystem->exists($destinationPathname); 50 | $this->filesystem->mkdir(\dirname($destinationPathname), 0755); 51 | $this->filesystem->dumpFile($destinationPathname, $output); 52 | 53 | $this->io->write(sprintf('%s file: %s', $fileExists ? 'Updated' : 'Created', $destinationPathname)); 54 | } 55 | 56 | public function uninstall(array $file): void 57 | { 58 | $destinationPathname = $this->getDestinationRealPathname($file); 59 | 60 | if (!$this->filesystem->exists($destinationPathname)) { 61 | return; 62 | } 63 | 64 | $content = file_get_contents($destinationPathname); 65 | $output = preg_replace( 66 | sprintf( 67 | '/%s.*%s\n/simU', 68 | preg_quote($this->getRecipeIdOpeningComment(), '/'), 69 | preg_quote($this->getRecipeIdClosingComment(), '/'), 70 | ), 71 | '', 72 | $content, 73 | ); 74 | 75 | if ($content === $output) { 76 | return; 77 | } 78 | 79 | if (!trim($output)) { 80 | $this->filesystem->remove($destinationPathname); 81 | $this->io->write(sprintf('Removed file: %s', $destinationPathname)); 82 | 83 | return; 84 | } 85 | 86 | $this->filesystem->dumpFile($destinationPathname, $output); 87 | $this->io->write(sprintf('Updated file: %s', $destinationPathname)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Merger/YamlMerger.php: -------------------------------------------------------------------------------- 1 | getSourceContent($file)) === null) { 22 | return; 23 | } 24 | 25 | if (\array_key_exists('valid_sections', $file)) { 26 | $this->validSections = $file['valid_sections']; 27 | } 28 | 29 | if (\array_key_exists('blank_line_after', $file)) { 30 | $this->blankLineAfter = $file['blank_line_after']; 31 | } 32 | 33 | $inputParsed = Yaml::parse($input); 34 | $destinationPathname = $this->getDestinationRealPathname($file); 35 | $output = $this->filesystem->exists($destinationPathname) ? file_get_contents($destinationPathname) : ''; 36 | $updated = false; 37 | 38 | foreach ($this->validSections ?? array_keys($inputParsed) as $section) { 39 | if ( 40 | !\array_key_exists($section, $inputParsed) 41 | || !preg_match('/(?:^|\n)' . $section . ':\s+?(\s{4}.*)(?:\n\w|$)/sU', $input, $inputMatch) 42 | ) { 43 | continue; 44 | } 45 | 46 | $recipeSection = $this->wrapRecipeId(trim($inputMatch[1], "\n"), false); 47 | 48 | if ( 49 | preg_match(sprintf( 50 | '/(?:(?:^|\n)' . $section . ':\n).*(%s.*%s)/smU', 51 | preg_quote($this->getRecipeIdOpeningComment(), '/'), 52 | preg_quote($this->getRecipeIdClosingComment(), '/'), 53 | ), $output, $recipeMatch) 54 | ) { 55 | if ($recipeMatch[1] !== trim($recipeSection) && $this->state->getOverwrite()) { 56 | $output = str_replace($recipeMatch[1], trim($recipeSection), $output); 57 | $updated = true; 58 | } 59 | } else { 60 | if (preg_match('/((?:^|\n)' . $section . ':\n)/', $output, $outputMatch)) { 61 | $output = str_replace( 62 | $outputMatch[1], 63 | $outputMatch[1] . $recipeSection . $this->appendBlankLine($section), 64 | $output, 65 | ); 66 | } else { 67 | $output .= sprintf("\n%s:\n%s\n", $section, $recipeSection . $this->appendBlankLine($section)); 68 | } 69 | 70 | $updated = true; 71 | } 72 | } 73 | 74 | if (!$updated) { 75 | return; 76 | } 77 | 78 | $output = preg_replace( 79 | '/(' . preg_quote($this->getRecipeIdClosingComment(), '/') . ')\n{3,}/', 80 | "$1\n\n", 81 | $output, 82 | ); 83 | 84 | $fileExists = $this->filesystem->exists($destinationPathname); 85 | $this->filesystem->mkdir(\dirname($destinationPathname), 0755); 86 | $this->filesystem->dumpFile($destinationPathname, trim($output) . "\n"); 87 | 88 | $this->io->write(sprintf('%s file: %s', $fileExists ? 'Updated' : 'Created', $destinationPathname)); 89 | } 90 | 91 | public function uninstall(array $file): void 92 | { 93 | $destinationPathname = $this->getDestinationRealPathname($file); 94 | 95 | if (!$this->filesystem->exists($destinationPathname)) { 96 | return; 97 | } 98 | 99 | $content = file_get_contents($destinationPathname); 100 | $output = preg_replace( 101 | sprintf( 102 | '/^\s*%s.*%s(?:\r?\n)+?/simU', 103 | preg_quote($this->getRecipeIdOpeningComment(), '/'), 104 | preg_quote($this->getRecipeIdClosingComment(), '/'), 105 | ), 106 | '', 107 | $content, 108 | ); 109 | 110 | if ($content === $output) { 111 | return; 112 | } 113 | 114 | if (!empty($file['uninstall_empty_sections'])) { 115 | $recipe = Yaml::parse($this->getSourceContent($file)); 116 | $after = Yaml::parse($output); 117 | 118 | $recipeSections = array_keys(array_intersect_key($after, $recipe)); 119 | 120 | foreach ($recipeSections as $section) { 121 | if (empty($after[$section])) { 122 | $output = preg_replace([sprintf('/^%s:\s*$/m', $section), '/\n+/'], ['', "\n"], $output); 123 | } 124 | } 125 | } 126 | 127 | if (!trim($output)) { 128 | $this->filesystem->remove($destinationPathname); 129 | $this->io->write(sprintf('Removed file: %s', $destinationPathname)); 130 | 131 | return; 132 | } 133 | 134 | $this->filesystem->dumpFile($destinationPathname, $output); 135 | $this->io->write(sprintf('Updated file: %s', $destinationPathname)); 136 | } 137 | 138 | private function appendBlankLine(string $section): string 139 | { 140 | return \in_array($section, $this->blankLineAfter ?? [], true) ? "\n" : ''; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Options.php: -------------------------------------------------------------------------------- 1 | */ 12 | private array $options = []; 13 | 14 | public function __construct( 15 | private Composer $composer 16 | ) { 17 | $this->initOptions(); 18 | } 19 | 20 | public function all(): array 21 | { 22 | return $this->options; 23 | } 24 | 25 | public function get(string $option): ?string 26 | { 27 | return $this->options[$option] ?? null; 28 | } 29 | 30 | private function initOptions(): void 31 | { 32 | $extra = $this->composer->getPackage() 33 | ->getExtra(); 34 | 35 | $this->options = array_merge([ 36 | 'bin-dir' => 'bin', 37 | 'config-dir' => 'config', 38 | 'src-dir' => 'src', 39 | 'var-dir' => 'var', 40 | 'public-dir' => 'public', 41 | 'root-dir' => $extra['symfony']['root-dir'] ?? '.', 42 | ], $extra); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Oven.php: -------------------------------------------------------------------------------- 1 | state->setOverwrite($overwrite); 38 | 39 | foreach ($this->getRequiredPackages($packages) as $package) { 40 | $this->state->setCurrentPackage($package); 41 | 42 | if ($this->executePackageRecipe() === false) { 43 | $this->io->write(sprintf('Aborting %s recipe execution.', $package)); 44 | } 45 | } 46 | 47 | $this->io->write(''); 48 | } 49 | 50 | public function uninstallRecipes(array $packages): void 51 | { 52 | $this->state->setOverwrite(false); 53 | 54 | foreach ($packages as $package) { 55 | $this->state->setCurrentPackage($package); 56 | 57 | if ($this->uninstallPackageRecipe() === false) { 58 | $this->io->write(sprintf('Aborting %s recipe uninstallation.', $package)); 59 | } 60 | } 61 | 62 | $this->io->write(''); 63 | } 64 | 65 | public function displayPostInstallOutput(?array $packages = null): void 66 | { 67 | $this->state->setOverwrite(false); 68 | 69 | foreach ($this->getRequiredPackages($packages) as $package) { 70 | $this->state->setCurrentPackage($package); 71 | $this->displayPackageRecipePostInstallOutput(); 72 | } 73 | 74 | $this->io->write(''); 75 | } 76 | 77 | private function executePackageRecipe(): ?bool 78 | { 79 | if (!$this->state->getCurrentPackageRecipePathname()) { 80 | return null; 81 | } 82 | 83 | $this->io->write(sprintf("\nFound Cook recipe for %s", $this->state->getCurrentPackage())); 84 | 85 | if (!($recipe = $this->loadAndValidateRecipe())) { 86 | return false; 87 | } 88 | 89 | foreach ($recipe['files'] ?? [] as $file) { 90 | if (!$this->mergers->has($file['type'])) { 91 | $this->io->write(sprintf( 92 | 'Error found in %s recipe: type "%s" unknown.', 93 | $this->state->getCurrentPackage(), 94 | $file['type'], 95 | )); 96 | 97 | continue; 98 | } 99 | 100 | $this->mergers->get($file['type'])->merge($file); 101 | } 102 | 103 | foreach ($recipe['directories'] ?? [] as $destination => $source) { 104 | $this->copyDirectory($source, $destination); 105 | } 106 | 107 | return true; 108 | } 109 | 110 | private function uninstallPackageRecipe(): ?bool 111 | { 112 | if (!$this->state->getCurrentPackageRecipePathname()) { 113 | return null; 114 | } 115 | 116 | $this->io->write(sprintf("\nUninstalling Cook recipe for %s", $this->state->getCurrentPackage())); 117 | 118 | if (!($recipe = $this->loadAndValidateRecipe())) { 119 | return false; 120 | } 121 | 122 | foreach ($recipe['files'] ?? [] as $file) { 123 | if (!$this->mergers->has($file['type'])) { 124 | $this->io->write(sprintf( 125 | 'Error found in %s recipe: type "%s" unknown.', 126 | $this->state->getCurrentPackage(), 127 | $file['type'], 128 | )); 129 | 130 | continue; 131 | } 132 | 133 | $this->mergers->get($file['type'])->uninstall($file); 134 | } 135 | 136 | foreach ($recipe['directories'] ?? [] as $destination => $source) { 137 | $this->removeFilesFromDirectory($source, $destination); 138 | } 139 | 140 | return true; 141 | } 142 | 143 | private function loadAndValidateRecipe(): ?array 144 | { 145 | if (($recipe = $this->loadRecipe()) === null) { 146 | return null; 147 | } 148 | 149 | $recipe = $this->transformRecipe($recipe); 150 | 151 | if (!$this->validateRecipeSchema($recipe)) { 152 | return null; 153 | } 154 | 155 | return $this->setRecipeDefaults($recipe); 156 | } 157 | 158 | private function loadRecipe(): ?array 159 | { 160 | $isYamlRecipe = str_ends_with($this->state->getCurrentPackageRecipePathname(), '.yaml'); 161 | 162 | if ($isYamlRecipe && !\in_array('symfony/yaml', InstalledVersions::getInstalledPackages())) { 163 | $this->io->error(sprintf( 164 | 'Recipe for package %s is in YAML format but symfony/yaml is not installed.', 165 | $this->state->getCurrentPackage(), 166 | )); 167 | 168 | return null; 169 | } 170 | 171 | return $isYamlRecipe ? $this->loadYamlRecipe() : $this->loadJsonRecipe(); 172 | } 173 | 174 | private function loadYamlRecipe(): ?array 175 | { 176 | try { 177 | return Yaml::parseFile($this->state->getCurrentPackageRecipePathname()); 178 | } catch (ParseException) { 179 | $this->io->error(sprintf( 180 | 'Invalid YAML syntax in %s', 181 | $this->state->getCurrentPackageRecipePathname(), 182 | )); 183 | } 184 | 185 | return null; 186 | } 187 | 188 | private function loadJsonRecipe(): ?array 189 | { 190 | try { 191 | return json_decode( 192 | trim(file_get_contents($this->state->getCurrentPackageRecipePathname())), 193 | true, 194 | 512, 195 | JSON_THROW_ON_ERROR, 196 | ); 197 | } catch (\JsonException) { 198 | $this->io->error(sprintf('Invalid JSON syntax in %s', $this->state->getCurrentPackageRecipePathname())); 199 | } 200 | 201 | return null; 202 | } 203 | 204 | private function transformRecipe(array $recipe): array 205 | { 206 | foreach ($recipe['files'] ?? [] as $destination => $file) { 207 | if (\is_string($file)) { 208 | $recipe['files'][$destination] = [ 209 | 'source' => $file, 210 | ]; 211 | } 212 | 213 | $recipe['files'][$destination]['destination'] = $destination; 214 | } 215 | 216 | return $recipe; 217 | } 218 | 219 | private function validateRecipeSchema(array $recipe): bool 220 | { 221 | $constraints = new Assert\Collection([ 222 | 'files' => new Assert\Optional( 223 | new Assert\All( 224 | new Assert\Collection([ 225 | 'type' => new Assert\Optional(new Assert\Choice([ 226 | 'text', 227 | 'php_array', 228 | 'json', 229 | 'yaml', 230 | 'docker_compose', 231 | ])), 232 | 'source' => new Assert\Optional([new Assert\NotBlank(), new Assert\Type('string')]), 233 | 'destination' => [new Assert\NotBlank(), new Assert\Type('string')], 234 | 'entries' => new Assert\Optional(new Assert\Type('array')), 235 | 'content' => new Assert\Optional(new Assert\Type('string')), 236 | 'filters' => new Assert\Optional([ 237 | new Assert\Collection([ 238 | 'keys' => new Assert\Optional(new Assert\All(new Assert\Choice(['class_constant']))), 239 | 'values' => new Assert\Optional( 240 | new Assert\All(new Assert\Choice(['class_constant', 'single_line_array'])), 241 | ), 242 | ]), 243 | ]), 244 | 'valid_sections' => new Assert\Optional(new Assert\Type('array')), 245 | 'blank_line_after' => new Assert\Optional(new Assert\Type('array')), 246 | ]), 247 | ), 248 | ), 249 | 'directories' => new Assert\Optional(new Assert\Type('array')), 250 | 'post_install_output' => new Assert\Optional(new Assert\Type('string')), 251 | ]); 252 | 253 | $validator = Validation::createValidator(); 254 | $violations = $validator->validate($recipe, $constraints); 255 | 256 | if ($violations->count() > 0) { 257 | foreach ($violations as $violation) { 258 | $this->io->write(sprintf( 259 | 'Error found in %s recipe: %s %s', 260 | $this->state->getCurrentPackage(), 261 | $violation->getPropertyPath(), 262 | $violation->getMessage() 263 | )); 264 | } 265 | } 266 | 267 | return $violations->count() === 0; 268 | } 269 | 270 | private function setRecipeDefaults(?array $recipe): array 271 | { 272 | $resolver = (new OptionsResolver()) 273 | ->setDefaults([ 274 | 'files' => static function (OptionsResolver $filesResolver) { 275 | $filesResolver 276 | ->setPrototype(true) 277 | ->setDefined([ 278 | 'type', 279 | 'destination', 280 | 'entries', 281 | 'filters', 282 | 'content', 283 | 'source', 284 | 'valid_sections', 285 | 'blank_line_after', 286 | ]) 287 | ->setDefaults([ 288 | 'type' => 'text', 289 | ]); 290 | }, 291 | ]) 292 | ->setDefined(['files', 'directories', 'post_install_output']); 293 | 294 | return $resolver->resolve($recipe); 295 | } 296 | 297 | private function copyDirectory(string $source, string $destination): void 298 | { 299 | $sourceDir = $this->state->getCurrentPackageDirectory() . '/' . $source; 300 | 301 | if (!$this->filesystem->exists($sourceDir)) { 302 | $this->io->write(sprintf( 303 | "Error executing %s recipe: %s directory doesn't exist.", 304 | $this->state->getCurrentPackage(), 305 | $sourceDir, 306 | )); 307 | } 308 | 309 | $finder = new Finder(); 310 | $finder->in($sourceDir) 311 | ->files(); 312 | 313 | foreach ($finder as $file) { 314 | $destinationPathname = str_replace( 315 | [$this->state->getCurrentPackageDirectory(), $source], 316 | [$this->state->getProjectDirectory(), $this->state->replacePathPlaceholders($destination)], 317 | $file->getPathname(), 318 | ); 319 | 320 | $this->filesystem->mkdir(\dirname($destinationPathname)); 321 | $fileExists = $this->filesystem->exists($destinationPathname); 322 | 323 | if ( 324 | !$fileExists 325 | || ( 326 | $this->state->getOverwrite() 327 | && $file->getContents() !== file_get_contents($destinationPathname) 328 | ) 329 | ) { 330 | $this->filesystem->copy($file->getPathname(), $destinationPathname, true); 331 | $this->io->write(sprintf('%s file: %s', $fileExists ? 'Updated' : 'Created', $destinationPathname)); 332 | } 333 | } 334 | } 335 | 336 | private function removeFilesFromDirectory(string $source, string $destination): void 337 | { 338 | $sourceDir = $this->state->getCurrentPackageDirectory() . '/' . $source; 339 | 340 | if (!$this->filesystem->exists($sourceDir)) { 341 | $this->io->write(sprintf( 342 | "Error executing %s recipe: %s directory doesn't exist.", 343 | $this->state->getCurrentPackage(), 344 | $sourceDir, 345 | )); 346 | } 347 | 348 | $finder = new Finder(); 349 | $finder->in($sourceDir) 350 | ->files(); 351 | 352 | foreach ($finder as $file) { 353 | $destinationPathname = str_replace( 354 | [$this->state->getCurrentPackageDirectory(), $source], 355 | [$this->state->getProjectDirectory(), $this->state->replacePathPlaceholders($destination)], 356 | $file->getPathname(), 357 | ); 358 | 359 | $fileExists = $this->filesystem->exists($destinationPathname); 360 | 361 | if ($fileExists && $file->getContents() === file_get_contents($destinationPathname)) { 362 | $this->filesystem->remove($destinationPathname); 363 | $this->io->write(sprintf('Removed file: %s', $destinationPathname)); 364 | } 365 | } 366 | } 367 | 368 | private function getRequiredPackages(?array $packages = null): array 369 | { 370 | if ($this->requiredPackages === null) { 371 | $rootPackage = $this->composer->getPackage(); 372 | $this->requiredPackages = $packages 373 | ?? array_keys($rootPackage->getRequires() + $rootPackage->getDevRequires()); 374 | } 375 | 376 | return $this->requiredPackages; 377 | } 378 | 379 | private function displayPackageRecipePostInstallOutput(): void 380 | { 381 | if (!$this->state->getCurrentPackageRecipePathname()) { 382 | return; 383 | } 384 | 385 | if (!($recipe = $this->loadAndValidateRecipe())) { 386 | return; 387 | } 388 | 389 | if (!empty($recipe['post_install_output'])) { 390 | $this->io->write(sprintf( 391 | "\n%s instructions:\n\n%s", 392 | $this->state->getCurrentPackage(), 393 | $recipe['post_install_output'], 394 | )); 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/ServiceContainer.php: -------------------------------------------------------------------------------- 1 | initContainer($composer, $io); 20 | } 21 | 22 | public function get(string $id): ?object 23 | { 24 | return $this->container->get($id); 25 | } 26 | 27 | private function initContainer(Composer $composer, IOInterface $io): void 28 | { 29 | $container = new ContainerBuilder(); 30 | $container->set(Composer::class, $composer); 31 | $container->set(IOInterface::class, $io); 32 | 33 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../config')); 34 | $loader->load('services.yaml'); 35 | 36 | $container->compile(); 37 | 38 | $this->container = $container; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/State.php: -------------------------------------------------------------------------------- 1 | currentPackage; 26 | } 27 | 28 | public function setCurrentPackage(?string $currentPackage): self 29 | { 30 | $this->currentPackage = $currentPackage; 31 | 32 | return $this; 33 | } 34 | 35 | public function getOverwrite(): bool 36 | { 37 | return $this->overwrite; 38 | } 39 | 40 | public function setOverwrite(bool $overwrite): self 41 | { 42 | $this->overwrite = $overwrite; 43 | 44 | return $this; 45 | } 46 | 47 | public function getProjectDirectory(): string 48 | { 49 | if ($this->composerFile === null) { 50 | $this->composerFile = \dirname(Factory::getComposerFile()); 51 | } 52 | 53 | return $this->composerFile; 54 | } 55 | 56 | public function getVendorDirectory(): string 57 | { 58 | return $this->getProjectDirectory() . '/vendor'; 59 | } 60 | 61 | public function getCurrentPackageDirectory(): string 62 | { 63 | return $this->getVendorDirectory() . '/' . $this->currentPackage; 64 | } 65 | 66 | public function getCurrentPackageRecipePathname(): ?string 67 | { 68 | if (!\array_key_exists($this->currentPackage, $this->recipes)) { 69 | $jsonRecipe = $this->getCurrentPackageDirectory() . '/cook.json'; 70 | $yamlRecipe = $this->getCurrentPackageDirectory() . '/cook.yaml'; 71 | 72 | if ($this->filesystem->exists($yamlRecipe)) { 73 | $recipePathname = $yamlRecipe; 74 | } elseif ($this->filesystem->exists($jsonRecipe)) { 75 | $recipePathname = $jsonRecipe; 76 | } else { 77 | $recipePathname = null; 78 | } 79 | 80 | $this->recipes[$this->currentPackage] = $recipePathname; 81 | } 82 | 83 | return $this->recipes[$this->currentPackage]; 84 | } 85 | 86 | public function replacePathPlaceholders(string $pathname): string 87 | { 88 | return preg_replace_callback('/%(.+?)%/', function (array $matches) { 89 | $option = str_replace('_', '-', strtolower($matches[1])); 90 | 91 | if (!($opt = $this->options->get($option))) { 92 | return $matches[0]; 93 | } 94 | 95 | return rtrim($opt, '/'); 96 | }, $pathname); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/StateInterface.php: -------------------------------------------------------------------------------- 1 | williarin/cook-example ### 6 | SOME_ENV_VARIABLE='hello' 7 | ANOTHER_ENV_VARIABLE='world' 8 | ###< williarin/cook-example ### 9 | -------------------------------------------------------------------------------- /tests/Dummy/after/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 6 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 7 | Williarin\CookExampleBundle::class => ['dev' => true, 'test' => true], 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/Dummy/after/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "williarin/cook", 3 | "scripts": { 4 | "post-update-cmd": "MyVendor\\MyClass::postUpdate", 5 | "post-package-install": [ 6 | "MyVendor\\MyClass::postPackageInstall" 7 | ], 8 | "post-create-project-cmd": "php -r \"copy('config/local-example.php', 'config/local.php');\"" 9 | }, 10 | "new-entry": "some-config" 11 | } 12 | -------------------------------------------------------------------------------- /tests/Dummy/after/routes.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: 3 | path: ../src/Controller/ 4 | namespace: App\Controller 5 | type: attribute 6 | 7 | other_routes: 8 | ###> williarin/cook-example ### 9 | resource: . 10 | type: other_routes_loader 11 | ###< williarin/cook-example ### 12 | -------------------------------------------------------------------------------- /tests/Dummy/after/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | ###> williarin/cook-example ### 3 | locale: fr 4 | ###< williarin/cook-example ### 5 | some_parameter: true 6 | another_parameter: Hello world 7 | 8 | services: 9 | ###> williarin/cook-example ### 10 | Some\Service: ~ 11 | ###< williarin/cook-example ### 12 | 13 | _defaults: 14 | autowire: true 15 | autoconfigure: true 16 | 17 | App\: 18 | resource: '../src/' 19 | exclude: 20 | - '../src/DependencyInjection/' 21 | - '../src/Entity/' 22 | - '../src/Kernel.php' 23 | -------------------------------------------------------------------------------- /tests/Dummy/before/.env: -------------------------------------------------------------------------------- 1 | APP_ENV=dev 2 | APP_SECRET=10a4bee52442dcf74a9f6b5a9afd319a 3 | DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8" 4 | -------------------------------------------------------------------------------- /tests/Dummy/before/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 6 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 7 | ]; 8 | -------------------------------------------------------------------------------- /tests/Dummy/before/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "williarin/cook", 3 | "scripts": { 4 | "post-update-cmd": "MyVendor\\MyClass::postUpdate", 5 | "post-package-install": [ 6 | "MyVendor\\MyClass::postPackageInstall" 7 | ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/Dummy/before/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | some_parameter: true 3 | another_parameter: Hello world 4 | 5 | services: 6 | _defaults: 7 | autowire: true 8 | autoconfigure: true 9 | 10 | App\: 11 | resource: '../src/' 12 | exclude: 13 | - '../src/DependencyInjection/' 14 | - '../src/Entity/' 15 | - '../src/Kernel.php' 16 | -------------------------------------------------------------------------------- /tests/Dummy/recipe/.env: -------------------------------------------------------------------------------- 1 | SOME_ENV_VARIABLE='hello' 2 | ANOTHER_ENV_VARIABLE='world' 3 | -------------------------------------------------------------------------------- /tests/Dummy/recipe/routes.yaml: -------------------------------------------------------------------------------- 1 | other_routes: 2 | resource: . 3 | type: other_routes_loader 4 | -------------------------------------------------------------------------------- /tests/Dummy/recipe/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | locale: fr 3 | 4 | services: 5 | Some\Service: ~ 6 | -------------------------------------------------------------------------------- /tests/Filter/ClassConstantFilterTest.php: -------------------------------------------------------------------------------- 1 | filter = new ClassConstantFilter(); 19 | } 20 | 21 | public function testGetName(): void 22 | { 23 | $this->assertSame('class_constant', $this->filter::getName()); 24 | } 25 | 26 | public function testProcess(): void 27 | { 28 | $this->assertSame('Williarin\Cook::class', $this->filter->process('Williarin\Cook', 'Williarin\Cook')); 29 | } 30 | 31 | public function testProcessWithSingleQuotedString(): void 32 | { 33 | $this->assertSame('Williarin\Cook::class', $this->filter->process("'Williarin\Cook'", 'Williarin\Cook')); 34 | } 35 | 36 | public function testProcessForNonString(): void 37 | { 38 | $this->assertSame([12], $this->filter->process([12], [12])); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Filter/SingleLineArrayFilterTest.php: -------------------------------------------------------------------------------- 1 | filter = new SingleLineArrayFilter(); 19 | } 20 | 21 | public function testGetName(): void 22 | { 23 | $this->assertSame('single_line_array', $this->filter::getName()); 24 | } 25 | 26 | public function testProcess(): void 27 | { 28 | $this->assertSame( 29 | "['name' => 'John', 'age' => 12]", 30 | $this->filter->process([ 31 | 'name' => 'John', 32 | 'age' => 12, 33 | ], [ 34 | 'name' => 'John', 35 | 'age' => 12, 36 | ]) 37 | ); 38 | } 39 | 40 | public function testProcessNonArray(): void 41 | { 42 | $this->assertSame( 43 | [ 44 | 'name' => 'John', 45 | 'age' => 12, 46 | ], 47 | $this->filter->process([ 48 | 'name' => 'John', 49 | 'age' => 12, 50 | ], 'some string') 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Merger/DockerComposeMergerTest.php: -------------------------------------------------------------------------------- 1 | merger = new DockerComposeMerger($this->io, $this->state, $this->filesystem, $this->filters); 17 | } 18 | 19 | public function testGetName(): void 20 | { 21 | $this->assertSame('docker_compose', $this->merger::getName()); 22 | } 23 | 24 | public function testMergeWithoutContent(): void 25 | { 26 | $this->io->shouldReceive('write') 27 | ->once() 28 | ->with( 29 | 'Error found in williarin/cook-example recipe: "source" or "content" field is required for "docker_compose" file type.' 30 | ) 31 | ; 32 | 33 | $this->merger->merge([]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Merger/JsonMergerTest.php: -------------------------------------------------------------------------------- 1 | merger = new JsonMerger($this->io, $this->state, $this->filesystem, $this->filters); 17 | } 18 | 19 | public function testGetName(): void 20 | { 21 | $this->assertSame('json', $this->merger::getName()); 22 | } 23 | 24 | public function testMergeWithoutEntries(): void 25 | { 26 | $this->io->shouldReceive('write') 27 | ->once() 28 | ->with( 29 | 'Error found in williarin/cook-example recipe: file of type "json" requires "entries" field.' 30 | ) 31 | ; 32 | 33 | $this->merger->merge([]); 34 | } 35 | 36 | public function testMergeNewFile(): void 37 | { 38 | $file = [ 39 | 'destination' => 'var/cache/tests/composer.json', 40 | 'entries' => [ 41 | 'scripts' => [ 42 | 'post-create-project-cmd' => "php -r \"copy('config/local-example.php', 'config/local.php');\"", 43 | ], 44 | ], 45 | ]; 46 | 47 | $this->filesystem->shouldReceive('mkdir') 48 | ->once() 49 | ->with('./var/cache/tests', 0755); 50 | 51 | $this->filesystem->shouldReceive('exists') 52 | ->atLeast() 53 | ->once() 54 | ->andReturn(false); 55 | 56 | $this->filesystem->shouldReceive('dumpFile') 57 | ->once() 58 | ->with( 59 | './var/cache/tests/composer.json', 60 | <<io->shouldReceive('write') 71 | ->once() 72 | ->with('Created file: ./var/cache/tests/composer.json'); 73 | 74 | $this->merger->merge($file); 75 | } 76 | 77 | public function testMergeExistingFile(): void 78 | { 79 | $file = [ 80 | 'destination' => 'tests/Dummy/before/composer.json', 81 | 'entries' => [ 82 | 'scripts' => [ 83 | 'post-create-project-cmd' => "php -r \"copy('config/local-example.php', 'config/local.php');\"", 84 | ], 85 | 'new-entry' => 'some-config', 86 | ], 87 | ]; 88 | 89 | $this->filesystem->shouldReceive('mkdir') 90 | ->once() 91 | ->with('./tests/Dummy/before', 0755); 92 | 93 | $this->filesystem->shouldReceive('exists') 94 | ->twice() 95 | ->andReturn(true); 96 | 97 | $this->filesystem->shouldReceive('dumpFile') 98 | ->once() 99 | ->with( 100 | './tests/Dummy/before/composer.json', 101 | <<io->shouldReceive('write') 118 | ->once() 119 | ->with('Updated file: ./tests/Dummy/before/composer.json'); 120 | 121 | $this->merger->merge($file); 122 | } 123 | 124 | public function testUninstallWithoutEntries(): void 125 | { 126 | $this->io->shouldReceive('write') 127 | ->once() 128 | ->with( 129 | 'Error found in williarin/cook-example recipe: file of type "json" requires "entries" field.' 130 | ) 131 | ; 132 | 133 | $this->merger->uninstall([]); 134 | } 135 | 136 | public function testUninstallRecipe(): void 137 | { 138 | $file = [ 139 | 'destination' => 'tests/Dummy/after/composer.json', 140 | 'entries' => [ 141 | 'scripts' => [ 142 | 'post-create-project-cmd' => "php -r \"copy('config/local-example.php', 'config/local.php');\"", 143 | ], 144 | 'new-entry' => 'some-config', 145 | ], 146 | ]; 147 | 148 | $this->filesystem->shouldReceive('exists') 149 | ->once() 150 | ->andReturn(true); 151 | 152 | $this->filesystem->shouldReceive('dumpFile') 153 | ->once() 154 | ->with( 155 | './tests/Dummy/after/composer.json', 156 | <<io->shouldReceive('write') 171 | ->once() 172 | ->with('Updated file: ./tests/Dummy/after/composer.json'); 173 | 174 | $this->merger->uninstall($file); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /tests/Merger/MergerTestCase.php: -------------------------------------------------------------------------------- 1 | io = Mockery::mock(IOInterface::class); 26 | $this->filesystem = Mockery::mock(Filesystem::class); 27 | $this->filters = Mockery::mock(ServiceLocator::class); 28 | 29 | $this->state = Mockery::mock(StateInterface::class); 30 | $this->state->shouldReceive('getCurrentPackage') 31 | ->andReturn('williarin/cook-example'); 32 | $this->state->shouldReceive('getCurrentPackageDirectory') 33 | ->andReturn('tests/Dummy/recipe'); 34 | $this->state->shouldReceive('getProjectDirectory') 35 | ->andReturn('.'); 36 | $this->state->shouldReceive('replacePathPlaceholders') 37 | ->andReturnArg(0); 38 | } 39 | 40 | protected function addFilter(string $name, string $className): void 41 | { 42 | $this->filters->shouldReceive('has') 43 | ->atLeast() 44 | ->once() 45 | ->with($name) 46 | ->andReturn(true); 47 | 48 | $this->filters->shouldReceive('get') 49 | ->atLeast() 50 | ->once() 51 | ->with($name) 52 | ->andReturn(new $className()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Merger/PhpArrayMergerTest.php: -------------------------------------------------------------------------------- 1 | merger = new PhpArrayMerger($this->io, $this->state, $this->filesystem, $this->filters); 19 | } 20 | 21 | public function testGetName(): void 22 | { 23 | $this->assertSame('php_array', $this->merger::getName()); 24 | } 25 | 26 | public function testMergeWithoutEntries(): void 27 | { 28 | $this->io->shouldReceive('write') 29 | ->once() 30 | ->with( 31 | 'Error found in williarin/cook-example recipe: file of type "php_array" requires "entries" field.' 32 | ) 33 | ; 34 | 35 | $this->merger->merge([]); 36 | } 37 | 38 | public function testMergeNewFileWithoutFilters(): void 39 | { 40 | $file = [ 41 | 'destination' => 'var/cache/tests/bundles.php', 42 | 'entries' => [ 43 | 'Williarin\CookExampleBundle' => [ 44 | 'dev' => true, 45 | 'test' => true, 46 | ], 47 | ], 48 | 'filters' => [], 49 | ]; 50 | 51 | $this->filesystem->shouldReceive('mkdir') 52 | ->once() 53 | ->with('./var/cache/tests', 0755); 54 | 55 | $this->filesystem->shouldReceive('exists') 56 | ->atLeast() 57 | ->once() 58 | ->andReturn(false); 59 | 60 | $this->filesystem->shouldReceive('dumpFile') 61 | ->once() 62 | ->with( 63 | './var/cache/tests/bundles.php', 64 | << [ 69 | 'dev' => true, 70 | 'test' => true, 71 | ], 72 | ]; 73 | 74 | CODE_SAMPLE 75 | , 76 | ); 77 | 78 | $this->io->shouldReceive('write') 79 | ->once() 80 | ->with('Created file: ./var/cache/tests/bundles.php'); 81 | 82 | $this->merger->merge($file); 83 | } 84 | 85 | public function testMergeNewFileWithFilters(): void 86 | { 87 | $file = [ 88 | 'destination' => 'var/cache/tests/bundles.php', 89 | 'entries' => [ 90 | 'Williarin\CookExampleBundle' => [ 91 | 'dev' => true, 92 | 'test' => true, 93 | ], 94 | ], 95 | 'filters' => [ 96 | 'keys' => ['class_constant'], 97 | 'values' => ['single_line_array'], 98 | ], 99 | ]; 100 | 101 | $this->addFilter('class_constant', ClassConstantFilter::class); 102 | $this->addFilter('single_line_array', SingleLineArrayFilter::class); 103 | 104 | $this->filesystem->shouldReceive('mkdir') 105 | ->once() 106 | ->with('./var/cache/tests', 0755); 107 | 108 | $this->filesystem->shouldReceive('exists') 109 | ->atLeast() 110 | ->once() 111 | ->andReturn(false); 112 | 113 | $this->filesystem->shouldReceive('dumpFile') 114 | ->once() 115 | ->with( 116 | './var/cache/tests/bundles.php', 117 | << ['dev' => true, 'test' => true], 122 | ]; 123 | 124 | CODE_SAMPLE 125 | , 126 | ); 127 | 128 | $this->io->shouldReceive('write') 129 | ->once() 130 | ->with('Created file: ./var/cache/tests/bundles.php'); 131 | 132 | $this->merger->merge($file); 133 | } 134 | 135 | public function testMergeExistingFileWithFilters(): void 136 | { 137 | $file = [ 138 | 'destination' => 'tests/Dummy/before/bundles.php', 139 | 'entries' => [ 140 | 'Williarin\CookExampleBundle' => [ 141 | 'dev' => true, 142 | 'test' => true, 143 | ], 144 | ], 145 | 'filters' => [ 146 | 'keys' => ['class_constant'], 147 | 'values' => ['single_line_array'], 148 | ], 149 | ]; 150 | 151 | $this->addFilter('class_constant', ClassConstantFilter::class); 152 | $this->addFilter('single_line_array', SingleLineArrayFilter::class); 153 | 154 | $this->filesystem->shouldReceive('mkdir') 155 | ->once() 156 | ->with('./tests/Dummy/before', 0755); 157 | 158 | $this->filesystem->shouldReceive('exists') 159 | ->twice() 160 | ->andReturn(true); 161 | 162 | $this->filesystem->shouldReceive('dumpFile') 163 | ->once() 164 | ->with( 165 | './tests/Dummy/before/bundles.php', 166 | << ['all' => true], 171 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 172 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 173 | Williarin\CookExampleBundle::class => ['dev' => true, 'test' => true], 174 | ]; 175 | 176 | CODE_SAMPLE 177 | , 178 | ); 179 | 180 | $this->io->shouldReceive('write') 181 | ->once() 182 | ->with('Updated file: ./tests/Dummy/before/bundles.php'); 183 | 184 | $this->merger->merge($file); 185 | } 186 | 187 | public function testUninstallWithoutEntries(): void 188 | { 189 | $this->io->shouldReceive('write') 190 | ->once() 191 | ->with( 192 | 'Error found in williarin/cook-example recipe: file of type "php_array" requires "entries" field.' 193 | ) 194 | ; 195 | 196 | $this->merger->uninstall([]); 197 | } 198 | 199 | public function testUninstallRecipe(): void 200 | { 201 | $file = [ 202 | 'destination' => 'tests/Dummy/after/bundles.php', 203 | 'entries' => [ 204 | 'Williarin\CookExampleBundle' => [ 205 | 'dev' => true, 206 | 'test' => true, 207 | ], 208 | ], 209 | 'filters' => [ 210 | 'keys' => ['class_constant'], 211 | 'values' => ['single_line_array'], 212 | ], 213 | ]; 214 | 215 | $this->addFilter('class_constant', ClassConstantFilter::class); 216 | $this->addFilter('single_line_array', SingleLineArrayFilter::class); 217 | 218 | $this->filesystem->shouldReceive('exists') 219 | ->once() 220 | ->andReturn(true); 221 | 222 | $this->filesystem->shouldReceive('dumpFile') 223 | ->once() 224 | ->with( 225 | './tests/Dummy/after/bundles.php', 226 | << ['all' => true], 231 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 232 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 233 | ]; 234 | 235 | CODE_SAMPLE 236 | , 237 | ); 238 | 239 | $this->io->shouldReceive('write') 240 | ->once() 241 | ->with('Updated file: ./tests/Dummy/after/bundles.php'); 242 | 243 | $this->merger->uninstall($file); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /tests/Merger/TextMergerTest.php: -------------------------------------------------------------------------------- 1 | merger = new TextMerger($this->io, $this->state, $this->filesystem, $this->filters); 17 | } 18 | 19 | public function testGetName(): void 20 | { 21 | $this->assertSame('text', $this->merger::getName()); 22 | } 23 | 24 | public function testMergeWithoutContent(): void 25 | { 26 | $this->io->shouldReceive('write') 27 | ->once() 28 | ->with( 29 | 'Error found in williarin/cook-example recipe: "source" or "content" field is required for "text" file type.' 30 | ) 31 | ; 32 | 33 | $this->merger->merge([]); 34 | } 35 | 36 | public function testMergeNewFileWithContentAsString(): void 37 | { 38 | $file = [ 39 | 'destination' => 'var/cache/tests/.env', 40 | 'content' => <<filesystem->shouldReceive('mkdir') 48 | ->once() 49 | ->with('./var/cache/tests', 0755); 50 | 51 | $this->filesystem->shouldReceive('exists') 52 | ->twice() 53 | ->andReturn(false); 54 | 55 | $this->filesystem->shouldReceive('dumpFile') 56 | ->once() 57 | ->with( 58 | './var/cache/tests/.env', 59 | << williarin/cook-example ### 61 | SOME_ENV_VARIABLE='hello' 62 | ANOTHER_ENV_VARIABLE='world' 63 | ###< williarin/cook-example ### 64 | 65 | CODE_SAMPLE 66 | , 67 | ); 68 | 69 | $this->io->shouldReceive('write') 70 | ->once() 71 | ->with('Created file: ./var/cache/tests/.env'); 72 | 73 | $this->merger->merge($file); 74 | } 75 | 76 | public function testMergeNewFileWithContentAsFile(): void 77 | { 78 | $file = [ 79 | 'destination' => 'var/cache/tests/.env', 80 | 'source' => '.env', 81 | ]; 82 | 83 | $this->filesystem->shouldReceive('mkdir') 84 | ->once() 85 | ->with('./var/cache/tests', 0755); 86 | 87 | $this->filesystem->shouldReceive('exists') 88 | ->times(3) 89 | ->andReturn(true, false, false); 90 | 91 | $this->filesystem->shouldReceive('dumpFile') 92 | ->once() 93 | ->with( 94 | './var/cache/tests/.env', 95 | << williarin/cook-example ### 97 | SOME_ENV_VARIABLE='hello' 98 | ANOTHER_ENV_VARIABLE='world' 99 | ###< williarin/cook-example ### 100 | 101 | CODE_SAMPLE 102 | , 103 | ); 104 | 105 | $this->io->shouldReceive('write') 106 | ->once() 107 | ->with('Created file: ./var/cache/tests/.env'); 108 | 109 | $this->merger->merge($file); 110 | } 111 | 112 | public function testMergeExistingFile(): void 113 | { 114 | $file = [ 115 | 'destination' => 'tests/Dummy/before/.env', 116 | 'source' => '.env', 117 | ]; 118 | 119 | $this->filesystem->shouldReceive('mkdir') 120 | ->once() 121 | ->with('./tests/Dummy/before', 0755); 122 | 123 | $this->filesystem->shouldReceive('exists') 124 | ->times(3) 125 | ->andReturn(true); 126 | 127 | $this->filesystem->shouldReceive('dumpFile') 128 | ->once() 129 | ->with( 130 | './tests/Dummy/before/.env', 131 | << williarin/cook-example ### 137 | SOME_ENV_VARIABLE='hello' 138 | ANOTHER_ENV_VARIABLE='world' 139 | ###< williarin/cook-example ### 140 | 141 | CODE_SAMPLE 142 | , 143 | ); 144 | 145 | $this->io->shouldReceive('write') 146 | ->once() 147 | ->with('Updated file: ./tests/Dummy/before/.env'); 148 | 149 | $this->merger->merge($file); 150 | } 151 | 152 | public function testUninstallRecipe(): void 153 | { 154 | $file = [ 155 | 'destination' => 'tests/Dummy/after/.env', 156 | 'source' => '.env', 157 | ]; 158 | 159 | $this->filesystem->shouldReceive('exists') 160 | ->once() 161 | ->andReturn(true); 162 | 163 | $this->filesystem->shouldReceive('dumpFile') 164 | ->once() 165 | ->with( 166 | './tests/Dummy/after/.env', 167 | <<io->shouldReceive('write') 177 | ->once() 178 | ->with('Updated file: ./tests/Dummy/after/.env'); 179 | 180 | $this->merger->uninstall($file); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/Merger/YamlMergerTest.php: -------------------------------------------------------------------------------- 1 | merger = new YamlMerger($this->io, $this->state, $this->filesystem, $this->filters); 17 | } 18 | 19 | public function testGetName(): void 20 | { 21 | $this->assertSame('yaml', $this->merger::getName()); 22 | } 23 | 24 | public function testMergeWithoutContent(): void 25 | { 26 | $this->io->shouldReceive('write') 27 | ->once() 28 | ->with( 29 | 'Error found in williarin/cook-example recipe: "source" or "content" field is required for "yaml" file type.' 30 | ) 31 | ; 32 | 33 | $this->merger->merge([]); 34 | } 35 | 36 | public function testMergeNewFileWithContentAsString(): void 37 | { 38 | $file = [ 39 | 'destination' => 'var/cache/tests/services.yaml', 40 | 'content' => <<filesystem->shouldReceive('mkdir') 51 | ->once() 52 | ->with('./var/cache/tests', 0755); 53 | 54 | $this->filesystem->shouldReceive('exists') 55 | ->twice() 56 | ->andReturn(false); 57 | 58 | $this->filesystem->shouldReceive('dumpFile') 59 | ->once() 60 | ->with( 61 | './var/cache/tests/services.yaml', 62 | << williarin/cook-example ### 65 | locale: fr 66 | ###< williarin/cook-example ### 67 | 68 | services: 69 | ###> williarin/cook-example ### 70 | Some\Service: ~ 71 | ###< williarin/cook-example ### 72 | 73 | CODE_SAMPLE 74 | , 75 | ); 76 | 77 | $this->io->shouldReceive('write') 78 | ->once() 79 | ->with('Created file: ./var/cache/tests/services.yaml'); 80 | 81 | $this->merger->merge($file); 82 | } 83 | 84 | public function testMergeNewFileWithContentAsFile(): void 85 | { 86 | $file = [ 87 | 'destination' => 'var/cache/tests/services.yaml', 88 | 'source' => 'services.yaml', 89 | ]; 90 | 91 | $this->filesystem->shouldReceive('mkdir') 92 | ->once() 93 | ->with('./var/cache/tests', 0755); 94 | 95 | $this->filesystem->shouldReceive('exists') 96 | ->times(3) 97 | ->andReturn(true, false, false); 98 | 99 | $this->filesystem->shouldReceive('dumpFile') 100 | ->once() 101 | ->with( 102 | './var/cache/tests/services.yaml', 103 | << williarin/cook-example ### 106 | locale: fr 107 | ###< williarin/cook-example ### 108 | 109 | services: 110 | ###> williarin/cook-example ### 111 | Some\Service: ~ 112 | ###< williarin/cook-example ### 113 | 114 | CODE_SAMPLE 115 | , 116 | ); 117 | 118 | $this->io->shouldReceive('write') 119 | ->once() 120 | ->with('Created file: ./var/cache/tests/services.yaml'); 121 | 122 | $this->merger->merge($file); 123 | } 124 | 125 | public function testMergeExistingFile(): void 126 | { 127 | $file = [ 128 | 'destination' => 'tests/Dummy/before/services.yaml', 129 | 'source' => 'services.yaml', 130 | ]; 131 | 132 | $this->filesystem->shouldReceive('mkdir') 133 | ->once() 134 | ->with('./tests/Dummy/before', 0755); 135 | 136 | $this->filesystem->shouldReceive('exists') 137 | ->atLeast() 138 | ->once() 139 | ->andReturn(true); 140 | 141 | $this->filesystem->shouldReceive('dumpFile') 142 | ->once() 143 | ->with( 144 | './tests/Dummy/before/services.yaml', 145 | << williarin/cook-example ### 148 | locale: fr 149 | ###< williarin/cook-example ### 150 | some_parameter: true 151 | another_parameter: Hello world 152 | 153 | services: 154 | ###> williarin/cook-example ### 155 | Some\Service: ~ 156 | ###< williarin/cook-example ### 157 | _defaults: 158 | autowire: true 159 | autoconfigure: true 160 | 161 | App\: 162 | resource: '../src/' 163 | exclude: 164 | - '../src/DependencyInjection/' 165 | - '../src/Entity/' 166 | - '../src/Kernel.php' 167 | 168 | CODE_SAMPLE 169 | , 170 | ); 171 | 172 | $this->io->shouldReceive('write') 173 | ->once() 174 | ->with('Updated file: ./tests/Dummy/before/services.yaml'); 175 | 176 | $this->merger->merge($file); 177 | } 178 | 179 | public function testMergeWithBlankLines(): void 180 | { 181 | $file = [ 182 | 'destination' => 'tests/Dummy/before/services.yaml', 183 | 'source' => 'services.yaml', 184 | 'blank_line_after' => ['services'], 185 | ]; 186 | 187 | $this->filesystem->shouldReceive('mkdir') 188 | ->once() 189 | ->with('./tests/Dummy/before', 0755); 190 | 191 | $this->filesystem->shouldReceive('exists') 192 | ->atLeast() 193 | ->once() 194 | ->andReturn(true); 195 | 196 | $this->filesystem->shouldReceive('dumpFile') 197 | ->once() 198 | ->with( 199 | './tests/Dummy/before/services.yaml', 200 | << williarin/cook-example ### 203 | locale: fr 204 | ###< williarin/cook-example ### 205 | some_parameter: true 206 | another_parameter: Hello world 207 | 208 | services: 209 | ###> williarin/cook-example ### 210 | Some\Service: ~ 211 | ###< williarin/cook-example ### 212 | 213 | _defaults: 214 | autowire: true 215 | autoconfigure: true 216 | 217 | App\: 218 | resource: '../src/' 219 | exclude: 220 | - '../src/DependencyInjection/' 221 | - '../src/Entity/' 222 | - '../src/Kernel.php' 223 | 224 | CODE_SAMPLE 225 | , 226 | ); 227 | 228 | $this->io->shouldReceive('write') 229 | ->once() 230 | ->with('Updated file: ./tests/Dummy/before/services.yaml'); 231 | 232 | $this->merger->merge($file); 233 | } 234 | 235 | public function testMergeValidSectionsOnly(): void 236 | { 237 | $file = [ 238 | 'destination' => 'tests/Dummy/before/services.yaml', 239 | 'source' => 'services.yaml', 240 | 'valid_sections' => ['services'], 241 | ]; 242 | 243 | $this->filesystem->shouldReceive('mkdir') 244 | ->once() 245 | ->with('./tests/Dummy/before', 0755); 246 | 247 | $this->filesystem->shouldReceive('exists') 248 | ->atLeast() 249 | ->once() 250 | ->andReturn(true); 251 | 252 | $this->filesystem->shouldReceive('dumpFile') 253 | ->once() 254 | ->with( 255 | './tests/Dummy/before/services.yaml', 256 | << williarin/cook-example ### 263 | Some\Service: ~ 264 | ###< williarin/cook-example ### 265 | _defaults: 266 | autowire: true 267 | autoconfigure: true 268 | 269 | App\: 270 | resource: '../src/' 271 | exclude: 272 | - '../src/DependencyInjection/' 273 | - '../src/Entity/' 274 | - '../src/Kernel.php' 275 | 276 | CODE_SAMPLE 277 | , 278 | ); 279 | 280 | $this->io->shouldReceive('write') 281 | ->once() 282 | ->with('Updated file: ./tests/Dummy/before/services.yaml'); 283 | 284 | $this->merger->merge($file); 285 | } 286 | 287 | public function testUninstallRecipe(): void 288 | { 289 | $file = [ 290 | 'destination' => 'tests/Dummy/after/services.yaml', 291 | 'source' => 'services.yaml', 292 | 'blank_line_after' => ['services'], 293 | ]; 294 | 295 | $this->filesystem->expects('exists') 296 | ->andReturn(true); 297 | 298 | $this->filesystem->expects() 299 | ->dumpFile( 300 | './tests/Dummy/after/services.yaml', 301 | <<io->expects() 323 | ->write('Updated file: ./tests/Dummy/after/services.yaml'); 324 | 325 | $this->merger->uninstall($file); 326 | } 327 | 328 | public function testUninstallRecipeUninstallEmptySection(): void 329 | { 330 | $file = [ 331 | 'destination' => 'tests/Dummy/after/routes.yaml', 332 | 'source' => 'routes.yaml', 333 | 'uninstall_empty_sections' => true, 334 | ]; 335 | 336 | $this->filesystem->expects('exists') 337 | ->atLeast() 338 | ->once() 339 | ->andReturn(true); 340 | 341 | $this->filesystem->expects() 342 | ->dumpFile( 343 | './tests/Dummy/after/routes.yaml', 344 | <<io->expects() 356 | ->write('Updated file: ./tests/Dummy/after/routes.yaml'); 357 | 358 | $this->merger->uninstall($file); 359 | } 360 | 361 | public function testUninstallRecipeWithoutUninstallEmptySection(): void 362 | { 363 | $file = [ 364 | 'destination' => 'tests/Dummy/after/routes.yaml', 365 | 'source' => 'routes.yaml', 366 | ]; 367 | 368 | $this->filesystem->expects('exists') 369 | ->atLeast() 370 | ->once() 371 | ->andReturn(true); 372 | 373 | $this->filesystem->expects() 374 | ->dumpFile( 375 | './tests/Dummy/after/routes.yaml', 376 | <<io->expects() 390 | ->write('Updated file: ./tests/Dummy/after/routes.yaml'); 391 | 392 | $this->merger->uninstall($file); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /tests/OptionsTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getPackage->getExtra'); 18 | $options = new Options($composer); 19 | 20 | $this->assertSame([ 21 | 'bin-dir' => 'bin', 22 | 'config-dir' => 'config', 23 | 'src-dir' => 'src', 24 | 'var-dir' => 'var', 25 | 'public-dir' => 'public', 26 | 'root-dir' => '.', 27 | ], $options->all()); 28 | } 29 | 30 | public function testInitOptionsWithOverridenVariable(): void 31 | { 32 | $composer = Mockery::mock(Composer::class); 33 | $composer->shouldReceive('getPackage->getExtra') 34 | ->andReturn([ 35 | 'bin-dir' => 'custom/bin', 36 | 'config-dir' => 'custom/config', 37 | 'src-dir' => 'custom/src', 38 | 'var-dir' => 'custom/var', 39 | 'public-dir' => 'custom/public', 40 | 'root-dir' => 'custom/', 41 | ]); 42 | $options = new Options($composer); 43 | 44 | $this->assertSame([ 45 | 'bin-dir' => 'custom/bin', 46 | 'config-dir' => 'custom/config', 47 | 'src-dir' => 'custom/src', 48 | 'var-dir' => 'custom/var', 49 | 'public-dir' => 'custom/public', 50 | 'root-dir' => 'custom/', 51 | ], $options->all()); 52 | } 53 | 54 | public function testGetOption(): void 55 | { 56 | $composer = Mockery::mock(Composer::class); 57 | $composer->shouldReceive('getPackage->getExtra'); 58 | $options = new Options($composer); 59 | 60 | $this->assertSame('bin', $options->get('bin-dir')); 61 | $this->assertNull($options->get('non-existent-option')); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/StateTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getPackage->getExtra'); 25 | $options = new Options($composer); 26 | $this->filesystem = Mockery::mock(Filesystem::class); 27 | $this->state = new State($this->filesystem, $options); 28 | } 29 | 30 | public function testGetCurrentPackage(): void 31 | { 32 | $this->assertNull($this->state->getCurrentPackage()); 33 | $this->state->setCurrentPackage('williarin/cook-example'); 34 | $this->assertSame('williarin/cook-example', $this->state->getCurrentPackage()); 35 | } 36 | 37 | public function testGetOverwrite(): void 38 | { 39 | $this->state->setOverwrite(true); 40 | $this->assertTrue($this->state->getOverwrite()); 41 | } 42 | 43 | public function testGetProjectDirectory(): void 44 | { 45 | $this->state->setCurrentPackage('williarin/cook-example'); 46 | $this->assertSame('.', $this->state->getProjectDirectory()); 47 | } 48 | 49 | public function testGetVendorDirectory(): void 50 | { 51 | $this->state->setCurrentPackage('williarin/cook-example'); 52 | $this->assertSame('./vendor', $this->state->getVendorDirectory()); 53 | } 54 | 55 | public function testGetCurrentPackageDirectory(): void 56 | { 57 | $this->state->setCurrentPackage('williarin/cook-example'); 58 | $this->assertSame('./vendor/williarin/cook-example', $this->state->getCurrentPackageDirectory()); 59 | } 60 | 61 | public function testGetCurrentPackageRecipePathnameYaml(): void 62 | { 63 | $this->state->setCurrentPackage('williarin/cook-example'); 64 | 65 | $this->filesystem->shouldReceive('exists') 66 | ->once() 67 | ->andReturn(true); 68 | 69 | $this->assertSame('./vendor/williarin/cook-example/cook.yaml', $this->state->getCurrentPackageRecipePathname()); 70 | } 71 | 72 | public function testGetCurrentPackageRecipePathnameJson(): void 73 | { 74 | $this->state->setCurrentPackage('williarin/cook-example'); 75 | 76 | $this->filesystem->shouldReceive('exists') 77 | ->twice() 78 | ->andReturn(false, true); 79 | 80 | $this->assertSame('./vendor/williarin/cook-example/cook.json', $this->state->getCurrentPackageRecipePathname()); 81 | } 82 | 83 | public function testGetCurrentPackageRecipePathnameNotFound(): void 84 | { 85 | $this->state->setCurrentPackage('williarin/cook-example'); 86 | 87 | $this->filesystem->shouldReceive('exists') 88 | ->twice() 89 | ->andReturn(false); 90 | 91 | $this->assertNull($this->state->getCurrentPackageRecipePathname()); 92 | } 93 | 94 | public function testReplacePathPlaceholders(): void 95 | { 96 | $this->assertSame('config/bundles.php', $this->state->replacePathPlaceholders('%CONFIG_DIR%/bundles.php')); 97 | } 98 | 99 | public function testReplacePathPlaceholdersWithNonExistentPlaceholders(): void 100 | { 101 | $this->assertSame( 102 | '%UNDEFINED_PLACEHOLDER%/bundles.php', 103 | $this->state->replacePathPlaceholders('%UNDEFINED_PLACEHOLDER%/bundles.php') 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |