├── .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 | [](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:**
- `text`
- `php_array`
- `json`
- `yaml`
- `docker_compose`
**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 |