From ae8d80c8585d4f25f3d9fe7fbf457e1eb92aa4f0 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula <42547589+terabytesoftw@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:02:41 -0300 Subject: [PATCH] Fix compatibility composer 2.5. (#1) --- .editorconfig | 21 - .gitattributes | 39 - .github/CODE_OF_CONDUCT.md | 102 - .github/ISSUE_TEMPLATE.md | 14 - .github/PULL_REQUEST_TEMPLATE.md | 6 - .github/{workflows => }/build.yml | 66 +- .github/dependabot.yml | 14 - .github/workflows/dependency-check.yml | 33 - .github/workflows/ecs.yml | 34 - .github/workflows/mutation.yml | 31 - .github/workflows/static.yml | 31 - .gitignore | 26 +- .php-cs-fixer.dist.php | 28 + .scrutinizer.yml | 13 + .styleci.yml | 86 - .travis.yml | 104 + CODE_OF_CONDUCT.md | 73 + CONTRIBUTING.md | 17 + LICENSE | 12 +- README.md | 186 +- Tests/Asset/AbstractAssetManagerTest.php | 374 ++ Tests/Asset/AssetManagerFinderTest.php | 131 + Tests/Asset/AssetPackageTest.php | 281 ++ Tests/Asset/NpmAssetManagerTest.php | 72 + Tests/Asset/PnpmAssetManagerTest.php | 72 + Tests/Asset/YarnAssetManagerTest.php | 95 + Tests/Asset/YarnNextAssetManagerTest.php | 93 + Tests/Config/ConfigTest.php | 230 + Tests/Converter/SemverConverterTest.php | 107 + Tests/Event/AbstractSolveEventTest.php | 66 + Tests/Event/GetAssetsEventTest.php | 72 + Tests/Event/PostSolveEventTest.php | 40 + Tests/Event/PreSolveEventTest.php | 34 + Tests/Fallback/AssetFallbackTest.php | 177 + Tests/Fallback/ComposerFallbackTest.php | 269 ++ .../Util/AbstractProcessExecutorMock.php | 144 + Tests/Fixtures/Util/ProcessExecutorMock.php | 44 + .../Fixtures/Util/ProcessExecutorMockTest.php | 75 + Tests/Fixtures/package/global/composer.json | 8 + Tests/Fixtures/package/global/config.json | 8 + Tests/FoxyTest.php | 191 + Tests/Json/JsonFileTest.php | 176 + Tests/Json/JsonFormatterTest.php | 77 + Tests/Solver/SolverTest.php | 322 ++ Tests/Util/AssetUtilTest.php | 409 ++ Tests/Util/ComposerUtilTest.php | 58 + Tests/Util/ConsoleUtilTest.php | 96 + Tests/Util/PackageUtilTest.php | 107 + Tests/bootstrap.php | 16 + changelog.md | 2 - composer.json | 58 +- composer.lock | 3893 +++++++++++++++++ docs/testing.md | 35 - ecs.php | 47 - infection.json.dist | 16 - phpunit.xml.dist | 44 +- psalm.xml | 14 - src/Asset/AbstractAssetManager.php | 297 ++ src/Asset/AssetManagerFinder.php | 95 + src/Asset/AssetManagerInterface.php | 123 + src/Asset/AssetPackage.php | 169 + src/Asset/AssetPackageInterface.php | 68 + src/Asset/NpmManager.php | 70 + src/Asset/PnpmManager.php | 68 + src/Asset/YarnManager.php | 107 + src/Config/Config.php | 247 ++ src/Config/ConfigBuilder.php | 104 + src/Converter/SemverConverter.php | 35 + src/Converter/SemverUtil.php | 187 + src/Converter/VersionConverterInterface.php | 29 + src/Event/AbstractSolveEvent.php | 68 + src/Event/GetAssetsEvent.php | 87 + src/Event/PostSolveEvent.php | 52 + src/Event/PreSolveEvent.php | 34 + src/Example.php | 13 - src/Exception/ExceptionInterface.php | 21 + src/Exception/RuntimeException.php | 21 + src/Fallback/AssetFallback.php | 86 + src/Fallback/ComposerFallback.php | 231 + src/Fallback/FallbackInterface.php | 32 + src/Foxy.php | 232 + src/FoxyEvents.php | 43 + src/Json/JsonFile.php | 114 + src/Json/JsonFormatter.php | 104 + src/Resources/doc/config.md | 368 ++ src/Resources/doc/events.md | 28 + src/Resources/doc/faqs.md | 124 + src/Resources/doc/index.md | 47 + src/Resources/doc/usage.md | 68 + src/Solver/Solver.php | 167 + src/Solver/SolverInterface.php | 40 + src/Util/AssetUtil.php | 206 + src/Util/ComposerUtil.php | 53 + src/Util/ConsoleUtil.php | 86 + src/Util/LockerUtil.php | 43 + src/Util/PackageUtil.php | 88 + tests/ExampleTest.php | 18 - 97 files changed, 12224 insertions(+), 738 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .gitattributes delete mode 100644 .github/CODE_OF_CONDUCT.md delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md rename .github/{workflows => }/build.yml (91%) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/dependency-check.yml delete mode 100644 .github/workflows/ecs.yml delete mode 100644 .github/workflows/mutation.yml delete mode 100644 .github/workflows/static.yml create mode 100644 .php-cs-fixer.dist.php create mode 100644 .scrutinizer.yml delete mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Tests/Asset/AbstractAssetManagerTest.php create mode 100644 Tests/Asset/AssetManagerFinderTest.php create mode 100644 Tests/Asset/AssetPackageTest.php create mode 100644 Tests/Asset/NpmAssetManagerTest.php create mode 100644 Tests/Asset/PnpmAssetManagerTest.php create mode 100644 Tests/Asset/YarnAssetManagerTest.php create mode 100644 Tests/Asset/YarnNextAssetManagerTest.php create mode 100644 Tests/Config/ConfigTest.php create mode 100644 Tests/Converter/SemverConverterTest.php create mode 100644 Tests/Event/AbstractSolveEventTest.php create mode 100644 Tests/Event/GetAssetsEventTest.php create mode 100644 Tests/Event/PostSolveEventTest.php create mode 100644 Tests/Event/PreSolveEventTest.php create mode 100644 Tests/Fallback/AssetFallbackTest.php create mode 100644 Tests/Fallback/ComposerFallbackTest.php create mode 100644 Tests/Fixtures/Util/AbstractProcessExecutorMock.php create mode 100644 Tests/Fixtures/Util/ProcessExecutorMock.php create mode 100644 Tests/Fixtures/Util/ProcessExecutorMockTest.php create mode 100644 Tests/Fixtures/package/global/composer.json create mode 100644 Tests/Fixtures/package/global/config.json create mode 100644 Tests/FoxyTest.php create mode 100644 Tests/Json/JsonFileTest.php create mode 100644 Tests/Json/JsonFormatterTest.php create mode 100644 Tests/Solver/SolverTest.php create mode 100644 Tests/Util/AssetUtilTest.php create mode 100644 Tests/Util/ComposerUtilTest.php create mode 100644 Tests/Util/ConsoleUtilTest.php create mode 100644 Tests/Util/PackageUtilTest.php create mode 100644 Tests/bootstrap.php delete mode 100644 changelog.md create mode 100644 composer.lock delete mode 100644 docs/testing.md delete mode 100644 ecs.php delete mode 100644 infection.json.dist delete mode 100644 psalm.xml create mode 100644 src/Asset/AbstractAssetManager.php create mode 100644 src/Asset/AssetManagerFinder.php create mode 100644 src/Asset/AssetManagerInterface.php create mode 100644 src/Asset/AssetPackage.php create mode 100644 src/Asset/AssetPackageInterface.php create mode 100644 src/Asset/NpmManager.php create mode 100644 src/Asset/PnpmManager.php create mode 100644 src/Asset/YarnManager.php create mode 100644 src/Config/Config.php create mode 100644 src/Config/ConfigBuilder.php create mode 100644 src/Converter/SemverConverter.php create mode 100644 src/Converter/SemverUtil.php create mode 100644 src/Converter/VersionConverterInterface.php create mode 100644 src/Event/AbstractSolveEvent.php create mode 100644 src/Event/GetAssetsEvent.php create mode 100644 src/Event/PostSolveEvent.php create mode 100644 src/Event/PreSolveEvent.php delete mode 100644 src/Example.php create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 src/Fallback/AssetFallback.php create mode 100644 src/Fallback/ComposerFallback.php create mode 100644 src/Fallback/FallbackInterface.php create mode 100644 src/Foxy.php create mode 100644 src/FoxyEvents.php create mode 100644 src/Json/JsonFile.php create mode 100644 src/Json/JsonFormatter.php create mode 100644 src/Resources/doc/config.md create mode 100644 src/Resources/doc/events.md create mode 100644 src/Resources/doc/faqs.md create mode 100644 src/Resources/doc/index.md create mode 100644 src/Resources/doc/usage.md create mode 100644 src/Solver/Solver.php create mode 100644 src/Solver/SolverInterface.php create mode 100644 src/Util/AssetUtil.php create mode 100644 src/Util/ComposerUtil.php create mode 100644 src/Util/ConsoleUtil.php create mode 100644 src/Util/LockerUtil.php create mode 100644 src/Util/PackageUtil.php delete mode 100644 tests/ExampleTest.php diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 8802775..0000000 --- a/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# editorconfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true - -[*.php] -ij_php_space_before_short_closure_left_parenthesis = false -ij_php_space_after_type_cast = true - -[*.md] -trim_trailing_whitespace = false - -[*.yml] -indent_size = 2 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 65a6a4e..0000000 --- a/.gitattributes +++ /dev/null @@ -1,39 +0,0 @@ -# Autodetect text files -* text=auto eol=lf - -# ...Unless the name matches the following overriding patterns - -# Definitively text files -*.php text -*.css text -*.js text -*.txt text -*.md text -*.xml text -*.json text -*.bat text -*.sql text -*.yml text - -# Ensure those won't be messed up with -*.png binary -*.jpg binary -*.gif binary -*.ttf binary - -# Avoid merge conflicts in CHANGELOG -# https://about.gitlab.com/2015/02/10/gitlab-reduced-merge-conflicts-by-90-percent-with-changelog-placeholders/ -/CHANGELOG.md merge=union - -# Exclude files from the archive -/.gitattributes export-ignore -/.github export-ignore -/.gitignore export-ignore -/.styleci.yml export-ignore -/codeception.yml export-ignore -/composer-require-checker.json export-ignore -/docs export-ignore -/phpunit.xml.dist export-ignore -/psalm.xml export-ignore -/rector.php export-ignore -/tests export-ignore diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1946f00..0000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,102 +0,0 @@ -# Code of Conduct - -## Our Pledge - -As contributors and maintainers of this project, and in order to keep community open and welcoming, we ask to -respect all community members. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Enforcement Responsibilities - -Core team members are responsible for clarifying and enforcing our standards of acceptable behavior and will take -appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Core team members have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, -issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for -moderation decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing -the community in public spaces. Examples of representing a project or community include using an official e-mail -address, posting via an official social media account, within project GitHub, official forum or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting core team members. All -complaints will be reviewed and investigated promptly and fairly. - -All core team members are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Core team members will follow these Community Impact Guidelines in determining the consequences for any action they -deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in -the community. - -**Consequence**: A private, written warning from core team members, providing clarity around the nature of the violation -and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series of actions. - -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including -unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding -interactions in community spaces as well as external channels like social media. Violating these terms may lead to -a temporary or permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified -period of time. No public or private interaction with the people involved, including unsolicited interaction with those -enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate -behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. - -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. - -[homepage]: https://www.contributor-covenant.org -[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html -[Mozilla CoC]: https://github.com/mozilla/diversity -[FAQ]: https://www.contributor-covenant.org/faq -[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 7e17783..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -### What steps will reproduce the problem? - -### What is the expected result? - -### What do you get instead? - - -### Additional info - -| Q | A -| ---------------- | --- -| Version | 1.0.? -| PHP version | -| Operating system | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index cecccf6..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,6 +0,0 @@ -| Q | A -| ------------- | --- -| Is bugfix? | ✔️/❌ -| New feature? | ✔️/❌ -| Breaks BC? | ✔️/❌ -| Fixed issues | diff --git a/.github/workflows/build.yml b/.github/build.yml similarity index 91% rename from .github/workflows/build.yml rename to .github/build.yml index d286d28..7185041 100644 --- a/.github/workflows/build.yml +++ b/.github/build.yml @@ -1,33 +1,33 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'psalm.xml' - - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'psalm.xml' - -name: build - -jobs: - phpunit: - uses: php-forge/actions/.github/workflows/phpunit.yml@main - secrets: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - with: - os: >- - ['ubuntu-latest', 'windows-latest'] - php: >- - ['8.1', '8.2', '8.3'] +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - 'infection.json.dist' + - 'psalm.xml' + + push: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - 'infection.json.dist' + - 'psalm.xml' + +name: build + +jobs: + phpunit: + uses: php-forge/actions/.github/workflows/phpunit.yml@main + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + os: >- + ['ubuntu-latest', 'windows-latest'] + php: >- + ['8.1', '8.2', '8.3'] \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index c8150bf..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: 2 -updates: - # Maintain dependencies for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - # Maintain dependencies for Composer - - package-ecosystem: "composer" - directory: "/" - schedule: - interval: "daily" - versioning-strategy: increase-if-necessary diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml deleted file mode 100644 index dde6447..0000000 --- a/.github/workflows/dependency-check.yml +++ /dev/null @@ -1,33 +0,0 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - - 'psalm.xml' - - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - - 'psalm.xml' - -name: dependency-check - -jobs: - composer-require-checker: - uses: php-forge/actions/.github/workflows/composer-require-checker.yml@main - with: - os: >- - ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/.github/workflows/ecs.yml b/.github/workflows/ecs.yml deleted file mode 100644 index dff0db6..0000000 --- a/.github/workflows/ecs.yml +++ /dev/null @@ -1,34 +0,0 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - - push: - branches: ['main'] - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - -name: ecs - -jobs: - easy-coding-standard: - uses: php-forge/actions/.github/workflows/ecs.yml@main - secrets: - AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} - with: - os: >- - ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml deleted file mode 100644 index ef6929b..0000000 --- a/.github/workflows/mutation.yml +++ /dev/null @@ -1,31 +0,0 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'psalm.xml' - - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'psalm.xml' - -name: mutation test - -jobs: - mutation: - uses: php-forge/actions/.github/workflows/roave-infection.yml@main - secrets: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - with: - os: >- - ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml deleted file mode 100644 index 3654420..0000000 --- a/.github/workflows/static.yml +++ /dev/null @@ -1,31 +0,0 @@ -on: - pull_request: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - - push: - paths-ignore: - - 'docs/**' - - 'README.md' - - 'CHANGELOG.md' - - '.gitignore' - - '.gitattributes' - - 'infection.json.dist' - - 'phpunit.xml.dist' - -name: static analysis - -jobs: - psalm: - uses: php-forge/actions/.github/workflows/psalm.yml@main - with: - os: >- - ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/.gitignore b/.gitignore index 33918c0..08dc4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,4 @@ -#code coverage -/code_coverage - -# composer vendor dir -/vendor -/composer.lock - -#node_modules -/node_modules - -# phpstorm project files -.idea - -# phpunit -phpunit.phar +vendor/ +phpunit.xml +.php-cs-fixer.cache .phpunit.result.cache -.phpunit.cache -phpunit.xlm - -#yii3 config packages -/config/packages - -# windows thumbnail cache -Thumbs.db diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f8bef9d --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,28 @@ +setRules(array( + '@PhpCsFixer' => true, + '@PhpCsFixer:risky' => true, + '@PHPUnit60Migration:risky' => true, + 'array_syntax' => array('syntax' => 'long'), + 'class_definition' => array('single_line' => false), + 'declare_strict_types' => false, + 'ordered_imports' => true, + 'php_unit_expectation' => false, + 'php_unit_no_expectation_annotation' => false, + 'php_unit_strict' => false, + 'php_unit_test_class_requires_covers' => false, + 'self_accessor' => false, + 'single_line_comment_style' => false, + 'visibility_required' => array('elements' => array('property', 'method')), + )) + ->setRiskyAllowed(true) + ->setFinder( + ($finder) + ->in(__DIR__) + ) + ->setCacheFile('.php-cs-fixer.cache') +; diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..861e120 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,13 @@ +tools: + external_code_coverage: false + php_code_sniffer: true + php_sim: true + php_changetracking: true + php_cs_fixer: true + php_mess_detector: true + php_pdepend: true + php_analyzer: true + sensiolabs_security_checker: true +filter: + excluded_paths: + - Tests/* diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index 5b7ddb2..0000000 --- a/.styleci.yml +++ /dev/null @@ -1,86 +0,0 @@ -preset: psr12 -risky: true - -version: 8.1 - -finder: - exclude: - - docs - - vendor - -enabled: - - alpha_ordered_traits - - array_indentation - - array_push - - combine_consecutive_issets - - combine_consecutive_unsets - - combine_nested_dirname - - declare_strict_types - - dir_constant - - fully_qualified_strict_types - - function_to_constant - - hash_to_slash_comment - - is_null - - logical_operators - - magic_constant_casing - - magic_method_casing - - method_separation - - modernize_types_casting - - native_function_casing - - native_function_type_declaration_casing - - no_alias_functions - - no_empty_comment - - no_empty_phpdoc - - no_empty_statement - - no_extra_block_blank_lines - - no_short_bool_cast - - no_superfluous_elseif - - no_unneeded_control_parentheses - - no_unneeded_curly_braces - - no_unneeded_final_method - - no_unset_cast - - no_unused_imports - - no_unused_lambda_imports - - no_useless_else - - no_useless_return - - normalize_index_brace - - php_unit_dedicate_assert - - php_unit_dedicate_assert_internal_type - - php_unit_expectation - - php_unit_mock - - php_unit_mock_short_will_return - - php_unit_namespaced - - php_unit_no_expectation_annotation - - phpdoc_no_empty_return - - phpdoc_no_useless_inheritdoc - - phpdoc_order - - phpdoc_property - - phpdoc_scalar - - phpdoc_separation - - phpdoc_singular_inheritdoc - - phpdoc_trim - - phpdoc_trim_consecutive_blank_line_separation - - phpdoc_type_to_var - - phpdoc_types - - phpdoc_types_order - - print_to_echo - - regular_callable_call - - return_assignment - - self_accessor - - self_static_accessor - - set_type_to_cast - - short_array_syntax - - short_list_syntax - - simplified_if_return - - single_quote - - standardize_not_equals - - ternary_to_null_coalescing - - trailing_comma_in_multiline_array - - unalign_double_arrow - - unalign_equals - - empty_loop_body_braces - - integer_literal_case - - union_type_without_spaces - -disabled: - - function_declaration diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..eb5fd15 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,104 @@ +os: linux +dist: xenial + +language: php + +cache: + directories: + - $HOME/.composer/cache/files + +jobs: + include: + - php: 7.4 + env: COMPOSER_VERSION="1.10.*" + - php: 7.4 + env: COMPOSER_VERSION="2.0.*" + - php: 7.4 + env: COMPOSER_VERSION="2.1.*" + - php: 7.4 + env: COMPOSER_VERSION="2.2.*" + - php: 7.4 + env: COMPOSER_VERSION="2.3.*" + - php: 7.4 + env: COMPOSER_VERSION="2.4.*" + - php: 7.4 + env: COMPOSER_VERSION="2.5.*" + - php: 8.0 + env: COMPOSER_VERSION="1.10.*" + - php: 8.0 + env: COMPOSER_VERSION="2.0.*" + - php: 8.0 + env: COMPOSER_VERSION="2.1.*" + - php: 8.0 + env: COMPOSER_VERSION="2.2.*" + - php: 8.0 + env: COMPOSER_VERSION="2.3.*" + - php: 8.0 + env: COMPOSER_VERSION="2.4.*" + - php: 8.0 + env: COMPOSER_VERSION="2.5.*" + - php: 8.1 + env: COMPOSER_VERSION="1.10.*" + - php: 8.1 + env: COMPOSER_VERSION="2.0.*" + - php: 8.1 + env: COMPOSER_VERSION="2.1.*" + - php: 8.1 + env: COMPOSER_VERSION="2.2.*" + - php: 8.1 + env: COMPOSER_VERSION="2.3.*" + - php: 8.1 + env: COMPOSER_VERSION="2.4.*" + - php: 8.1 + env: COMPOSER_VERSION="2.5.*" + - php: 8.2 + env: COMPOSER_VERSION="1.10.*" + - php: 8.2 + env: COMPOSER_VERSION="2.0.*" + - php: 8.2 + env: COMPOSER_VERSION="2.1.*" + - php: 8.2 + env: COMPOSER_VERSION="2.2.*" + - php: 8.2 + env: COMPOSER_VERSION="2.3.*" + - php: 8.2 + env: COMPOSER_VERSION="2.4.*" + - php: 8.2 + env: COMPOSER_VERSION="2.5.*" + - php: nightly + env: COMPOSER_VERSION="1.10.*" + - php: nightly + env: COMPOSER_VERSION="2.0.*" + - php: nightly + env: COMPOSER_VERSION="2.1.*" + - php: nightly + env: COMPOSER_VERSION="2.2.*" + - php: nightly + env: COMPOSER_VERSION="2.3.*" + - php: nightly + env: COMPOSER_VERSION="2.4.*" + - php: nightly + env: COMPOSER_VERSION="2.5.*" + + allow_failures: + - php: nightly + + fast_finish: true + +before_script: + - | + if [ "$COMPOSER_VERSION" == "" ]; then composer install; fi; + if [ "$COMPOSER_VERSION" != "" ]; then composer require --dev --with-all-dependencies "composer/composer:${COMPOSER_VERSION}"; fi; + composer require --dev --with-all-dependencies phpunit/phpunit:"^9.5.0" php-coveralls/php-coveralls:"^2.4.0" + mkdir -p ./build/logs + +script: + - | + vendor/bin/phpunit -v --coverage-clover ./build/logs/clover.xml + +after_success: + - | + if [ "$TRAVIS_PHP_VERSION" != nightly ]; then php vendor/bin/php-coveralls -v; fi + +notifications: + email: false diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8fc8b54 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at francois.pluchino@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct/ + +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..aacc56c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your +interactions with the project. + +## Pull Request Process + +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `master` +2. If you've added code that should be tested, add tests +3. If you've changed APIs, update the documentation +4. Ensure the test suite passes with PHPUnit: `phpunit` +5. Make sure your code style with [PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer): `php-cs-fixer fix` diff --git a/LICENSE b/LICENSE index fab6bc9..53e62ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,11 @@ -MIT License - -Copyright (c) 2023 yii-tools +Copyright (c) 2017-2021 François Pluchino Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. @@ -17,5 +15,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index ff1f785..a6c217b 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,119 @@ -

- - - -

Template.

-
-

- -

- - PHPUnit - - - Codecov - - - Infection - - - Psalm - - - Psalm Coverage - - - Style ci - -

- -## Installation - -The preferred way to install this extension is through [composer](https://getcomposer.org/download/). - -Either run - -```shell -composer require --prefer-dist package -``` - -or add - -```json -"package": "version" -``` - -to the require-dev section of your `composer.json` file. - -## Usage - -[Check the documentation docs](/docs/README.md) to learn about usage. - -## Support versions - -[![PHP81](https://img.shields.io/badge/PHP-%3E%3D8.1-787CB5)](https://www.php.net/releases/8.1/en.php) -[![Yii30](https://img.shields.io/badge/Yii%20version-3.0-blue)](https://yiiframework.com) - -## Testing - -[Check the documentation testing](/docs/testing.md) to learn about testing. - -## Our social networks - -[![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/Terabytesoftw) - -## License - -The MIT License. Please see [License File](LICENSE.md) for more information. +

+ Foxy +

+ +[![Latest Version](https://img.shields.io/packagist/v/foxy/foxy.svg)](https://packagist.org/packages/foxy/foxy) +[![Build Status](https://img.shields.io/travis/com/fxpio/foxy.svg)](https://travis-ci.com/github/fxpio/foxy) +[![Coverage Status](https://img.shields.io/coveralls/github/fxpio/foxy.svg)](https://coveralls.io/r/fxpio/foxy) +[![Packagist Downloads](https://img.shields.io/packagist/dt/foxy/foxy.svg)](https://packagist.org/packages/foxy/foxy/stats) + +Foxy is a Composer plugin to automate the validation, installation, updating and removing of PHP libraries +asset dependencies (javaScript, stylesheets, etc.) defined in the NPM `package.json` file of the project and +PHP libraries during the execution of Composer. It handles restoring the project state in case +[NPM](https://www.npmjs.com) or [Yarn](https://yarnpkg.com) or [pnpm](https://pnpm.io) terminates with an error. All features and tools +are available: [Npmrc](https://docs.npmjs.com/files/npmrc), [Yarnrc](https://yarnpkg.com/en/docs/yarnrc), +[Webpack](https://webpack.js.org), [Gulp](https://gulpjs.com), [Grunt](https://gruntjs.com), +[Babel](https://babeljs.io), [TypeScript](https://www.typescriptlang.org), [Scss/Sass](http://sass-lang.com), +[Less](http://lesscss.org), etc. + +It is certain that each language has its own dependency management system, and that it is highly recommended to use +each package manager. NPM, Yarn or pnpm works very well when the asset dependencies are managed only in the PHP project, +but when you create PHP libraries that using assets, there is no way to automatically add asset dependencies, +and most importantly, no validation of versions can be done automatically. You must tell the developers +the list of asset dependencies that using by your PHP library, and you must ask him to add manually the asset +dependencies to its asset manager of his project. + +However, another solution exist - what many projects propose - you must add the assets in the folder of the +PHP library (like `/assets`, `/Resources/public`). Of course, with this method, the code is duplicated, it +pollutes the source code of the PHP library, no version management/validation is possible, and it is even +less possible, to use all tools such as Babel, Scss, Less, etc ... + +Foxy focuses solely on automation of the validation, addition, updating and deleting of the dependencies in +the definition file of the asset package, while restoring the project state, as well as PHP dependencies if +NPM, Yarn or pnpm terminates with an error. + +#### It is Fast + +Foxy retrieves the list of all Composer dependencies to inject the asset dependencies in the file `package.json`, +and leaves the execution of the analysis, validation and downloading of the libraries to NPM, Yarn or pnpm. Therefore, +no VCS Repository of Composer is used for analyzing the asset dependencies, and you keep the performance +of native package manager used. + +#### It is Reliable + +Foxy creates mock packages of the PHP libraries containing only the asset dependencies definition file +in a local directory, and associates these packages in the asset dependencies definition file of the +project. Given that Foxy does not manipulate any asset dependencies, and let alone the version constraints, +this allows NPM, Yarn or pnpm to solve the asset dependencies without any intermediary. Moreover, the entire +validation with the lock file and installation process is left to NPM, Yarn or pnpm. + +#### It is Secure + +Foxy restores the Composer lock file with all its PHP dependencies, as well as the asset dependencies +definition file, in the previous state if NPM, Yarn or pnpm ends with an error. + +Features +-------- + +- Compatible with [Symfony Webpack Encore](http://symfony.com/doc/current/frontend.html) + and [Laravel Mix](https://laravel.com/docs/master/mix) +- Works with Node.js and NPM, Yarn or pnpm +- Works with the asset dependencies defined in the `package.json` file for projects and PHP libraries +- Works with the installation in the dependencies of the project or libraries (not in global mode) +- Works with public or private repositories +- Works with all features of Composer, NPM, Yarn and pnpm +- Retains the native performance of Composer, NPM, Yarn and pnpm +- Restores previous versions of PHP dependencies and the lock file if NPM, Yarn or pnpm terminates with an error +- Validates the NPM, Yarn or pnpm version with a version range +- Configuration of the plugin per project, globally or with the environment variables: + - Enable/disable the plugin + - Choose the asset manager: NPM, Yarn or pnpm (`npm` is used by default) + - Lock the version of the asset manager with the Composer version range + - Define the custom path of binary of the asset manager + - Enable/disable the fallback for the asset package file of the project + - Enable/disable the fallback for the Composer lock file and its dependencies + - Enable/disable the running of asset manager to keep only the manipulation of the asset package file + - Override the install command options for the asset manager + - Override the update command options for the asset manager + - Define the custom path of the mock package of PHP library + - Enable/disable manually the asset packages for the PHP libraries +- Works with the Composer commands: + - `install` + - `update` + - `require` + - `remove` + +Documentation +------------- + +- [Guide](Resources/doc/index.md) +- [FAQs](Resources/doc/faqs.md) +- [Release Notes](https://github.com/fxpio/foxy/releases) + +Installation +------------ + +Installation instructions are located in [the guide](Resources/doc/index.md). + +License +------- + +Foxy is released under the MIT license. See the complete license in: + +[LICENSE](LICENSE) + +About +----- + +Foxy is a [François Pluchino](https://github.com/francoispluchino) initiative. +See also the list of [contributors](https://github.com/fxpio/foxy/contributors). + +Reporting an issue or a feature request +--------------------------------------- + +Issues and feature requests are tracked in the [Github issue tracker](https://github.com/fxpio/foxy/issues). + +Acknowledgments +--------------- + +Thanks to [Tobias Munk](https://github.com/schmunk42) to have suggesting this name diff --git a/Tests/Asset/AbstractAssetManagerTest.php b/Tests/Asset/AbstractAssetManagerTest.php new file mode 100644 index 0000000..2c601a7 --- /dev/null +++ b/Tests/Asset/AbstractAssetManagerTest.php @@ -0,0 +1,374 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; +use Composer\Package\RootPackageInterface; +use Composer\Util\Filesystem; +use Foxy\Asset\AbstractAssetManager; +use Foxy\Asset\AssetManagerInterface; +use Foxy\Config\Config; +use Foxy\Fallback\FallbackInterface; +use Foxy\Tests\Fixtures\Util\ProcessExecutorMock; + +/** + * Abstract class for asset manager tests. + * + * @author François Pluchino + */ +abstract class AbstractAssetManagerTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Config + */ + protected $config; + + /** + * @var IOInterface + */ + protected $io; + + /** + * @var ProcessExecutorMock + */ + protected $executor; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fs; + + /** + * @var \Symfony\Component\Filesystem\Filesystem + */ + protected $sfs; + + /** + * @var FallbackInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fallback; + + /** + * @var AssetManagerInterface + */ + protected $manager; + + /** + * @var string + */ + protected $oldCwd; + + /** + * @var string + */ + protected $cwd; + + protected function setUp(): void + { + parent::setUp(); + + $this->config = new Config(array()); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->executor = new ProcessExecutorMock($this->io); + $this->fs = $this->getMockBuilder('Composer\Util\Filesystem')->disableOriginalConstructor()->getMock(); + $this->sfs = new \Symfony\Component\Filesystem\Filesystem(); + $this->fallback = $this->getMockBuilder('Foxy\Fallback\FallbackInterface')->getMock(); + $this->manager = $this->getManager(); + $this->oldCwd = getcwd(); + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_asset_manager_test_', true); + $this->sfs->mkdir($this->cwd); + chdir($this->cwd); + } + + protected function tearDown(): void + { + parent::tearDown(); + + chdir($this->oldCwd); + $this->sfs->remove($this->cwd); + $this->config = null; + $this->io = null; + $this->executor = null; + $this->fs = null; + $this->sfs = null; + $this->fallback = null; + $this->manager = null; + $this->oldCwd = null; + $this->cwd = null; + } + + public function testGetName() + { + static::assertSame($this->getValidName(), $this->manager->getName()); + } + + public function testGetLockPackageName() + { + static::assertSame($this->getValidLockPackageName(), $this->manager->getLockPackageName()); + } + + public function testGetPackageName() + { + static::assertSame('package.json', $this->manager->getPackageName()); + } + + public function testHasLockFile() + { + static::assertFalse($this->manager->hasLockFile()); + } + + public function testIsInstalled() + { + static::assertFalse($this->manager->isInstalled()); + } + + public function testIsUpdatable() + { + static::assertFalse($this->manager->isUpdatable()); + } + + public function testSetUpdatable() + { + $res = $this->manager->setUpdatable(false); + static::assertInstanceOf('Foxy\Asset\AssetManagerInterface', $res); + } + + public function testValidateWithoutInstalledManager() + { + static::expectException('Foxy\Exception\RuntimeException'); + static::expectExceptionMessageMatches('/The binary of "(\w+)" must be installed/'); + + $this->manager->validate(); + } + + public function testValidateWithInstalledManagerAndWithoutValidVersion() + { + static::expectException('Foxy\Exception\RuntimeException'); + static::expectExceptionMessageMatches('/The installed (\w+) version "42.0.0" doesn\'t match with the constraint version ">=50.0"/'); + + $this->config = new Config(array(), array( + 'manager-version' => '>=50.0', + )); + $this->manager = $this->getManager(); + + $this->executor->addExpectedValues(0, '42.0.0'); + + $this->manager->validate(); + } + + public function testValidateWithInstalledManagerAndWithValidVersion() + { + $this->config = new Config(array(), array( + 'manager-version' => '>=41.0', + )); + $this->manager = $this->getManager(); + + $this->executor->addExpectedValues(0, '42.0.0'); + + $this->manager->validate(); + static::assertSame('>=41.0', $this->config->get('manager-version')); + } + + public function testValidateWithInstalledManagerAndWithoutValidationVersion() + { + $this->executor->addExpectedValues(0, '42.0.0'); + + $this->manager->validate(); + static::assertNull($this->config->get('manager-version')); + } + + public function testAddDependenciesForInstallCommand() + { + $expectedPackage = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@composer-asset/new--dependency' => 'file:./path/new/dependency', + ), + ); + $allDependencies = array( + '@composer-asset/foo--bar' => 'path/foo/bar/package.json', + '@composer-asset/new--dependency' => 'path/new/dependency/package.json', + ); + + /** @var \PHPUnit_Framework_MockObject_MockObject|RootPackageInterface $rootPackage */ + $rootPackage = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + $rootPackage->expects(static::any()) + ->method('getLicense') + ->willReturn(array()) + ; + + static::assertFalse($this->manager->isInstalled()); + static::assertFalse($this->manager->isUpdatable()); + + $assetPackage = $this->manager->addDependencies($rootPackage, $allDependencies); + static::assertInstanceOf('Foxy\Asset\AssetPackageInterface', $assetPackage); + + static::assertEquals($expectedPackage, $assetPackage->getPackage()); + } + + public function testAddDependenciesForUpdateCommand() + { + $this->actionForTestAddDependenciesForUpdateCommand(); + + $expectedPackage = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@composer-asset/new--dependency' => 'file:./path/new/dependency', + ), + ); + $package = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@composer-asset/baz--bar' => 'file:./path/baz/bar', + ), + ); + $allDependencies = array( + '@composer-asset/foo--bar' => 'path/foo/bar/package.json', + '@composer-asset/new--dependency' => 'path/new/dependency/package.json', + ); + $jsonFile = new JsonFile($this->cwd.'/package.json'); + + /** @var \PHPUnit_Framework_MockObject_MockObject|RootPackageInterface $rootPackage */ + $rootPackage = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + $rootPackage->expects(static::any()) + ->method('getLicense') + ->willReturn(array()) + ; + $nodeModulePath = $this->cwd.ltrim(AbstractAssetManager::NODE_MODULES_PATH, '.'); + + $jsonFile->write($package); + static::assertFileExists($jsonFile->getPath()); + $this->sfs->mkdir($nodeModulePath); + static::assertFileExists($nodeModulePath); + $lockFilePath = $this->cwd.\DIRECTORY_SEPARATOR.$this->manager->getLockPackageName(); + file_put_contents($lockFilePath, '{}'); + static::assertFileExists($lockFilePath); + static::assertTrue($this->manager->isInstalled()); + static::assertTrue($this->manager->isUpdatable()); + + $assetPackage = $this->manager->addDependencies($rootPackage, $allDependencies); + static::assertInstanceOf('Foxy\Asset\AssetPackageInterface', $assetPackage); + + static::assertEquals($expectedPackage, $assetPackage->getPackage()); + } + + public function testRunWithDisableOption() + { + $this->config = new Config(array(), array( + 'run-asset-manager' => false, + )); + + static::assertSame(0, $this->getManager()->run()); + } + + public function getRunData() + { + return array( + array(0, 'install'), + array(0, 'update'), + array(1, 'install'), + array(1, 'update'), + ); + } + + /** + * @dataProvider getRunData + * + * @param int $expectedRes + * @param string $action + */ + public function testRunForInstallCommand($expectedRes, $action) + { + $this->actionForTestRunForInstallCommand($action); + + $this->config = new Config(array(), array( + 'run-asset-manager' => true, + 'fallback-asset' => true, + )); + $this->manager = $this->getManager(); + + if ('install' === $action) { + $expectedCommand = $this->getValidInstallCommand(); + } else { + $expectedCommand = $this->getValidUpdateCommand(); + file_put_contents($this->cwd.\DIRECTORY_SEPARATOR.$this->manager->getPackageName(), '{}'); + $nodeModulePath = $this->cwd.ltrim(AbstractAssetManager::NODE_MODULES_PATH, '.'); + $this->sfs->mkdir($nodeModulePath); + static::assertFileExists($nodeModulePath); + $lockFilePath = $this->cwd.\DIRECTORY_SEPARATOR.$this->manager->getLockPackageName(); + file_put_contents($lockFilePath, '{}'); + static::assertFileExists($lockFilePath); + static::assertTrue($this->manager->isInstalled()); + static::assertTrue($this->manager->isUpdatable()); + } + + if (0 === $expectedRes) { + $this->fallback->expects(static::never()) + ->method('restore') + ; + } else { + $this->fallback->expects(static::once()) + ->method('restore') + ; + } + + $this->executor->addExpectedValues($expectedRes, 'ASSET MANAGER OUTPUT'); + + static::assertSame($expectedRes, $this->getManager()->run()); + static::assertSame($expectedCommand, $this->executor->getLastCommand()); + static::assertSame('ASSET MANAGER OUTPUT', $this->executor->getLastOutput()); + } + + /** + * @return AssetManagerInterface + */ + abstract protected function getManager(); + + /** + * @return string + */ + abstract protected function getValidName(); + + /** + * @return string + */ + abstract protected function getValidLockPackageName(); + + /** + * @return string + */ + abstract protected function getValidVersionCommand(); + + /** + * @return string + */ + abstract protected function getValidInstallCommand(); + + /** + * @return string + */ + abstract protected function getValidUpdateCommand(); + + protected function actionForTestAddDependenciesForUpdateCommand() + { + // do nothing by default + } + + /** + * @param string $action The action + */ + protected function actionForTestRunForInstallCommand($action) + { + // do nothing by default + } +} diff --git a/Tests/Asset/AssetManagerFinderTest.php b/Tests/Asset/AssetManagerFinderTest.php new file mode 100644 index 0000000..bded1d9 --- /dev/null +++ b/Tests/Asset/AssetManagerFinderTest.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Foxy\Asset\AssetManagerFinder; +use PHPUnit\Framework\TestCase; + +/** + * Asset manager finder tests. + * + * @author François Pluchino + * + * @internal + */ +final class AssetManagerFinderTest extends TestCase +{ + public function testFindManagerWithValidManager() + { + $am = $this->getMockBuilder('Foxy\Asset\AssetManagerInterface')->getMock(); + + $am->expects(static::once()) + ->method('getName') + ->willReturn('foo') + ; + + $amf = new AssetManagerFinder(array($am)); + $res = $amf->findManager('foo'); + + static::assertSame($am, $res); + } + + public function testFindManagerWithInvalidManager() + { + static::expectException('Foxy\Exception\RuntimeException'); + static::expectExceptionMessage('The asset manager "bar" doesn\'t exist'); + + $am = $this->getMockBuilder('Foxy\Asset\AssetManagerInterface')->getMock(); + + $am->expects(static::once()) + ->method('getName') + ->willReturn('foo') + ; + + $amf = new AssetManagerFinder(array($am)); + $amf->findManager('bar'); + } + + public function testFindManagerWithAutoManagerAndAvailableManagerByLockFile() + { + $am = $this->getMockBuilder('Foxy\Asset\AssetManagerInterface')->getMock(); + + $am->expects(static::once()) + ->method('getName') + ->willReturn('foo') + ; + + $am->expects(static::once()) + ->method('hasLockFile') + ->willReturn(true) + ; + + $am->expects(static::never()) + ->method('isAvailable') + ; + + $amf = new AssetManagerFinder(array($am)); + $res = $amf->findManager(null); + + static::assertSame($am, $res); + } + + public function testFindManagerWithAutoManagerAndAvailableManagerByAvailability() + { + $am = $this->getMockBuilder('Foxy\Asset\AssetManagerInterface')->getMock(); + + $am->expects(static::once()) + ->method('getName') + ->willReturn('foo') + ; + + $am->expects(static::once()) + ->method('hasLockFile') + ->willReturn(false) + ; + + $am->expects(static::once()) + ->method('isAvailable') + ->willReturn(true) + ; + + $amf = new AssetManagerFinder(array($am)); + $res = $amf->findManager(null); + + static::assertSame($am, $res); + } + + public function testFindManagerWithAutoManagerAndNoAvailableManager() + { + static::expectException('Foxy\Exception\RuntimeException'); + static::expectExceptionMessage('No asset manager is found'); + + $am = $this->getMockBuilder('Foxy\Asset\AssetManagerInterface')->getMock(); + + $am->expects(static::atLeastOnce()) + ->method('getName') + ->willReturn('foo') + ; + + $am->expects(static::once()) + ->method('hasLockFile') + ->willReturn(false) + ; + + $am->expects(static::once()) + ->method('isAvailable') + ->willReturn(false) + ; + + $amf = new AssetManagerFinder(array($am)); + $amf->findManager(null); + } +} diff --git a/Tests/Asset/AssetPackageTest.php b/Tests/Asset/AssetPackageTest.php new file mode 100644 index 0000000..b3fa56d --- /dev/null +++ b/Tests/Asset/AssetPackageTest.php @@ -0,0 +1,281 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Composer\Json\JsonFile; +use Composer\Package\RootPackageInterface; +use Foxy\Asset\AssetPackage; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Asset package tests. + * + * @author François Pluchino + * + * @internal + */ +final class AssetPackageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var string + */ + protected $cwd; + + /** + * @var Filesystem + */ + protected $sfs; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RootPackageInterface + */ + protected $rootPackage; + + /** + * @var JsonFile|\PHPUnit_Framework_MockObject_MockObject + */ + protected $jsonFile; + + protected function setUp(): void + { + parent::setUp(); + + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_asset_package_test_', true); + $this->sfs = new Filesystem(); + $this->rootPackage = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + $this->jsonFile = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor() + ->setMethods(array('exists', 'getPath', 'read', 'write')) + ->getMock() + ; + + $this->rootPackage->expects(static::any()) + ->method('getLicense') + ->willReturn(array()) + ; + + $this->sfs->mkdir($this->cwd); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->sfs->remove($this->cwd); + $this->jsonFile = null; + $this->rootPackage = null; + $this->sfs = null; + $this->cwd = null; + } + + public function testGetPackageWithExistingFile() + { + $package = array( + 'name' => '@foo/bar', + ); + $contentString = json_encode($package); + $this->addPackageFile($package, $contentString); + + $assetPackage = new AssetPackage($this->rootPackage, $this->jsonFile); + + static::assertSame($package, $assetPackage->getPackage()); + } + + public function testWrite() + { + $package = array( + 'name' => '@foo/bar', + ); + + $this->jsonFile->expects(static::once()) + ->method('exists') + ->willReturn(false) + ; + + $this->jsonFile->expects(static::once()) + ->method('write') + ->with($package) + ; + + $assetPackage = new AssetPackage($this->rootPackage, $this->jsonFile); + $assetPackage->setPackage($package); + $assetPackage->write(); + } + + public function getDataRequiredKeys() + { + return array( + array( + array( + 'name' => '@foo/bar', + 'license' => 'MIT', + ), + array( + 'name' => '@foo/bar', + 'license' => 'MIT', + ), + 'proprietary', + ), + array( + array( + 'name' => '@foo/bar', + 'license' => 'MIT', + ), + array( + 'name' => '@foo/bar', + ), + 'MIT', + ), + array( + array( + 'name' => '@foo/bar', + 'private' => true, + ), + array( + 'name' => '@foo/bar', + ), + 'proprietary', + ), + ); + } + + /** + * @dataProvider getDataRequiredKeys + * + * @param string $license + */ + public function testInjectionOfRequiredKeys(array $expected, array $package, $license) + { + $this->addPackageFile($package); + + $this->rootPackage = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + $this->rootPackage->expects(static::any()) + ->method('getLicense') + ->willReturn(array($license)) + ; + + $assetPackage = new AssetPackage($this->rootPackage, $this->jsonFile); + + static::assertEquals($expected, $assetPackage->getPackage()); + } + + public function testGetInstalledDependencies() + { + $expected = array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@composer-asset/baz--bar' => 'file:./path/baz/bar', + ); + $package = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@bar/foo' => '^1.0.0', + '@composer-asset/baz--bar' => 'file:./path/baz/bar', + ), + ); + $this->addPackageFile($package); + + $assetPackage = new AssetPackage($this->rootPackage, $this->jsonFile); + + static::assertEquals($expected, $assetPackage->getInstalledDependencies()); + } + + public function testAddNewDependencies() + { + $expected = array( + 'dependencies' => array( + '@bar/foo' => '^1.0.0', + '@composer-asset/baz--bar' => 'file:./path/baz/bar', + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@composer-asset/new--dependency' => 'file:./path/new/dependency', + ), + ); + $expectedExisting = array( + '@composer-asset/foo--bar', + '@composer-asset/baz--bar', + ); + + $package = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@bar/foo' => '^1.0.0', + '@composer-asset/baz--bar' => 'file:./path/baz/bar', + ), + ); + $dependencies = array( + '@composer-asset/foo--bar' => 'path/foo/bar/package.json', + '@composer-asset/baz--bar' => 'path/baz/bar/package.json', + '@composer-asset/new--dependency' => 'path/new/dependency/package.json', + ); + $this->addPackageFile($package); + + $assetPackage = new AssetPackage($this->rootPackage, $this->jsonFile); + $existing = $assetPackage->addNewDependencies($dependencies); + + static::assertSame($expected, $assetPackage->getPackage()); + static::assertSame($expectedExisting, $existing); + } + + public function testRemoveUnusedDependencies() + { + $expected = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@bar/foo' => '^1.0.0', + ), + ); + + $package = array( + 'dependencies' => array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + '@bar/foo' => '^1.0.0', + '@composer-asset/baz--bar' => 'file:./path/baz/bar', + ), + ); + $dependencies = array( + '@composer-asset/foo--bar' => 'file:./path/foo/bar', + ); + $this->addPackageFile($package); + + $assetPackage = new AssetPackage($this->rootPackage, $this->jsonFile); + $assetPackage->removeUnusedDependencies($dependencies); + + static::assertEquals($expected, $assetPackage->getPackage()); + } + + /** + * Add the package in file. + * + * @param array $package The package + * @param null|string $contentString The string content of package + */ + protected function addPackageFile(array $package, $contentString = null) + { + $filename = $this->cwd.'/package.json'; + $contentString = null !== $contentString ? $contentString : json_encode($package); + + $this->jsonFile->expects(static::any()) + ->method('exists') + ->willReturn(true) + ; + + $this->jsonFile->expects(static::any()) + ->method('getPath') + ->willReturn($filename) + ; + + $this->jsonFile->expects(static::any()) + ->method('read') + ->willReturn($package) + ; + + file_put_contents($filename, $contentString); + } +} diff --git a/Tests/Asset/NpmAssetManagerTest.php b/Tests/Asset/NpmAssetManagerTest.php new file mode 100644 index 0000000..f44c60a --- /dev/null +++ b/Tests/Asset/NpmAssetManagerTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Foxy\Asset\NpmManager; + +/** + * NPM asset manager tests. + * + * @author François Pluchino + * + * @internal + */ +final class NpmAssetManagerTest extends AbstractAssetManagerTest +{ + /** + * {@inheritdoc} + */ + protected function getManager() + { + return new NpmManager($this->io, $this->config, $this->executor, $this->fs, $this->fallback); + } + + /** + * {@inheritdoc} + */ + protected function getValidName() + { + return 'npm'; + } + + /** + * {@inheritdoc} + */ + protected function getValidLockPackageName() + { + return 'package-lock.json'; + } + + /** + * {@inheritdoc} + */ + protected function getValidVersionCommand() + { + return 'npm --version'; + } + + /** + * {@inheritdoc} + */ + protected function getValidInstallCommand() + { + return 'npm install'; + } + + /** + * {@inheritdoc} + */ + protected function getValidUpdateCommand() + { + return 'npm update'; + } +} diff --git a/Tests/Asset/PnpmAssetManagerTest.php b/Tests/Asset/PnpmAssetManagerTest.php new file mode 100644 index 0000000..3a655c4 --- /dev/null +++ b/Tests/Asset/PnpmAssetManagerTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Foxy\Asset\PnpmManager; + +/** + * Pnpm asset manager tests. + * + * @author Steffen Dietz + * + * @internal + */ +final class PnpmAssetManagerTest extends AbstractAssetManagerTest +{ + /** + * {@inheritdoc} + */ + protected function getManager() + { + return new PnpmManager($this->io, $this->config, $this->executor, $this->fs, $this->fallback); + } + + /** + * {@inheritdoc} + */ + protected function getValidName() + { + return 'pnpm'; + } + + /** + * {@inheritdoc} + */ + protected function getValidLockPackageName() + { + return 'pnpm-lock.yaml'; + } + + /** + * {@inheritdoc} + */ + protected function getValidVersionCommand() + { + return 'pnpm --version'; + } + + /** + * {@inheritdoc} + */ + protected function getValidInstallCommand() + { + return 'pnpm install'; + } + + /** + * {@inheritdoc} + */ + protected function getValidUpdateCommand() + { + return 'pnpm update'; + } +} diff --git a/Tests/Asset/YarnAssetManagerTest.php b/Tests/Asset/YarnAssetManagerTest.php new file mode 100644 index 0000000..bc86fb0 --- /dev/null +++ b/Tests/Asset/YarnAssetManagerTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Foxy\Asset\YarnManager; + +/** + * Yarn asset manager tests. + * + * @author François Pluchino + * + * @internal + */ +final class YarnAssetManagerTest extends AbstractAssetManagerTest +{ + /** + * {@inheritdoc} + */ + public function actionForTestRunForInstallCommand($action) + { + $this->executor->addExpectedValues(0, '1.0.0'); + + if ('update' === $action) { + $this->executor->addExpectedValues(0, '1.0.0'); + $this->executor->addExpectedValues(0, '1.0.0'); + $this->executor->addExpectedValues(0, 'CHECK OUTPUT'); + } + } + + /** + * {@inheritdoc} + */ + protected function getManager() + { + return new YarnManager($this->io, $this->config, $this->executor, $this->fs, $this->fallback); + } + + /** + * {@inheritdoc} + */ + protected function getValidName() + { + return 'yarn'; + } + + /** + * {@inheritdoc} + */ + protected function getValidLockPackageName() + { + return 'yarn.lock'; + } + + /** + * {@inheritdoc} + */ + protected function getValidVersionCommand() + { + return 'yarn --version'; + } + + /** + * {@inheritdoc} + */ + protected function getValidInstallCommand() + { + return 'yarn install --non-interactive'; + } + + /** + * {@inheritdoc} + */ + protected function getValidUpdateCommand() + { + return 'yarn upgrade --non-interactive'; + } + + /** + * {@inheritdoc} + */ + protected function actionForTestAddDependenciesForUpdateCommand() + { + $this->executor->addExpectedValues(0, '1.0.0'); + $this->executor->addExpectedValues(0, 'CHECK OUTPUT'); + } +} diff --git a/Tests/Asset/YarnNextAssetManagerTest.php b/Tests/Asset/YarnNextAssetManagerTest.php new file mode 100644 index 0000000..5953db5 --- /dev/null +++ b/Tests/Asset/YarnNextAssetManagerTest.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Asset; + +use Foxy\Asset\YarnManager; + +/** + * Yarn Next asset manager tests. + * + * @author François Pluchino + * + * @internal + */ +final class YarnNextAssetManagerTest extends AbstractAssetManagerTest +{ + /** + * {@inheritdoc} + */ + public function actionForTestRunForInstallCommand($action) + { + $this->executor->addExpectedValues(0, '2.0.0'); + + if ('update' === $action) { + $this->executor->addExpectedValues(0, '2.0.0'); + } + } + + /** + * {@inheritdoc} + */ + protected function getManager() + { + return new YarnManager($this->io, $this->config, $this->executor, $this->fs, $this->fallback); + } + + /** + * {@inheritdoc} + */ + protected function getValidName() + { + return 'yarn'; + } + + /** + * {@inheritdoc} + */ + protected function getValidLockPackageName() + { + return 'yarn.lock'; + } + + /** + * {@inheritdoc} + */ + protected function getValidVersionCommand() + { + return 'yarn --version'; + } + + /** + * {@inheritdoc} + */ + protected function getValidInstallCommand() + { + return 'yarn install'; + } + + /** + * {@inheritdoc} + */ + protected function getValidUpdateCommand() + { + return 'yarn up'; + } + + /** + * {@inheritdoc} + */ + protected function actionForTestAddDependenciesForUpdateCommand() + { + $this->executor->addExpectedValues(0, '2.0.0'); + $this->executor->addExpectedValues(0, 'CHECK OUTPUT'); + } +} diff --git a/Tests/Config/ConfigTest.php b/Tests/Config/ConfigTest.php new file mode 100644 index 0000000..f241412 --- /dev/null +++ b/Tests/Config/ConfigTest.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Config; + +use Composer\Composer; +use Composer\Config; +use Composer\IO\IOInterface; +use Composer\Package\RootPackageInterface; +use Foxy\Config\ConfigBuilder; + +/** + * Tests for config. + * + * @author François Pluchino + * + * @internal + */ +final class ConfigTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Composer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composer; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composerConfig; + + /** + * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $io; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RootPackageInterface + */ + protected $package; + + protected function setUp(): void + { + $this->composer = $this->getMockBuilder('Composer\Composer')->disableOriginalConstructor()->getMock(); + $this->composerConfig = $this->getMockBuilder('Composer\Config')->disableOriginalConstructor()->getMock(); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->package = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + + $this->composer->expects(static::any()) + ->method('getPackage') + ->willReturn($this->package) + ; + + $this->composer->expects(static::any()) + ->method('getConfig') + ->willReturn($this->composerConfig) + ; + } + + public function getDataForGetConfig() + { + return array( + array('foo', 42, 42), + array('bar', 'foo', 'empty'), + array('baz', false, true), + array('test', 0, 0), + array('manager-bar', 23, 0), + array('manager-baz', 0, 0), + array('global-composer-foo', 90, 0), + array('global-composer-bar', 70, 0), + array('global-config-foo', 23, 0), + array('env-boolean', false, true, 'FOXY__ENV_BOOLEAN=false'), + array('env-integer', -32, 0, 'FOXY__ENV_INTEGER=-32'), + array('env-json', array('foo' => 'bar'), array(), 'FOXY__ENV_JSON="{"foo": "bar"}"'), + array('env-json-array', array(array('foo' => 'bar')), array(), 'FOXY__ENV_JSON_ARRAY="[{"foo": "bar"}]"'), + array('env-string', 'baz', 'foo', 'FOXY__ENV_STRING=baz'), + array('test-p1', 'def', 'def', null, array()), + array('test-p1', 'def', 'def', null, array('test-p1' => 'ok')), + array('test-p1', 'ok', null, null, array('test-p1' => 'ok')), + ); + } + + /** + * @dataProvider getDataForGetConfig + * + * @param string $key The key + * @param mixed $expected The expected value + * @param null|mixed $default The default value + * @param null|string $env The env variable + * @param array $defaults The configured default values + */ + public function testGetConfig($key, $expected, $default = null, $env = null, array $defaults = array()) + { + // add env variables + if (null !== $env) { + putenv($env); + } + + $globalLogComposer = true; + $globalLogConfig = true; + + $globalPath = realpath(__DIR__.'/../Fixtures/package/global'); + $this->composerConfig->expects(static::any()) + ->method('has') + ->with('home') + ->willReturn(true) + ; + + $this->composerConfig->expects(static::any()) + ->method('get') + ->with('home') + ->willReturn($globalPath) + ; + + $this->package->expects(static::any()) + ->method('getConfig') + ->willReturn(array( + 'foxy' => array( + 'bar' => 'foo', + 'baz' => false, + 'env-foo' => 55, + 'manager' => 'quill', + 'manager-bar' => array( + 'peter' => 42, + 'quill' => 23, + ), + 'manager-baz' => array( + 'peter' => 42, + ), + ), + )) + ; + + if (0 === strpos($key, 'global-')) { + $this->io->expects(static::atLeast(2)) + ->method('isDebug') + ->willReturn(true) + ; + + $globalLogComposer = false; + $globalLogConfig = false; + + $this->io->expects(static::atLeastOnce()) + ->method('writeError') + ->willReturnCallback(static function ($message) use ($globalPath, &$globalLogComposer, &$globalLogConfig) { + if (sprintf('Loading Foxy config in file %s/composer.json', $globalPath)) { + $globalLogComposer = true; + } + + if (sprintf('Loading Foxy config in file %s/config.json', $globalPath)) { + $globalLogConfig = true; + } + }) + ; + } + + $config = ConfigBuilder::build($this->composer, $defaults, $this->io); + $value = $config->get($key, $default); + + // remove env variables + if (null !== $env) { + $envKey = substr($env, 0, strpos($env, '=')); + putenv($envKey); + static::assertFalse(getenv($envKey)); + } + + static::assertTrue($globalLogComposer); + static::assertTrue($globalLogConfig); + + static::assertSame($expected, $value); + // test cache + static::assertSame($expected, $config->get($key, $default)); + } + + public function getDataForGetArrayConfig() + { + return array( + array('foo', array(), array()), + array('foo', array(42), array(42)), + array('foo', array(42), array(), array('foo' => array(42))), + ); + } + + /** + * @dataProvider getDataForGetArrayConfig + * + * @param string $key The key + * @param array $expected The expected value + * @param array $default The default value + * @param array $defaults The configured default values + */ + public function testGetArrayConfig($key, array $expected, array $default, array $defaults = array()) + { + $config = ConfigBuilder::build($this->composer, $defaults, $this->io); + + static::assertSame($expected, $config->getArray($key, $default)); + } + + public function testGetEnvConfigWithInvalidJson() + { + static::expectException('Foxy\Exception\RuntimeException'); + static::expectExceptionMessage('The "FOXY__ENV_JSON" environment variable isn\'t a valid JSON'); + + putenv('FOXY__ENV_JSON="{"foo"}"'); + $config = ConfigBuilder::build($this->composer, array(), $this->io); + $ex = null; + + try { + $config->get('env-json'); + } catch (\Exception $e) { + $ex = $e; + } + + putenv('FOXY__ENV_JSON'); + static::assertFalse(getenv('FOXY__ENV_JSON')); + + if (null === $ex) { + throw new \Exception('The expected exception was not thrown'); + } + + throw $ex; + } +} diff --git a/Tests/Converter/SemverConverterTest.php b/Tests/Converter/SemverConverterTest.php new file mode 100644 index 0000000..5766adb --- /dev/null +++ b/Tests/Converter/SemverConverterTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Converter; + +use Foxy\Converter\SemverConverter; +use Foxy\Converter\VersionConverterInterface; +use PHPUnit\Framework\TestCase; + +/** + * Tests for the conversion of Semver syntax to composer syntax. + * + * @author François Pluchino + * + * @internal + */ +final class SemverConverterTest extends TestCase +{ + /** + * @var VersionConverterInterface + */ + protected $converter; + + protected function setUp(): void + { + $this->converter = new SemverConverter(); + } + + protected function tearDown(): void + { + $this->converter = null; + } + + /** + * @dataProvider getTestVersions + * + * @param string $semver + * @param string $composer + */ + public function testConverter($semver, $composer) + { + static::assertEquals($composer, $this->converter->convertVersion($semver)); + + if (!ctype_alpha((string) $semver) && !\in_array($semver, array(null, ''), true)) { + static::assertEquals('v'.$composer, $this->converter->convertVersion('v'.$semver)); + } + } + + public function getTestVersions() + { + return array( + array('1.2.3', '1.2.3'), + array('1.2.3alpha', '1.2.3-alpha1'), + array('1.2.3-alpha', '1.2.3-alpha1'), + array('1.2.3a', '1.2.3-alpha1'), + array('1.2.3a1', '1.2.3-alpha1'), + array('1.2.3-a', '1.2.3-alpha1'), + array('1.2.3-a1', '1.2.3-alpha1'), + array('1.2.3b', '1.2.3-beta1'), + array('1.2.3b1', '1.2.3-beta1'), + array('1.2.3-b', '1.2.3-beta1'), + array('1.2.3-b1', '1.2.3-beta1'), + array('1.2.3beta', '1.2.3-beta1'), + array('1.2.3-beta', '1.2.3-beta1'), + array('1.2.3beta1', '1.2.3-beta1'), + array('1.2.3-beta1', '1.2.3-beta1'), + array('1.2.3rc1', '1.2.3-RC1'), + array('1.2.3-rc1', '1.2.3-RC1'), + array('1.2.3rc2', '1.2.3-RC2'), + array('1.2.3-rc2', '1.2.3-RC2'), + array('1.2.3rc.2', '1.2.3-RC.2'), + array('1.2.3-rc.2', '1.2.3-RC.2'), + array('1.2.3+0', '1.2.3-patch0'), + array('1.2.3-0', '1.2.3-patch0'), + array('1.2.3pre', '1.2.3-beta1'), + array('1.2.3-pre', '1.2.3-beta1'), + array('1.2.3dev', '1.2.3-dev'), + array('1.2.3-dev', '1.2.3-dev'), + array('1.2.3+build2012', '1.2.3-patch2012'), + array('1.2.3-build2012', '1.2.3-patch2012'), + array('1.2.3+build.2012', '1.2.3-patch.2012'), + array('1.2.3-build.2012', '1.2.3-patch.2012'), + array('1.3.0–rc30.79', '1.3.0-RC30.79'), + array('1.2.3-SNAPSHOT', '1.2.3-dev'), + array('1.2.3-20123131.3246', '1.2.3-patch20123131.3246'), + array('1.x.x-dev', '1.x-dev'), + array('20170124.0.0', '20170124.000000'), + array('20170124.1.0', '20170124.001000'), + array('20170124.1.1', '20170124.001001'), + array('20170124.100.200', '20170124.100200'), + array('20170124.0', '20170124.000000'), + array('20170124.1', '20170124.001000'), + array('20170124', '20170124'), + array('latest', 'default || *'), + array(null, '*'), + array('', '*'), + ); + } +} diff --git a/Tests/Event/AbstractSolveEventTest.php b/Tests/Event/AbstractSolveEventTest.php new file mode 100644 index 0000000..481d0c9 --- /dev/null +++ b/Tests/Event/AbstractSolveEventTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Event; + +use Composer\Package\PackageInterface; +use Foxy\Event\AbstractSolveEvent; + +/** + * Tests for solve events. + * + * @author François Pluchino + */ +abstract class AbstractSolveEventTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var string + */ + protected $assetDir; + + /** + * @var PackageInterface[]|\PHPUnit_Framework_MockObject_MockObject[] + */ + protected $packages; + + protected function setUp(): void + { + $this->assetDir = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_event_test_', true); + $this->packages = array( + $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(), + ); + } + + protected function tearDown(): void + { + $this->assetDir = null; + $this->packages = null; + } + + /** + * Get the event instance. + * + * @return AbstractSolveEvent + */ + abstract public function getEvent(); + + public function testGetAssetDir() + { + $event = $this->getEvent(); + static::assertSame($this->assetDir, $event->getAssetDir()); + } + + public function testGetPackages() + { + $event = $this->getEvent(); + static::assertSame($this->packages, $event->getPackages()); + } +} diff --git a/Tests/Event/GetAssetsEventTest.php b/Tests/Event/GetAssetsEventTest.php new file mode 100644 index 0000000..4e83645 --- /dev/null +++ b/Tests/Event/GetAssetsEventTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Event; + +use Foxy\Event\GetAssetsEvent; + +/** + * Tests for get assets event. + * + * @author François Pluchino + * + * @internal + */ +final class GetAssetsEventTest extends AbstractSolveEventTest +{ + /** + * @var array + */ + protected $assets = array( + '@composer-asset/foo--bar' => 'file:./vendor/foxy/composer-asset/foo/bar', + ); + + /** + * {@inheritdoc} + * + * @return GetAssetsEvent + */ + public function getEvent() + { + return new GetAssetsEvent($this->assetDir, $this->packages, $this->assets); + } + + public function testHasAsset() + { + $event = $this->getEvent(); + static::assertTrue($event->hasAsset('@composer-asset/foo--bar')); + } + + public function testAddAsset() + { + $assetPackageName = '@composer-asset/bar--foo'; + $assetPackagePath = 'file:./vendor/foxy/composer-asset/bar/foo'; + $event = $this->getEvent(); + + static::assertFalse($event->hasAsset($assetPackageName)); + $event->addAsset($assetPackageName, $assetPackagePath); + static::assertTrue($event->hasAsset($assetPackageName)); + } + + public function testGetAssets() + { + $event = $this->getEvent(); + static::assertSame($this->assets, $event->getAssets()); + + $expectedAssets = array( + '@composer-asset/foo--bar' => 'file:./vendor/foxy/composer-asset/foo/bar', + '@composer-asset/bar--foo' => 'file:./vendor/foxy/composer-asset/bar/foo', + ); + + $event->addAsset('@composer-asset/bar--foo', 'file:./vendor/foxy/composer-asset/bar/foo'); + static::assertSame($expectedAssets, $event->getAssets()); + } +} diff --git a/Tests/Event/PostSolveEventTest.php b/Tests/Event/PostSolveEventTest.php new file mode 100644 index 0000000..3373e29 --- /dev/null +++ b/Tests/Event/PostSolveEventTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Event; + +use Foxy\Event\PostSolveEvent; + +/** + * Tests for post solve event. + * + * @author François Pluchino + * + * @internal + */ +final class PostSolveEventTest extends AbstractSolveEventTest +{ + /** + * {@inheritdoc} + * + * @return PostSolveEvent + */ + public function getEvent() + { + return new PostSolveEvent($this->assetDir, $this->packages, 42); + } + + public function testGetRunResult() + { + $event = $this->getEvent(); + static::assertSame(42, $event->getRunResult()); + } +} diff --git a/Tests/Event/PreSolveEventTest.php b/Tests/Event/PreSolveEventTest.php new file mode 100644 index 0000000..e210b8f --- /dev/null +++ b/Tests/Event/PreSolveEventTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Event; + +use Foxy\Event\PreSolveEvent; + +/** + * Tests for pre solve event. + * + * @author François Pluchino + * + * @internal + */ +final class PreSolveEventTest extends AbstractSolveEventTest +{ + /** + * {@inheritdoc} + * + * @return PreSolveEvent + */ + public function getEvent() + { + return new PreSolveEvent($this->assetDir, $this->packages); + } +} diff --git a/Tests/Fallback/AssetFallbackTest.php b/Tests/Fallback/AssetFallbackTest.php new file mode 100644 index 0000000..406b852 --- /dev/null +++ b/Tests/Fallback/AssetFallbackTest.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Fallback; + +use Composer\IO\IOInterface; +use Composer\Util\Filesystem; +use Foxy\Config\Config; +use Foxy\Fallback\AssetFallback; + +/** + * Tests for composer fallback. + * + * @author François Pluchino + * + * @internal + */ +final class AssetFallbackTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Config + */ + protected $config; + + /** + * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $io; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fs; + + /** + * @var \Symfony\Component\Filesystem\Filesystem + */ + protected $sfs; + + /** + * @var string + */ + protected $oldCwd; + + /** + * @var string + */ + protected $cwd; + + /** + * @var AssetFallback + */ + protected $assetFallback; + + protected function setUp(): void + { + parent::setUp(); + + $this->oldCwd = getcwd(); + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_asset_fallback_test_', true); + $this->config = new Config(array( + 'fallback-asset' => true, + )); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->fs = $this->getMockBuilder('Composer\Util\Filesystem')->disableOriginalConstructor()->setMethods(array('remove'))->getMock(); + $this->sfs = new \Symfony\Component\Filesystem\Filesystem(); + $this->sfs->mkdir($this->cwd); + chdir($this->cwd); + + $this->assetFallback = new AssetFallback($this->io, $this->config, 'package.json', $this->fs); + } + + protected function tearDown(): void + { + parent::tearDown(); + + chdir($this->oldCwd); + $this->sfs->remove($this->cwd); + $this->config = null; + $this->io = null; + $this->fs = null; + $this->sfs = null; + $this->assetFallback = null; + $this->oldCwd = null; + $this->cwd = null; + } + + public function getSaveData() + { + return array( + array(true), + array(false), + ); + } + + /** + * @dataProvider getSaveData + * + * @param bool $withPackageFile + */ + public function testSave($withPackageFile) + { + if ($withPackageFile) { + file_put_contents($this->cwd.'/package.json', '{}'); + } + + static::assertInstanceOf('Foxy\Fallback\AssetFallback', $this->assetFallback->save()); + } + + public function testRestoreWithDisableOption() + { + $config = new Config(array( + 'fallback-asset' => false, + )); + $assetFallback = new AssetFallback($this->io, $config, 'package.json', $this->fs); + + $this->io->expects(static::never()) + ->method('write') + ; + + $this->fs->expects(static::never()) + ->method('remove') + ; + + $assetFallback->restore(); + } + + public function getRestoreData() + { + return array( + array(true), + array(false), + ); + } + + /** + * @dataProvider getRestoreData + * + * @param bool $withPackageFile + */ + public function testRestore($withPackageFile) + { + $content = '{}'; + $path = $this->cwd.'/package.json'; + + if ($withPackageFile) { + file_put_contents($path, $content); + } + + $this->io->expects(static::once()) + ->method('write') + ; + + $this->fs->expects(static::once()) + ->method('remove') + ->with('package.json') + ; + + $this->assetFallback->save(); + $this->assetFallback->restore(); + + if ($withPackageFile) { + static::assertFileExists($path); + static::assertSame($content, file_get_contents($path)); + } else { + static::assertFileDoesNotExist($path); + } + } +} diff --git a/Tests/Fallback/ComposerFallbackTest.php b/Tests/Fallback/ComposerFallbackTest.php new file mode 100644 index 0000000..f21d7fd --- /dev/null +++ b/Tests/Fallback/ComposerFallbackTest.php @@ -0,0 +1,269 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Fallback; + +use Composer\Composer; +use Composer\Installer; +use Composer\IO\IOInterface; +use Composer\Util\Filesystem; +use Foxy\Config\Config; +use Foxy\Fallback\ComposerFallback; +use Foxy\Util\LockerUtil; +use Symfony\Component\Console\Input\InputInterface; + +/** + * Tests for composer fallback. + * + * @author François Pluchino + * + * @internal + */ +final class ComposerFallbackTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Config + */ + protected $config; + + /** + * @var Composer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composer; + + /** + * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $io; + + /** + * @var InputInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $input; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fs; + + /** + * @var Installer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $installer; + + /** + * @var \Symfony\Component\Filesystem\Filesystem + */ + protected $sfs; + + /** + * @var string + */ + protected $oldCwd; + + /** + * @var string + */ + protected $cwd; + + /** + * @var ComposerFallback + */ + protected $composerFallback; + + protected function setUp(): void + { + parent::setUp(); + + $this->oldCwd = getcwd(); + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_composer_fallback_test_', true); + $this->config = new Config(array( + 'fallback-composer' => true, + )); + $this->composer = $this->getMockBuilder('Composer\Composer')->disableOriginalConstructor()->getMock(); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->input = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); + $this->fs = $this->getMockBuilder('Composer\Util\Filesystem')->disableOriginalConstructor()->setMethods(array('remove'))->getMock(); + $this->installer = $this->getMockBuilder('Composer\Installer')->disableOriginalConstructor()->setMethods(array('run'))->getMock(); + $this->sfs = new \Symfony\Component\Filesystem\Filesystem(); + $this->sfs->mkdir($this->cwd); + chdir($this->cwd); + + $this->composerFallback = new ComposerFallback($this->composer, $this->io, $this->config, $this->input, $this->fs, $this->installer); + } + + protected function tearDown(): void + { + parent::tearDown(); + + chdir($this->oldCwd); + $this->sfs->remove($this->cwd); + $this->config = null; + $this->composer = null; + $this->io = null; + $this->input = null; + $this->fs = null; + $this->installer = null; + $this->sfs = null; + $this->composerFallback = null; + $this->oldCwd = null; + $this->cwd = null; + } + + public function getSaveData() + { + return array( + array(true), + array(false), + ); + } + + /** + * @dataProvider getSaveData + * + * @param bool $withLockFile + */ + public function testSave($withLockFile) + { + $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getRepositoryManager') + ->willReturn($rm) + ; + + $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getInstallationManager') + ->willReturn($im) + ; + + file_put_contents($this->cwd.'/composer.json', '{}'); + + if ($withLockFile) { + file_put_contents($this->cwd.'/composer.lock', json_encode(array('content-hash' => 'HASH_VALUE'))); + } + + static::assertInstanceOf('Foxy\Fallback\ComposerFallback', $this->composerFallback->save()); + } + + public function testRestoreWithDisableOption() + { + $config = new Config(array( + 'fallback-composer' => false, + )); + $composerFallback = new ComposerFallback($this->composer, $this->io, $config, $this->input); + + $this->io->expects(static::never()) + ->method('write') + ; + + $composerFallback->restore(); + } + + public function getRestoreData() + { + return array( + array(array()), + array(array( + array( + 'name' => 'foo/bar', + 'version' => '1.0.0.0', + ), + )), + ); + } + + /** + * @dataProvider getRestoreData + */ + public function testRestore(array $packages) + { + $composerFile = 'composer.json'; + $composerContent = '{}'; + $lockFile = 'composer.lock'; + $vendorDir = $this->cwd.'/vendor/'; + + file_put_contents($this->cwd.'/'.$composerFile, $composerContent); + file_put_contents($this->cwd.'/'.$lockFile, json_encode(array( + 'content-hash' => 'HASH_VALUE', + 'packages' => $packages, + 'packages-dev' => array(), + 'prefer-stable' => true, + ))); + + $this->input->expects(static::any()) + ->method('getOption') + ->willReturnCallback(function ($option) { + return 'verbose' === $option ? false : null; + }) + ; + + $ed = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getEventDispatcher') + ->willReturn($ed) + ; + + $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getRepositoryManager') + ->willReturn($rm) + ; + + $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getInstallationManager') + ->willReturn($im) + ; + + $this->io->expects(static::once()) + ->method('write') + ; + + $locker = LockerUtil::getLocker($this->io, $rm, $im, $composerFile); + + $this->composer->expects(static::atLeastOnce()) + ->method('getLocker') + ->willReturn($locker) + ; + + $config = $this->getMockBuilder('Composer\Config')->disableOriginalConstructor()->setMethods(array('get'))->getMock(); + $this->composer->expects(static::atLeastOnce()) + ->method('getConfig') + ->willReturn($config) + ; + + $config->expects(static::atLeastOnce()) + ->method('get') + ->willReturnCallback(function ($key, $default = null) use ($vendorDir) { + return 'vendor-dir' === $key ? $vendorDir : $default; + }) + ; + + if (0 === \count($packages)) { + $this->fs->expects(static::once()) + ->method('remove') + ->with($vendorDir) + ; + } else { + $this->fs->expects(static::never()) + ->method('remove') + ; + + $this->installer->expects(static::once()) + ->method('run') + ; + } + + $this->composerFallback->save(); + $this->composerFallback->restore(); + } +} diff --git a/Tests/Fixtures/Util/AbstractProcessExecutorMock.php b/Tests/Fixtures/Util/AbstractProcessExecutorMock.php new file mode 100644 index 0000000..3661d63 --- /dev/null +++ b/Tests/Fixtures/Util/AbstractProcessExecutorMock.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Fixtures\Util; + +use Composer\Util\ProcessExecutor; + +/** + * Mock of ProcessExecutor. + * + * @author François Pluchino + */ +abstract class AbstractProcessExecutorMock extends ProcessExecutor +{ + /** + * @var array + */ + private $expectedValues = array(); + + /** + * @var array + */ + private $executedCommands = array(); + + /** + * @var int + */ + private $position = 0; + + public function doExecute($command, &$output = null, ?string $cwd = null): int + { + $expected = isset($this->expectedValues[$this->position]) + ? $this->expectedValues[$this->position] + : array(0, $output); + + list($returnedCode, $output) = $expected; + $this->executedCommands[] = array($command, $returnedCode, $output); + ++$this->position; + + return $returnedCode; + } + + /** + * @param int $returnedCode The returned code + * @param null $output The output + * + * @return self + */ + public function addExpectedValues($returnedCode = 0, $output = null) + { + $this->expectedValues[] = array($returnedCode, $output); + + return $this; + } + + /** + * Get the executed command. + * + * @param int $position The position of executed command + * + * @return null|string + */ + public function getExecutedCommand($position) + { + return $this->getExecutedValue($position, 0); + } + + /** + * Get the executed returned code. + * + * @param int $position The position of executed command + * + * @return null|int + */ + public function getExecutedReturnedCode($position) + { + return $this->getExecutedValue($position, 1); + } + + /** + * Get the executed command. + * + * @param int $position The position of executed command + * + * @return null|string + */ + public function getExecutedOutput($position) + { + return $this->getExecutedValue($position, 2); + } + + /** + * Get the last executed command. + * + * @return null|string + */ + public function getLastCommand() + { + return $this->getExecutedCommand(\count($this->executedCommands) - 1); + } + + /** + * Get the last executed returned code. + * + * @return null|int + */ + public function getLastReturnedCode() + { + return $this->getExecutedReturnedCode(\count($this->executedCommands) - 1); + } + + /** + * Get the last executed output. + * + * @return null|string + */ + public function getLastOutput() + { + return $this->getExecutedOutput(\count($this->executedCommands) - 1); + } + + /** + * Get the value of the executed command. + * + * @param int $position The position + * @param int $index The index of value + * + * @return null|int|string + */ + private function getExecutedValue($position, $index) + { + return isset($this->executedCommands[$position]) + ? $this->executedCommands[$position][$index] + : null; + } +} diff --git a/Tests/Fixtures/Util/ProcessExecutorMock.php b/Tests/Fixtures/Util/ProcessExecutorMock.php new file mode 100644 index 0000000..de79a69 --- /dev/null +++ b/Tests/Fixtures/Util/ProcessExecutorMock.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Fixtures\Util; + +use Composer\Composer; +use Composer\Util\ProcessExecutor; + +/* + * Mock of ProcessExecutor. + * + * @author François Pluchino + */ +if (version_compare(Composer::VERSION, '2.3.0', '<')) { + class ProcessExecutorMock extends AbstractProcessExecutorMock + { + /** + * {@inheritdoc} + */ + public function execute($command, &$output = null, $cwd = null) + { + return $this->doExecute($command, $output, $cwd); + } + } +} else { + class ProcessExecutorMock extends AbstractProcessExecutorMock + { + /** + * {@inheritdoc} + */ + public function execute($command, &$output = null, ?string $cwd = null): int + { + return $this->doExecute($command, $output, $cwd); + } + } +} diff --git a/Tests/Fixtures/Util/ProcessExecutorMockTest.php b/Tests/Fixtures/Util/ProcessExecutorMockTest.php new file mode 100644 index 0000000..39b1087 --- /dev/null +++ b/Tests/Fixtures/Util/ProcessExecutorMockTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Json; + +use Foxy\Tests\Fixtures\Util\ProcessExecutorMock; + +/** + * Tests for the process executor mock. + * + * @author François Pluchino + * + * @internal + */ +final class ProcessExecutorMockTest extends \PHPUnit\Framework\TestCase +{ + public function testExecuteWithoutExpectedValues() + { + $executor = new ProcessExecutorMock(); + + $executor->execute('run', $output); + + static::assertSame('run', $executor->getExecutedCommand(0)); + static::assertEquals(0, $executor->getExecutedReturnedCode(0)); + static::assertNull($executor->getExecutedOutput(0)); + + static::assertNull($executor->getExecutedCommand(1)); + static::assertNull($executor->getExecutedReturnedCode(1)); + static::assertNull($executor->getExecutedOutput(1)); + + static::assertSame('run', $executor->getLastCommand()); + static::assertEquals(0, $executor->getLastReturnedCode()); + static::assertNull($executor->getLastOutput()); + + static::assertNull($output); + } + + public function testExecuteWithExpectedValues() + { + $executor = new ProcessExecutorMock(); + + $executor->addExpectedValues(0, 'TEST'); + $executor->addExpectedValues(42, 'TEST 2'); + + $executor->execute('run', $output); + $executor->execute('run2', $output2); + + static::assertSame('run', $executor->getExecutedCommand(0)); + static::assertSame(0, $executor->getExecutedReturnedCode(0)); + static::assertSame('TEST', $executor->getExecutedOutput(0)); + + static::assertSame('run2', $executor->getExecutedCommand(1)); + static::assertSame(42, $executor->getExecutedReturnedCode(1)); + static::assertSame('TEST 2', $executor->getExecutedOutput(1)); + + static::assertNull($executor->getExecutedCommand(2)); + static::assertNull($executor->getExecutedReturnedCode(2)); + static::assertNull($executor->getExecutedOutput(2)); + + static::assertSame('run2', $executor->getLastCommand()); + static::assertSame(42, $executor->getLastReturnedCode()); + static::assertSame('TEST 2', $executor->getLastOutput()); + + static::assertSame('TEST', $output); + static::assertSame('TEST 2', $output2); + } +} diff --git a/Tests/Fixtures/package/global/composer.json b/Tests/Fixtures/package/global/composer.json new file mode 100644 index 0000000..dcaf275 --- /dev/null +++ b/Tests/Fixtures/package/global/composer.json @@ -0,0 +1,8 @@ +{ + "config": { + "foxy": { + "global-composer-foo": 70, + "global-composer-bar": 70 + } + } +} diff --git a/Tests/Fixtures/package/global/config.json b/Tests/Fixtures/package/global/config.json new file mode 100644 index 0000000..9d400a8 --- /dev/null +++ b/Tests/Fixtures/package/global/config.json @@ -0,0 +1,8 @@ +{ + "config": { + "foxy": { + "global-composer-foo": 90, + "global-config-foo": 23 + } + } +} diff --git a/Tests/FoxyTest.php b/Tests/FoxyTest.php new file mode 100644 index 0000000..dfbff9b --- /dev/null +++ b/Tests/FoxyTest.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests; + +use Composer\Composer; +use Composer\Config; +use Composer\Installer\PackageEvent; +use Composer\IO\IOInterface; +use Composer\Package\RootPackageInterface; +use Composer\Script\Event; +use Foxy\Foxy; +use Foxy\Solver\SolverInterface; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Tests for foxy. + * + * @author François Pluchino + * + * @internal + */ +final class FoxyTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Composer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composer; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composerConfig; + + /** + * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $io; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RootPackageInterface + */ + protected $package; + + protected function setUp(): void + { + $this->composer = $this->getMockBuilder('Composer\Composer')->disableOriginalConstructor()->getMock(); + $this->composerConfig = $this->getMockBuilder('Composer\Config')->disableOriginalConstructor()->getMock(); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->package = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + + $this->composer->expects(static::any()) + ->method('getPackage') + ->willReturn($this->package) + ; + + $this->composer->expects(static::any()) + ->method('getConfig') + ->willReturn($this->composerConfig) + ; + + $rm = $this->getMockBuilder('Composer\Repository\RepositoryManager')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getRepositoryManager') + ->willReturn($rm) + ; + + $im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); + $this->composer->expects(static::any()) + ->method('getInstallationManager') + ->willReturn($im) + ; + } + + public function testGetSubscribedEvents() + { + static::assertCount(4, Foxy::getSubscribedEvents()); + } + + public function testActivate() + { + $foxy = new Foxy(); + $foxy->activate($this->composer, $this->io); + $foxy->init(); + static::assertTrue(true); + } + + public function testDeactivate() + { + $foxy = new Foxy(); + $foxy->deactivate($this->composer, $this->io); + static::assertTrue(true); + } + + public function testUninstall() + { + $foxy = new Foxy(); + $foxy->uninstall($this->composer, $this->io); + static::assertTrue(true); + } + + public function testActivateOnInstall() + { + $package = $this->getMockBuilder('Composer\Package\Package') + ->disableOriginalConstructor() + ->getMock() + ; + $package->expects(static::once()) + ->method('getName') + ->willReturn('foxy/foxy') + ; + + $operation = $this->getMockBuilder('Composer\DependencyResolver\Operation\InstallOperation') + ->disableOriginalConstructor()->getMock(); + $operation->expects(static::once()) + ->method('getPackage') + ->willReturn($package) + ; + + /** @var MockObject|PackageEvent $event */ + $event = $this->getMockBuilder('Composer\Installer\PackageEvent')->disableOriginalConstructor()->getMock(); + $event->expects(static::once()) + ->method('getOperation') + ->willReturn($operation) + ; + + $foxy = new Foxy(); + $foxy->activate($this->composer, $this->io); + $foxy->initOnInstall($event); + } + + public function testActivateWithInvalidManager() + { + static::expectException('Foxy\Exception\RuntimeException'); + static::expectExceptionMessage('The asset manager "invalid_manager" doesn\'t exist'); + + $this->package->expects(static::any()) + ->method('getConfig') + ->willReturn(array( + 'foxy' => array( + 'manager' => 'invalid_manager', + ), + )) + ; + + $foxy = new Foxy(); + $foxy->activate($this->composer, $this->io); + } + + public function getSolveAssetsData() + { + return array( + array('solve_event_install', false), + array('solve_event_update', true), + ); + } + + /** + * @dataProvider getSolveAssetsData + * + * @param string $eventName + * @param bool $expectedUpdatable + */ + public function testSolveAssets($eventName, $expectedUpdatable) + { + $event = new Event($eventName, $this->composer, $this->io); + + /** @var \PHPUnit_Framework_MockObject_MockObject|SolverInterface $solver */ + $solver = $this->getMockBuilder('Foxy\Solver\SolverInterface')->getMock(); + $solver->expects(static::once()) + ->method('setUpdatable') + ->with($expectedUpdatable) + ; + $solver->expects(static::once()) + ->method('solve') + ->with($this->composer, $this->io) + ; + + $foxy = new Foxy(); + $foxy->setSolver($solver); + $foxy->solveAssets($event); + } +} diff --git a/Tests/Json/JsonFileTest.php b/Tests/Json/JsonFileTest.php new file mode 100644 index 0000000..e55d7a7 --- /dev/null +++ b/Tests/Json/JsonFileTest.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Json; + +use Foxy\Json\JsonFile; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Tests for json file. + * + * @author François Pluchino + * + * @internal + */ +final class JsonFileTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Filesystem + */ + protected $sfs; + + /** + * @var string + */ + protected $oldCwd; + + /** + * @var string + */ + protected $cwd; + + protected function setUp(): void + { + parent::setUp(); + + $this->oldCwd = getcwd(); + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_asset_json_file_test_', true); + $this->sfs = new Filesystem(); + $this->sfs->mkdir($this->cwd); + chdir($this->cwd); + } + + protected function tearDown(): void + { + parent::tearDown(); + + chdir($this->oldCwd); + $this->sfs->remove($this->cwd); + $this->sfs = null; + $this->oldCwd = null; + $this->cwd = null; + } + + public function testGetArrayKeysWithoutFile() + { + $filename = './package.json'; + $jsonFile = new JsonFile($filename); + + static::assertSame(array(), $jsonFile->getArrayKeys()); + } + + public function testGetArrayKeysWithExistingFile() + { + $expected = array( + 'contributors', + ); + $content = <<<'JSON' +{ + "name": "test", + "contributors": [], + "dependencies": {} +} + +JSON; + + $filename = './package.json'; + file_put_contents($filename, $content); + static::assertFileExists($filename); + + $jsonFile = new JsonFile($filename); + + static::assertSame($expected, $jsonFile->getArrayKeys()); + } + + public function testGetIndentWithoutFile() + { + $filename = './package.json'; + $jsonFile = new JsonFile($filename); + + static::assertSame(4, $jsonFile->getIndent()); + } + + public function testGetIndentWithExistingFile() + { + $content = <<<'JSON' +{ + "name": "test" +} +JSON; + + $filename = './package.json'; + file_put_contents($filename, $content); + static::assertFileExists($filename); + + $jsonFile = new JsonFile($filename); + + static::assertSame(2, $jsonFile->getIndent()); + } + + public function testWriteWithoutFile() + { + $expected = <<<'JSON' +{ + "name": "test" +} + +JSON; + + $filename = './package.json'; + $data = array( + 'name' => 'test', + ); + + $jsonFile = new JsonFile($filename); + $jsonFile->write($data); + + static::assertFileExists($filename); + $content = file_get_contents($filename); + + static::assertSame($expected, $content); + } + + public function testWriteWithExistingFile() + { + $expected = <<<'JSON' +{ + "name": "test", + "contributors": [], + "dependencies": {}, + "private": true +} + +JSON; + $content = <<<'JSON' +{ + "name": "test", + "contributors": [], + "dependencies": {} +} + +JSON; + + $filename = './package.json'; + file_put_contents($filename, $content); + static::assertFileExists($filename); + + $jsonFile = new JsonFile($filename); + $data = (array) $jsonFile->read(); + $data['private'] = true; + $jsonFile->write($data); + + static::assertFileExists($filename); + $content = file_get_contents($filename); + + static::assertSame($expected, $content); + } +} diff --git a/Tests/Json/JsonFormatterTest.php b/Tests/Json/JsonFormatterTest.php new file mode 100644 index 0000000..c93b95d --- /dev/null +++ b/Tests/Json/JsonFormatterTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Json; + +use Foxy\Json\JsonFormatter; + +/** + * Tests for json formatter. + * + * @author François Pluchino + * + * @internal + */ +final class JsonFormatterTest extends \PHPUnit\Framework\TestCase +{ + public function testGetArrayKeys() + { + $content = <<<'JSON' +{ + "name": "test", + "contributors": [], + "dependencies": {} +} +JSON; + $expected = array( + 'contributors', + ); + + static::assertSame($expected, JsonFormatter::getArrayKeys($content)); + } + + public function testGetIndent() + { + $content = <<<'JSON' +{ + "name": "test", + "dependencies": {} +} +JSON; + + static::assertSame(2, JsonFormatter::getIndent($content)); + } + + public function testFormat() + { + $expected = <<<'JSON' +{ + "name": "test", + "contributors": [], + "dependencies": { + "@foo/bar": "^1.0.0" + }, + "devDependencies": {} +} +JSON; + $data = array( + 'name' => 'test', + 'contributors' => array(), + 'dependencies' => array( + '@foo/bar' => '^1.0.0', + ), + 'devDependencies' => array(), + ); + $content = json_encode($data); + + static::assertSame($expected, JsonFormatter::format($content, array('contributors'), 2)); + } +} diff --git a/Tests/Solver/SolverTest.php b/Tests/Solver/SolverTest.php new file mode 100644 index 0000000..944624b --- /dev/null +++ b/Tests/Solver/SolverTest.php @@ -0,0 +1,322 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Solver; + +use Composer\Composer; +use Composer\EventDispatcher\EventDispatcher; +use Composer\Installer\InstallationManager; +use Composer\IO\IOInterface; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Composer\Package\RootPackageInterface; +use Composer\Repository\RepositoryManager; +use Composer\Repository\WritableRepositoryInterface; +use Composer\Semver\Constraint\Constraint; +use Composer\Util\Filesystem; +use Composer\Util\HttpDownloader; +use Foxy\Asset\AssetManagerInterface; +use Foxy\Config\Config; +use Foxy\Fallback\FallbackInterface; +use Foxy\Solver\Solver; +use Foxy\Solver\SolverInterface; + +/** + * Tests for solver. + * + * @author François Pluchino + * + * @internal + */ +final class SolverTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Config + */ + protected $config; + + /** + * @var Composer|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composer; + + /** + * @var Config|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composerConfig; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|WritableRepositoryInterface + */ + protected $localRepo; + + /** + * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $io; + + /** + * @var Filesystem|\PHPUnit_Framework_MockObject_MockObject + */ + protected $fs; + + /** + * @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $im; + + /** + * @var \Symfony\Component\Filesystem\Filesystem + */ + protected $sfs; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|RootPackageInterface + */ + protected $package; + + /** + * @var AssetManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $manager; + + /** + * @var FallbackInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $composerFallback; + + /** + * @var string + */ + protected $oldCwd; + + /** + * @var string + */ + protected $cwd; + + /** + * @var SolverInterface + */ + protected $solver; + + protected function setUp(): void + { + parent::setUp(); + + $this->oldCwd = getcwd(); + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_solver_test_', true); + $this->config = new Config(array( + 'enabled' => true, + 'composer-asset-dir' => $this->cwd.'/composer-asset-dir', + )); + $this->composer = $this->getMockBuilder('Composer\Composer')->disableOriginalConstructor()->getMock(); + $this->composerConfig = $this->getMockBuilder('Composer\Config')->disableOriginalConstructor()->getMock(); + $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->fs = $this->getMockBuilder('Composer\Util\Filesystem')->disableOriginalConstructor()->getMock(); + $this->im = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor() + ->setMethods(array('getInstallPath'))->getMock(); + $this->sfs = new \Symfony\Component\Filesystem\Filesystem(); + $this->package = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); + $this->manager = $this->getMockBuilder('Foxy\Asset\AssetManagerInterface')->getMock(); + $this->composerFallback = $this->getMockBuilder('Foxy\Fallback\FallbackInterface')->getMock(); + $this->sfs->mkdir($this->cwd); + chdir($this->cwd); + + $this->localRepo = $this->getMockBuilder('Composer\Repository\InstalledArrayRepository') + ->setMethods(array('getCanonicalPackages')) + ->getMock() + ; + + if (class_exists('Composer\Util\HttpDownloader')) { + $rm = new RepositoryManager($this->io, $this->composerConfig, new HttpDownloader($this->io, $this->composerConfig)); + $rm->setLocalRepository($this->localRepo); + } else { + $rm = new RepositoryManager($this->io, $this->composerConfig); + $rm->setLocalRepository($this->localRepo); + } + + $this->composer->expects(static::any()) + ->method('getRepositoryManager') + ->willReturn($rm) + ; + + $this->composer->expects(static::any()) + ->method('getInstallationManager') + ->willReturn($this->im) + ; + + $this->composer->expects(static::any()) + ->method('getPackage') + ->willReturn($this->package) + ; + + $this->composer->expects(static::any()) + ->method('getConfig') + ->willReturn($this->composerConfig) + ; + + $this->composer->expects(static::any()) + ->method('getEventDispatcher') + ->willReturn(new EventDispatcher($this->composer, $this->io)) + ; + + $sfs = $this->sfs; + $this->fs->expects(static::any()) + ->method('findShortestPath') + ->willReturnCallback(function ($from, $to) use ($sfs) { + return rtrim($sfs->makePathRelative($to, $from), '/'); + }) + ; + + $this->solver = new Solver($this->manager, $this->config, $this->fs, $this->composerFallback); + } + + protected function tearDown(): void + { + parent::tearDown(); + + chdir($this->oldCwd); + $this->sfs->remove($this->cwd); + $this->config = null; + $this->composer = null; + $this->composerConfig = null; + $this->localRepo = null; + $this->io = null; + $this->fs = null; + $this->im = null; + $this->sfs = null; + $this->package = null; + $this->manager = null; + $this->composerFallback = null; + $this->solver = null; + $this->oldCwd = null; + $this->cwd = null; + } + + public function testSetUpdatable() + { + $this->manager->expects(static::once()) + ->method('setUpdatable') + ->with(false) + ; + + $this->solver->setUpdatable(false); + } + + public function testSolveWithDisableOption() + { + $config = new Config(array( + 'enabled' => false, + )); + $solver = new Solver($this->manager, $config, $this->fs); + + $this->manager->expects(static::never()) + ->method('run') + ; + + $solver->solve($this->composer, $this->io); + } + + public function getSolveData() + { + return array( + array(0), + array(1), + ); + } + + /** + * @dataProvider getSolveData + * + * @param int $resRunManager The result value of the run command of asset manager + */ + public function testSolve($resRunManager) + { + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $requirePackage */ + $requirePackage = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $requirePackage->expects(static::any()) + ->method('getPrettyVersion') + ->willReturn('1.0.0') + ; + $requirePackage->expects(static::any()) + ->method('getName') + ->willReturn('foo/bar') + ; + $requirePackage->expects(static::any()) + ->method('getRequires') + ->willReturn(array( + new Link('root/package', 'foxy/foxy', new Constraint('=', '1.0.0')), + )) + ; + $requirePackage->expects(static::any()) + ->method('getDevRequires') + ->willReturn(array()) + ; + + $this->addInstalledPackages(array( + $requirePackage, + )); + + $requirePackagePath = $this->cwd.'/vendor/foo/bar'; + + $this->im->expects(static::once()) + ->method('getInstallPath') + ->willReturn($requirePackagePath) + ; + + $this->manager->expects(static::exactly(2)) + ->method('getPackageName') + ->willReturn('package.json') + ; + + $this->manager->expects(static::once()) + ->method('addDependencies') + ; + + $this->manager->expects(static::once()) + ->method('run') + ->willReturn($resRunManager) + ; + + if (0 === $resRunManager) { + $this->composerFallback->expects(static::never()) + ->method('restore') + ; + } else { + $this->composerFallback->expects(static::once()) + ->method('restore') + ; + + $this->expectException('RuntimeException'); + $this->expectExceptionMessage('The asset manager ended with an error'); + } + + $requirePackageFilename = $requirePackagePath.\DIRECTORY_SEPARATOR.$this->manager->getPackageName(); + $this->sfs->mkdir(\dirname($requirePackageFilename)); + file_put_contents($requirePackageFilename, '{}'); + + $this->solver->solve($this->composer, $this->io); + } + + /** + * Add the installed packages in local repository. + * + * @param PackageInterface[] $packages The installed packages + */ + protected function addInstalledPackages(array $packages = array()) + { + $this->localRepo->expects(static::any()) + ->method('getCanonicalPackages') + ->willReturn($packages) + ; + } +} diff --git a/Tests/Util/AssetUtilTest.php b/Tests/Util/AssetUtilTest.php new file mode 100644 index 0000000..488bcf5 --- /dev/null +++ b/Tests/Util/AssetUtilTest.php @@ -0,0 +1,409 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Util; + +use Composer\Installer\InstallationManager; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Composer\Semver\Constraint\Constraint; +use Foxy\Asset\AbstractAssetManager; +use Foxy\Util\AssetUtil; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Tests for asset util. + * + * @author François Pluchino + * + * @internal + */ +final class AssetUtilTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Filesystem + */ + protected $sfs; + + /** + * @var string + */ + protected $cwd; + + protected function setUp(): void + { + parent::setUp(); + + $this->cwd = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('foxy_asset_util_test_', true); + $this->sfs = new Filesystem(); + $this->sfs->mkdir($this->cwd); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->sfs->remove($this->cwd); + $this->sfs = null; + $this->cwd = null; + } + + public function testGetName() + { + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $package->expects(static::once()) + ->method('getName') + ->willReturn('foo/bar') + ; + + static::assertSame('@composer-asset/foo--bar', AssetUtil::getName($package)); + } + + public function testGetPathWithoutRequiredFoxy() + { + /** @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject $installationManager */ + $installationManager = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->setMethods(array('getInstallPath')) + ->getMock() + ; + $installationManager->expects(static::never()) + ->method('getInstallPath') + ; + + /** @var AbstractAssetManager|\PHPUnit_Framework_MockObject_MockObject $assetManager */ + $assetManager = $this->getMockBuilder('Foxy\Asset\AbstractAssetManager') + ->disableOriginalConstructor() + ->getMockForAbstractClass() + ; + + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $package->expects(static::once()) + ->method('getRequires') + ->willReturn(array()) + ; + $package->expects(static::once()) + ->method('getDevRequires') + ->willReturn(array()) + ; + + $res = AssetUtil::getPath($installationManager, $assetManager, $package); + + static::assertNull($res); + } + + public function getRequiresData() + { + return array( + array(array(new Link('root/package', 'foxy/foxy', new Constraint('=', '1.0.0'))), array(), false), + array(array(), array(new Link('root/package', 'foxy/foxy', new Constraint('=', '1.0.0'))), false), + array(array(new Link('root/package', 'foxy/foxy', new Constraint('=', '1.0.0'))), array(), true), + array(array(), array(new Link('root/package', 'foxy/foxy', new Constraint('=', '1.0.0'))), true), + ); + } + + /** + * @dataProvider getRequiresData + * + * @param Link[] $requires + * @param Link[] $devRequires + * @param bool $fileExists + */ + public function testGetPathWithRequiredFoxy(array $requires, array $devRequires, $fileExists = false) + { + /** @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject $installationManager */ + $installationManager = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->setMethods(array('getInstallPath')) + ->getMock() + ; + $installationManager->expects(static::once()) + ->method('getInstallPath') + ->willReturn($this->cwd) + ; + + /** @var AbstractAssetManager|\PHPUnit_Framework_MockObject_MockObject $assetManager */ + $assetManager = $this->getMockBuilder('Foxy\Asset\AbstractAssetManager') + ->disableOriginalConstructor() + ->getMockForAbstractClass() + ; + + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $package->expects(static::once()) + ->method('getRequires') + ->willReturn($requires) + ; + + if (0 === \count($devRequires)) { + $package->expects(static::never()) + ->method('getDevRequires') + ; + } else { + $package->expects(static::once()) + ->method('getDevRequires') + ->willReturn($devRequires) + ; + } + + if ($fileExists) { + $expectedFilename = $this->cwd.\DIRECTORY_SEPARATOR.$assetManager->getPackageName(); + file_put_contents($expectedFilename, '{}'); + $expectedFilename = str_replace('\\', '/', realpath($expectedFilename)); + } else { + $expectedFilename = null; + } + + $res = AssetUtil::getPath($installationManager, $assetManager, $package); + + static::assertSame($expectedFilename, $res); + } + + public function getExtraData() + { + return array( + array(false, false), + array(true, false), + array(false, true), + array(true, true), + ); + } + + /** + * @dataProvider getExtraData + * + * @param bool $withExtra + * @param bool $fileExists + */ + public function testGetPathWithExtraActivation($withExtra, $fileExists = false) + { + /** @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject $installationManager */ + $installationManager = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->disableOriginalConstructor() + ->setMethods(array('getInstallPath')) + ->getMock() + ; + + if ($withExtra && $fileExists) { + $installationManager->expects(static::once()) + ->method('getInstallPath') + ->willReturn($this->cwd) + ; + } + + /** @var AbstractAssetManager|\PHPUnit_Framework_MockObject_MockObject $assetManager */ + $assetManager = $this->getMockBuilder('Foxy\Asset\AbstractAssetManager') + ->disableOriginalConstructor() + ->getMockForAbstractClass() + ; + + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $package->expects(static::any()) + ->method('getRequires') + ->willReturn(array()) + ; + + $package->expects(static::any()) + ->method('getDevRequires') + ->willReturn(array()) + ; + + $package->expects(static::atLeastOnce()) + ->method('getExtra') + ->willReturn(array( + 'foxy' => $withExtra, + )) + ; + + if ($fileExists) { + $expectedFilename = $this->cwd.\DIRECTORY_SEPARATOR.$assetManager->getPackageName(); + file_put_contents($expectedFilename, '{}'); + $expectedFilename = $withExtra ? str_replace('\\', '/', realpath($expectedFilename)) : null; + } else { + $expectedFilename = null; + } + + $res = AssetUtil::getPath($installationManager, $assetManager, $package); + + static::assertSame($expectedFilename, $res); + } + + public function testHasNoPluginDependency() + { + static::assertFalse(AssetUtil::hasPluginDependency(array( + new Link('root/package', 'foo/bar', new Constraint('=', '1.0.0')), + ))); + } + + public function testHasPluginDependency() + { + static::assertTrue(AssetUtil::hasPluginDependency(array( + new Link('root/package', 'foo/bar', new Constraint('=', '1.0.0')), + new Link('root/package', 'foxy/foxy', new Constraint('=', '1.0.0')), + new Link('root/package', 'bar/foo', new Constraint('=', '1.0.0')), + ))); + } + + public function getIsProjectActivationData() + { + return array( + array('full/qualified', true), + array('full-disable/qualified', false), + array('foo/bar', true), + array('baz/foo', false), + array('baz/foo-test', false), + array('bar/test', true), + array('other/package', false), + array('test-string/package', true), + ); + } + + /** + * @dataProvider getIsProjectActivationData + * + * @param string $packageName + * @param bool $expected + */ + public function testIsProjectActivation($packageName, $expected) + { + $enablePackages = array( + 0 => 'test-string/*', + 'foo/*' => true, + 'baz/foo' => false, + '/^bar\/*/' => true, + 'full/qualified' => true, + 'full-disable/qualified' => false, + ); + + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $package->expects(static::once()) + ->method('getName') + ->willReturn($packageName) + ; + + $res = AssetUtil::isProjectActivation($package, $enablePackages); + static::assertSame($expected, $res); + } + + public function getIsProjectActivationWithWildcardData() + { + return array( + array('full/qualified', true), + array('full-disable/qualified', false), + array('foo/bar', true), + array('baz/foo', false), + array('baz/foo-test', false), + array('bar/test', true), + array('other/package', true), + array('test-string/package', true), + ); + } + + /** + * @dataProvider getIsProjectActivationWithWildcardData + * + * @param string $packageName + * @param bool $expected + */ + public function testIsProjectActivationWithWildcardPattern($packageName, $expected) + { + $enablePackages = array( + 'baz/foo*' => false, + 'full-disable/qualified' => false, + '*' => true, + ); + + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + $package->expects(static::once()) + ->method('getName') + ->willReturn($packageName) + ; + + $res = AssetUtil::isProjectActivation($package, $enablePackages); + static::assertSame($expected, $res); + } + + public function getFormatPackageData() + { + return array( + array('1.0.0', null, '1.0.0'), + array('1.0.1', '1.0.0', '1.0.0'), + array('1.0.0.x-dev', null, '1.0.0'), + array('1.0.0.x', null, '1.0.0'), + array('1.0.0.1', null, '1.0.0'), + array('dev-master', null, '1.0.0', '1-dev'), + array('dev-master', null, '1.0.0', '1.0-dev'), + array('dev-master', null, '1.0.0', '1.0.0-dev'), + array('dev-master', null, '1.0.0', '1.x-dev'), + array('dev-master', null, '1.0.0', '1.0.x-dev'), + array('dev-master', null, '1.0.0', '1.*-dev'), + array('dev-master', null, '1.0.0', '1.0.*-dev'), + ); + } + + /** + * @dataProvider getFormatPackageData + * + * @param string $packageVersion + * @param null|string $assetVersion + * @param string $expectedAssetVersion + * @param null|string $branchAlias + */ + public function testFormatPackage($packageVersion, $assetVersion, $expectedAssetVersion, $branchAlias = null) + { + $packageName = '@composer-asset/foo--bar'; + + /** @var PackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ + $package = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); + + $assetPackage = array(); + + if (null !== $assetVersion) { + $assetPackage['version'] = $assetVersion; + + $package->expects(static::never()) + ->method('getPrettyVersion') + ; + $package->expects(static::never()) + ->method('getExtra') + ; + } else { + $extra = array(); + + if (null !== $branchAlias) { + $extra['branch-alias'][$packageVersion] = $branchAlias; + } + + $package->expects(static::once()) + ->method('getPrettyVersion') + ->willReturn($packageVersion) + ; + $package->expects(static::once()) + ->method('getExtra') + ->willReturn($extra) + ; + } + + $expected = array( + 'name' => $packageName, + 'version' => $expectedAssetVersion, + ); + + $res = AssetUtil::formatPackage($package, $packageName, $assetPackage); + + static::assertEquals($expected, $res); + } +} diff --git a/Tests/Util/ComposerUtilTest.php b/Tests/Util/ComposerUtilTest.php new file mode 100644 index 0000000..d39a346 --- /dev/null +++ b/Tests/Util/ComposerUtilTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Util; + +use Foxy\Util\ComposerUtil; + +/** + * Tests for composer util. + * + * @author François Pluchino + * + * @internal + */ +final class ComposerUtilTest extends \PHPUnit\Framework\TestCase +{ + public function getValidateVersionData() + { + return array( + array('@package_version@', '^1.5.0', true), + array('@package_version@', '^1.5.0|^2.0.0', true), + array('d173af2d7ac1408655df2cf6670ea0262e06d137', '^1.5.0|^2.0.0', true), + array('1.6.0', '^1.5.0', true), + array('1.5.1', '^1.5.0', true), + array('1.5.0', '^1.5.0', true), + array('1.5.0', '^1.5.0|^2.0.0', true), + array('1.5.0', '^1.5.1', false), + array('1.0.0', '^1.5.0', false), + ); + } + + /** + * @dataProvider getValidateVersionData + * + * @param string $composerVersion + * @param string $requiredVersion + * @param bool $valid + */ + public function testValidateVersion($composerVersion, $requiredVersion, $valid) + { + if ($valid) { + static::assertTrue(true, 'Composer\'s version is valid'); + } else { + $this->expectException('Foxy\Exception\RuntimeException'); + $this->expectExceptionMessageMatches('/Foxy requires the Composer\'s minimum version "([\d\.^|, ]+)", current version is "([\d\.]+)"/'); + } + + ComposerUtil::validateVersion($requiredVersion, $composerVersion); + } +} diff --git a/Tests/Util/ConsoleUtilTest.php b/Tests/Util/ConsoleUtilTest.php new file mode 100644 index 0000000..0404c88 --- /dev/null +++ b/Tests/Util/ConsoleUtilTest.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Util; + +use Composer\Config; +use Composer\IO\ConsoleIO; +use Composer\IO\IOInterface; +use Foxy\Util\ConsoleUtil; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\NullOutput; + +/** + * Tests for console util. + * + * @author François Pluchino + * + * @internal + */ +final class ConsoleUtilTest extends \PHPUnit\Framework\TestCase +{ + public function testGetInput() + { + $input = new ArgvInput(); + $output = new NullOutput(); + $helperSet = new HelperSet(); + $io = new ConsoleIO($input, $output, $helperSet); + + static::assertSame($input, ConsoleUtil::getInput($io)); + } + + public function testGetInputWithoutValidInput() + { + /** @var IOInterface $io */ + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + + static::assertInstanceOf('Symfony\Component\Console\Input\ArgvInput', ConsoleUtil::getInput($io)); + } + + public function getPreferredInstallOptionsData() + { + return array( + array(false, false, 'auto', false), + array(false, true, 'auto', true), + array(true, false, 'source', false), + array(false, true, 'dist', false), + ); + } + + /** + * @dataProvider getPreferredInstallOptionsData + * + * @param bool $expectedPreferSource + * @param bool $expectedPreferDist + * @param string $preferedInstall + * @param bool $inputPrefer + */ + public function testGetPreferredInstallOptions($expectedPreferSource, $expectedPreferDist, $preferedInstall, $inputPrefer) + { + /** @var Config|\PHPUnit_Framework_MockObject_MockObject $config */ + $config = $this->getMockBuilder(Config::class)->disableOriginalConstructor() + ->setMethods(array('get'))->getMock(); + + /** @var InputInterface|\PHPUnit_Framework_MockObject_MockObject $input */ + $input = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); + + $config->expects(static::once()) + ->method('get') + ->with('preferred-install') + ->willReturn($preferedInstall) + ; + + if ($inputPrefer) { + $input->expects(static::atLeastOnce()) + ->method('getOption') + ->willReturnCallback(static function ($option) { + return !('prefer-source' === $option); + }) + ; + } + + $res = ConsoleUtil::getPreferredInstallOptions($config, $input); + + static::assertEquals(array($expectedPreferSource, $expectedPreferDist), $res); + } +} diff --git a/Tests/Util/PackageUtilTest.php b/Tests/Util/PackageUtilTest.php new file mode 100644 index 0000000..4cbf5c1 --- /dev/null +++ b/Tests/Util/PackageUtilTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Tests\Util; + +use Composer\Package\CompletePackage; +use Foxy\Util\PackageUtil; + +/** + * Tests for package util. + * + * @author François Pluchino + * + * @internal + */ +final class PackageUtilTest extends \PHPUnit\Framework\TestCase +{ + public function testLoadLockPackages() + { + $lockData = array( + 'packages' => array( + array( + 'name' => 'foo/bar', + 'version' => '1.0.0.0', + ), + ), + 'packages-dev' => array( + array( + 'name' => 'bar/foo', + 'version' => '1.0.0.0', + ), + ), + ); + + $package = new CompletePackage('foo/bar', '1.0.0.0', '1.0.0.0'); + $package->setType('library'); + + $packageDev = new CompletePackage('bar/foo', '1.0.0.0', '1.0.0.0'); + $packageDev->setType('library'); + + $expectedPackages = array( + $package, + ); + $expectedDevPackages = array( + $packageDev, + ); + + $lockDataLoaded = PackageUtil::loadLockPackages($lockData); + + static::assertArrayHasKey('packages', $lockDataLoaded); + static::assertArrayHasKey('packages-dev', $lockDataLoaded); + static::assertEquals($lockDataLoaded['packages'], $expectedPackages); + static::assertEquals($lockDataLoaded['packages-dev'], $expectedDevPackages); + } + + public function testLoadLockPackagesWithoutPackages() + { + static::assertSame(array(), PackageUtil::loadLockPackages(array())); + } + + public function testConvertLockAlias() + { + $lockData = array( + 'aliases' => array( + array( + 'alias' => '1.0.0', + 'alias_normalized' => '1.0.0.0', + 'version' => 'dev-feature/1.0-test', + 'package' => 'foo/bar', + ), + array( + 'alias' => '2.2.0', + 'alias_normalized' => '2.2.0.0', + 'version' => 'dev-feature/2.2-test', + 'package' => 'foo/baz', + ), + ), + ); + $expectedAliases = array( + 'foo/bar' => array( + 'dev-feature/1.0-test' => array( + 'alias' => '1.0.0', + 'alias_normalized' => '1.0.0.0', + ), + ), + 'foo/baz' => array( + 'dev-feature/2.2-test' => array( + 'alias' => '2.2.0', + 'alias_normalized' => '2.2.0.0', + ), + ), + ); + + $convertedAliases = PackageUtil::convertLockAlias($lockData); + + static::assertArrayHasKey('aliases', $convertedAliases); + static::assertEquals($convertedAliases['aliases'], $expectedAliases); + } +} diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php new file mode 100644 index 0000000..09f3b88 --- /dev/null +++ b/Tests/bootstrap.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +require __DIR__.'/../vendor/autoload.php'; + +if (!class_exists('\PHPUnit_Framework_TestCase') && class_exists('\PHPUnit\Framework\TestCase')) { + class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase'); +} diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 9a203e1..0000000 --- a/changelog.md +++ /dev/null @@ -1,2 +0,0 @@ -Change Log -========== diff --git a/composer.json b/composer.json index a79c6f6..b6d9b01 100644 --- a/composer.json +++ b/composer.json @@ -1,52 +1,44 @@ { - "name": "yii-tools/template", - "type": "library", - "description": "template", - "keywords": [ - "template" + "name": "php-forge/foxy", + "description": "Fast, reliable, and secure NPM/Yarn/pnpm bridge for Composer", + "keywords": ["npm", "yarn", "composer", "bridge", "dependency manager", "package", "asset", "nodejs"], + "homepage": "https://github.com/fxpio/foxy", + "type": "composer-plugin", + "license": "MIT", + "authors": [ + { + "name": "François Pluchino", + "email": "francois.pluchino@gmail.com" + } ], - "license": "mit", - "minimum-stability": "dev", - "prefer-stable": true, "require": { - "php": "^8.1" + "php": "^8.1", + "composer-plugin-api": "^2.0" }, "require-dev": { - "maglnet/composer-require-checker": "^4.7", - "phpunit/phpunit": "^10.5", - "roave/infection-static-analysis-plugin": "^1.34", - "symplify/easy-coding-standard": "^12.1", - "vimeo/psalm": "^5.19" + "composer/composer": "^2.0.0", + "phpunit/phpunit": "^9.6.1" + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true }, "autoload": { "psr-4": { - "Template\\": "src" + "Foxy\\": "src" } }, "autoload-dev": { "psr-4": { - "Template\\Tests\\": "tests" + "Foxy\\Tests\\": "tests" } }, "extra": { + "class": "Foxy\\Foxy", "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.2-dev" } - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "infection/extension-installer": true - } - }, - "scripts": { - "check-dependencies": "composer-require-checker", - "easy-coding-standard": "ecs check", - "mutation": [ - "Composer\\Config::disableProcessTimeout", - "roave-infection-static-analysis-plugin" - ], - "psalm": "psalm", - "test": "phpunit" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..07f56e5 --- /dev/null +++ b/composer.lock @@ -0,0 +1,3893 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a3f6c13b4164ff43f732496a9bae1b32", + "packages": [], + "packages-dev": [ + { + "name": "composer/ca-bundle", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "b66d11b7479109ab547f9405b97205640b17d385" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", + "reference": "b66d11b7479109ab547f9405b97205640b17d385", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-12-18T12:05:55+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/filesystem": "^5.4 || ^6", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.1.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-06-30T13:58:57+00:00" + }, + { + "name": "composer/composer", + "version": "2.6.6", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "683557bd2466072777309d039534bb1332d0dda5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/683557bd2466072777309d039534bb1332d0dda5", + "reference": "683557bd2466072777309d039534bb1332d0dda5", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/class-map-generator": "^1.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.1 || ^3.1", + "composer/semver": "^3.2.5", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "justinrainbow/json-schema": "^5.2.11", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^2.8 || ^3", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.11 || ^6.0.11", + "symfony/filesystem": "^5.4 || ^6.0 || ^7", + "symfony/finder": "^5.4 || ^6.0 || ^7", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/process": "^5.4 || ^6.0 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.9.3", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1", + "phpstan/phpstan-symfony": "^1.2.10", + "symfony/phpunit-bridge": "^6.0 || ^7" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.6-dev" + }, + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.6.6" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-12-08T17:32:26+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, + { + "name": "composer/pcre", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-10-11T07:11:09+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.5.8", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-11-20T07:44:33+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-02-25T21:32:43+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "v5.2.13", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + }, + "time": "2023-09-26T02:20:38+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0" + }, + "time": "2024-01-07T17:17:35+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.30", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:47:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", + "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-01-19T07:03:14+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "react/promise", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.1.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-16T16:21:57+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-07T05:35:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bde739e7565280bda77be70044ac1047bc007e34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", + "reference": "bde739e7565280bda77be70044ac1047bc007e34", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-02T09:26:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "76d449a358ece77d6f1d6331c68453e657172202" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/76d449a358ece77d6f1d6331c68453e657172202", + "reference": "76d449a358ece77d6f1d6331c68453e657172202", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2023-12-18T13:03:25+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, + { + "name": "seld/signal-handler", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\Signal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" + }, + "time": "2023-09-03T09:24:00+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0254811a143e6bc6c8deea08b589a7e68a37f625" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0254811a143e6bc6c8deea08b589a7e68a37f625", + "reference": "0254811a143e6bc6c8deea08b589a7e68a37f625", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-10T16:15:48+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/952a8cb588c3bc6ce76f6023000fb932f16a6e59", + "reference": "952a8cb588c3bc6ce76f6023000fb932f16a6e59", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-26T17:27:13+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T17:30:12+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c4b1ef0bc80533d87a2e969806172f1c2a980241", + "reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-22T16:42:54+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "7cb80bc10bfcdf6b5492741c0b9357dac66940bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/7cb80bc10bfcdf6b5492741c0b9357dac66940bc", + "reference": "7cb80bc10bfcdf6b5492741c0b9357dac66940bc", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-10T16:15:48+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2023-11-20T00:12:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1", + "composer-plugin-api": "^2.0" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/docs/testing.md b/docs/testing.md deleted file mode 100644 index 63478f7..0000000 --- a/docs/testing.md +++ /dev/null @@ -1,35 +0,0 @@ -# Testing - -## Checking dependencies - -This package uses [composer-require-checker](https://github.com/maglnet/ComposerRequireChecker) to check if all dependencies are correctly defined in `composer.json`. - -To run the checker, execute the following command: - -```shell -composer run check-dependencies -``` - -## Mutation testing - -Mutation testing is checked with [Infection](https://infection.github.io/). To run it: - -```shell -composer run mutation -``` - -## Static analysis - -The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: - -```shell -composer run psalm -``` - -## Unit tests - -The code is tested with [PHPUnit](https://phpunit.de/). To run tests: - -``` -composer run test -``` diff --git a/ecs.php b/ecs.php deleted file mode 100644 index 78342b8..0000000 --- a/ecs.php +++ /dev/null @@ -1,47 +0,0 @@ -paths( - [ - __DIR__ . '/src', - __DIR__ . '/tests', - ] - ); - - // this way you add a single rule - $ecsConfig->rules( - [ - OrderedClassElementsFixer::class, - OrderedTraitsFixer::class, - NoUnusedImportsFixer::class, - ] - ); - - // this way you can add sets - group of rules - $ecsConfig->sets( - [ - // run and fix, one by one - SetList::DOCBLOCK, - SetList::NAMESPACES, - SetList::COMMENTS, - SetList::PSR_12, - ] - ); - - // this way configures a rule - $ecsConfig->ruleWithConfiguration( - ClassDefinitionFixer::class, - [ - 'space_before_parenthesis' => true, - ], - ); -}; diff --git a/infection.json.dist b/infection.json.dist deleted file mode 100644 index d06b334..0000000 --- a/infection.json.dist +++ /dev/null @@ -1,16 +0,0 @@ -{ - "source": { - "directories": [ - "src" - ] - }, - "logs": { - "text": "php:\/\/stderr", - "stryker": { - "report": "main" - } - }, - "mutators": { - "@default": true - } -} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f29a28d..41e8cdc 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,24 +1,30 @@ - - - - tests - - - - + - ./src + ./ - + + + .php-cs-fixer.dist.php + ./Tests + ./Resources + ./vendor + + + + + + ./Tests/ + + diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index 19f0435..0000000 --- a/psalm.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/src/Asset/AbstractAssetManager.php b/src/Asset/AbstractAssetManager.php new file mode 100644 index 0000000..d919a9d --- /dev/null +++ b/src/Asset/AbstractAssetManager.php @@ -0,0 +1,297 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +use Composer\IO\IOInterface; +use Composer\Package\RootPackageInterface; +use Composer\Semver\VersionParser; +use Composer\Util\Filesystem; +use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; +use Foxy\Config\Config; +use Foxy\Converter\SemverConverter; +use Foxy\Converter\VersionConverterInterface; +use Foxy\Exception\RuntimeException; +use Foxy\Fallback\FallbackInterface; +use Foxy\Json\JsonFile; + +/** + * Abstract Manager. + * + * @author François Pluchino + */ +abstract class AbstractAssetManager implements AssetManagerInterface +{ + const NODE_MODULES_PATH = './node_modules'; + + /** + * @var IOInterface + */ + protected $io; + + /** + * @var Config + */ + protected $config; + + /** + * @var ProcessExecutor + */ + protected $executor; + + /** + * @var Filesystem + */ + protected $fs; + + /** + * @var VersionConverterInterface + */ + protected $versionConverter; + + /** + * @var null|FallbackInterface + */ + protected $fallback; + + /** + * @var bool + */ + protected $updatable = true; + + /** + * @var null|string + */ + private $version = ''; + + /** + * Constructor. + * + * @param IOInterface $io The IO + * @param Config $config The config + * @param ProcessExecutor $executor The process + * @param Filesystem $fs The filesystem + * @param null|FallbackInterface $fallback The asset fallback + * @param null|VersionConverterInterface $versionConverter The version converter + */ + public function __construct( + IOInterface $io, + Config $config, + ProcessExecutor $executor, + Filesystem $fs, + FallbackInterface $fallback = null, + VersionConverterInterface $versionConverter = null + ) { + $this->io = $io; + $this->config = $config; + $this->executor = $executor; + $this->fs = $fs; + $this->fallback = $fallback; + $this->versionConverter = null !== $versionConverter ? $versionConverter : new SemverConverter(); + } + + /** + * {@inheritdoc} + */ + public function isAvailable() + { + return null !== $this->getVersion(); + } + + /** + * {@inheritdoc} + */ + public function getPackageName() + { + return 'package.json'; + } + + /** + * {@inheritdoc} + */ + public function hasLockFile() + { + return file_exists($this->getLockPackageName()); + } + + /** + * {@inheritdoc} + */ + public function isInstalled() + { + return is_dir(self::NODE_MODULES_PATH) && file_exists($this->getPackageName()); + } + + /** + * {@inheritdoc} + */ + public function setFallback(FallbackInterface $fallback) + { + $this->fallback = $fallback; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setUpdatable($updatable) + { + $this->updatable = $updatable; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isUpdatable() + { + return $this->updatable && $this->isInstalled() && $this->isValidForUpdate(); + } + + /** + * {@inheritdoc} + */ + public function isValidForUpdate() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function validate() + { + $version = $this->getVersion(); + $constraintVersion = $this->config->get('manager-version'); + + if (null === $version) { + throw new RuntimeException(sprintf('The binary of "%s" must be installed', $this->getName())); + } + + if ($constraintVersion) { + $parser = new VersionParser(); + $constraint = $parser->parseConstraints($constraintVersion); + + if (!$constraint->matches($parser->parseConstraints($version))) { + throw new RuntimeException(sprintf('The installed %s version "%s" doesn\'t match with the constraint version "%s"', $this->getName(), $version, $constraintVersion)); + } + } + } + + /** + * {@inheritdoc} + */ + public function addDependencies(RootPackageInterface $rootPackage, array $dependencies) + { + $assetPackage = new AssetPackage($rootPackage, new JsonFile($this->getPackageName(), null, $this->io)); + $assetPackage->removeUnusedDependencies($dependencies); + $alreadyInstalledDependencies = $assetPackage->addNewDependencies($dependencies); + + $this->actionWhenComposerDependenciesAreAlreadyInstalled($alreadyInstalledDependencies); + $this->io->write('Merging Composer dependencies in the asset package'); + + return $assetPackage->write(); + } + + /** + * {@inheritdoc} + */ + public function run() + { + if (true !== $this->config->get('run-asset-manager')) { + return 0; + } + + $updatable = $this->isUpdatable(); + $info = sprintf('%s %s dependencies', $updatable ? 'Updating' : 'Installing', $this->getName()); + $this->io->write($info); + + $timeout = ProcessExecutor::getTimeout(); + ProcessExecutor::setTimeout($this->config->get('manager-timeout', PHP_INT_MAX)); + $cmd = $updatable ? $this->getUpdateCommand() : $this->getInstallCommand(); + $res = (int) $this->executor->execute($cmd); + ProcessExecutor::setTimeout($timeout); + + if ($res > 0 && null !== $this->fallback) { + $this->fallback->restore(); + } + + return $res; + } + + /** + * Action when the composer dependencies are already installed. + * + * @param string[] $names the asset package name of composer dependencies + */ + protected function actionWhenComposerDependenciesAreAlreadyInstalled($names) + { + // do nothing by default + } + + /** + * Build the command with binary and command options. + * + * @param string $defaultBin The default binary of command if option isn't defined + * @param string $action The command action to retrieve the options in config + * @param string|string[] $command The command + * + * @return string + */ + protected function buildCommand($defaultBin, $action, $command) + { + $bin = $this->config->get('manager-bin', $defaultBin); + $bin = Platform::isWindows() ? str_replace('/', '\\', $bin) : $bin; + $gOptions = trim($this->config->get('manager-options', '')); + $options = trim($this->config->get('manager-'.$action.'-options', '')); + + return $bin.' '.implode(' ', (array) $command) + .(empty($gOptions) ? '' : ' '.$gOptions) + .(empty($options) ? '' : ' '.$options); + } + + /** + * @return null|string + */ + protected function getVersion() + { + if ('' === $this->version) { + $this->executor->execute($this->getVersionCommand(), $version); + $this->version = '' !== trim((string) $version) ? $this->versionConverter->convertVersion(trim((string) $version)) : null; + } + + return $this->version; + } + + /** + * Get the command to retrieve the version. + * + * @return string + */ + abstract protected function getVersionCommand(); + + /** + * Get the command to install the asset dependencies. + * + * @return string + */ + abstract protected function getInstallCommand(); + + /** + * Get the command to update the asset dependencies. + * + * @return string + */ + abstract protected function getUpdateCommand(); +} diff --git a/src/Asset/AssetManagerFinder.php b/src/Asset/AssetManagerFinder.php new file mode 100644 index 0000000..7e86cb1 --- /dev/null +++ b/src/Asset/AssetManagerFinder.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +use Foxy\Exception\RuntimeException; + +/** + * Asset Manager finder. + * + * @author François Pluchino + */ +class AssetManagerFinder +{ + /** + * @var AssetManagerInterface[] + */ + private $managers; + + /** + * Constructor. + * + * @param AssetManagerInterface[] $managers The asset managers + */ + public function __construct(array $managers = array()) + { + foreach ($managers as $manager) { + if ($manager instanceof AssetManagerInterface) { + $this->addManager($manager); + } + } + } + + public function addManager(AssetManagerInterface $manager) + { + $this->managers[$manager->getName()] = $manager; + } + + /** + * Find the asset manager. + * + * @param null|string $manager The name of the asset manager + * + * @return AssetManagerInterface + * + * @throws RuntimeException When the asset manager does not exist + * @throws RuntimeException When the asset manager is not found + */ + public function findManager($manager = null) + { + if (null !== $manager) { + if (isset($this->managers[$manager])) { + return $this->managers[$manager]; + } + + throw new RuntimeException(sprintf('The asset manager "%s" doesn\'t exist', $manager)); + } + + return $this->findAvailableManager(); + } + + /** + * Find the available asset manager. + * + * @return AssetManagerInterface + * + * @throws RuntimeException When no asset manager is found + */ + private function findAvailableManager() + { + // find asset manager by lockfile + foreach ($this->managers as $manager) { + if ($manager->hasLockFile()) { + return $manager; + } + } + + // find asset manager by availability + foreach ($this->managers as $manager) { + if ($manager->isAvailable()) { + return $manager; + } + } + + throw new RuntimeException('No asset manager is found'); + } +} diff --git a/src/Asset/AssetManagerInterface.php b/src/Asset/AssetManagerInterface.php new file mode 100644 index 0000000..39a66bf --- /dev/null +++ b/src/Asset/AssetManagerInterface.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +use Composer\Package\RootPackageInterface; +use Foxy\Exception\RuntimeException; +use Foxy\Fallback\FallbackInterface; + +/** + * Interface of asset manager. + * + * @author François Pluchino + */ +interface AssetManagerInterface +{ + /** + * Get the name of asset manager. + * + * @return string + */ + public function getName(); + + /** + * Check if the asset manager is available. + * + * @return bool + */ + public function isAvailable(); + + /** + * Get the filename of the asset package. + * + * @return string + */ + public function getPackageName(); + + /** + * Check if the lock file is present or not. + * + * @return bool + */ + public function hasLockFile(); + + /** + * Check if the asset dependencies are installed or not. + * + * @return bool + */ + public function isInstalled(); + + /** + * Set the fallback. + * + * @param FallbackInterface $fallback The fallback + * + * @return self + */ + public function setFallback(FallbackInterface $fallback); + + /** + * Define if the asset manager can be use the update command. + * + * @param bool $updatable The value + * + * @return self + */ + public function setUpdatable($updatable); + + /** + * Check if the asset manager can be use the update command or not. + * + * @return bool + */ + public function isUpdatable(); + + /** + * Check if the asset package is valid for the update. + * + * @return bool + */ + public function isValidForUpdate(); + + /** + * Get the filename of the lock file. + * + * @return string + */ + public function getLockPackageName(); + + /** + * Validate the version of asset manager. + * + * @throws RuntimeException When the binary isn't installed + * @throws RuntimeException When the version doesn't match + */ + public function validate(); + + /** + * Add the asset dependencies in asset package file. + * + * @param RootPackageInterface $rootPackage The composer root package + * @param array $dependencies The asset local dependencies + * + * @return AssetPackageInterface + */ + public function addDependencies(RootPackageInterface $rootPackage, array $dependencies); + + /** + * Run the asset manager to install/update the asset dependencies. + * + * @return int + */ + public function run(); +} diff --git a/src/Asset/AssetPackage.php b/src/Asset/AssetPackage.php new file mode 100644 index 0000000..d3f03dc --- /dev/null +++ b/src/Asset/AssetPackage.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +use Composer\Json\JsonFile; +use Composer\Package\RootPackageInterface; + +/** + * Asset package. + * + * @author François Pluchino + */ +class AssetPackage implements AssetPackageInterface +{ + const SECTION_DEPENDENCIES = 'dependencies'; + const SECTION_DEV_DEPENDENCIES = 'devDependencies'; + const COMPOSER_PREFIX = '@composer-asset/'; + + /** + * @var JsonFile + */ + protected $jsonFile; + + /** + * @var array + */ + protected $package = array(); + + /** + * Constructor. + * + * @param RootPackageInterface $rootPackage The composer root package + * @param JsonFile $jsonFile The json file + */ + public function __construct(RootPackageInterface $rootPackage, JsonFile $jsonFile) + { + $this->jsonFile = $jsonFile; + + if ($jsonFile->exists()) { + $this->setPackage((array) $jsonFile->read()); + } + + $this->injectRequiredKeys($rootPackage); + } + + /** + * {@inheritdoc} + */ + public function write() + { + $this->jsonFile->write($this->package); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setPackage(array $package) + { + $this->package = $package; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPackage() + { + return $this->package; + } + + /** + * {@inheritdoc} + */ + public function getInstalledDependencies() + { + $installedAssets = array(); + + if (isset($this->package[self::SECTION_DEPENDENCIES]) && \is_array($this->package[self::SECTION_DEPENDENCIES])) { + foreach ($this->package[self::SECTION_DEPENDENCIES] as $dependency => $version) { + if (0 === strpos($dependency, self::COMPOSER_PREFIX)) { + $installedAssets[$dependency] = $version; + } + } + } + + return $installedAssets; + } + + /** + * {@inheritdoc} + */ + public function addNewDependencies(array $dependencies) + { + $installedAssets = $this->getInstalledDependencies(); + $existingPackages = array(); + + foreach ($dependencies as $name => $path) { + if (isset($installedAssets[$name])) { + $existingPackages[] = $name; + } else { + $this->package[self::SECTION_DEPENDENCIES][$name] = 'file:./'.\dirname($path); + } + } + + $this->orderPackages(self::SECTION_DEPENDENCIES); + $this->orderPackages(self::SECTION_DEV_DEPENDENCIES); + + return $existingPackages; + } + + /** + * {@inheritdoc} + */ + public function removeUnusedDependencies(array $dependencies) + { + $installedAssets = $this->getInstalledDependencies(); + $removeDependencies = array_diff_key($installedAssets, $dependencies); + + foreach ($removeDependencies as $dependency => $version) { + unset($this->package[self::SECTION_DEPENDENCIES][$dependency]); + } + + return $this; + } + + /** + * Inject the required keys for asset package defined in root composer package. + * + * @param RootPackageInterface $rootPackage The composer root package + */ + protected function injectRequiredKeys(RootPackageInterface $rootPackage) + { + if (!isset($this->package['license']) && \count($rootPackage->getLicense()) > 0) { + $license = current($rootPackage->getLicense()); + + if ('proprietary' === $license) { + if (!isset($this->package['private'])) { + $this->package['private'] = true; + } + } else { + $this->package['license'] = $license; + } + } + } + + /** + * Order the packages section. + * + * @param string $section The package section + */ + protected function orderPackages($section) + { + if (isset($this->package[$section]) && \is_array($this->package[$section])) { + ksort($this->package[$section], SORT_STRING); + } + } +} diff --git a/src/Asset/AssetPackageInterface.php b/src/Asset/AssetPackageInterface.php new file mode 100644 index 0000000..3d13b20 --- /dev/null +++ b/src/Asset/AssetPackageInterface.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +/** + * Interface of asset package. + * + * @author François Pluchino + */ +interface AssetPackageInterface +{ + /** + * Write the asset package in file. + * + * @return self + */ + public function write(); + + /** + * Set the asset package. + * + * @param array $package The asset package + * + * @return self + */ + public function setPackage(array $package); + + /** + * Get the asset package. + * + * @return array + */ + public function getPackage(); + + /** + * Get the installed asset dependencies. + * + * @return array The installed asset dependencies + */ + public function getInstalledDependencies(); + + /** + * Add the new asset dependencies and return the names of already installed asset dependencies. + * + * @param array $dependencies The asset dependencies + * + * @return array The asset package name of the already asset dependencies + */ + public function addNewDependencies(array $dependencies); + + /** + * Remove the unused asset dependencies. + * + * @param array $dependencies All asset dependencies + * + * @return self + */ + public function removeUnusedDependencies(array $dependencies); +} diff --git a/src/Asset/NpmManager.php b/src/Asset/NpmManager.php new file mode 100644 index 0000000..c7cff60 --- /dev/null +++ b/src/Asset/NpmManager.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +/** + * NPM Manager. + * + * @author François Pluchino + */ +class NpmManager extends AbstractAssetManager +{ + /** + * {@inheritdoc} + */ + public function getName() + { + return 'npm'; + } + + /** + * {@inheritdoc} + */ + public function getLockPackageName() + { + return 'package-lock.json'; + } + + /** + * {@inheritdoc} + */ + protected function getVersionCommand() + { + return $this->buildCommand('npm', 'version', '--version'); + } + + /** + * {@inheritdoc} + */ + protected function getInstallCommand() + { + return $this->buildCommand('npm', 'install', 'install'); + } + + /** + * {@inheritdoc} + */ + protected function getUpdateCommand() + { + return $this->buildCommand('npm', 'update', 'update'); + } + + /** + * {@inheritdoc} + */ + protected function actionWhenComposerDependenciesAreAlreadyInstalled($names) + { + foreach ($names as $name) { + $this->fs->remove(self::NODE_MODULES_PATH.'/'.$name); + } + } +} diff --git a/src/Asset/PnpmManager.php b/src/Asset/PnpmManager.php new file mode 100644 index 0000000..dc9cc5c --- /dev/null +++ b/src/Asset/PnpmManager.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +/** + * Pnpm Manager. + * + * @author Steffen Dietz + */ +class PnpmManager extends AbstractAssetManager +{ + /** + * {@inheritdoc} + */ + public function getName() + { + return 'pnpm'; + } + + /** + * {@inheritdoc} + */ + public function getLockPackageName() + { + return 'pnpm-lock.yaml'; + } + + /** + * {@inheritdoc} + */ + public function isInstalled() + { + return parent::isInstalled() && file_exists($this->getLockPackageName()); + } + + /** + * {@inheritdoc} + */ + protected function getVersionCommand() + { + return $this->buildCommand('pnpm', 'version', '--version'); + } + + /** + * {@inheritdoc} + */ + protected function getInstallCommand() + { + return $this->buildCommand('pnpm', 'install', 'install'); + } + + /** + * {@inheritdoc} + */ + protected function getUpdateCommand() + { + return $this->buildCommand('pnpm', 'update', 'update'); + } +} diff --git a/src/Asset/YarnManager.php b/src/Asset/YarnManager.php new file mode 100644 index 0000000..6777653 --- /dev/null +++ b/src/Asset/YarnManager.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Asset; + +use Composer\Semver\VersionParser; + +/** + * Yarn Manager. + * + * @author François Pluchino + */ +class YarnManager extends AbstractAssetManager +{ + /** + * {@inheritdoc} + */ + public function getName() + { + return 'yarn'; + } + + /** + * {@inheritdoc} + */ + public function getLockPackageName() + { + return 'yarn.lock'; + } + + /** + * {@inheritdoc} + */ + public function isInstalled() + { + return parent::isInstalled() && file_exists($this->getLockPackageName()); + } + + /** + * {@inheritdoc} + */ + public function isValidForUpdate() + { + if ($this->isYarnNext()) { + return true; + } + + $cmd = $this->buildCommand('yarn', 'check', $this->mergeInteractiveCommand(array('check'))); + + return 0 === $this->executor->execute($cmd); + } + + /** + * {@inheritdoc} + */ + protected function getVersionCommand() + { + return $this->buildCommand('yarn', 'version', '--version'); + } + + /** + * {@inheritdoc} + */ + protected function getInstallCommand() + { + return $this->buildCommand('yarn', 'install', $this->mergeInteractiveCommand(array('install'))); + } + + /** + * {@inheritdoc} + */ + protected function getUpdateCommand() + { + $commandName = $this->isYarnNext() ? 'up' : 'upgrade'; + + return $this->buildCommand('yarn', 'update', $this->mergeInteractiveCommand(array($commandName))); + } + + /** + * @return bool + */ + private function isYarnNext() + { + $version = $this->getVersion(); + $parser = new VersionParser(); + $constraint = $parser->parseConstraints('>=2.0.0'); + + return $constraint->matches($parser->parseConstraints($version)); + } + + private function mergeInteractiveCommand(array $command) + { + if (!$this->isYarnNext()) { + $command[] = '--non-interactive'; + } + + return $command; + } +} diff --git a/src/Config/Config.php b/src/Config/Config.php new file mode 100644 index 0000000..7c6bb49 --- /dev/null +++ b/src/Config/Config.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Config; + +use Foxy\Exception\RuntimeException; + +/** + * Helper of package config. + * + * @author François Pluchino + */ +final class Config +{ + /** + * @var array + */ + private $config; + + /** + * @var array + */ + private $defaults; + + /** + * @var array + */ + private $cacheEnv = array(); + + /** + * Constructor. + * + * @param array $config The config + * @param array $defaults The default values + */ + public function __construct(array $config, array $defaults = array()) + { + $this->config = $config; + $this->defaults = $defaults; + } + + /** + * Get the array config value. + * + * @param string $key The config key + * @param array $default The default value + * + * @return array + */ + public function getArray($key, array $default = array()) + { + $value = $this->get($key, null); + + return null !== $value ? (array) $value : (array) $default; + } + + /** + * Get the config value. + * + * @param string $key The config key + * @param null|mixed $default The default value + * + * @return null|mixed + */ + public function get($key, $default = null) + { + if (\array_key_exists($key, $this->cacheEnv)) { + return $this->cacheEnv[$key]; + } + + $envKey = $this->convertEnvKey($key); + $envValue = getenv($envKey); + + if (false !== $envValue) { + return $this->cacheEnv[$key] = $this->convertEnvValue($envValue, $envKey); + } + + $defaultValue = $this->getDefaultValue($key, $default); + + return \array_key_exists($key, $this->config) + ? $this->getByManager($key, $this->config[$key], $defaultValue) + : $defaultValue; + } + + /** + * Convert the config key into environment variable. + * + * @param string $key The config key + * + * @return string + */ + private function convertEnvKey($key) + { + return 'FOXY__'.strtoupper(str_replace('-', '_', $key)); + } + + /** + * Convert the value of environment variable into php variable. + * + * @param string $value The value of environment variable + * @param string $environmentVariable The environment variable name + * + * @return array|bool|int|string + */ + private function convertEnvValue($value, $environmentVariable) + { + $value = trim(trim(trim($value, '\''), '"')); + + if ($this->isBoolean($value)) { + $value = $this->convertBoolean($value); + } elseif ($this->isInteger($value)) { + $value = $this->convertInteger($value); + } elseif ($this->isJson($value)) { + $value = $this->convertJson($value, $environmentVariable); + } + + return $value; + } + + /** + * Check if the value of environment variable is a boolean. + * + * @param string $value The value of environment variable + * + * @return bool + */ + private function isBoolean($value) + { + $value = strtolower($value); + + return \in_array($value, array('true', 'false', '1', '0', 'yes', 'no', 'y', 'n'), true); + } + + /** + * Convert the value of environment variable into a boolean. + * + * @param string $value The value of environment variable + * + * @return bool + */ + private function convertBoolean($value) + { + return \in_array($value, array('true', '1', 'yes', 'y'), true); + } + + /** + * Check if the value of environment variable is a integer. + * + * @param string $value The value of environment variable + * + * @return bool + */ + private function isInteger($value) + { + return ctype_digit(trim($value, '-')); + } + + /** + * Convert the value of environment variable into a integer. + * + * @param string $value The value of environment variable + * + * @return bool + */ + private function convertInteger($value) + { + return (int) $value; + } + + /** + * Check if the value of environment variable is a string JSON. + * + * @param string $value The value of environment variable + * + * @return bool + */ + private function isJson($value) + { + return 0 === strpos($value, '{') || 0 === strpos($value, '['); + } + + /** + * Convert the value of environment variable into a json array. + * + * @param string $value The value of environment variable + * @param string $environmentVariable The environment variable name + * + * @return array + */ + private function convertJson($value, $environmentVariable) + { + $value = json_decode($value, true); + + if (json_last_error()) { + throw new RuntimeException(sprintf('The "%s" environment variable isn\'t a valid JSON', $environmentVariable)); + } + + return $value; + } + + /** + * Get the configured default value or custom default value. + * + * @param string $key The config key + * @param null|mixed $default The default value + * + * @return null|mixed + */ + private function getDefaultValue($key, $default = null) + { + $value = null === $default && \array_key_exists($key, $this->defaults) + ? $this->defaults[$key] + : $default; + + return $this->getByManager($key, $value, $default); + } + + /** + * Get the value defined by the manager name in the key. + * + * @param string $key The config key + * @param array|mixed $value The value + * @param null|mixed $default The default value + * + * @return null|mixed + */ + private function getByManager($key, $value, $default = null) + { + if (0 === strpos($key, 'manager-') && \is_array($value)) { + $manager = $manager = $this->get('manager', ''); + + $value = \array_key_exists($manager, $value) + ? $value[$manager] + : $default; + } + + return $value; + } +} diff --git a/src/Config/ConfigBuilder.php b/src/Config/ConfigBuilder.php new file mode 100644 index 0000000..3ac7d65 --- /dev/null +++ b/src/Config/ConfigBuilder.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Config; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; + +/** + * Plugin Config builder. + * + * @author François Pluchino + */ +abstract class ConfigBuilder +{ + /** + * Build the config of plugin. + * + * @param Composer $composer The composer + * @param array $defaults The default values + * @param null|IOInterface $io The composer input/output + * + * @return Config + */ + public static function build(Composer $composer, array $defaults = array(), $io = null) + { + $config = self::getConfigBase($composer, $io); + + return new Config($config, $defaults); + } + + /** + * Get the base of data. + * + * @param Composer $composer The composer + * @param null|IOInterface $io The composer input/output + * + * @return array + */ + private static function getConfigBase(Composer $composer, $io = null) + { + $globalPackageConfig = self::getGlobalConfig($composer, 'composer', $io); + $globalConfig = self::getGlobalConfig($composer, 'config', $io); + $packageConfig = $composer->getPackage()->getConfig(); + $packageConfig = isset($packageConfig['foxy']) && \is_array($packageConfig['foxy']) + ? $packageConfig['foxy'] + : array(); + + return array_merge($globalPackageConfig, $globalConfig, $packageConfig); + } + + /** + * Get the data of the global config. + * + * @param Composer $composer The composer + * @param string $filename The filename + * @param null|IOInterface $io The composer input/output + * + * @return array + */ + private static function getGlobalConfig(Composer $composer, $filename, $io = null) + { + $home = self::getComposerHome($composer); + $file = new JsonFile($home.'/'.$filename.'.json'); + $config = array(); + + if ($file->exists()) { + $data = $file->read(); + + if (isset($data['config']['foxy']) && \is_array($data['config']['foxy'])) { + $config = $data['config']['foxy']; + + if ($io instanceof IOInterface && $io->isDebug()) { + $io->writeError('Loading Foxy config in file '.$file->getPath()); + } + } + } + + return $config; + } + + /** + * Get the home directory of composer. + * + * @param Composer $composer The composer + * + * @return string + */ + private static function getComposerHome(Composer $composer) + { + return null !== $composer->getConfig() && $composer->getConfig()->has('home') + ? $composer->getConfig()->get('home') + : ''; + } +} diff --git a/src/Converter/SemverConverter.php b/src/Converter/SemverConverter.php new file mode 100644 index 0000000..adeda1e --- /dev/null +++ b/src/Converter/SemverConverter.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Converter; + +/** + * Converter for Semver syntax version to composer syntax version. + * + * @author François Pluchino + */ +class SemverConverter implements VersionConverterInterface +{ + public function convertVersion($version) + { + if (\in_array($version, array(null, '', 'latest'), true)) { + return ('latest' === $version ? 'default || ' : '').'*'; + } + + $version = str_replace('–', '-', $version); + $prefix = preg_match('/^[a-z]/', $version) && 0 !== strpos($version, 'dev-') ? substr($version, 0, 1) : ''; + $version = substr($version, \strlen($prefix)); + $version = SemverUtil::convertVersionMetadata($version); + $version = SemverUtil::convertDateVersion($version); + + return $prefix.$version; + } +} diff --git a/src/Converter/SemverUtil.php b/src/Converter/SemverUtil.php new file mode 100644 index 0000000..1d27d48 --- /dev/null +++ b/src/Converter/SemverUtil.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Converter; + +use Composer\Package\Version\VersionParser; + +/** + * Utils for semver converter. + * + * @author François Pluchino + */ +abstract class SemverUtil +{ + /** + * Converts the date or datetime version. + * + * @param string $version The version + * + * @return string + */ + public static function convertDateVersion($version) + { + if (preg_match('/^\d{7,}\./', $version)) { + $pos = strpos($version, '.'); + $version = substr($version, 0, $pos).self::convertDateMinorVersion(substr($version, $pos + 1)); + } + + return $version; + } + + /** + * Converts the version metadata. + * + * @param string $version + * + * @return string + */ + public static function convertVersionMetadata($version) + { + if (preg_match_all( + self::createPattern('([a-zA-Z]+|(\-|\+)[a-zA-Z]+|(\-|\+)[0-9]+)'), + $version, + $matches, + PREG_OFFSET_CAPTURE + )) { + list($type, $version, $end) = self::cleanVersion(strtolower($version), $matches); + list($version, $patchVersion) = self::matchVersion($version, $type); + + $matches = array(); + $hasPatchNumber = preg_match('/[0-9]+\.[0-9]+|[0-9]+|\.[0-9]+$/', $end, $matches); + $end = $hasPatchNumber ? $matches[0] : '1'; + + if ($patchVersion) { + $version .= $end; + } + } + + return static::cleanWildcard($version); + } + + /** + * Creates a pattern with the version prefix pattern. + * + * @param string $pattern The pattern without '/' + * + * @return string The full pattern with '/' + */ + public static function createPattern($pattern) + { + $numVer = '([0-9]+|x|\*)'; + $numVer2 = '('.$numVer.'\.'.$numVer.')'; + $numVer3 = '('.$numVer.'\.'.$numVer.'\.'.$numVer.')'; + + return '/^('.$numVer.'|'.$numVer2.'|'.$numVer3.')'.$pattern.'/'; + } + + /** + * Clean the wildcard in version. + * + * @param string $version The version + * + * @return string The cleaned version + */ + private static function cleanWildcard($version) + { + while (false !== strpos($version, '.x.x')) { + $version = str_replace('.x.x', '.x', $version); + } + + return $version; + } + + /** + * Clean the raw version. + * + * @param string $version The version + * @param array $matches The match of pattern asset version + * + * @return array The list of $type, $version and $end + */ + private static function cleanVersion($version, array $matches) + { + $end = substr($version, \strlen($matches[1][0][0])); + $version = $matches[1][0][0].'-'; + + $matches = array(); + if (preg_match('/^([-+])/', $end, $matches)) { + $end = substr($end, 1); + } + + $matches = array(); + preg_match('/^[a-z]+/', $end, $matches); + $type = isset($matches[0]) ? VersionParser::normalizeStability($matches[0]) : ''; + $end = substr($end, \strlen($type)); + + return array($type, $version, $end); + } + + /** + * Match the version. + * + * @param string $version + * @param string $type + * + * @return array The list of $version and $patchVersion + */ + private static function matchVersion($version, $type) + { + $patchVersion = true; + + switch ($type) { + case 'dev': + case 'snapshot': + $type = 'dev'; + $patchVersion = false; + + break; + + case 'a': + $type = 'alpha'; + + break; + + case 'b': + case 'pre': + $type = 'beta'; + + break; + + default: + if (!\in_array($type, array('alpha', 'beta', 'RC'), true)) { + $type = 'patch'; + } + + break; + } + + $version .= $type; + + return array($version, $patchVersion); + } + + /** + * Convert the minor version of date. + * + * @param string $minor The minor version + * + * @return string + */ + private static function convertDateMinorVersion($minor) + { + $split = explode('.', $minor); + $minor = (int) $split[0]; + $revision = isset($split[1]) ? (int) $split[1] : 0; + + return '.'.sprintf('%03d', $minor).sprintf('%03d', $revision); + } +} diff --git a/src/Converter/VersionConverterInterface.php b/src/Converter/VersionConverterInterface.php new file mode 100644 index 0000000..afa48d4 --- /dev/null +++ b/src/Converter/VersionConverterInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Converter; + +/** + * Interface for the converter for asset syntax version to composer syntax version. + * + * @author François Pluchino + */ +interface VersionConverterInterface +{ + /** + * Converts the asset version to composer version. + * + * @param string $version The asset version + * + * @return string The composer version + */ + public function convertVersion($version); +} diff --git a/src/Event/AbstractSolveEvent.php b/src/Event/AbstractSolveEvent.php new file mode 100644 index 0000000..7152cac --- /dev/null +++ b/src/Event/AbstractSolveEvent.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Event; + +use Composer\EventDispatcher\Event; +use Composer\Package\PackageInterface; + +/** + * Abstract event for solve event. + * + * @author François Pluchino + */ +abstract class AbstractSolveEvent extends Event +{ + /** + * @var string + */ + private $assetDir; + + /** + * @var PackageInterface[] The composer packages + */ + private $packages; + + /** + * Constructor. + * + * @param string $name The event name + * @param string $assetDir The directory of mock assets + * @param PackageInterface[] $packages All installed Composer packages + */ + public function __construct($name, $assetDir, array $packages) + { + parent::__construct($name, array(), array()); + + $this->assetDir = $assetDir; + $this->packages = $packages; + } + + /** + * Get the directory of mock assets. + * + * @return string + */ + public function getAssetDir() + { + return $this->assetDir; + } + + /** + * Get the installed Composer packages. + * + * @return PackageInterface[] + */ + public function getPackages() + { + return $this->packages; + } +} diff --git a/src/Event/GetAssetsEvent.php b/src/Event/GetAssetsEvent.php new file mode 100644 index 0000000..1be9591 --- /dev/null +++ b/src/Event/GetAssetsEvent.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Event; + +use Composer\Package\PackageInterface; +use Foxy\FoxyEvents; + +/** + * Get assets event. + * + * @author François Pluchino + */ +class GetAssetsEvent extends AbstractSolveEvent +{ + /** + * @var array + */ + private $assets; + + /** + * Constructor. + * + * @param string $assetDir The directory of mock assets + * @param PackageInterface[] $packages All installed Composer packages + * @param array $assets The map of asset package name and the asset package path + */ + public function __construct($assetDir, array $packages, array $assets) + { + parent::__construct(FoxyEvents::GET_ASSETS, $assetDir, $packages); + + $this->assets = $assets; + } + + /** + * Check if the asset package is present. + * + * @param string $name The asset package name + * + * @return bool + */ + public function hasAsset($name) + { + return isset($this->assets[$name]); + } + + /** + * Add the asset package. + * + * @param string $name The asset package name + * @param string $path The asset package path (relative path form root project + * and started with `file:`) + * + * Example: + * + * For the Composer package `foo/bar`. + * + * $event->addAsset('@composer-asset/foo--bar', + * 'file:./vendor/foxy/composer-asset/foo/bar'); + * + * @return self + */ + public function addAsset($name, $path) + { + $this->assets[$name] = $path; + + return $this; + } + + /** + * Get the map of asset package name and the asset package path. + * + * @return array + */ + public function getAssets() + { + return $this->assets; + } +} diff --git a/src/Event/PostSolveEvent.php b/src/Event/PostSolveEvent.php new file mode 100644 index 0000000..8ffb6f2 --- /dev/null +++ b/src/Event/PostSolveEvent.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Event; + +use Composer\Package\PackageInterface; +use Foxy\FoxyEvents; + +/** + * Post solve event. + * + * @author François Pluchino + */ +class PostSolveEvent extends AbstractSolveEvent +{ + /** + * @var int + */ + private $runResult; + + /** + * Constructor. + * + * @param string $assetDir The directory of mock assets + * @param PackageInterface[] $packages All installed Composer packages + * @param int $runResult The process result of asset manager execution + */ + public function __construct($assetDir, array $packages, $runResult) + { + parent::__construct(FoxyEvents::POST_SOLVE, $assetDir, $packages); + + $this->runResult = $runResult; + } + + /** + * Get the process result of asset manager execution. + * + * @return int + */ + public function getRunResult() + { + return $this->runResult; + } +} diff --git a/src/Event/PreSolveEvent.php b/src/Event/PreSolveEvent.php new file mode 100644 index 0000000..d5a0fdf --- /dev/null +++ b/src/Event/PreSolveEvent.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Event; + +use Composer\Package\PackageInterface; +use Foxy\FoxyEvents; + +/** + * Pre solve event. + * + * @author François Pluchino + */ +class PreSolveEvent extends AbstractSolveEvent +{ + /** + * Constructor. + * + * @param string $assetDir The directory of mock assets + * @param PackageInterface[] $packages All installed Composer packages + */ + public function __construct($assetDir, array $packages) + { + parent::__construct(FoxyEvents::PRE_SOLVE, $assetDir, $packages); + } +} diff --git a/src/Example.php b/src/Example.php deleted file mode 100644 index 4dfa14e..0000000 --- a/src/Example.php +++ /dev/null @@ -1,13 +0,0 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Exception; + +/** + * The interface of exception. + * + * @author François Pluchino + */ +interface ExceptionInterface +{ +} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php new file mode 100644 index 0000000..61acda2 --- /dev/null +++ b/src/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Exception; + +/** + * The Runtime Exception. + * + * @author François Pluchino + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Fallback/AssetFallback.php b/src/Fallback/AssetFallback.php new file mode 100644 index 0000000..da0d232 --- /dev/null +++ b/src/Fallback/AssetFallback.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Fallback; + +use Composer\IO\IOInterface; +use Composer\Util\Filesystem; +use Foxy\Config\Config; + +/** + * Asset fallback. + * + * @author François Pluchino + */ +class AssetFallback implements FallbackInterface +{ + /** + * @var IOInterface + */ + protected $io; + + /** + * @var Config + */ + protected $config; + + /** + * @var string + */ + protected $path; + + /** + * @var Filesystem + */ + protected $fs; + + /** + * @var null|string + */ + protected $originalContent; + + public function __construct(IOInterface $io, Config $config, $path, Filesystem $fs = null) + { + $this->io = $io; + $this->config = $config; + $this->path = $path; + $this->fs = $fs ?: new Filesystem(); + } + + /** + * {@inheritdoc} + */ + public function save() + { + if (file_exists($this->path) && is_file($this->path)) { + $this->originalContent = file_get_contents($this->path); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function restore() + { + if (!$this->config->get('fallback-asset')) { + return; + } + + $this->io->write('Fallback to previous state for the Asset package'); + $this->fs->remove($this->path); + + if (null !== $this->originalContent) { + file_put_contents($this->path, $this->originalContent); + } + } +} diff --git a/src/Fallback/ComposerFallback.php b/src/Fallback/ComposerFallback.php new file mode 100644 index 0000000..483c06d --- /dev/null +++ b/src/Fallback/ComposerFallback.php @@ -0,0 +1,231 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Fallback; + +use Composer\Composer; +use Composer\Factory; +use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; +use Composer\Installer; +use Composer\IO\IOInterface; +use Composer\Util\Filesystem; +use Foxy\Config\Config; +use Foxy\Util\ConsoleUtil; +use Foxy\Util\LockerUtil; +use Foxy\Util\PackageUtil; +use Symfony\Component\Console\Input\InputInterface; + +/** + * Composer fallback. + * + * @author François Pluchino + */ +class ComposerFallback implements FallbackInterface +{ + /** + * @var Composer + */ + protected $composer; + + /** + * @var IOInterface + */ + protected $io; + + /** + * @var Config + */ + protected $config; + + /** + * @var InputInterface + */ + protected $input; + + /** + * @var Filesystem + */ + protected $fs; + + /** + * @var null|Installer + */ + protected $installer; + + /** + * @var array + */ + protected $lock = array(); + + /** + * Constructor. + * + * @param Composer $composer The composer + * @param IOInterface $io The IO + * @param Config $config The config + * @param InputInterface $input The input + * @param null|Filesystem $fs The composer filesystem + * @param null|Installer $installer The installer + */ + public function __construct( + Composer $composer, + IOInterface $io, + Config $config, + InputInterface $input, + Filesystem $fs = null, + Installer $installer = null + ) { + $this->composer = $composer; + $this->io = $io; + $this->config = $config; + $this->input = $input; + $this->fs = $fs ?: new Filesystem(); + $this->installer = $installer; + } + + /** + * {@inheritdoc} + */ + public function save() + { + $rm = $this->composer->getRepositoryManager(); + $im = $this->composer->getInstallationManager(); + $composerFile = Factory::getComposerFile(); + $locker = LockerUtil::getLocker($this->io, $rm, $im, $composerFile); + + try { + $lock = $locker->getLockData(); + $this->lock = PackageUtil::loadLockPackages($lock); + } catch (\LogicException $e) { + $this->lock = array(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function restore() + { + if (!$this->config->get('fallback-composer')) { + return; + } + + $this->io->write('Fallback to previous state for Composer'); + $hasLock = $this->restoreLockData(); + + if ($hasLock) { + $this->restorePreviousLockFile(); + } else { + $this->fs->remove($this->composer->getConfig()->get('vendor-dir')); + } + } + + /** + * Restore the data of lock file. + * + * @return bool + */ + protected function restoreLockData() + { + $this->composer->getLocker()->setLockData( + $this->getLockValue('packages', array()), + $this->getLockValue('packages-dev'), + $this->getLockValue('platform', array()), + $this->getLockValue('platform-dev', array()), + $this->getLockValue('aliases', array()), + $this->getLockValue('minimum-stability', ''), + $this->getLockValue('stability-flags', array()), + $this->getLockValue('prefer-stable', false), + $this->getLockValue('prefer-lowest', false), + $this->getLockValue('platform-overrides', array()) + ); + + $isLocked = $this->composer->getLocker()->isLocked(); + $lockData = $isLocked ? $this->composer->getLocker()->getLockData() : null; + $hasPackage = \is_array($lockData) && isset($lockData['packages']) && !empty($lockData['packages']); + + return $isLocked && $hasPackage; + } + + /** + * Restore the PHP dependencies with the previous lock file. + */ + protected function restorePreviousLockFile() + { + $config = $this->composer->getConfig(); + list($preferSource, $preferDist) = ConsoleUtil::getPreferredInstallOptions($config, $this->input); + $optimize = $this->input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $authoritative = $this->input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); + $apcu = $this->input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); + $dispatcher = $this->composer->getEventDispatcher(); + + $installer = $this->getInstaller() + ->setVerbose($this->input->getOption('verbose')) + ->setPreferSource($preferSource) + ->setPreferDist($preferDist) + ->setDevMode(!$this->input->getOption('no-dev')) + ->setDumpAutoloader(!$this->input->getOption('no-autoloader')) + ->setOptimizeAutoloader($optimize) + ->setClassMapAuthoritative($authoritative) + ->setApcuAutoloader($apcu) + ; + + // @codeCoverageIgnoreStart + if (\defined('Composer\Composer::RUNTIME_API_VERSION') && version_compare(Composer::RUNTIME_API_VERSION, '2.2.0', '>=')) { + $ignorePlatformReqs = $this->input->getOption('ignore-platform-reqs') ?: ($this->input->getOption('ignore-platform-req') ?: false); + $installer->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); + $dispatcher->setRunScripts(false); + } else { + $installer + ->setRunScripts(false) + ->setIgnorePlatformRequirements($this->input->getOption('ignore-platform-reqs')) + ; + } + + if (method_exists($installer, 'setSkipSuggest')) { + $installer->setSkipSuggest(true); + } + // @codeCoverageIgnoreEnd + + $installer->run(); + + // @codeCoverageIgnoreStart + if (\defined('Composer\Composer::RUNTIME_API_VERSION') && version_compare(Composer::RUNTIME_API_VERSION, '2.2.0', '>=')) { + $dispatcher->setRunScripts(!$this->input->getOption('no-scripts')); + } + // @codeCoverageIgnoreEnd + } + + /** + * Get the lock value. + * + * @param string $key The key + * @param null|mixed $default The default value + * + * @return null|mixed + */ + private function getLockValue($key, $default = null) + { + return isset($this->lock[$key]) ? $this->lock[$key] : $default; + } + + /** + * Get the installer. + * + * @return Installer + */ + private function getInstaller() + { + return null !== $this->installer ? $this->installer : Installer::create($this->io, $this->composer); + } +} diff --git a/src/Fallback/FallbackInterface.php b/src/Fallback/FallbackInterface.php new file mode 100644 index 0000000..6c996cd --- /dev/null +++ b/src/Fallback/FallbackInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Fallback; + +/** + * Interface of fallback. + * + * @author François Pluchino + */ +interface FallbackInterface +{ + /** + * Save the state. + * + * @return self + */ + public function save(); + + /** + * Restore the state. + */ + public function restore(); +} diff --git a/src/Foxy.php b/src/Foxy.php new file mode 100644 index 0000000..279eeaa --- /dev/null +++ b/src/Foxy.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy; + +use Composer\Composer; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\Installer\PackageEvent; +use Composer\Installer\PackageEvents; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; +use Composer\Script\Event; +use Composer\Script\ScriptEvents; +use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; +use Foxy\Asset\AssetManagerFinder; +use Foxy\Asset\AssetManagerInterface; +use Foxy\Config\Config; +use Foxy\Config\ConfigBuilder; +use Foxy\Exception\RuntimeException; +use Foxy\Fallback\AssetFallback; +use Foxy\Fallback\ComposerFallback; +use Foxy\Solver\Solver; +use Foxy\Solver\SolverInterface; +use Foxy\Util\ComposerUtil; +use Foxy\Util\ConsoleUtil; + +/** + * Composer plugin. + * + * @author François Pluchino + */ +class Foxy implements PluginInterface, EventSubscriberInterface +{ + const REQUIRED_COMPOSER_VERSION = '^1.5.0|^2.0.0'; + + /** + * @var Config + */ + protected $config; + + /** + * @var AssetManagerInterface + */ + protected $assetManager; + + /** + * @var AssetFallback + */ + protected $assetFallback; + + /** + * @var ComposerFallback + */ + protected $composerFallback; + + /** + * @var SolverInterface + */ + protected $solver; + + /** + * @var bool + */ + protected $initialized = false; + + /** + * The list of the classes of asset managers. + */ + private static $assetManagers = array( + 'Foxy\Asset\NpmManager', + 'Foxy\Asset\PnpmManager', + 'Foxy\Asset\YarnManager', + ); + + /** + * The default values of config. + */ + private static $defaultConfig = array( + 'enabled' => true, + 'manager' => null, + 'manager-version' => array( + 'npm' => '>=5.0.0', + 'pnpm' => '>=7.0.0', + 'yarn' => '>=1.0.0', + ), + 'manager-bin' => null, + 'manager-options' => null, + 'manager-install-options' => null, + 'manager-update-options' => null, + 'manager-timeout' => null, + 'composer-asset-dir' => null, + 'run-asset-manager' => true, + 'fallback-asset' => true, + 'fallback-composer' => true, + 'enable-packages' => array(), + ); + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + ComposerUtil::getInitEventName() => array( + array('init', 100), + ), + PackageEvents::POST_PACKAGE_INSTALL => array( + array('initOnInstall', 100), + ), + ScriptEvents::POST_INSTALL_CMD => array( + array('solveAssets', 100), + ), + ScriptEvents::POST_UPDATE_CMD => array( + array('solveAssets', 100), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function activate(Composer $composer, IOInterface $io) + { + ComposerUtil::validateVersion(static::REQUIRED_COMPOSER_VERSION, Composer::VERSION); + + $input = ConsoleUtil::getInput($io); + $executor = new ProcessExecutor($io); + $fs = new Filesystem($executor); + + $this->config = ConfigBuilder::build($composer, self::$defaultConfig, $io); + $this->assetManager = $this->getAssetManager($io, $this->config, $executor, $fs); + $this->assetFallback = new AssetFallback($io, $this->config, $this->assetManager->getPackageName(), $fs); + $this->composerFallback = new ComposerFallback($composer, $io, $this->config, $input, $fs); + $this->solver = new Solver($this->assetManager, $this->config, $fs, $this->composerFallback); + + $this->assetManager->setFallback($this->assetFallback); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + // Do nothing + } + + public function uninstall(Composer $composer, IOInterface $io) + { + // Do nothing + } + + /** + * Init the plugin just after the first installation. + * + * @param PackageEvent $event The package event + */ + public function initOnInstall(PackageEvent $event) + { + $operation = $event->getOperation(); + + if ($operation instanceof InstallOperation && 'foxy/foxy' === $operation->getPackage()->getName()) { + $this->init(); + } + } + + /** + * Init the plugin. + */ + public function init() + { + if (!$this->initialized) { + $this->initialized = true; + $this->assetFallback->save(); + $this->composerFallback->save(); + + if ($this->config->get('enabled')) { + $this->assetManager->validate(); + } + } + } + + /** + * Set the solver. + * + * @param SolverInterface $solver The solver + */ + public function setSolver(SolverInterface $solver) + { + $this->solver = $solver; + } + + /** + * Solve the assets. + * + * @param Event $event The composer script event + */ + public function solveAssets(Event $event) + { + $this->solver->setUpdatable(false !== strpos($event->getName(), 'update')); + $this->solver->solve($event->getComposer(), $event->getIO()); + } + + /** + * Get the asset manager. + * + * @param IOInterface $io The IO + * @param Config $config The config + * @param ProcessExecutor $executor The process executor + * @param Filesystem $fs The composer filesystem + * + * @return AssetManagerInterface + * + * @throws RuntimeException When the asset manager is not found + */ + protected function getAssetManager(IOInterface $io, Config $config, ProcessExecutor $executor, Filesystem $fs) + { + $amf = new AssetManagerFinder(); + + foreach (self::$assetManagers as $class) { + $amf->addManager(new $class($io, $config, $executor, $fs)); + } + + return $amf->findManager($config->get('manager')); + } +} diff --git a/src/FoxyEvents.php b/src/FoxyEvents.php new file mode 100644 index 0000000..8736be6 --- /dev/null +++ b/src/FoxyEvents.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy; + +/** + * Events of Foxy. + * + * @author François Pluchino + */ +abstract class FoxyEvents +{ + /** + * The "PRE_SOLVE" event is triggered before the `solve` action of asset packages. + * + * @Event("Foxy\Event\PreSolveEvent") + */ + const PRE_SOLVE = 'foxy.pre-solve'; + + /** + * The "GET_ASSETS" event is triggered before the `solve` action of asset packages + * and during the retrieves the map of the asset packages. + * + * @Event("Foxy\Event\GetAssetsEvent") + */ + const GET_ASSETS = 'foxy.get-assets'; + + /** + * The "POST_SOLVE" event is triggered after the `solve` action of asset packages and before + * the execution of the composer's fallback. + * + * @Event("Foxy\Event\PostSolveEvent") + */ + const POST_SOLVE = 'foxy.post-solve'; +} diff --git a/src/Json/JsonFile.php b/src/Json/JsonFile.php new file mode 100644 index 0000000..c543845 --- /dev/null +++ b/src/Json/JsonFile.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Json; + +use Composer\Json\JsonFile as BaseJsonFile; + +/** + * The JSON file. + * + * @author François Pluchino + */ +class JsonFile extends BaseJsonFile +{ + /** + * @var string[] + */ + private $arrayKeys; + + /** + * @var int + */ + private $indent; + + /** + * @var string[] + */ + private static $encodeArrayKeys = array(); + + /** + * @var int + */ + private static $encodeIndent = JsonFormatter::DEFAULT_INDENT; + + /** + * Get the list of keys to be retained with an array representation if they are empty. + * + * @return string[] + */ + public function getArrayKeys() + { + if (null === $this->arrayKeys) { + $this->parseOriginalContent(); + } + + return $this->arrayKeys; + } + + /** + * Get the indent for this json file. + * + * @return int + */ + public function getIndent() + { + if (null === $this->indent) { + $this->parseOriginalContent(); + } + + return $this->indent; + } + + /** + * {@inheritdoc} + */ + public function read() + { + $data = parent::read(); + $this->getArrayKeys(); + $this->getIndent(); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function write(array $hash, int $options = 448) + { + self::$encodeArrayKeys = $this->getArrayKeys(); + self::$encodeIndent = $this->getIndent(); + parent::write($hash, $options); + self::$encodeArrayKeys = array(); + self::$encodeIndent = JsonFormatter::DEFAULT_INDENT; + } + + /** + * {@inheritdoc} + */ + public static function encode($data, int $options = 448, string $indent = self::INDENT_DEFAULT): string + { + $result = parent::encode($data, $options, $indent); + + return JsonFormatter::format($result, self::$encodeArrayKeys, self::$encodeIndent, false); + } + + /** + * Parse the original content. + */ + private function parseOriginalContent() + { + $content = $this->exists() ? file_get_contents($this->getPath()) : ''; + $this->arrayKeys = JsonFormatter::getArrayKeys($content); + $this->indent = JsonFormatter::getIndent($content); + } +} diff --git a/src/Json/JsonFormatter.php b/src/Json/JsonFormatter.php new file mode 100644 index 0000000..3b031bf --- /dev/null +++ b/src/Json/JsonFormatter.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Json; + +use Composer\Json\JsonFormatter as ComposerJsonFormatter; + +/** + * Formats JSON strings with a custom indent. + * + * @author François Pluchino + */ +class JsonFormatter +{ + const DEFAULT_INDENT = 4; + const ARRAY_KEYS_REGEX = '/["\']([\w\d_\-.]+)["\']:\s\[]/'; + const INDENT_REGEX = '/^[{\[][\r\n]([ ]+)["\']/'; + + /** + * Get the list of keys to be retained with an array representation if they are empty. + * + * @param string $content The content + * + * @return string[] + */ + public static function getArrayKeys($content) + { + preg_match_all(self::ARRAY_KEYS_REGEX, trim($content), $matches); + + return !empty($matches) ? $matches[1] : array(); + } + + /** + * Get the indent of file. + * + * @param string $content The content + * + * @return int + */ + public static function getIndent($content) + { + $indent = self::DEFAULT_INDENT; + preg_match(self::INDENT_REGEX, trim($content), $matches); + + if (!empty($matches)) { + $indent = \strlen($matches[1]); + } + + return $indent; + } + + /** + * Format the data in JSON. + * + * @param string $json The original JSON + * @param string[] $arrayKeys The list of keys to be retained with an array representation if they are empty + * @param int $indent The space count for indent + * @param bool $formatJson Check if the json must be formatted + * + * @return string + */ + public static function format($json, array $arrayKeys = array(), $indent = self::DEFAULT_INDENT, $formatJson = true) + { + if ($formatJson) { + $json = ComposerJsonFormatter::format($json, true, true); + } + + if (4 !== $indent) { + $json = str_replace(' ', sprintf('%'.$indent.'s', ''), $json); + } + + return self::replaceArrayByMap($json, $arrayKeys); + } + + /** + * Replace the empty array by empty map. + * + * @param string $json The original JSON + * @param string[] $arrayKeys The list of keys to be retained with an array representation if they are empty + * + * @return string + */ + private static function replaceArrayByMap($json, array $arrayKeys) + { + preg_match_all(self::ARRAY_KEYS_REGEX, $json, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + if (!\in_array($match[1], $arrayKeys, true)) { + $replace = str_replace('[]', '{}', $match[0]); + $json = str_replace($match[0], $replace, $json); + } + } + + return $json; + } +} diff --git a/src/Resources/doc/config.md b/src/Resources/doc/config.md new file mode 100644 index 0000000..46e5956 --- /dev/null +++ b/src/Resources/doc/config.md @@ -0,0 +1,368 @@ +Configuration +============= + +## Manipulate the configuration + +### Define the config for one project + +All options can be added in the `composer.json` file of the project in the `config.foxy.*` section. + +**Example:** + +```json +{ + "name": "root/package", + "config": { + "foxy": { + "enabled": false + } + } +} +``` + +### Define the config for all projects + +You can define the options in the `composer.json` file of each project, but you can also set an option +for all projects. + +To do this, you simply need to add your options in the Composer global configuration, +in the file of your choice: + +- `/composer.json` file +- `/config.json` file + +> **Note:** +> The `composer global config` command cannot be used, bacause Composer does not accept custom options. +> But you can use the command `composer global config -e` to edit the global `composer.json` file with +> your text editor. + +### Define the config in a environment variable + +You can define each option (`config.foxy.*`) directly in the PHP environment variables. For +this, all variables will start with `FOXY__` and uppercased, and each `-` will replaced by `_`. + +The accepted value types are: + +- string +- boolean +- integer +- JSON array or object + +**Example:** +```json +{ + "config": { + "foxy": { + "enabled": false + } + } +} +``` + +Can be overridden by `FOXY__ENABLED="false"` environment variable. + +**Example:** +```json +{ + "config": { + "foxy": { + "enable-packages": { + "foo/*": true + } + } + } +} +``` + +Can be overridden by `FOXY__ENABLE_PACKAGES="{"foo/*": true}"` environment variable. + +### Config priority order + +The config values are retrieved in priority in: + +1. the environment variables starting with `FOXY__` +2. the project `composer.json` file +3. the global `/config.json` file +4. the global `/composer.json` file + +### Define the config for multiple manager + +All keys starting with the prefix `manager-` can accept a unique value, but it will also can accept +a map containing the value for each manager defined by her name. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager-version": { + "npm": ">=5.0", + "yarn": ">=1.0.0" + } + } + } +} +``` + +> **Note:** +> +> This format is available only for the configuration file, and so, not available for the +> environment variables. + +## Use the config options + +### Enable/disable the plugin + +You can enable or disable the plugin with the option `config.foxy.enabled` [`boolean`, default: `true`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "enabled": false + } + } +} +``` + +### Choose the asset manager + +You can choose the asset manager with the option `config.foxy.manager` [`string`, default: `npm`]. + +**Available values:** + +- `npm` +- `yarn` + +**Example:** +```json +{ + "config": { + "foxy": { + "manager": "yarn" + } + } +} +``` + +### Lock the version of the asset manager with the Composer version range + +You can validate the version of the asset manager with the option +`config.foxy.manager-version` [`string`, default: `null`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager-version": "^5.3.0" + } + } +} +``` + +### Define the custom path of binary of the asset manager + +You can define the custom path of the binary of the asset manager with the option +`config.foxy.manager-bin` [`string`, default: `null`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager-bin": "/custom/path/of/asset/manager/binary" + } + } +} +``` + +### Override the install and update command options for the asset manager + +You can add custom options for the asset manager binary for the install and update commands with the +option `config.foxy.manager-options` [`string`, default: `null`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager": "yarn", + "manager-options": "--production=true --modules-folder=./assets" + } + } +} +``` + +> **Note:** +> +> It is rather recommended that you use the configuration files `.npmrc` for NPM, and `.yarnrc` for Yarn + +### Override the install command options for the asset manager + +You can add custom options for the asset manager binary for the install command with the +option `config.foxy.manager-install-options` [`string`, default: `null`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager": "npm", + "manager-install-options": "--dry-run" + } + } +} +``` + +> **Note:** +> +> For this example, the option allow you to keep only the manipulation of the asset package file, +> and validate the dependencies without the installation of the dependencies + +### Override the update command options for the asset manager + +You can add custom options for the asset manager binary for the update command with the +option `config.foxy.manager-update-options` [`string`, default: `null`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager": "yarn", + "manager-update-options": "--flat" + } + } +} +``` + +### Define the execution timeout of the asset manager + +You can define the execution timeout of the asset manager with the +option `config.foxy.manager-timeout` [`int`, default: `PHP_INT_MAX`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "manager-timeout": 420 + } + } +} +``` + +### Enable/disable the fallback for the asset package file of the project + +You can enable or disable the fallback of the asset package file with the option +`config.foxy.fallback-asset` [`boolean`, default: `true`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "fallback-asset": false + } + } +} +``` + +### Enable/disable the fallback for the Composer lock file and its dependencies + +You can enable or disable the fallback of the Composer lock file and its dependencies with the option +`config.foxy.fallback-composer` [`boolean`, default: `true`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "fallback-composer": false + } + } +} +``` + +### Enable/disable the running of asset manager + +You can enable or disable the running of the asset manager with the option +`config.foxy.run-asset-manager` [`boolean`, default: `true`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "run-asset-manager": false + } + } +} +``` + +> **Note:** +> +> This option allow you to keep only the manipulation of the asset package file, +> without the execution of the asset manager + +### Define the custom path of the mock package of PHP library + +You can define the custom path of the mock package of PHP library with the option +`config.foxy.composer-asset-dir` [`string`, default: `null`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "composer-asset-dir": "./my/mock/asset/path/of/project" + } + } +} +``` + +### Enable/disable manually the PHP packages with an asset package definition + +By default, Foxy looks in the `composer.json` file of the PHP dependencies, if the mock package needs +to be added into NPM or Yarn. However, some public PHP package already uses the `package.json` file +to handle their asset dependencies, but Foxy is not enabled for this package. In this case, you can +manually enable the PHP packages to be scanned in your project. + +Patterns can be written in Glob style or with regular expressions. In this case, the pattern must +start and end with slash (`/`). + +You can define the patterns to enable or disable the packages with the option +`config.foxy.enable-packages` [`array`, default: `array()`]. + +**Example:** +```json +{ + "config": { + "foxy": { + "enable-packages": { + "/^bar\/*/": true, + "foo/*": true, + "baz/test-*": false + } + } + } +} +``` + +If you do not deactivate any packages, you can use a simple array. + +**Example:** +```json +{ + "config": { + "foxy": { + "enable-packages": [ + "/^bar\/*/", + "foo/*" + ] + } + } +} +``` diff --git a/src/Resources/doc/events.md b/src/Resources/doc/events.md new file mode 100644 index 0000000..234abaa --- /dev/null +++ b/src/Resources/doc/events.md @@ -0,0 +1,28 @@ +Events +====== + +Foxy triggers events with the event dispatcher system of Composer allowing you to extend Foxy's +capabilities. To do this, you must create a Composer plugin requiring Foxy in addition to the +requirements required for the creation of a Composer plugin. You can read the documentation of +Composer: [Setting up and using plugin](https://getcomposer.org/doc/articles/plugins.md). + +## Event names + +All event names are listed in constants of the `Foxy\FoxyEvents` class containing the name of +the event class for each event. + +### pre-solve + +The `foxy.pre-solve` event occurs before the `solve` action of asset packages and after the +Composer's command events `post-install-cmd` and `post-update-cmd`. + +### get-assets + +The `foxy.get-assets` event occurs before the `solve` action of asset packages and during the +retrieves the map of the asset packages. It is in this event that you can add new asset packages +in the map. + +### post-solve + +The `foxy.post-solve` event occurs after the `solve` action of asset packages and before the +execution of the Composer's fallback. diff --git a/src/Resources/doc/faqs.md b/src/Resources/doc/faqs.md new file mode 100644 index 0000000..d22e2e9 --- /dev/null +++ b/src/Resources/doc/faqs.md @@ -0,0 +1,124 @@ +FAQs +==== + +What version required of Composer? +---------------------------------- + +See the documentation: [Installation](index.md#installation). + +Why this plugin? +---------------- + +It is certain that each language has its own dependency management system, and that it is highly recommended to use +each package manager. NPM or Yarn works very well when the asset dependencies are managed only in the PHP project, +but when you create PHP libraries that using assets, there is no way to automatically add asset dependencies, +and most importantly, no validation of versions can be done automatically. You must tell the developers +the list of asset dependencies that using by your PHP library, and you must ask him to add manually the asset +dependencies to its asset manager of his project. + +However, another solution exist - what many projects propose - you must add the assets in the folder of the +PHP library (like `/assets`, `/Resources/public`). Of course, with this method, the code is duplicated, it +pollutes the source code of the PHP library, no version management/validation is possible, and it is even +less possible, to use correctly all tools such as Babel, Scss, Less, etc ... + +Foxy focuses solely on automation of the validation, addition, updating and deleting of the dependencies in +the definition file of the asset package, while restoring the project state, as well as PHP dependencies if +NPM or Yarn terminates with an error. + +What is the difference between Foxy and Fxp Composer Asset Plugin? +------------------------------------------------------------------ + +When [Fxp Composer Asset Plugin](https://github.com/fxpio/composer-asset-plugin) has been created, +it lacked some important functionality to NPM and Bower as a true lock file, the access to +private repositories, the management of organizations (scope), and the assets was limited to a simple +download of the packages. The solution was to use the SAT solver, the VCS Repositories and the +Composer lock file to manage the asset dependencies of the PHP libraries. However, there are 3 major +disadvantages to this approach: + +1. The plugin must be installed in global mode +2. Nodejs must be used more and more to compile some libraries +3. The use of VCS Repositories coupled with the SAT Solver architecture of Composer is much less + efficient than NPM, despite the optimizations of the plugin to avoid the imports + +Now, Bower has been depreciated, NPM has a true lock file (since 5.x), as well as the possibility +of using the private repositories, Yarn arrived with his big performances, and more and more javascript +library requires a compilation because they use Babel, Typescript, Sass, Less, etc... + +Nodejs filling its gaps, and becoming more and more required, a plugin could finally perform the reverse operation, +retaining the benefits of Fxp Composer Asset Plugin and NPM. So, conversely, Foxy creates package mocks for NPM +in local directory, containing only the `package.json` file from the PHP library, and adding the path of the +mock package to the project's `package.json` file. The entire validation and installation process is left +to NPM or Yarn. However, the plugin manages the fallback if there is an error of the asset manager. + +To conclude, given that there is not a backward compatibility, and that it is impossible to have a version +of the plugin installed globally, and another version installed in the project - because Composer will +install the plugin in the project, but will only use the plugin installed globally - the Fxp Composer Asset +Plugin was become Foxy. + +How does the plugin work? +------------------------- + +Foxy creates the mocks of Composer packages for NPM in local directory, containing only the `package.json` +file from the PHP library, and adding the package path to the `package.json` file of the project. + +The name of the Composer package is converted to a format compatible with NPM, using the NPM scope +`@composer-asset` and replacing the separation slash `/` between the vendor and the package name by +2 dashes `--`, giving consequently, the following format `@composer-asset/--` +(this scope is reserved by Foxy in the registry of NPM and in Github). + +NPM will install in the `node_modules/@composer-asset` folder an updated copy of each Composer package mock +that is located by default in the folder `vendor/foxy/composer-asset`. + +For more details, the plugin work in this order: + +1. Validation of the asset manager installation, then checking of the compatible asset manager version (optional) +2. Saving the status of project +3. Installing/updating of the PHP dependencies by Composer +4. Retrieving the entire list of installed packages +5. Retains only PHP dependencies with the `foxy/foxy` dependency in the `require` or `require-dev` section of + the `composer.json` file and with the presence of the `package.json` file +6. Checking the lock file of asset manager +7. Comparing the difference between the installed asset dependencies and the new asset dependencies, to determine + whether the dependency must be installed, updated, or removed +8. Creating, updating, or deleting of the mock asset libraries in local directory, containing only the + `package.json` file of the PHP library, with a formatted name as: + `@composer-asset/--` +9. Adding, updating, or deleting the mock asset library in the `package.json` file of the project +10. Running the install or update command of asset manager +11. Restoring the `package.json` file with the previous dependencies if the asset manager terminates with an error +12. Restoring the `composer.lock` file and all PHP dependencies if the asset manager terminates with an error + +Is Foxy useful if my asset dependencies are defined only in my project? +----------------------------------------------------------------------- + +Foxy is mainly focused on automating of the asset management of the PHP libraries, avoiding potentially conflicting +manual management. + +Given that Foxy makes it possible to ensure that the entire management process is valid whether it is for Composer +and NPM or Yarn, you can use Foxy even if all of your asset dependencies are only defined in the `package.json` file +of your project. However, the value added by Foxy in this configuration will be low, and will be limited to the +management of the fallback of the PHP dependencies if there is an error of the asset manager. + +NPM/Yarn does not find the mock of the Composer dependencies +------------------------------------------------------------ + +The advantage of Foxy, is that it allows you to keep the workflows of each tool. However, Foxy creates PHP +package mocks for NPM, and in this case, Composer must be launched before NPM or Yarn. After, nothing prevents +you to using all available commands of your favorite asset manager. + +Why Foxy does nothing with the '--dry-run' option? +-------------------------------------------------- + +Foxy can work with Composer's `--dry-run` option, but chose to do nothing. Given that the PHP dependencies +are not installed, updated or deleted, Foxy can not update the `package.json` file, and so, NPM can not +check the new constraints, if any. To sum up, this amounts to running the commands +`composer update --dry-run` followed by `npm update --dry-run`. + +However, with the Foxy's fallbacks, this behavior is automatically reproduced, but by downloading the PHP +dependencies, and restoring the `package.json` file, the `composer.lock` file, and all the PHP dependencies +if the asset manager finishes with an error. + +How to increase the PHP memory limit? +------------------------------------- + +See the official documentation of Composer: [Memory limits errors](https://getcomposer.org/doc/articles/troubleshooting.md#memory-limit-errors). diff --git a/src/Resources/doc/index.md b/src/Resources/doc/index.md new file mode 100644 index 0000000..f943df2 --- /dev/null +++ b/src/Resources/doc/index.md @@ -0,0 +1,47 @@ +Getting started +=============== + +1. [Introduction](index.md#introduction) +2. [Required dependencies](index.md#required-dependencies) +3. [Installation](index.md#installation) +4. [Usage](usage.md) +5. [Configuration](config.md) +6. [Event](events.md) +7. [FAQs](faqs.md) + +## Introduction + +Foxy is a Composer plug-in that aggregates npm-packages from Composer packages. + +This makes it possible (and automates the process of) installing and updating npm-packages that ship with your Composer packages, leveraging the native (`npm` or `yarn`) package manager to do the heavy lifting. + +For this approach to work well, you should think of an npm-package in a Composer package not just as an "artifact", but as an actual npm-package *embedded* in your Composer package. + +Importantly, you should name it and *version* it, independently of your Composer version number - like you would normally do with a stand-alone npm-package. + +Note that, for npm-packages with no version number, Foxy will default to the Composer version, as a fallback only: versioning your npm-package explicitly is much safer in terms of correctly versioning breaking/non-breaking changes to any client-side APIs exposed by the embedded npm-package. + +## Required dependencies + +- [Nodejs](https://nodejs.org) +- [NPM](https://www.npmjs.com) or [Yarn](https://yarnpkg.com) +- [Git](https://git-scm.com) + +## Installation + +See the [Release Notes](https://github.com/fxpio/foxy/releases) +to know the Composer version required. + +```shell +composer require "foxy/foxy:^1.0.0" +``` + +Composer will install the plugin to your project's `vendor/foxy` directory. + +## Next step + +You can read how to: + +- [Use this plugin](usage.md) +- [Configure this plugin](config.md) +- [Expand Foxy with Composer events](events.md) diff --git a/src/Resources/doc/usage.md b/src/Resources/doc/usage.md new file mode 100644 index 0000000..6521e48 --- /dev/null +++ b/src/Resources/doc/usage.md @@ -0,0 +1,68 @@ +Usage +===== + +To use Foxy, whether for a PHP library or a PHP project, you must add the Foxy dependency in +the `require` section of the Composer file. + +**composer.json:** +```json +{ + "require": { + "foxy/foxy": "^1.0.0" + } +} +``` + +And create the `package.json` file to add your asset dependencies. + +**package.json:** +```json +{ + "dependencies": { + "@foo/bar": "latest" + } +} +``` + +Composer will install Foxy automatically before installing the PHP dependencies, and Foxy will immediately +deal with the asset dependencies. + +## Use the plugin in PHP library + +### With the Foxy dependency + +In the case if you use Foxy in a PHP library, you can render Foxy optional by adding its dependency in +the `require-dev` section of the Composer file. + +**composer.json:** +```json +{ + "require-dev": { + "foxy/foxy": "^1.0.0" + } +} +``` + +> **Note:** +> +> If no PHP dependency requires Foxy inevitably (always in `require-dev` section), you must add Foxy +> in the required dependencies to the `composer.json` file of your project. + +### With the Composer's extra option + +However, if you want enable the Foxy for your library, but without required dependencies or dev dependencies, +you can use the extra option `extra.foxy` in your `composer.json` file: + +**composer.json:** +```json +{ + "extra": { + "foxy": true + } +} +``` + +> **Note:** +> +> Like for the activation with the Foxy dependencies, you must add Foxy in the required dependencies +> to the `composer.json` file of your project. diff --git a/src/Solver/Solver.php b/src/Solver/Solver.php new file mode 100644 index 0000000..063d6b8 --- /dev/null +++ b/src/Solver/Solver.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Solver; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; +use Composer\Package\PackageInterface; +use Composer\Util\Filesystem; +use Foxy\Asset\AssetManagerInterface; +use Foxy\Config\Config; +use Foxy\Event\GetAssetsEvent; +use Foxy\Event\PostSolveEvent; +use Foxy\Event\PreSolveEvent; +use Foxy\Fallback\FallbackInterface; +use Foxy\FoxyEvents; +use Foxy\Util\AssetUtil; + +/** + * Solver of asset dependencies. + * + * @author François Pluchino + */ +class Solver implements SolverInterface +{ + /** + * @var Config + */ + protected $config; + + /** + * @var Filesystem + */ + protected $fs; + + /** + * @var AssetManagerInterface + */ + protected $assetManager; + + /** + * @var null|FallbackInterface + */ + protected $composerFallback; + + /** + * Constructor. + * + * @param AssetManagerInterface $assetManager The asset manager + * @param Config $config The config + * @param Filesystem $filesystem The composer filesystem + * @param null|FallbackInterface $composerFallback The composer fallback + */ + public function __construct( + AssetManagerInterface $assetManager, + Config $config, + Filesystem $filesystem, + FallbackInterface $composerFallback = null + ) { + $this->config = $config; + $this->fs = $filesystem; + $this->assetManager = $assetManager; + $this->composerFallback = $composerFallback; + } + + /** + * {@inheritdoc} + */ + public function setUpdatable($updatable) + { + $this->assetManager->setUpdatable($updatable); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function solve(Composer $composer, IOInterface $io) + { + if (!$this->config->get('enabled')) { + return; + } + + $dispatcher = $composer->getEventDispatcher(); + $packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); + $vendorDir = $composer->getConfig()->get('vendor-dir'); + $assetDir = $this->config->get('composer-asset-dir', $vendorDir.'/foxy/composer-asset/'); + $dispatcher->dispatch(FoxyEvents::PRE_SOLVE, new PreSolveEvent($assetDir, $packages)); + $this->fs->remove($assetDir); + + $assets = $this->getAssets($composer, $assetDir, $packages); + $this->assetManager->addDependencies($composer->getPackage(), $assets); + $res = $this->assetManager->run(); + $dispatcher->dispatch(FoxyEvents::POST_SOLVE, new PostSolveEvent($assetDir, $packages, $res)); + + if ($res > 0 && $this->composerFallback) { + $this->composerFallback->restore(); + + throw new \RuntimeException('The asset manager ended with an error'); + } + } + + /** + * Get the package of asset dependencies. + * + * @param Composer $composer The composer + * @param string $assetDir The asset directory + * @param PackageInterface[] $packages The package dependencies + * + * @return array[] + */ + protected function getAssets(Composer $composer, $assetDir, array $packages) + { + $installationManager = $composer->getInstallationManager(); + $configPackages = $this->config->getArray('enable-packages'); + $assets = array(); + + foreach ($packages as $package) { + $filename = AssetUtil::getPath($installationManager, $this->assetManager, $package, $configPackages); + + if (null !== $filename) { + list($packageName, $packagePath) = $this->getMockPackagePath($package, $assetDir, $filename); + $assets[$packageName] = $packagePath; + } + } + + $assetsEvent = new GetAssetsEvent($assetDir, $packages, $assets); + $composer->getEventDispatcher()->dispatch(FoxyEvents::GET_ASSETS, $assetsEvent); + + return $assetsEvent->getAssets(); + } + + /** + * Get the path of the mock package. + * + * @param PackageInterface $package The package dependency + * @param string $assetDir The asset directory + * @param string $filename The filename of asset package + * + * @return string[] The package name and the relative package path from the current directory + */ + protected function getMockPackagePath(PackageInterface $package, $assetDir, $filename) + { + $packageName = AssetUtil::getName($package); + $packagePath = rtrim($assetDir, '/').'/'.$package->getName(); + $newFilename = $packagePath.'/'.basename($filename); + mkdir($packagePath, 0777, true); + copy($filename, $newFilename); + + $jsonFile = new JsonFile($newFilename); + $packageValue = AssetUtil::formatPackage($package, $packageName, (array) $jsonFile->read()); + + $jsonFile->write($packageValue); + + return array($packageName, $this->fs->findShortestPath(getcwd(), $newFilename)); + } +} diff --git a/src/Solver/SolverInterface.php b/src/Solver/SolverInterface.php new file mode 100644 index 0000000..ab79148 --- /dev/null +++ b/src/Solver/SolverInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Solver; + +use Composer\Composer; +use Composer\IO\IOInterface; + +/** + * Interface of solver. + * + * @author François Pluchino + */ +interface SolverInterface +{ + /** + * Define if the update action can be used. + * + * @param bool $updatable The value + * + * @return self + */ + public function setUpdatable($updatable); + + /** + * Solve the asset dependencies. + * + * @param Composer $composer The composer + * @param IOInterface $io The IO + */ + public function solve(Composer $composer, IOInterface $io); +} diff --git a/src/Util/AssetUtil.php b/src/Util/AssetUtil.php new file mode 100644 index 0000000..c6085c3 --- /dev/null +++ b/src/Util/AssetUtil.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Util; + +use Composer\Installer\InstallationManager; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Foxy\Asset\AssetManagerInterface; +use Foxy\Asset\AssetPackage; + +/** + * Helper for Foxy. + * + * @author François Pluchino + */ +class AssetUtil +{ + /** + * Get the name for the asset dependency. + * + * @param PackageInterface $package The package + * + * @return string + */ + public static function getName(PackageInterface $package) + { + return AssetPackage::COMPOSER_PREFIX.str_replace(array('/'), '--', $package->getName()); + } + + /** + * Get the path of asset file. + * + * @param InstallationManager $installationManager The installation manager + * @param AssetManagerInterface $assetManager The asset manager + * @param PackageInterface $package The package + * @param array $configPackages The packages defined in config + * + * @return null|string + */ + public static function getPath(InstallationManager $installationManager, AssetManagerInterface $assetManager, PackageInterface $package, array $configPackages = array()) + { + $path = null; + + if (static::isAsset($package, $configPackages)) { + $installPath = $installationManager->getInstallPath($package); + $filename = $installPath.'/'.$assetManager->getPackageName(); + $path = file_exists($filename) ? str_replace('\\', '/', realpath($filename)) : null; + } + + return $path; + } + + /** + * Check if the package is available for Foxy. + * + * @param PackageInterface $package The package + * @param array $configPackages The packages defined in config + * + * @return bool + */ + public static function isAsset(PackageInterface $package, array $configPackages = array()) + { + $projectConfig = self::getProjectActivation($package, $configPackages); + $enabled = false !== $projectConfig; + + return $enabled && (static::hasExtraActivation($package) + || static::hasPluginDependency($package->getRequires()) + || static::hasPluginDependency($package->getDevRequires()) + || true === $projectConfig); + } + + /** + * Check if foxy is enabled in extra section of package. + * + * @param PackageInterface $package The package + * + * @return bool + */ + public static function hasExtraActivation(PackageInterface $package) + { + $extra = $package->getExtra(); + + return isset($extra['foxy']) && true === $extra['foxy']; + } + + /** + * Check if the package contains assets. + * + * @param Link[] $requires The require links + * + * @return bool + */ + public static function hasPluginDependency(array $requires) + { + $assets = false; + + foreach ($requires as $require) { + if ('foxy/foxy' === $require->getTarget()) { + $assets = true; + + break; + } + } + + return $assets; + } + + /** + * Check if the package is enabled by the project config. + * + * @param PackageInterface $package The package + * @param array $configPackages The packages defined in config + * + * @return bool + */ + public static function isProjectActivation(PackageInterface $package, array $configPackages) + { + return true === self::getProjectActivation($package, $configPackages); + } + + /** + * Format the asset package. + * + * @param PackageInterface $package The composer package + * @param string $packageName The package name + * @param array $packageValue The package value + * + * @return array + */ + public static function formatPackage(PackageInterface $package, $packageName, array $packageValue) + { + $packageValue['name'] = $packageName; + + if (!isset($packageValue['version'])) { + $extra = $package->getExtra(); + $version = $package->getPrettyVersion(); + + if (0 === strpos($version, 'dev-') && isset($extra['branch-alias'][$version])) { + $version = $extra['branch-alias'][$version]; + } + + $packageValue['version'] = self::formatVersion(str_replace('-dev', '', $version)); + } + + return $packageValue; + } + + /** + * Format the version for the asset package. + * + * @param string $version The branch alias version + * + * @return string + */ + private static function formatVersion($version) + { + $version = str_replace(array('x', 'X', '*'), '0', $version); + $exp = explode('.', $version); + + if (($size = \count($exp)) < 3) { + for ($i = $size; $i < 3; ++$i) { + $exp[] = '0'; + } + } + + return $exp[0].'.'.$exp[1].'.'.$exp[2]; + } + + /** + * Get the activation of the package defined in the project config. + * + * @param PackageInterface $package The package + * @param array $configPackages The packages defined in config + * + * @return null|bool returns NULL, if the package isn't defined in the project config + */ + private static function getProjectActivation(PackageInterface $package, array $configPackages) + { + $name = $package->getName(); + $value = null; + + foreach ($configPackages as $pattern => $activation) { + if (\is_int($pattern) && \is_string($activation)) { + $pattern = $activation; + $activation = true; + } + + if ((0 === strpos($pattern, '/') && preg_match($pattern, $name)) || fnmatch($pattern, $name)) { + $value = $activation; + + break; + } + } + + return $value; + } +} diff --git a/src/Util/ComposerUtil.php b/src/Util/ComposerUtil.php new file mode 100644 index 0000000..10606d4 --- /dev/null +++ b/src/Util/ComposerUtil.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Util; + +use Composer\Installer\InstallerEvents; +use Composer\Semver\Semver; +use Foxy\Exception\RuntimeException; + +/** + * Helper for Composer. + * + * @author François Pluchino + */ +class ComposerUtil +{ + /** + * Get the event name to init the plugin. + * + * @return string + */ + public static function getInitEventName() + { + return \defined('Composer\Installer\InstallerEvents::PRE_DEPENDENCIES_SOLVING') + ? InstallerEvents::PRE_DEPENDENCIES_SOLVING : InstallerEvents::PRE_OPERATIONS_EXEC; + } + + /** + * Validate the composer version. + * + * @param string $requiredVersion The composer required version + * @param string $composerVersion The composer version + */ + public static function validateVersion($requiredVersion, $composerVersion) + { + $isBranch = false !== strpos($composerVersion, '@'); + $isSnapshot = (bool) preg_match('/^[0-9a-f]{40}$/i', $composerVersion); + + if (!$isBranch && !$isSnapshot && !Semver::satisfies($composerVersion, $requiredVersion)) { + $msg = 'Foxy requires the Composer\'s minimum version "%s", current version is "%s"'; + + throw new RuntimeException(sprintf($msg, $requiredVersion, $composerVersion)); + } + } +} diff --git a/src/Util/ConsoleUtil.php b/src/Util/ConsoleUtil.php new file mode 100644 index 0000000..afc5360 --- /dev/null +++ b/src/Util/ConsoleUtil.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Util; + +use Composer\Config; +use Composer\IO\IOInterface; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; + +/** + * Helper for console. + * + * @author François Pluchino + */ +class ConsoleUtil +{ + /** + * Get the console input. + * + * @param IOInterface $io The IO + * + * @return InputInterface + */ + public static function getInput(IOInterface $io) + { + $ref = new \ReflectionClass($io); + + if ($ref->hasProperty('input')) { + $prop = $ref->getProperty('input'); + $prop->setAccessible(true); + $input = $prop->getValue($io); + + if ($input instanceof InputInterface) { + return $input; + } + } + + return new ArgvInput(); + } + + /** + * Returns preferSource and preferDist values based on the configuration. + * + * @param Config $config The composer config + * @param InputInterface $input The console input + * + * @return bool[] An array composed of the preferSource and preferDist values + */ + public static function getPreferredInstallOptions(Config $config, InputInterface $input) + { + $preferSource = false; + $preferDist = false; + + switch ($config->get('preferred-install')) { + case 'source': + $preferSource = true; + + break; + + case 'dist': + $preferDist = true; + + break; + + case 'auto': + default: + break; + } + + if ($input->getOption('prefer-source') || $input->getOption('prefer-dist')) { + $preferSource = $input->getOption('prefer-source'); + $preferDist = $input->getOption('prefer-dist'); + } + + return array($preferSource, $preferDist); + } +} diff --git a/src/Util/LockerUtil.php b/src/Util/LockerUtil.php new file mode 100644 index 0000000..12ce7ed --- /dev/null +++ b/src/Util/LockerUtil.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Util; + +use Composer\Composer; +use Composer\Installer\InstallationManager; +use Composer\IO\IOInterface; +use Composer\Json\JsonFile; +use Composer\Package\Locker; +use Composer\Repository\RepositoryManager; + +/** + * Helper for Locker. + * + * @author François Pluchino + */ +class LockerUtil +{ + /** + * Get the locker. + * + * @param string $composerFile + * + * @return Locker + */ + public static function getLocker(IOInterface $io, RepositoryManager $rm, InstallationManager $im, $composerFile) + { + $lockFile = str_replace('.json', '.lock', $composerFile); + // @codeCoverageIgnoreStart + return \defined('Composer\Composer::RUNTIME_API_VERSION') && version_compare(Composer::RUNTIME_API_VERSION, '2.0.0', '>=') + ? new Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)) + : new Locker($io, new JsonFile($lockFile, null, $io), $rm, $im, file_get_contents($composerFile)); + } +} diff --git a/src/Util/PackageUtil.php b/src/Util/PackageUtil.php new file mode 100644 index 0000000..d5aac0a --- /dev/null +++ b/src/Util/PackageUtil.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Foxy\Util; + +use Composer\Package\AliasPackage; +use Composer\Package\Loader\ArrayLoader; + +/** + * Helper for package. + * + * @author François Pluchino + */ +class PackageUtil +{ + /** + * Load all packages in the lock data of locker. + * + * @param array $lockData The lock data of locker + * + * @return array The lock data + */ + public static function loadLockPackages(array $lockData) + { + $loader = new ArrayLoader(); + $lockData = static::loadLockPackage($loader, $lockData); + $lockData = static::loadLockPackage($loader, $lockData, true); + $lockData = static::convertLockAlias($lockData); + + return $lockData; + } + + /** + * Load the packages in the packages section of the locker load data. + * + * @param ArrayLoader $loader The package loader + * @param array $lockData The lock data of locker + * @param bool $dev Check if the dev packages must be loaded + * + * @return array The lock data + */ + public static function loadLockPackage(ArrayLoader $loader, array $lockData, $dev = false) + { + $key = $dev ? 'packages-dev' : 'packages'; + + if (isset($lockData[$key])) { + foreach ($lockData[$key] as $i => $package) { + $package = $loader->load($package); + $lockData[$key][$i] = $package instanceof AliasPackage ? $package->getAliasOf() : $package; + } + } + + return $lockData; + } + + /** + * Convert the package aliases of the locker load data. + * + * @param array $lockData The lock data of locker + * + * @return array The lock data + */ + public static function convertLockAlias(array $lockData) + { + if (isset($lockData['aliases'])) { + $aliases = array(); + + foreach ($lockData['aliases'] as $i => $config) { + $aliases[$config['package']][$config['version']] = array( + 'alias' => $config['alias'], + 'alias_normalized' => $config['alias_normalized'], + ); + } + + $lockData['aliases'] = $aliases; + } + + return $lockData; + } +} diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 4e7a602..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue($example->getExample()); - } -}