├── .github └── workflows │ ├── code-style.yml │ ├── integration-tests.yml │ ├── phpmd.yml │ ├── phpstan.yml │ └── unit-tests.yml ├── .gitignore ├── .php-cs-fixer.php ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── phpstan.neon ├── phpunit.xml ├── ruleset.xml ├── src ├── CommandProvider.php ├── Commands │ ├── Command.php │ ├── LinkCommand.php │ ├── LinkedCommand.php │ ├── UnlinkAllCommand.php │ └── UnlinkCommand.php ├── InstallerFactory.php ├── LinkManager.php ├── LinkManagerFactory.php ├── Package │ ├── LinkedPackage.php │ └── LinkedPackageFactory.php ├── PathHelper.php ├── Plugin.php └── Repository │ ├── JsonStorage.php │ ├── Repository.php │ ├── RepositoryFactory.php │ ├── StorageInterface.php │ └── Transformer.php ├── support ├── Dockerfile └── docker-entrypoint.sh └── tests ├── Integration ├── LinuxExtraTest.php ├── LinuxMacosBasicTest.php ├── TestCase.php └── WindowsBasicTest.php ├── TestCase.php ├── Unit ├── CommandProviderTest.php ├── Commands │ ├── LinkCommandTest.php │ ├── LinkedCommandTest.php │ ├── UnlinkAllCommandTest.php │ └── UnlinkCommandTest.php ├── InstallerFactoryTest.php ├── LinkManagerFactoryTest.php ├── LinkManagerTest.php ├── Package │ ├── LinkedPackageFactoryTest.php │ └── LinkedPackageTest.php ├── PathHelperTest.php ├── PluginTest.php ├── Repository │ ├── JsonStorageTest.php │ ├── RepositoryFactoryTest.php │ ├── RepositoryTest.php │ └── TransformerTest.php └── TestCase.php └── mock ├── package-1 └── composer.json ├── package-2 └── composer.json ├── package-3 └── composer.json └── psr-container └── composer.json /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - v* 8 | pull_request: 9 | types: 10 | - synchronize 11 | - opened 12 | 13 | jobs: 14 | code-style: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: '8.3' 23 | 24 | - name: Validate composer.json 25 | run: composer validate --strict 26 | 27 | - name: Install dependencies 28 | run: composer install --prefer-dist --no-progress 29 | 30 | - name: Run style check 31 | run: ./vendor/bin/php-cs-fixer fix --dry-run 32 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | push: 7 | branches: 8 | - master 9 | - v* 10 | pull_request: 11 | types: 12 | - synchronize 13 | - opened 14 | - reopened 15 | - ready_for_review 16 | 17 | jobs: 18 | integration-tests: 19 | if: ${{ !github.event.pull_request.draft }} 20 | runs-on: ${{ matrix.operating-system }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | operating-system: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 25 | # First is php version, second composer version 26 | versions: [['8.1', '2.6'], ['8.3', '2.7'], ['8.4', 'v2']] 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.versions[0] }} 34 | tools: composer:${{ matrix.versions[1] }} 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Validate composer.json 39 | run: composer validate --strict 40 | 41 | - name: Install dependencies 42 | run: composer install --prefer-dist --no-progress 43 | 44 | - name: Run unit tests 45 | env: 46 | PHPUNIT_INTEGRATION: 1 47 | run: ./vendor/bin/phpunit --testsuite=Integration --group=${{ matrix.operating-system }} 48 | -------------------------------------------------------------------------------- /.github/workflows/phpmd.yml: -------------------------------------------------------------------------------- 1 | name: Code Static Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - v* 8 | pull_request: 9 | types: 10 | - synchronize 11 | - opened 12 | 13 | jobs: 14 | phpmd: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: '8.3' 23 | 24 | - name: Validate composer.json 25 | run: composer validate --strict 26 | 27 | - name: Install dependencies 28 | run: composer install --prefer-dist --no-progress 29 | 30 | - name: Run phpmd 31 | run: ./vendor/bin/phpmd ./src,./tests github ruleset.xml 32 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: Code Static Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - v* 8 | pull_request: 9 | types: 10 | - synchronize 11 | - opened 12 | 13 | jobs: 14 | phpstan: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: '8.3' 23 | 24 | - name: Validate composer.json 25 | run: composer validate --strict 26 | 27 | - name: Install dependencies 28 | run: composer install --prefer-dist --no-progress 29 | 30 | - name: Run static analyse 31 | run: ./vendor/bin/phpstan analyse --memory-limit=2G --error-format=github 32 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - v* 8 | pull_request: 9 | types: 10 | - synchronize 11 | - opened 12 | 13 | jobs: 14 | unit-tests: 15 | runs-on: ${{ matrix.operating-system }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | operating-system: ['ubuntu-latest', 'windows-latest', 'macos-latest'] 20 | php-versions: [ '8.1', '8.4' ] 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php-versions }} 28 | 29 | - name: Validate composer.json 30 | run: composer validate --strict 31 | 32 | - name: Install dependencies 33 | run: composer install --prefer-dist --no-progress 34 | 35 | - name: Run unit tests 36 | run: ./vendor/bin/phpunit --testsuite=Unit 37 | if: (matrix.php-versions == '8.4' && matrix.operating-system == 'ubuntu-latest') == false 38 | 39 | - name: Run unit tests with coverage 40 | run: ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml --testsuite=Unit --coverage-text 41 | if: matrix.php-versions == '8.4' && matrix.operating-system == 'ubuntu-latest' 42 | 43 | - name: Publish code coverage to codeclimate 44 | uses: paambaati/codeclimate-action@v3.2.0 45 | if: matrix.php-versions == '8.4' && matrix.operating-system == 'ubuntu-latest' 46 | env: 47 | CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE_REPORTER_ID }} 48 | with: 49 | coverageLocations: build/logs/clover.xml:clover 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /vendor 3 | .php-cs-fixer.cache 4 | /.idea 5 | .phpunit.result.cache 6 | /tests/tmp 7 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | $finder = PhpCsFixer\Finder::create() 17 | ->exclude(['vendor']) 18 | ->in([ 19 | __DIR__ . '/src', 20 | __DIR__ . '/tests', 21 | ]); 22 | 23 | $header = 'This file is part of the composer-link plugin. 24 | 25 | Created by: Sander Visser . 26 | 27 | For the full copyright and license information, please view the LICENSE.md 28 | file that was distributed with this source code. 29 | 30 | @link https://github.com/SanderSander/composer-link'; 31 | 32 | $config = new PhpCsFixer\Config(); 33 | 34 | return $config 35 | ->setRiskyAllowed(true) 36 | ->setRules([ 37 | '@PSR12' => true, 38 | '@PSR12:risky' => true, 39 | '@Symfony' => true, 40 | 'strict_param' => true, 41 | 'declare_strict_types' => true, 42 | 'no_unused_imports' => true, 43 | 'php_unit_test_case_static_method_calls' => true, 44 | 'single_blank_line_at_eof' => true, 45 | 'array_syntax' => ['syntax' => 'short'], 46 | 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 47 | 'header_comment' => ['header' => $header], 48 | 'php_unit_method_casing' => ['case' => 'snake_case'], 49 | 'concat_space' => ['spacing' => 'one'], 50 | 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], 51 | 'single_line_throw' => false, 52 | 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true], 53 | 'no_superfluous_phpdoc_tags' => ['remove_inheritdoc' => false], 54 | ])->setFinder($finder); 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Feel free to contribute, all help is welcome! 4 | 5 | 1. Fork it! 6 | 2. Create your feature branch: `git checkout -b my-new-feature` 7 | 3. Commit your changes: `git commit -am 'Add some feature'` 8 | 4. Push to the branch: `git push origin my-new-feature` 9 | 5. Submit a pull request :D 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2022 Sander Visser 6 | 7 | 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: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # composer-link 2 | ![phpunit](https://github.com/SanderSander/composer-link/actions/workflows/unit-tests.yml/badge.svg?branch=master) 3 | [![Test Coverage](https://api.codeclimate.com/v1/badges/3815e6abf2ec0e1d4ac8/test_coverage)](https://codeclimate.com/github/SanderSander/composer-link/test_coverage) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/3815e6abf2ec0e1d4ac8/maintainability)](https://codeclimate.com/github/SanderSander/composer-link/maintainability) 5 | 6 | Adds ability to link local packages in composer for development. 7 | 8 | This plugin won't alter your `composer.json` or `composer.lock` file, 9 | while maintaining the composer abilities to manage/upgrade your packages. 10 | 11 | ## Requirements 12 | 13 | - PHP >= 8.1 14 | - Composer >= 2.6 15 | 16 | ## Installation 17 | 18 | This plugin can be installed globally or per project 19 | 20 | Globally 21 | ``` 22 | composer global require sandersander/composer-link 23 | ``` 24 | 25 | Per project: 26 | ``` 27 | composer require --dev sandersander/composer-link 28 | ``` 29 | 30 | ## Usage 31 | 32 | The following three commands are made available by this plugin `link`, `unlink` and `linked`. 33 | When the plugin is installed globally you can prefix the commands with `global` as example `composer global linked` 34 | and install global packages. 35 | 36 | To link a package you can use the `link` commands, you can also link a global package. 37 | When linked to a global package absolute paths are used, when using a relative path composer-link resolves 38 | it to the absolute path. 39 | 40 | ``` 41 | composer link ../path/to/package 42 | composer global link ../path/to/package 43 | ``` 44 | 45 | It's also possible to use a wildcard in your path, note that this will install all packages found in the directory `../packages` 46 | If you don't want to link all the packages but only the ones originally installed you can pass the `--only-installed` flag. 47 | 48 | ``` 49 | composer link ../packages/* 50 | composer link ../packages/* --only-installed 51 | ``` 52 | 53 | Composer link will automatically install/update the required packages from the linked package, 54 | you can prevent this behavior by adding the `--without-dependencies` flag. 55 | 56 | When the `composer link` or `composer unlink` are used all packages defined in `require-dev` of the root package are 57 | installed by default, this can be prevented by using the `--no-dev` flag 58 | 59 | To unlink the package you can use the `unlink` command 60 | ``` 61 | composer unlink ../path/to/package 62 | composer unlink ../packages/* 63 | composer global unlink ../path/to/package 64 | ``` 65 | 66 | You can also unlink all package with the following command 67 | 68 | ``` 69 | composer unlink-all 70 | ``` 71 | 72 | To see all linked packages in your project you can use the `linked` command 73 | ``` 74 | composer linked 75 | composer global linked 76 | ``` 77 | 78 | ## Development 79 | 80 | The following tools are available for development. 81 | It's also possible to link this package to your global for testing changes. 82 | 83 | ``` 84 | composer run lint # Lints all files 85 | composer run test # Runs unit tests 86 | composer run phpmd # Runs phpmd 87 | composer run phpstan # Runs phpstan 88 | composer run test-integration # Runs integration tests for linux, this requires docker 89 | ``` 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandersander/composer-link", 3 | "description": "Adds ability to link local packages for development with composer", 4 | "type": "composer-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "SanderSander", 9 | "email": "themastersleader@hotmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "ComposerLink\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Tests\\": "tests" 20 | } 21 | }, 22 | "require": { 23 | "php": ">=8.1", 24 | "ext-json": "*", 25 | "composer-plugin-api": "^2.6" 26 | }, 27 | "require-dev": { 28 | "composer/composer": "^2.6", 29 | "friendsofphp/php-cs-fixer": "^v3.15.0", 30 | "phpstan/phpstan": "^2.0", 31 | "phpunit/phpunit": "^10.5.38", 32 | "phpstan/phpstan-phpunit": "^2.0", 33 | "phpmd/phpmd": "^2.12", 34 | "phpstan/phpstan-strict-rules": "2.0", 35 | "phpstan/phpstan-deprecation-rules": "^2.0" 36 | }, 37 | "extra": { 38 | "class": "ComposerLink\\Plugin" 39 | }, 40 | "scripts": { 41 | "phpmd": "./vendor/bin/phpmd ./src,./tests ansi ruleset.xml", 42 | "test": "./vendor/bin/phpunit --testsuite=Unit", 43 | "test-integration": [ 44 | "docker build -t composer-link-test ./support && docker run --rm -v $PWD:/composer-link composer-link-test" 45 | ], 46 | "phpstan": "./vendor/bin/phpstan --memory-limit=512M", 47 | "lint": "./vendor/bin/php-cs-fixer fix" 48 | }, 49 | "config": { 50 | "platform": { 51 | "php": "8.1.0" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | - tests 6 | ignoreErrors: 7 | - '#PHPDoc tag @SuppressWarnings has invalid value \(\(PHPMD.[a-zA-Z]+#' 8 | includes: 9 | - vendor/phpstan/phpstan-phpunit/extension.neon 10 | - vendor/phpstan/phpstan-phpunit/rules.neon 11 | - vendor/phpstan/phpstan-strict-rules/rules.neon 12 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 13 | - vendor/composer/composer/phpstan/rules.neon 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Unit 15 | 16 | 17 | ./tests/Integration 18 | 19 | 20 | 21 | 22 | ./src 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/CommandProvider.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink; 17 | 18 | use Composer\Command\BaseCommand; 19 | use Composer\IO\IOInterface; 20 | use Composer\Plugin\Capability\CommandProvider as ComposerCommandProvider; 21 | use ComposerLink\Commands\LinkCommand; 22 | use ComposerLink\Commands\LinkedCommand; 23 | use ComposerLink\Commands\UnlinkAllCommand; 24 | use ComposerLink\Commands\UnlinkCommand; 25 | 26 | class CommandProvider implements ComposerCommandProvider 27 | { 28 | protected IOInterface $io; 29 | 30 | protected Plugin $plugin; 31 | 32 | /** 33 | * @param array $arguments 34 | */ 35 | public function __construct(array $arguments) 36 | { 37 | $this->io = $arguments['io']; 38 | $this->plugin = $arguments['plugin']; 39 | } 40 | 41 | /** 42 | * @return BaseCommand[] 43 | */ 44 | public function getCommands(): array 45 | { 46 | $this->io->debug("[ComposerLink]\tInitializing commands."); 47 | 48 | return [ 49 | new LinkCommand($this->plugin), 50 | new UnlinkCommand($this->plugin), 51 | new LinkedCommand($this->plugin), 52 | new UnlinkAllCommand($this->plugin), 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Commands/Command.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Commands; 17 | 18 | use Composer\Command\BaseCommand; 19 | use ComposerLink\PathHelper; 20 | use ComposerLink\Plugin; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | 23 | abstract class Command extends BaseCommand 24 | { 25 | public function __construct( 26 | protected readonly Plugin $plugin, 27 | ) { 28 | parent::__construct(); 29 | } 30 | 31 | /** 32 | * @return PathHelper[] 33 | */ 34 | protected function getPaths(InputInterface $input): array 35 | { 36 | $helper = new PathHelper($input->getArgument('path')); 37 | 38 | // When run in global we should transform path to absolute path 39 | if ($this->plugin->isGlobal()) { 40 | /** @var string $working */ 41 | $working = $this->getApplication()->getInitialWorkingDirectory(); 42 | $helper = $helper->toAbsolutePath($working); 43 | } 44 | 45 | return $helper->isWildCard() ? $helper->getPathsFromWildcard() : [$helper]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Commands/LinkCommand.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Commands; 17 | 18 | use ComposerLink\Package\LinkedPackage; 19 | use ComposerLink\PathHelper; 20 | use Symfony\Component\Console\Input\InputArgument; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Input\InputOption; 23 | use Symfony\Component\Console\Output\OutputInterface; 24 | 25 | class LinkCommand extends Command 26 | { 27 | protected function configure(): void 28 | { 29 | $this->setName('link'); 30 | $this->setDescription('Link a package to a local directory'); 31 | $this->addArgument('path', InputArgument::REQUIRED, 'The path of the package'); 32 | $this->addOption( 33 | 'without-dependencies', 34 | null, 35 | InputOption::VALUE_NONE, 36 | 'Also install package dependencies', 37 | ); 38 | $this->addOption( 39 | 'no-dev', 40 | null, 41 | InputOption::VALUE_NONE, 42 | 'Disables installation of require-dev packages.', 43 | ); 44 | $this->addOption( 45 | 'only-installed', 46 | null, 47 | InputOption::VALUE_NONE, 48 | 'Link only installed packages', 49 | ); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | protected function execute(InputInterface $input, OutputInterface $output): int 56 | { 57 | /** @var bool $onlyInstalled */ 58 | $onlyInstalled = $input->getOption('only-installed'); 59 | $paths = $this->getPaths($input); 60 | $manager = $this->plugin->getLinkManager(); 61 | 62 | foreach ($paths as $path) { 63 | $package = $this->getPackage($path, $output); 64 | 65 | if (is_null($package)) { 66 | continue; 67 | } 68 | 69 | if ($onlyInstalled && is_null($package->getOriginalPackage())) { 70 | continue; 71 | } 72 | 73 | $package->setWithoutDependencies((bool) $input->getOption('without-dependencies')); 74 | $manager->add($package); 75 | } 76 | 77 | $manager->linkPackages(!(bool) $input->getOption('no-dev')); 78 | 79 | return 0; 80 | } 81 | 82 | protected function getPackage(PathHelper $helper, OutputInterface $output): ?LinkedPackage 83 | { 84 | $linkedPackage = $this->plugin->getPackageFactory()->fromPath($helper->getNormalizedPath()); 85 | $repository = $this->plugin->getRepository(); 86 | 87 | if (!is_null($repository->findByPath($helper->getNormalizedPath()))) { 88 | $output->writeln( 89 | sprintf('Package in path "%s" already linked', $helper->getNormalizedPath()) 90 | ); 91 | 92 | return null; 93 | } 94 | 95 | $currentLinked = $repository->findByName($linkedPackage->getName()); 96 | if (!is_null($currentLinked)) { 97 | $output->writeln( 98 | sprintf( 99 | 'Package "%s" in "%s" already linked from path "%s"', 100 | $linkedPackage->getName(), 101 | $linkedPackage->getPath(), 102 | $currentLinked->getPath() 103 | ) 104 | ); 105 | 106 | return null; 107 | } 108 | 109 | return $linkedPackage; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Commands/LinkedCommand.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Commands; 17 | 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | 21 | class LinkedCommand extends Command 22 | { 23 | protected function configure(): void 24 | { 25 | $this->setName('linked'); 26 | $this->setDescription('List all linked packages'); 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | protected function execute(InputInterface $input, OutputInterface $output): int 33 | { 34 | $linkedPackages = $this->plugin->getRepository()->all(); 35 | if (count($linkedPackages) === 0) { 36 | $output->writeln('No packages are linked'); 37 | 38 | return 0; 39 | } 40 | 41 | $longest = 0; 42 | foreach ($linkedPackages as $linkedPackage) { 43 | if (strlen($linkedPackage->getName()) > $longest) { 44 | $longest = strlen($linkedPackage->getName()); 45 | } 46 | } 47 | 48 | foreach ($linkedPackages as $linkedPackage) { 49 | $output->writeln(sprintf( 50 | "%s\t%s", 51 | str_pad($linkedPackage->getName(), $longest), 52 | $linkedPackage->getPath() 53 | )); 54 | } 55 | 56 | return 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Commands/UnlinkAllCommand.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Commands; 17 | 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Input\InputOption; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | class UnlinkAllCommand extends Command 23 | { 24 | protected function configure(): void 25 | { 26 | $this->setName('unlink-all'); 27 | $this->setDescription('Unlink all linked package'); 28 | $this->addOption( 29 | 'no-dev', 30 | null, 31 | InputOption::VALUE_NONE, 32 | 'Disables installation of require-dev packages.', 33 | ); 34 | } 35 | 36 | /** 37 | * {@inheritDoc} 38 | */ 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | $manager = $this->plugin->getLinkManager(); 42 | $repository = $this->plugin->getRepository(); 43 | 44 | foreach ($repository->all() as $package) { 45 | $manager->remove($package); 46 | } 47 | 48 | $manager->linkPackages(!(bool) $input->getOption('no-dev')); 49 | 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Commands/UnlinkCommand.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Commands; 17 | 18 | use Symfony\Component\Console\Input\InputArgument; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Input\InputOption; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | 23 | class UnlinkCommand extends Command 24 | { 25 | protected function configure(): void 26 | { 27 | $this->setName('unlink'); 28 | $this->setDescription('Unlink a linked package'); 29 | $this->addArgument('path', InputArgument::REQUIRED, 'The path of the package'); 30 | $this->addOption( 31 | 'no-dev', 32 | null, 33 | InputOption::VALUE_NONE, 34 | 'Disables installation of require-dev packages.', 35 | ); 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $paths = $this->getPaths($input); 44 | $manager = $this->plugin->getLinkManager(); 45 | 46 | foreach ($paths as $path) { 47 | $repository = $this->plugin->getRepository(); 48 | $linkedPackage = $repository->findByPath($path->getNormalizedPath()); 49 | 50 | if ($linkedPackage === null) { 51 | continue; 52 | } 53 | 54 | $manager->remove($linkedPackage); 55 | } 56 | 57 | $manager->linkPackages(!(bool) $input->getOption('no-dev')); 58 | 59 | return 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/InstallerFactory.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink; 17 | 18 | use Composer\Composer; 19 | use Composer\Installer; 20 | use Composer\IO\IOInterface; 21 | 22 | class InstallerFactory 23 | { 24 | public function __construct( 25 | protected IOInterface $io, 26 | protected Composer $composer, 27 | ) { 28 | } 29 | 30 | /** 31 | * @SuppressWarnings(PHPMD.StaticAccess) 32 | */ 33 | public function create(): Installer 34 | { 35 | return Installer::create($this->io, $this->composer); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/LinkManager.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink; 17 | 18 | use Composer\Composer; 19 | use Composer\DependencyResolver\Request; 20 | use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; 21 | use Composer\IO\IOInterface; 22 | use Composer\Package\AliasPackage; 23 | use Composer\Package\Link; 24 | use Composer\Repository\ArrayRepository; 25 | use Composer\Semver\Constraint\MatchAllConstraint; 26 | use ComposerLink\Package\LinkedPackage; 27 | use ComposerLink\Repository\Repository; 28 | 29 | class LinkManager 30 | { 31 | protected readonly ArrayRepository $linkedRepository; 32 | 33 | /** 34 | * @var array 35 | */ 36 | protected array $requires = []; 37 | 38 | public function __construct( 39 | protected readonly Repository $repository, 40 | protected readonly InstallerFactory $installerFactory, 41 | protected readonly IOInterface $io, 42 | protected readonly Composer $composer, 43 | ) { 44 | $this->linkedRepository = new ArrayRepository(); 45 | 46 | // Load linked packages 47 | foreach ($this->repository->all() as $package) { 48 | $this->registerPackage($package); 49 | } 50 | } 51 | 52 | public function add(LinkedPackage $package): void 53 | { 54 | $this->repository->store($package); 55 | $this->repository->persist(); 56 | 57 | $this->registerPackage($package); 58 | } 59 | 60 | private function registerPackage(LinkedPackage $package): void 61 | { 62 | $rootPackage = $this->composer->getPackage(); 63 | $locked = $this->composer->getLocker()->getLockedRepository()->findPackage($package->getName(), new MatchAllConstraint()); 64 | 65 | // If we have installed version in the lock file, we will add the specific version as alias to the linked package. 66 | // This way we prevent conflicts with transitive dependencies. 67 | if (!is_null($locked)) { 68 | $aliasPackage = new AliasPackage($package, $locked->getVersion(), $rootPackage->getPrettyVersion()); 69 | } 70 | 71 | $this->linkedRepository->addPackage($aliasPackage ?? $package); 72 | $this->requires[$package->getName()] = $package->createLink($rootPackage); 73 | } 74 | 75 | public function remove(LinkedPackage $package): void 76 | { 77 | $this->linkedRepository->removePackage($package); 78 | $internalPackages = $this->linkedRepository->findPackages($package->getName()); 79 | foreach ($internalPackages as $internalPackage) { 80 | $this->linkedRepository->removePackage($internalPackage); 81 | } 82 | 83 | unset($this->requires[$package->getName()]); 84 | 85 | $this->repository->remove($package); 86 | $this->repository->persist(); 87 | } 88 | 89 | public function hasLinkedPackages(): bool 90 | { 91 | return $this->linkedRepository->count() > 0; 92 | } 93 | 94 | public function linkPackages(bool $isDev): void 95 | { 96 | $repositoryManager = $this->composer->getRepositoryManager(); 97 | $eventDispatcher = $this->composer->getEventDispatcher(); 98 | $rootPackage = $this->composer->getPackage(); 99 | 100 | // Use the composer installer to install the linked packages with dependencies 101 | $repositoryManager->prependRepository($this->linkedRepository); 102 | 103 | // Add requirement to the current/loaded composer.json 104 | $rootPackage->setRequires(array_merge($rootPackage->getRequires(), $this->requires)); 105 | $this->io->warning('Linking packages, Lock file will be generated in memory but not written to disk.'); 106 | 107 | // We need to remove dev-requires from the list of packages that are linked 108 | $devRequires = $rootPackage->getDevRequires(); 109 | foreach ($this->linkedRepository->getPackages() as $package) { 110 | unset($devRequires[$package->getName()]); 111 | } 112 | $rootPackage->setDevRequires($devRequires); 113 | 114 | // Prevent circular call to script handler 'post-update-cmd' by creating a new composer instance 115 | // We also need to set this on the Installer while it's deprecated 116 | $eventDispatcher->setRunScripts(false); 117 | $installer = $this->installerFactory->create(); 118 | 119 | /* @phpstan-ignore method.deprecated */ 120 | $installer->setUpdate(true) 121 | ->setInstall(true) 122 | ->setWriteLock(false) 123 | ->setRunScripts(false) 124 | ->setUpdateAllowList(array_keys($this->requires)) 125 | ->setPlatformRequirementFilter(new IgnoreAllPlatformRequirementFilter()) 126 | ->setDevMode($isDev) 127 | ->setUpdateAllowTransitiveDependencies(Request::UPDATE_ONLY_LISTED); 128 | 129 | $installer->run(); 130 | 131 | $eventDispatcher->setRunScripts(); 132 | $this->io->warning('Linking packages finished!'); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/LinkManagerFactory.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink; 17 | 18 | use Composer\Composer; 19 | use Composer\IO\IOInterface; 20 | use ComposerLink\Repository\Repository; 21 | 22 | class LinkManagerFactory 23 | { 24 | public function create( 25 | Repository $repository, 26 | InstallerFactory $installerFactory, 27 | IOInterface $io, 28 | Composer $composer): LinkManager 29 | { 30 | return new LinkManager( 31 | $repository, 32 | $installerFactory, 33 | $io, 34 | $composer 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Package/LinkedPackage.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Package; 17 | 18 | use Composer\Package\BasePackage; 19 | use Composer\Package\CompletePackageInterface; 20 | use Composer\Package\Link; 21 | use Composer\Package\PackageInterface; 22 | use Composer\Package\RootPackageInterface; 23 | use Composer\Repository\RepositoryInterface; 24 | use Composer\Semver\Constraint\Constraint; 25 | use DateTimeInterface; 26 | 27 | /** 28 | * @SuppressWarnings(PHPMD) 29 | */ 30 | class LinkedPackage extends BasePackage implements CompletePackageInterface 31 | { 32 | protected bool $withoutDependencies = false; 33 | 34 | /** 35 | * @param non-empty-string $path 36 | */ 37 | public function __construct( 38 | protected CompletePackageInterface $linkedPackage, 39 | protected string $path, 40 | protected string $installationPath, 41 | protected ?PackageInterface $original, 42 | ) { 43 | parent::__construct($this->linkedPackage->getName()); 44 | } 45 | 46 | /** 47 | * Creates a Link to this package from the given root. 48 | */ 49 | public function createLink(RootPackageInterface $root): Link 50 | { 51 | return new Link( 52 | $root->getName(), 53 | $this->getName(), 54 | new Constraint('=', 'dev-linked'), 55 | Link::TYPE_REQUIRE 56 | ); 57 | } 58 | 59 | public function getOriginalPackage(): ?PackageInterface 60 | { 61 | return $this->original; 62 | } 63 | 64 | public function setOriginalPackage(?PackageInterface $package): void 65 | { 66 | $this->original = $package; 67 | } 68 | 69 | public function getPath(): string 70 | { 71 | return $this->path; 72 | } 73 | 74 | public function getInstallationPath(): string 75 | { 76 | return $this->installationPath; 77 | } 78 | 79 | public function setWithoutDependencies(bool $withoutDependencies): void 80 | { 81 | $this->withoutDependencies = $withoutDependencies; 82 | } 83 | 84 | public function isWithoutDependencies(): bool 85 | { 86 | return $this->withoutDependencies; 87 | } 88 | 89 | public function getRequires(): array 90 | { 91 | if ($this->withoutDependencies) { 92 | return $this->original?->getRequires() ?? []; 93 | } 94 | 95 | return $this->linkedPackage->getRequires(); 96 | } 97 | 98 | public function getDevRequires(): array 99 | { 100 | if ($this->withoutDependencies) { 101 | return $this->original?->getDevRequires() ?? []; 102 | } 103 | 104 | return $this->linkedPackage->getDevRequires(); 105 | } 106 | 107 | public function getLinkedPackage(): CompletePackageInterface 108 | { 109 | return $this->linkedPackage; 110 | } 111 | 112 | /** 113 | * We always install from dist because we load the package from a path. 114 | */ 115 | public function getInstallationSource(): ?string 116 | { 117 | return 'dist'; 118 | } 119 | 120 | /** 121 | * Force loading from path. 122 | */ 123 | public function getDistType(): ?string 124 | { 125 | return 'path'; 126 | } 127 | 128 | /** 129 | * Return the path from where this package is linked. 130 | */ 131 | public function getDistUrl(): ?string 132 | { 133 | return $this->path; 134 | } 135 | 136 | /** 137 | * We always return our own stability, this way we can link the package without considering minimal-stability settings. 138 | */ 139 | public function getStability(): string 140 | { 141 | return 'stable'; 142 | } 143 | 144 | public function getVersion(): string 145 | { 146 | return 'dev-linked'; 147 | } 148 | 149 | // 150 | // Decorated functions, move altered function above this line 151 | // 152 | 153 | public function getScripts(): array 154 | { 155 | return $this->linkedPackage->getScripts(); 156 | } 157 | 158 | public function setScripts(array $scripts): void 159 | { 160 | $this->linkedPackage->setScripts($scripts); 161 | } 162 | 163 | public function getRepositories(): array 164 | { 165 | return $this->linkedPackage->getRepositories(); 166 | } 167 | 168 | public function setRepositories(array $repositories): void 169 | { 170 | $this->linkedPackage->setRepositories($repositories); 171 | } 172 | 173 | public function getLicense(): array 174 | { 175 | return $this->linkedPackage->getLicense(); 176 | } 177 | 178 | public function setLicense(array $license): void 179 | { 180 | $this->linkedPackage->setLicense($license); 181 | } 182 | 183 | public function getKeywords(): array 184 | { 185 | return $this->linkedPackage->getKeywords(); 186 | } 187 | 188 | public function setKeywords(array $keywords): void 189 | { 190 | $this->linkedPackage->setKeywords($keywords); 191 | } 192 | 193 | public function getDescription(): ?string 194 | { 195 | return $this->linkedPackage->getDescription(); 196 | } 197 | 198 | public function setDescription(string $description): void 199 | { 200 | $this->linkedPackage->setDescription($description); 201 | } 202 | 203 | public function getHomepage(): ?string 204 | { 205 | return $this->linkedPackage->getHomepage(); 206 | } 207 | 208 | public function setHomepage(string $homepage): void 209 | { 210 | $this->linkedPackage->setHomepage($homepage); 211 | } 212 | 213 | public function getAuthors(): array 214 | { 215 | return $this->linkedPackage->getAuthors(); 216 | } 217 | 218 | public function setAuthors(array $authors): void 219 | { 220 | $this->linkedPackage->setAuthors($authors); 221 | } 222 | 223 | public function getSupport(): array 224 | { 225 | return $this->linkedPackage->getSupport(); 226 | } 227 | 228 | public function setSupport(array $support): void 229 | { 230 | $this->linkedPackage->setSupport($support); 231 | } 232 | 233 | public function getFunding(): array 234 | { 235 | return $this->linkedPackage->getFunding(); 236 | } 237 | 238 | public function setFunding(array $funding): void 239 | { 240 | $this->linkedPackage->setFunding($funding); 241 | } 242 | 243 | public function isAbandoned(): bool 244 | { 245 | return $this->linkedPackage->isAbandoned(); 246 | } 247 | 248 | public function getReplacementPackage(): ?string 249 | { 250 | return $this->linkedPackage->getReplacementPackage(); 251 | } 252 | 253 | public function setAbandoned($abandoned): void 254 | { 255 | $this->linkedPackage->setAbandoned($abandoned); 256 | } 257 | 258 | public function getArchiveName(): ?string 259 | { 260 | return $this->linkedPackage->getArchiveName(); 261 | } 262 | 263 | public function setArchiveName(string $name): void 264 | { 265 | $this->linkedPackage->setArchiveName($name); 266 | } 267 | 268 | public function getArchiveExcludes(): array 269 | { 270 | return $this->linkedPackage->getArchiveExcludes(); 271 | } 272 | 273 | public function setArchiveExcludes(array $excludes): void 274 | { 275 | $this->linkedPackage->setArchiveExcludes($excludes); 276 | } 277 | 278 | public function getName(): string 279 | { 280 | return $this->linkedPackage->getName(); 281 | } 282 | 283 | public function getPrettyName(): string 284 | { 285 | return $this->linkedPackage->getPrettyName(); 286 | } 287 | 288 | public function getNames($provides = true): array 289 | { 290 | return $this->linkedPackage->getNames($provides); 291 | } 292 | 293 | public function setId(int $id): void 294 | { 295 | $this->linkedPackage->setId($id); 296 | } 297 | 298 | public function getId(): int 299 | { 300 | return $this->linkedPackage->getId(); 301 | } 302 | 303 | public function isDev(): bool 304 | { 305 | return $this->linkedPackage->isDev(); 306 | } 307 | 308 | public function getType(): string 309 | { 310 | return $this->linkedPackage->getType(); 311 | } 312 | 313 | public function getTargetDir(): ?string 314 | { 315 | return $this->linkedPackage->getTargetDir(); 316 | } 317 | 318 | public function getExtra(): array 319 | { 320 | return $this->linkedPackage->getExtra(); 321 | } 322 | 323 | public function setInstallationSource(?string $type): void 324 | { 325 | $this->linkedPackage->setInstallationSource($type); 326 | } 327 | 328 | public function getSourceType(): ?string 329 | { 330 | return $this->linkedPackage->getSourceType(); 331 | } 332 | 333 | public function getSourceUrl(): ?string 334 | { 335 | return $this->linkedPackage->getSourceUrl(); 336 | } 337 | 338 | public function getSourceUrls(): array 339 | { 340 | return $this->linkedPackage->getSourceUrls(); 341 | } 342 | 343 | public function getSourceReference(): ?string 344 | { 345 | return $this->linkedPackage->getSourceReference(); 346 | } 347 | 348 | public function getSourceMirrors(): ?array 349 | { 350 | return $this->linkedPackage->getSourceMirrors(); 351 | } 352 | 353 | public function setSourceMirrors(?array $mirrors): void 354 | { 355 | $this->linkedPackage->setSourceMirrors($mirrors); 356 | } 357 | 358 | public function getDistUrls(): array 359 | { 360 | return $this->linkedPackage->getDistUrls(); 361 | } 362 | 363 | public function getDistReference(): ?string 364 | { 365 | return $this->linkedPackage->getDistReference(); 366 | } 367 | 368 | public function getDistSha1Checksum(): ?string 369 | { 370 | return $this->linkedPackage->getDistSha1Checksum(); 371 | } 372 | 373 | public function getDistMirrors(): ?array 374 | { 375 | return $this->linkedPackage->getDistMirrors(); 376 | } 377 | 378 | public function setDistMirrors(?array $mirrors): void 379 | { 380 | $this->linkedPackage->setDistMirrors($mirrors); 381 | } 382 | 383 | public function getPrettyVersion(): string 384 | { 385 | return $this->linkedPackage->getPrettyVersion(); 386 | } 387 | 388 | public function getFullPrettyVersion(bool $truncate = true, int $displayMode = self::DISPLAY_SOURCE_REF_IF_DEV): string 389 | { 390 | return $this->linkedPackage->getFullPrettyVersion($truncate, $displayMode); 391 | } 392 | 393 | public function getReleaseDate(): ?DateTimeInterface 394 | { 395 | return $this->linkedPackage->getReleaseDate(); 396 | } 397 | 398 | public function getConflicts(): array 399 | { 400 | return $this->linkedPackage->getConflicts(); 401 | } 402 | 403 | public function getProvides(): array 404 | { 405 | return $this->linkedPackage->getProvides(); 406 | } 407 | 408 | public function getReplaces(): array 409 | { 410 | return $this->linkedPackage->getReplaces(); 411 | } 412 | 413 | public function getSuggests(): array 414 | { 415 | return $this->linkedPackage->getSuggests(); 416 | } 417 | 418 | public function getAutoload(): array 419 | { 420 | return $this->linkedPackage->getAutoload(); 421 | } 422 | 423 | public function getDevAutoload(): array 424 | { 425 | return $this->linkedPackage->getDevAutoload(); 426 | } 427 | 428 | public function getIncludePaths(): array 429 | { 430 | return $this->linkedPackage->getIncludePaths(); 431 | } 432 | 433 | public function getPhpExt(): ?array 434 | { 435 | return $this->linkedPackage->getPhpExt(); 436 | } 437 | 438 | public function setRepository(RepositoryInterface $repository): void 439 | { 440 | $this->linkedPackage->setRepository($repository); 441 | } 442 | 443 | public function getRepository(): ?RepositoryInterface 444 | { 445 | return $this->linkedPackage->getRepository(); 446 | } 447 | 448 | public function getBinaries(): array 449 | { 450 | return $this->linkedPackage->getBinaries(); 451 | } 452 | 453 | public function getUniqueName(): string 454 | { 455 | return $this->linkedPackage->getUniqueName(); 456 | } 457 | 458 | public function getNotificationUrl(): ?string 459 | { 460 | return $this->linkedPackage->getNotificationUrl(); 461 | } 462 | 463 | public function __toString(): string 464 | { 465 | return $this->linkedPackage->__toString(); 466 | } 467 | 468 | public function getPrettyString(): string 469 | { 470 | return $this->linkedPackage->getPrettyString(); 471 | } 472 | 473 | public function isDefaultBranch(): bool 474 | { 475 | return $this->linkedPackage->isDefaultBranch(); 476 | } 477 | 478 | public function getTransportOptions(): array 479 | { 480 | return $this->linkedPackage->getTransportOptions(); 481 | } 482 | 483 | public function setTransportOptions(array $options): void 484 | { 485 | $this->linkedPackage->setTransportOptions($options); 486 | } 487 | 488 | public function setSourceReference(?string $reference): void 489 | { 490 | $this->linkedPackage->setSourceReference($reference); 491 | } 492 | 493 | public function setDistUrl(?string $url): void 494 | { 495 | $this->linkedPackage->setDistUrl($url); 496 | } 497 | 498 | public function setDistType(?string $type): void 499 | { 500 | $this->linkedPackage->setDistType($type); 501 | } 502 | 503 | public function setDistReference(?string $reference): void 504 | { 505 | $this->linkedPackage->setDistReference($reference); 506 | } 507 | 508 | public function setSourceDistReferences(string $reference): void 509 | { 510 | $this->linkedPackage->setSourceDistReferences($reference); 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /src/Package/LinkedPackageFactory.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Package; 17 | 18 | use Composer\Installer\InstallationManager; 19 | use Composer\Json\JsonFile; 20 | use Composer\Package\CompletePackage; 21 | use Composer\Package\Loader\ArrayLoader; 22 | use Composer\Repository\InstalledRepositoryInterface; 23 | use RuntimeException; 24 | 25 | class LinkedPackageFactory 26 | { 27 | public function __construct( 28 | protected readonly InstallationManager $installationManager, 29 | protected readonly InstalledRepositoryInterface $installedRepository, 30 | ) { 31 | } 32 | 33 | /** 34 | * @param non-empty-string $path 35 | */ 36 | private function loadFromJsonFile(string $path): CompletePackage 37 | { 38 | if (!file_exists($path . DIRECTORY_SEPARATOR . 'composer.json')) { 39 | throw new RuntimeException(sprintf('No composer.json file found in "%s".', $path)); 40 | } 41 | 42 | $json = (new JsonFile($path . DIRECTORY_SEPARATOR . 'composer.json'))->read(); 43 | 44 | if (!is_array($json)) { 45 | throw new RuntimeException(sprintf('Unable to read composer.json in "%s"', $path)); 46 | } 47 | 48 | // Version is required here because we load it from a directory 49 | if (!isset($json['version'])) { 50 | $json['version'] = 'dev-linked'; 51 | } 52 | 53 | /** @var CompletePackage $package */ 54 | $package = (new ArrayLoader())->load($json); 55 | 56 | return $package; 57 | } 58 | 59 | /** 60 | * @param non-empty-string $path 61 | */ 62 | public function fromPath(string $path): LinkedPackage 63 | { 64 | $originalPackage = null; 65 | $linkedPackage = $this->loadFromJsonFile($path); 66 | $packages = $this->installedRepository->getCanonicalPackages(); 67 | foreach ($packages as $package) { 68 | if ($package->getName() === $linkedPackage->getName()) { 69 | $originalPackage = $package; 70 | } 71 | } 72 | 73 | // TODO installation path exists only if package is installed 74 | // we should add support when the package isn't required yet in composer.json 75 | /** @var string $destination */ 76 | $destination = $this->installationManager->getInstallPath($linkedPackage); 77 | 78 | return new LinkedPackage( 79 | $linkedPackage, 80 | $path, 81 | $destination, 82 | $originalPackage 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/PathHelper.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink; 17 | 18 | use InvalidArgumentException; 19 | 20 | class PathHelper 21 | { 22 | /** 23 | * @param non-empty-string $path 24 | */ 25 | public function __construct( 26 | protected readonly string $path, 27 | ) { 28 | } 29 | 30 | public function isWildCard(): bool 31 | { 32 | return substr($this->path, -2) === DIRECTORY_SEPARATOR . '*'; 33 | } 34 | 35 | /** 36 | * @return PathHelper[] 37 | */ 38 | public function getPathsFromWildcard(): array 39 | { 40 | /** @var list $entries */ 41 | $entries = glob($this->path, GLOB_ONLYDIR); 42 | $helpers = []; 43 | 44 | /** @var non-empty-string $entry */ 45 | foreach ($entries as $entry) { 46 | if (!file_exists($entry . DIRECTORY_SEPARATOR . 'composer.json')) { 47 | continue; 48 | } 49 | 50 | $helpers[] = new PathHelper($entry); 51 | } 52 | 53 | return $helpers; 54 | } 55 | 56 | public function toAbsolutePath(string $workingDirectory): PathHelper 57 | { 58 | if ($this->isAbsolutePath($this->path)) { 59 | return $this; 60 | } 61 | 62 | $path = $this->isWildCard() ? substr($this->path, 0, -1) : $this->path; 63 | $real = realpath($workingDirectory . DIRECTORY_SEPARATOR . $path); 64 | 65 | if ($real === false) { 66 | throw new InvalidArgumentException( 67 | sprintf('Cannot resolve absolute path to %s from %s.', $path, $workingDirectory) 68 | ); 69 | } 70 | 71 | if ($this->isWildCard()) { 72 | $real .= DIRECTORY_SEPARATOR . '*'; 73 | } 74 | 75 | return new PathHelper($real); 76 | } 77 | 78 | /** 79 | * @return non-empty-string 80 | */ 81 | public function getNormalizedPath(): string 82 | { 83 | if (substr($this->path, -1) === DIRECTORY_SEPARATOR) { 84 | /** @var non-empty-string $path */ 85 | $path = substr($this->path, 0, -1); 86 | 87 | return $path; 88 | } 89 | 90 | return $this->path; 91 | } 92 | 93 | public function isAbsolutePath(string $path): bool 94 | { 95 | return str_starts_with($path, '/') || substr($path, 1, 1) === ':' || str_starts_with($path, '\\\\'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink; 17 | 18 | use Composer\Composer; 19 | use Composer\EventDispatcher\EventSubscriberInterface; 20 | use Composer\IO\IOInterface; 21 | use Composer\Plugin\Capability\CommandProvider as ComposerCommandProvider; 22 | use Composer\Plugin\Capable; 23 | use Composer\Plugin\PluginInterface; 24 | use Composer\Script\Event; 25 | use Composer\Script\ScriptEvents; 26 | use Composer\Util\Filesystem as ComposerFileSystem; 27 | use ComposerLink\Package\LinkedPackageFactory; 28 | use ComposerLink\Repository\Repository; 29 | use ComposerLink\Repository\RepositoryFactory; 30 | use RuntimeException; 31 | use Throwable; 32 | 33 | class Plugin implements PluginInterface, Capable, EventSubscriberInterface 34 | { 35 | protected ?Repository $repository = null; 36 | 37 | protected ComposerFileSystem $filesystem; 38 | protected ?LinkedPackageFactory $packageFactory = null; 39 | 40 | protected Composer $composer; 41 | 42 | protected ?LinkManager $linkManager = null; 43 | 44 | protected RepositoryFactory $repositoryFactory; 45 | 46 | protected LinkManagerFactory $linkManagerFactory; 47 | 48 | /** 49 | * It can happen that activation doesn't work, this happens when this plugin is upgraded. 50 | * Composer runs this file through an eval() with renamed class names, but all other classes 51 | * in this library are still the old ones loaded in memory. 52 | * 53 | * We try to detect this, and skip the event callbacks if it happens 54 | */ 55 | protected bool $couldNotActivate = false; 56 | 57 | public function __construct( 58 | ?ComposerFileSystem $filesystem = null, 59 | ?RepositoryFactory $repositoryFactory = null, 60 | ?LinkManagerFactory $linkManagerFactory = null, 61 | ) { 62 | $this->filesystem = $filesystem ?? new ComposerFileSystem(); 63 | $this->repositoryFactory = $repositoryFactory ?? new RepositoryFactory(); 64 | $this->linkManagerFactory = $linkManagerFactory ?? new LinkManagerFactory(); 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | */ 70 | public function deactivate(Composer $composer, IOInterface $io): void 71 | { 72 | $io->debug("[ComposerLink]\tPlugin is deactivated"); 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | public function uninstall(Composer $composer, IOInterface $io): void 79 | { 80 | $io->debug("[ComposerLink]\tPlugin uninstalling"); 81 | } 82 | 83 | /** 84 | * {@inheritDoc} 85 | */ 86 | public function activate(Composer $composer, IOInterface $io): void 87 | { 88 | $io->debug("[ComposerLink]\tPlugin is activating"); 89 | $this->composer = $composer; 90 | try { 91 | $this->packageFactory = $this->initializeLinkedPackageFactory(); 92 | $this->repository = $this->initializeRepository(); 93 | $this->linkManager = $this->initializeLinkManager($io); 94 | } catch (Throwable $e) { 95 | $io->debug("[ComposerLink]\tException: " . $e->getMessage()); 96 | $this->couldNotActivate = true; 97 | } 98 | } 99 | 100 | protected function initializeRepository(): Repository 101 | { 102 | $storageFile = $this->composer->getConfig() 103 | ->get('vendor-dir') . DIRECTORY_SEPARATOR . 'linked-packages.json'; 104 | 105 | return $this->repositoryFactory->create($storageFile, $this->getPackageFactory()); 106 | } 107 | 108 | protected function initializeLinkedPackageFactory(): LinkedPackageFactory 109 | { 110 | return new LinkedPackageFactory( 111 | $this->composer->getInstallationManager(), 112 | $this->composer->getRepositoryManager()->getLocalRepository() 113 | ); 114 | } 115 | 116 | protected function initializeLinkManager(IOInterface $io): LinkManager 117 | { 118 | return $this->linkManagerFactory->create( 119 | $this->getRepository(), 120 | new InstallerFactory($io, $this->composer), 121 | $io, 122 | $this->composer 123 | ); 124 | } 125 | 126 | public static function getSubscribedEvents(): array 127 | { 128 | return [ 129 | ScriptEvents::POST_UPDATE_CMD => [ 130 | ['postUpdate'], 131 | ], 132 | ScriptEvents::POST_INSTALL_CMD => [ 133 | ['postInstall'], 134 | ], 135 | ]; 136 | } 137 | 138 | public function postInstall(Event $event): void 139 | { 140 | $linkManager = $this->getLinkManager(); 141 | if ($linkManager->hasLinkedPackages()) { 142 | $linkManager->linkPackages($event->isDevMode()); 143 | } 144 | } 145 | 146 | public function postUpdate(Event $event): void 147 | { 148 | // Plugin couldn't be activated probably because the plugin was updated 149 | if ($this->couldNotActivate) { 150 | $event->getIO()->warning('Composer link couldn\'t be activated because it was probably upgraded, run `composer install` again to link packages'); 151 | 152 | return; 153 | } 154 | 155 | $linkManager = $this->getLinkManager(); 156 | $repository = $this->getRepository(); 157 | 158 | if ($linkManager->hasLinkedPackages()) { 159 | $localRepository = $this->composer->getRepositoryManager()->getLocalRepository(); 160 | // It can happen that a original package is updated, 161 | // in those cases we need to update the state of the linked package by setting the original package 162 | foreach ($repository->all() as $package) { 163 | $original = $localRepository->findPackage($package->getName(), '*'); 164 | $package->setOriginalPackage($original); 165 | } 166 | $repository->persist(); 167 | 168 | $linkManager->linkPackages($event->isDevMode()); 169 | } 170 | } 171 | 172 | public function getLinkManager(): LinkManager 173 | { 174 | if (is_null($this->linkManager)) { 175 | throw new RuntimeException('Plugin not activated'); 176 | } 177 | 178 | return $this->linkManager; 179 | } 180 | 181 | public function getCapabilities(): array 182 | { 183 | return [ 184 | ComposerCommandProvider::class => CommandProvider::class, 185 | ]; 186 | } 187 | 188 | public function getRepository(): Repository 189 | { 190 | if (is_null($this->repository)) { 191 | throw new RuntimeException('Plugin not activated'); 192 | } 193 | 194 | return $this->repository; 195 | } 196 | 197 | public function getPackageFactory(): LinkedPackageFactory 198 | { 199 | if (is_null($this->packageFactory)) { 200 | throw new RuntimeException('Plugin not activated'); 201 | } 202 | 203 | return $this->packageFactory; 204 | } 205 | 206 | /** 207 | * Check if this plugin is running from global or local project. 208 | */ 209 | public function isGlobal(): bool 210 | { 211 | return getcwd() === $this->composer->getConfig()->get('home'); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Repository/JsonStorage.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Repository; 17 | 18 | use RuntimeException; 19 | 20 | class JsonStorage implements StorageInterface 21 | { 22 | public function __construct( 23 | protected readonly string $file, 24 | ) { 25 | } 26 | 27 | public function write(array $data): void 28 | { 29 | $json = json_encode($data); 30 | file_put_contents($this->file, $json); 31 | } 32 | 33 | public function read(): array 34 | { 35 | if (!$this->hasData()) { 36 | throw new RuntimeException('Cannot read data, no data stored.'); 37 | } 38 | 39 | /** @var string $data */ 40 | $data = file_get_contents($this->file); 41 | 42 | return json_decode($data, true); 43 | } 44 | 45 | public function hasData(): bool 46 | { 47 | return file_exists($this->file); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Repository/Repository.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Repository; 17 | 18 | use ComposerLink\Package\LinkedPackage; 19 | use RuntimeException; 20 | 21 | class Repository 22 | { 23 | /** 24 | * @var array 25 | */ 26 | protected array $linkedPackages = []; 27 | 28 | public function __construct( 29 | protected readonly StorageInterface $storage, 30 | protected readonly Transformer $transformer, 31 | ) { 32 | $this->load(); 33 | } 34 | 35 | public function store(LinkedPackage $linkedPackage): void 36 | { 37 | $index = $this->findIndex($linkedPackage); 38 | 39 | if (is_null($index)) { 40 | $this->linkedPackages[] = clone $linkedPackage; 41 | 42 | return; 43 | } 44 | 45 | $this->linkedPackages[$index] = clone $linkedPackage; 46 | } 47 | 48 | /** 49 | * @return LinkedPackage[] 50 | */ 51 | public function all(): array 52 | { 53 | $all = []; 54 | foreach ($this->linkedPackages as $package) { 55 | $all[] = clone $package; 56 | } 57 | 58 | return $all; 59 | } 60 | 61 | public function findByPath(string $path): ?LinkedPackage 62 | { 63 | foreach ($this->linkedPackages as $linkedPackage) { 64 | if ($linkedPackage->getPath() === $path) { 65 | return clone $linkedPackage; 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public function findByName(string $name): ?LinkedPackage 73 | { 74 | foreach ($this->linkedPackages as $linkedPackage) { 75 | if ($linkedPackage->getName() === $name) { 76 | return clone $linkedPackage; 77 | } 78 | } 79 | 80 | return null; 81 | } 82 | 83 | public function remove(LinkedPackage $linkedPackage): void 84 | { 85 | $index = $this->findIndex($linkedPackage); 86 | 87 | if (is_null($index)) { 88 | throw new RuntimeException('Linked package not found'); 89 | } 90 | 91 | array_splice($this->linkedPackages, $index, 1); 92 | } 93 | 94 | public function persist(): void 95 | { 96 | $data = [ 97 | 'packages' => [], 98 | ]; 99 | foreach ($this->linkedPackages as $package) { 100 | $data['packages'][] = $this->transformer->export($package); 101 | } 102 | 103 | $this->storage->write($data); 104 | } 105 | 106 | private function load(): void 107 | { 108 | if (!$this->storage->hasData()) { 109 | return; 110 | } 111 | 112 | $data = $this->storage->read(); 113 | 114 | foreach ($data['packages'] as $package) { 115 | $this->linkedPackages[] = $this->transformer->load($package); 116 | } 117 | } 118 | 119 | private function findIndex(LinkedPackage $package): ?int 120 | { 121 | foreach ($this->linkedPackages as $index => $linkedPackage) { 122 | if ($linkedPackage->getName() === $package->getName()) { 123 | return $index; 124 | } 125 | } 126 | 127 | return null; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Repository/RepositoryFactory.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Repository; 17 | 18 | use ComposerLink\Package\LinkedPackageFactory; 19 | 20 | class RepositoryFactory 21 | { 22 | public function create(string $storageFile, LinkedPackageFactory $linkedPackageFactory): Repository 23 | { 24 | return new Repository( 25 | new JsonStorage($storageFile), 26 | new Transformer($linkedPackageFactory) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Repository/StorageInterface.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Repository; 17 | 18 | interface StorageInterface 19 | { 20 | /** 21 | * Write data to storage. 22 | * 23 | * @param array $data 24 | */ 25 | public function write(array $data): void; 26 | 27 | /** 28 | * Read data from storage. 29 | * 30 | * @return array 31 | */ 32 | public function read(): array; 33 | 34 | /** 35 | * Check if storage has data stored. 36 | */ 37 | public function hasData(): bool; 38 | } 39 | -------------------------------------------------------------------------------- /src/Repository/Transformer.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace ComposerLink\Repository; 17 | 18 | use Composer\Package\Dumper\ArrayDumper; 19 | use Composer\Package\Loader\ArrayLoader; 20 | use ComposerLink\Package\LinkedPackage; 21 | use ComposerLink\Package\LinkedPackageFactory; 22 | 23 | class Transformer 24 | { 25 | protected ArrayLoader $composerLoader; 26 | 27 | protected ArrayDumper $composerDumper; 28 | 29 | public function __construct( 30 | protected LinkedPackageFactory $linkedPackageFactory, 31 | ) { 32 | $this->composerLoader = new ArrayLoader(); 33 | $this->composerDumper = new ArrayDumper(); 34 | } 35 | 36 | /** 37 | * Load a Linked package from array data. 38 | * 39 | * @param array $data 40 | */ 41 | public function load(array $data): LinkedPackage 42 | { 43 | // Load from path again since the composer.json can be changed 44 | $linkedPackage = $this->linkedPackageFactory->fromPath($data['path']); 45 | $linkedPackage->setWithoutDependencies($data['withoutDependencies'] ?? true); 46 | 47 | return $linkedPackage; 48 | } 49 | 50 | /** 51 | * Export LinkedPackage to array data. 52 | * 53 | * @return array 54 | */ 55 | public function export(LinkedPackage $package): array 56 | { 57 | $data = []; 58 | $data['path'] = $package->getPath(); 59 | $data['withoutDependencies'] = $package->isWithoutDependencies(); 60 | 61 | return $data; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /support/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM composer:latest 2 | 3 | COPY docker-entrypoint.sh /usr/bin/docker-entrypoint.sh 4 | 5 | RUN chmod +x /usr/bin/docker-entrypoint.sh && mkdir /composer-link 6 | 7 | WORKDIR /composer-link 8 | 9 | VOLUME /composer-link 10 | 11 | ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"] 12 | -------------------------------------------------------------------------------- /support/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git config --global --add safe.directory /composer-link 4 | 5 | export PHPUNIT_INTEGRATION=1 6 | ./vendor/bin/phpunit --testsuite=Integration --group=ubuntu-latest "$@" 7 | -------------------------------------------------------------------------------- /tests/Integration/LinuxExtraTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Integration; 17 | 18 | /** 19 | * @group ubuntu-latest 20 | */ 21 | class LinuxExtraTest extends TestCase 22 | { 23 | public function test_upgrade_safety_mechanism(): void 24 | { 25 | $this->useComposerLinkLocalOld(); 26 | 27 | // Alter composer file so that we update from the current version 28 | $composerFile = $this->getCurrentComposeFile(); 29 | $composerFile['require']['sandersander/composer-link'] = '@dev'; 30 | $composerFile['repositories'] = [[ 31 | 'type' => 'path', 32 | 'url' => $this->getThisPackagePath(), 33 | ]]; 34 | $this->setCurrentComposeFile($composerFile); 35 | 36 | static::assertStringContainsString( 37 | 'Composer link couldn\'t be activated because it was probably upgraded', 38 | $this->runComposerCommand('update'), 39 | ); 40 | } 41 | 42 | public function test_link_with_dependencies(): void 43 | { 44 | $this->useComposerLinkLocal(); 45 | $output = $this->runComposerCommand('link ' . self::RELATIVE_PATH_MOCK . '/package-2'); 46 | 47 | static::assertStringContainsString( 48 | 'Installing psr/container (2.0.1): Extracting archive', 49 | $output 50 | ); 51 | static::assertStringContainsString( 52 | ' Installing test/package-2 (dev-linked)', 53 | $output 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Integration/LinuxMacosBasicTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Integration; 17 | 18 | /** 19 | * @group ubuntu-latest 20 | * @group macos-latest 21 | */ 22 | class LinuxMacosBasicTest extends TestCase 23 | { 24 | /** 25 | * Test if we can link a package in a project while using relative paths. 26 | * The plugin is installed in project. 27 | */ 28 | public function test_link_package_in_project_with_relative_paths_with_local_plugin(): void 29 | { 30 | $this->useComposerLinkLocal(); 31 | 32 | static::assertStringContainsString( 33 | 'No packages are linked', 34 | $this->runComposerCommand('linked') 35 | ); 36 | static::assertStringContainsString( 37 | ' - Installing test/package-1 (dev-linked): Symlinking from ' . self::RELATIVE_PATH_MOCK . '/package-1', 38 | $this->runComposerCommand('link ' . self::RELATIVE_PATH_MOCK . '/package-1') 39 | ); 40 | static::assertStringContainsString( 41 | 'test/package-1 ' . self::RELATIVE_PATH_MOCK . '/package-1', 42 | $this->runComposerCommand('linked') 43 | ); 44 | static::assertStringContainsString( 45 | ' - Removing test/package-1 (dev-linked)', 46 | $this->runComposerCommand('unlink ' . self::RELATIVE_PATH_MOCK . '/package-1') 47 | ); 48 | static::assertStringContainsString( 49 | 'No packages are linked', 50 | $this->runComposerCommand('linked') 51 | ); 52 | } 53 | 54 | /** 55 | * Test if we can link a package in a project while using absolute paths. 56 | * The plugin is installed in project. 57 | */ 58 | public function test_link_package_in_project_with_absolute_paths_with_local_plugin(): void 59 | { 60 | $this->useComposerLinkLocal(); 61 | 62 | static::assertStringContainsString( 63 | 'No packages are linked', 64 | $this->runComposerCommand('linked') 65 | ); 66 | static::assertStringContainsString( 67 | ' - Installing test/package-1 (dev-linked): Symlinking from ' . $this->getMockDirectory() . '/package-1', 68 | $this->runComposerCommand('link ' . $this->getMockDirectory() . '/package-1') 69 | ); 70 | static::assertStringContainsString( 71 | 'test/package-1 ' . $this->getMockDirectory() . '/package-1', 72 | $this->runComposerCommand('linked') 73 | ); 74 | static::assertStringContainsString( 75 | ' - Removing test/package-1 (dev-linked)', 76 | $this->runComposerCommand('unlink ' . $this->getMockDirectory() . '/package-1') 77 | ); 78 | static::assertStringContainsString( 79 | 'No packages are linked', 80 | $this->runComposerCommand('linked') 81 | ); 82 | } 83 | 84 | /** 85 | * Test if we can link a package in a project while using relative paths. 86 | * The plugin is installed globally. 87 | */ 88 | public function test_link_package_in_project_with_relative_paths_with_global_plugin(): void 89 | { 90 | $this->useComposerLinkGlobal(); 91 | 92 | static::assertStringContainsString( 93 | 'No packages are linked', 94 | $this->runComposerCommand('linked') 95 | ); 96 | static::assertStringContainsString( 97 | ' - Installing test/package-1 (dev-linked): Symlinking from ' . self::RELATIVE_PATH_MOCK . '/package-1', 98 | $this->runComposerCommand('link ' . self::RELATIVE_PATH_MOCK . '/package-1') 99 | ); 100 | static::assertStringContainsString( 101 | 'test/package-1 ' . self::RELATIVE_PATH_MOCK . '/package-1', 102 | $this->runComposerCommand('linked') 103 | ); 104 | static::assertStringContainsString( 105 | ' - Removing test/package-1 (dev-linked)', 106 | $this->runComposerCommand('unlink ' . self::RELATIVE_PATH_MOCK . '/package-1') 107 | ); 108 | static::assertStringContainsString( 109 | 'No packages are linked', 110 | $this->runComposerCommand('linked') 111 | ); 112 | } 113 | 114 | /** 115 | * Test if we can link a package in a project while using relative paths. 116 | * The plugin is installed globally. 117 | */ 118 | public function test_link_package_in_project_with_absolute_paths_with_global_plugin(): void 119 | { 120 | $this->useComposerLinkGlobal(); 121 | 122 | static::assertStringContainsString( 123 | 'No packages are linked', 124 | $this->runComposerCommand('linked') 125 | ); 126 | static::assertStringContainsString( 127 | ' - Installing test/package-1 (dev-linked): Symlinking from ' . $this->getMockDirectory() . '/package-1', 128 | $this->runComposerCommand('link ' . $this->getMockDirectory() . '/package-1') 129 | ); 130 | static::assertStringContainsString( 131 | 'test/package-1 ' . $this->getMockDirectory() . '/package-1', 132 | $this->runComposerCommand('linked') 133 | ); 134 | static::assertStringContainsString( 135 | ' - Removing test/package-1 (dev-linked)', 136 | $this->runComposerCommand('unlink ' . $this->getMockDirectory() . '/package-1') 137 | ); 138 | static::assertStringContainsString( 139 | 'No packages are linked', 140 | $this->runComposerCommand('linked') 141 | ); 142 | } 143 | 144 | /** 145 | * Test if we can link a package globally while using relative paths. 146 | * The plugin is installed globally. 147 | */ 148 | public function test_link_package_in_global_with_relative_paths_with_global_plugin(): void 149 | { 150 | $this->useComposerLinkGlobal(); 151 | 152 | static::assertStringContainsString( 153 | 'No packages are linked', 154 | $this->runComposerCommand('global linked') 155 | ); 156 | static::assertStringContainsString( 157 | ' - Installing test/package-1 (dev-linked): Symlinking from ' . $this->getMockDirectory() . '/package-1', 158 | $this->runComposerCommand('global link ' . self::RELATIVE_PATH_MOCK . '/package-1') 159 | ); 160 | static::assertStringContainsString( 161 | 'test/package-1 ' . $this->getMockDirectory() . '/package-1', 162 | $this->runComposerCommand('global linked') 163 | ); 164 | static::assertStringContainsString( 165 | ' - Removing test/package-1 (dev-linked)', 166 | $this->runComposerCommand('global unlink ' . self::RELATIVE_PATH_MOCK . '/package-1') 167 | ); 168 | static::assertStringContainsString( 169 | 'No packages are linked', 170 | $this->runComposerCommand('global linked') 171 | ); 172 | } 173 | 174 | /** 175 | * Test if we can link a package globally while using absolute paths. 176 | * The plugin is installed globally. 177 | */ 178 | public function test_link_package_in_global_with_absolute_paths_with_global_plugin(): void 179 | { 180 | $this->useComposerLinkGlobal(); 181 | 182 | static::assertStringContainsString( 183 | 'No packages are linked', 184 | $this->runComposerCommand('global linked') 185 | ); 186 | static::assertStringContainsString( 187 | ' - Installing test/package-1 (dev-linked): Symlinking from ' . $this->getMockDirectory() . '/package-1', 188 | $this->runComposerCommand('global link ' . $this->getMockDirectory() . '/package-1') 189 | ); 190 | static::assertStringContainsString( 191 | 'test/package-1 ' . $this->getMockDirectory() . '/package-1', 192 | $this->runComposerCommand('global linked') 193 | ); 194 | static::assertStringContainsString( 195 | ' - Removing test/package-1 (dev-linked)', 196 | $this->runComposerCommand('global unlink ' . $this->getMockDirectory() . '/package-1') 197 | ); 198 | static::assertStringContainsString( 199 | 'No packages are linked', 200 | $this->runComposerCommand('global linked') 201 | ); 202 | } 203 | 204 | public function test_link_with_transitive_dependencies(): void 205 | { 206 | $this->useComposerLinkGlobal(); 207 | 208 | // Add package-2 directory as repository 209 | $composerFile = [ 210 | 'repositories' => [ 211 | [ 212 | 'type' => 'path', 213 | 'url' => $this->getMockDirectory() . '/package-2', 214 | ], 215 | ], 216 | ]; 217 | $this->setCurrentComposeFile($composerFile); 218 | 219 | // Require test/package-2 with dependency to psr/container 2.0.1 220 | static::assertStringContainsString( 221 | 'Installing psr/container (2.0.1): Extracting archive', 222 | $this->runComposerCommand('require test/package-2 @dev') 223 | ); 224 | 225 | static::assertStringContainsString( 226 | 'Installing psr/container (dev-linked): Symlinking from', 227 | $this->runComposerCommand('link ' . self::RELATIVE_PATH_MOCK . '/psr-container'), 228 | ); 229 | 230 | // Unlink and test if 2.0.1 is installed again 231 | static::assertStringContainsString( 232 | 'Installing psr/container (2.0.1): Extracting archive', 233 | $this->runComposerCommand('unlink ' . self::RELATIVE_PATH_MOCK . '/psr-container'), 234 | ); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /tests/Integration/TestCase.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Integration; 17 | 18 | use Composer\Console\Application; 19 | use RuntimeException; 20 | use Tests\TestCase as BaseCase; 21 | 22 | abstract class TestCase extends BaseCase 23 | { 24 | public const RELATIVE_PATH_MOCK = '..' . DIRECTORY_SEPARATOR . 'composer-link' . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'mock'; 25 | 26 | protected Application $application; 27 | 28 | private string $thisPackagePath; 29 | 30 | protected string $composerGlobalDir; 31 | 32 | public function setUp(): void 33 | { 34 | parent::setUp(); 35 | if (getcwd() === false) { 36 | throw new RuntimeException('Unable to get CMD'); 37 | } 38 | $this->thisPackagePath = (string) getcwd(); 39 | $this->composerGlobalDir = (string) realpath((string) exec('composer config --global home')); 40 | 41 | chdir($this->tmpAbsoluteDir); 42 | } 43 | 44 | public function getMockDirectory(): string 45 | { 46 | return $this->thisPackagePath . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'mock'; 47 | } 48 | 49 | protected function runComposerCommand(string $command): string 50 | { 51 | $output = []; 52 | exec('composer ' . $command . ' 2>&1', $output); 53 | 54 | return implode(PHP_EOL, $output); 55 | } 56 | 57 | /** 58 | * @return array 59 | */ 60 | protected function getCurrentComposeFile(): array 61 | { 62 | /** @var string $content */ 63 | $content = file_get_contents('composer.json'); 64 | 65 | return json_decode($content, true); 66 | } 67 | 68 | /** 69 | * @param array $composeFile 70 | */ 71 | protected function setCurrentComposeFile(array $composeFile): void 72 | { 73 | file_put_contents('composer.json', json_encode($composeFile, JSON_PRETTY_PRINT)); 74 | } 75 | 76 | public function getThisPackagePath(): string 77 | { 78 | return $this->thisPackagePath; 79 | } 80 | 81 | /** 82 | * Loads an older version with upgrade protection. 83 | */ 84 | protected function useComposerLinkLocalOld(): void 85 | { 86 | file_put_contents('composer.json', '{ 87 | "config": { 88 | "allow-plugins": { 89 | "sandersander/composer-link": true 90 | } 91 | } 92 | }'); 93 | 94 | shell_exec('composer require sandersander/composer-link 0.4.1 2>&1'); 95 | } 96 | 97 | protected function useComposerLinkLocal(): void 98 | { 99 | file_put_contents('composer.json', '{ 100 | "repositories": [ 101 | { 102 | "type": "path", 103 | "url": "' . addslashes($this->thisPackagePath) . '" 104 | } 105 | ], 106 | "config": { 107 | "allow-plugins": { 108 | "sandersander/composer-link": true 109 | } 110 | } 111 | }'); 112 | 113 | shell_exec('composer require sandersander/composer-link @dev 2>&1'); 114 | } 115 | 116 | protected function useComposerLinkGlobal(): void 117 | { 118 | file_put_contents($this->composerGlobalDir . DIRECTORY_SEPARATOR . 'composer.json', '{ 119 | "repositories": [ 120 | { 121 | "type": "path", 122 | "url": "' . addslashes($this->thisPackagePath) . '" 123 | } 124 | ], 125 | "config": { 126 | "allow-plugins": { 127 | "sandersander/composer-link": true 128 | } 129 | } 130 | }'); 131 | 132 | shell_exec('composer global require sandersander/composer-link @dev 2>&1'); 133 | } 134 | 135 | public function tearDown(): void 136 | { 137 | // We have to change directory, before parent class remove the directory. 138 | // Windows has problems with removing directories when they are open in console 139 | chdir($this->thisPackagePath); 140 | parent::tearDown(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/Integration/WindowsBasicTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Integration; 17 | 18 | /** 19 | * @group windows-latest 20 | */ 21 | class WindowsBasicTest extends TestCase 22 | { 23 | /** 24 | * Test if we can link a package in a project while using relative paths. 25 | * The plugin is installed in project. 26 | */ 27 | public function test_link_package_in_project_with_relative_paths_with_local_plugin(): void 28 | { 29 | $this->useComposerLinkLocal(); 30 | 31 | static::assertStringContainsString( 32 | 'No packages are linked', 33 | $this->runComposerCommand('linked') 34 | ); 35 | static::assertStringContainsString( 36 | ' - Installing test/package-1 (dev-linked): Junctioning from ' . self::RELATIVE_PATH_MOCK . '\package-1', 37 | $this->runComposerCommand('link ' . self::RELATIVE_PATH_MOCK . '\package-1') 38 | ); 39 | static::assertStringContainsString( 40 | 'test/package-1 ' . self::RELATIVE_PATH_MOCK . '\package-1', 41 | $this->runComposerCommand('linked') 42 | ); 43 | static::assertStringContainsString( 44 | ' - Removing test/package-1 (dev-linked), source is still present in ' . $this->tmpAbsoluteDir . 'vendor/test/package-1', 45 | $this->runComposerCommand('unlink ' . self::RELATIVE_PATH_MOCK . '\package-1') 46 | ); 47 | static::assertStringContainsString( 48 | 'No packages are linked', 49 | $this->runComposerCommand('linked') 50 | ); 51 | } 52 | 53 | /** 54 | * Test if we can link a package in a project while using absolute paths. 55 | * The plugin is installed in project. 56 | */ 57 | public function test_link_package_in_project_with_absolute_paths_with_local_plugin(): void 58 | { 59 | $this->useComposerLinkLocal(); 60 | 61 | static::assertStringContainsString( 62 | 'No packages are linked', 63 | $this->runComposerCommand('linked') 64 | ); 65 | static::assertStringContainsString( 66 | ' - Installing test/package-1 (dev-linked): Junctioning from ' . $this->getMockDirectory() . '\package-1', 67 | $this->runComposerCommand('link ' . $this->getMockDirectory() . '\package-1') 68 | ); 69 | static::assertStringContainsString( 70 | 'test/package-1 ' . $this->getMockDirectory() . '\package-1', 71 | $this->runComposerCommand('linked') 72 | ); 73 | static::assertStringContainsString( 74 | ' - Removing test/package-1 (dev-linked), source is still present in ' . $this->tmpAbsoluteDir . 'vendor/test/package-1', 75 | $this->runComposerCommand('unlink ' . $this->getMockDirectory() . '\package-1') 76 | ); 77 | static::assertStringContainsString( 78 | 'No packages are linked', 79 | $this->runComposerCommand('linked') 80 | ); 81 | } 82 | 83 | /** 84 | * Test if we can link a package in a project while using relative paths. 85 | * The plugin is installed globally. 86 | */ 87 | public function test_link_package_in_project_with_relative_paths_with_global_plugin(): void 88 | { 89 | $this->useComposerLinkGlobal(); 90 | 91 | static::assertStringContainsString( 92 | 'No packages are linked', 93 | $this->runComposerCommand('linked') 94 | ); 95 | static::assertStringContainsString( 96 | ' - Installing test/package-1 (dev-linked): Junctioning from ' . self::RELATIVE_PATH_MOCK . '\package-1', 97 | $this->runComposerCommand('link ' . self::RELATIVE_PATH_MOCK . '\package-1') 98 | ); 99 | static::assertStringContainsString( 100 | 'test/package-1 ' . self::RELATIVE_PATH_MOCK . '\package-1', 101 | $this->runComposerCommand('linked') 102 | ); 103 | static::assertStringContainsString( 104 | ' - Removing test/package-1 (dev-linked), source is still present in ' . $this->composerGlobalDir . '\vendor/test/package-1', 105 | $this->runComposerCommand('unlink ' . self::RELATIVE_PATH_MOCK . '\package-1') 106 | ); 107 | static::assertStringContainsString( 108 | 'No packages are linked', 109 | $this->runComposerCommand('linked') 110 | ); 111 | } 112 | 113 | /** 114 | * Test if we can link a package in a project while using relative paths. 115 | * The plugin is installed globally. 116 | */ 117 | public function test_link_package_in_project_with_absolute_paths_with_global_plugin(): void 118 | { 119 | $this->useComposerLinkGlobal(); 120 | 121 | static::assertStringContainsString( 122 | 'No packages are linked', 123 | $this->runComposerCommand('linked') 124 | ); 125 | static::assertStringContainsString( 126 | ' - Installing test/package-1 (dev-linked): Junctioning from ' . $this->getMockDirectory() . '\package-1', 127 | $this->runComposerCommand('link ' . $this->getMockDirectory() . '\package-1') 128 | ); 129 | static::assertStringContainsString( 130 | 'test/package-1 ' . $this->getMockDirectory() . '\package-1', 131 | $this->runComposerCommand('linked') 132 | ); 133 | static::assertStringContainsString( 134 | ' - Removing test/package-1 (dev-linked), source is still present in ' . $this->composerGlobalDir . '\vendor/test/package-1', 135 | $this->runComposerCommand('unlink ' . $this->getMockDirectory() . '\package-1') 136 | ); 137 | static::assertStringContainsString( 138 | 'No packages are linked', 139 | $this->runComposerCommand('linked') 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests; 17 | 18 | use Composer\Util\Filesystem; 19 | use PHPUnit\Framework\TestCase as PHPUnitTestCase; 20 | 21 | abstract class TestCase extends PHPUnitTestCase 22 | { 23 | /** @var non-empty-string string */ 24 | protected string $tmpAbsoluteDir; 25 | 26 | /** @var non-empty-string string */ 27 | protected string $tmpRelativeDir; 28 | 29 | protected Filesystem $filesystem; 30 | 31 | protected bool $containerized; 32 | 33 | protected function setUp(): void 34 | { 35 | parent::setUp(); 36 | 37 | $this->containerized = getenv('PHPUNIT_INTEGRATION') !== false; 38 | $tmp = $this->containerized ? '../tmp-test' : 'tests' . DIRECTORY_SEPARATOR . 'tmp'; 39 | 40 | $this->filesystem = new Filesystem(); 41 | $this->filesystem->emptyDirectory($tmp); 42 | 43 | $this->tmpAbsoluteDir = realpath($tmp) . DIRECTORY_SEPARATOR; 44 | $this->tmpRelativeDir = $tmp . DIRECTORY_SEPARATOR; 45 | } 46 | 47 | protected function tearDown(): void 48 | { 49 | parent::tearDown(); 50 | $this->filesystem->removeDirectory($this->tmpAbsoluteDir); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Unit/CommandProviderTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use Composer\IO\IOInterface; 19 | use ComposerLink\CommandProvider; 20 | use ComposerLink\Commands\LinkCommand; 21 | use ComposerLink\Commands\LinkedCommand; 22 | use ComposerLink\Commands\UnlinkAllCommand; 23 | use ComposerLink\Commands\UnlinkCommand; 24 | use ComposerLink\Plugin; 25 | use PHPUnit\Framework\TestCase; 26 | 27 | class CommandProviderTest extends TestCase 28 | { 29 | public function test_command_provider(): void 30 | { 31 | $arguments = []; 32 | $arguments['io'] = static::createStub(IOInterface::class); 33 | $arguments['plugin'] = static::createStub(Plugin::class); 34 | 35 | $provider = new CommandProvider($arguments); 36 | $commands = $provider->getCommands(); 37 | 38 | static::assertCount(4, $commands); 39 | static::assertInstanceOf(LinkCommand::class, $commands[0]); 40 | static::assertInstanceOf(UnlinkCommand::class, $commands[1]); 41 | static::assertInstanceOf(LinkedCommand::class, $commands[2]); 42 | static::assertInstanceOf(UnlinkAllCommand::class, $commands[3]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/Commands/LinkCommandTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Commands; 17 | 18 | use Composer\Console\Application; 19 | use ComposerLink\Commands\LinkCommand; 20 | use ComposerLink\LinkManager; 21 | use ComposerLink\Package\LinkedPackage; 22 | use ComposerLink\Package\LinkedPackageFactory; 23 | use ComposerLink\Plugin; 24 | use ComposerLink\Repository\Repository; 25 | use PHPUnit\Framework\MockObject\MockObject; 26 | use Symfony\Component\Console\Input\StringInput; 27 | use Symfony\Component\Console\Output\OutputInterface; 28 | use Tests\Unit\TestCase; 29 | 30 | class LinkCommandTest extends TestCase 31 | { 32 | /** @var Plugin&MockObject */ 33 | protected Plugin $plugin; 34 | 35 | /** @var OutputInterface&MockObject */ 36 | protected OutputInterface $output; 37 | 38 | /** @var LinkManager&MockObject */ 39 | protected LinkManager $linkManager; 40 | 41 | /** @var Repository&MockObject */ 42 | protected Repository $repository; 43 | 44 | /** @var LinkedPackageFactory&MockObject */ 45 | protected LinkedPackageFactory $packageFactory; 46 | 47 | /** @var LinkedPackage&MockObject */ 48 | protected LinkedPackage $package; 49 | 50 | protected Application $application; 51 | 52 | protected function setUp(): void 53 | { 54 | parent::setUp(); 55 | 56 | $this->plugin = $this->createMock(Plugin::class); 57 | $this->output = $this->createMock(OutputInterface::class); 58 | $this->linkManager = $this->createMock(LinkManager::class); 59 | $this->repository = $this->createMock(Repository::class); 60 | $this->packageFactory = $this->createMock(LinkedPackageFactory::class); 61 | $this->package = $this->createMock(LinkedPackage::class); 62 | 63 | $this->plugin->method('getRepository')->willReturn($this->repository); 64 | $this->plugin->method('getLinkManager')->willReturn($this->linkManager); 65 | $this->plugin->method('getPackageFactory')->willReturn($this->packageFactory); 66 | 67 | $command = new LinkCommand($this->plugin); 68 | $this->application = new Application(); 69 | $this->application->setAutoExit(false); 70 | $this->application->setCatchExceptions(false); 71 | $this->application->add($command); 72 | } 73 | 74 | public function test_link_command(): void 75 | { 76 | $this->packageFactory->expects(static::once()) 77 | ->method('fromPath') 78 | ->with('/test-path'); 79 | 80 | $this->linkManager->expects(static::once())->method('add'); 81 | 82 | $input = new StringInput('link /test-path'); 83 | static::assertSame(0, $this->application->run($input, $this->output)); 84 | } 85 | 86 | public function test_only_installed_when_not_installed(): void 87 | { 88 | $this->packageFactory->expects(static::once()) 89 | ->method('fromPath') 90 | ->with('/test-path'); 91 | 92 | $this->linkManager->expects(static::never())->method('add'); 93 | 94 | $input = new StringInput('link /test-path --only-installed'); 95 | static::assertSame(0, $this->application->run($input, $this->output)); 96 | } 97 | 98 | public function test_only_installed_when_installed(): void 99 | { 100 | $this->packageFactory->expects(static::once()) 101 | ->method('fromPath') 102 | ->with('/test-path') 103 | ->willReturn($this->mockPackage()); 104 | 105 | $this->linkManager->expects(static::once())->method('add'); 106 | 107 | $input = new StringInput('link /test-path --only-installed'); 108 | static::assertSame(0, $this->application->run($input, $this->output)); 109 | } 110 | 111 | public function test_link_command_from_global(): void 112 | { 113 | $this->plugin->method('isGlobal')->willReturn(true); 114 | $this->packageFactory->expects(static::once()) 115 | ->method('fromPath') 116 | ->with(realpath(__DIR__ . '/../..')); 117 | 118 | $this->linkManager->expects(static::once())->method('add'); 119 | 120 | $input = new StringInput('link tests'); 121 | static::assertSame(0, $this->application->run($input, $this->output)); 122 | } 123 | 124 | public function test_existing_path(): void 125 | { 126 | $this->repository->expects(static::once())->method('findByPath') 127 | ->with('/test-path') 128 | ->willReturn($this->createMock(LinkedPackage::class)); 129 | 130 | $this->output->expects(static::once())->method('writeln') 131 | ->with('Package in path "/test-path" already linked'); 132 | 133 | $input = new StringInput('link /test-path'); 134 | static::assertSame(0, $this->application->run($input, $this->output)); 135 | } 136 | 137 | public function test_existing_package_name(): void 138 | { 139 | $this->package->method('getName')->willReturn('test/package'); 140 | $this->package->method('getPath')->willReturn('/test-path'); 141 | 142 | $this->repository->expects(static::once()) 143 | ->method('findByName') 144 | ->willReturn($this->package); 145 | 146 | $this->packageFactory->expects(static::once()) 147 | ->method('fromPath') 148 | ->with('/test-path') 149 | ->willReturn($this->package); 150 | 151 | $command = new LinkCommand($this->plugin); 152 | static::assertSame('link', $command->getName()); 153 | 154 | $this->output->expects(static::once())->method('writeln') 155 | ->with('Package "test/package" in "/test-path" already linked from path "/test-path"'); 156 | 157 | $input = new StringInput('link /test-path'); 158 | static::assertSame(0, $this->application->run($input, $this->output)); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/Unit/Commands/LinkedCommandTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Commands; 17 | 18 | use Composer\Console\Application; 19 | use ComposerLink\Commands\LinkedCommand; 20 | use ComposerLink\Package\LinkedPackage; 21 | use ComposerLink\Plugin; 22 | use ComposerLink\Repository\Repository; 23 | use PHPUnit\Framework\MockObject\MockObject; 24 | use PHPUnit\Framework\TestCase; 25 | use Symfony\Component\Console\Input\StringInput; 26 | use Symfony\Component\Console\Output\OutputInterface; 27 | 28 | class LinkedCommandTest extends TestCase 29 | { 30 | protected Application $application; 31 | 32 | /** @var Plugin&MockObject */ 33 | protected Plugin $plugin; 34 | 35 | protected function setUp(): void 36 | { 37 | parent::setUp(); 38 | 39 | $this->plugin = $this->createMock(Plugin::class); 40 | 41 | $this->application = new Application(); 42 | $this->application->setAutoExit(false); 43 | $this->application->setCatchExceptions(false); 44 | $this->application->add(new LinkedCommand($this->plugin)); 45 | } 46 | 47 | public function test_no_linked_packages(): void 48 | { 49 | $output = $this->createMock(OutputInterface::class); 50 | $output->expects(static::once()) 51 | ->method('writeln') 52 | ->with('No packages are linked'); 53 | 54 | $input = new StringInput('linked'); 55 | static::assertSame(0, $this->application->run($input, $output)); 56 | } 57 | 58 | public function test_linked_packages(): void 59 | { 60 | $repository = $this->createMock(Repository::class); 61 | $repository->method('all')->willReturn([ 62 | $this->getMockedLinkedPackage('test-1'), 63 | $this->getMockedLinkedPackage('test-2'), 64 | ]); 65 | $this->plugin->method('getRepository')->willReturn($repository); 66 | 67 | $output = $this->createMock(OutputInterface::class); 68 | $output->expects(static::exactly(2)) 69 | ->method('writeln') 70 | ->with(static::logicalOr( 71 | static::equalTo("package/test-1\t../package/test-1"), 72 | static::equalTo("package/test-2\t../package/test-2") 73 | )); 74 | 75 | $input = new StringInput('linked'); 76 | static::assertSame(0, $this->application->run($input, $output)); 77 | } 78 | 79 | private function getMockedLinkedPackage(string $name): LinkedPackage 80 | { 81 | $package = $this->createMock(LinkedPackage::class); 82 | $package->method('getName')->willReturn('package/' . $name); 83 | $package->method('getPath')->willReturn('../package/' . $name); 84 | 85 | return $package; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Unit/Commands/UnlinkAllCommandTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Commands; 17 | 18 | use Composer\Console\Application; 19 | use ComposerLink\Commands\UnlinkAllCommand; 20 | use ComposerLink\LinkManager; 21 | use ComposerLink\Package\LinkedPackage; 22 | use ComposerLink\Plugin; 23 | use ComposerLink\Repository\Repository; 24 | use PHPUnit\Framework\MockObject\MockObject; 25 | use PHPUnit\Framework\TestCase; 26 | use Symfony\Component\Console\Input\StringInput; 27 | use Symfony\Component\Console\Output\OutputInterface; 28 | 29 | class UnlinkAllCommandTest extends TestCase 30 | { 31 | /** @var Plugin&MockObject */ 32 | protected Plugin $plugin; 33 | 34 | /** @var OutputInterface&MockObject */ 35 | protected OutputInterface $output; 36 | 37 | /** @var LinkManager&MockObject */ 38 | protected LinkManager $linkManager; 39 | 40 | /** @var Repository&MockObject */ 41 | protected Repository $repository; 42 | 43 | /** @var LinkedPackage&MockObject */ 44 | protected LinkedPackage $package; 45 | 46 | protected Application $application; 47 | 48 | protected function setUp(): void 49 | { 50 | parent::setUp(); 51 | 52 | $this->plugin = $this->createMock(Plugin::class); 53 | $this->output = $this->createMock(OutputInterface::class); 54 | $this->linkManager = $this->createMock(LinkManager::class); 55 | $this->repository = $this->createMock(Repository::class); 56 | $this->package = $this->createMock(LinkedPackage::class); 57 | 58 | $this->plugin->method('getRepository')->willReturn($this->repository); 59 | $this->plugin->method('getLinkManager')->willReturn($this->linkManager); 60 | 61 | $this->application = new Application(); 62 | $this->application->setAutoExit(false); 63 | $this->application->setCatchExceptions(false); 64 | $this->application->add(new UnlinkAllCommand($this->plugin)); 65 | } 66 | 67 | public function test_unlink_all_command(): void 68 | { 69 | $this->repository->expects(static::once())->method('all')->willReturn([$this->package, $this->package]); 70 | $this->linkManager->expects(static::exactly(2))->method('remove')->with($this->package); 71 | 72 | $input = new StringInput('unlink-all'); 73 | static::assertSame(0, $this->application->run($input, $this->output)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Unit/Commands/UnlinkCommandTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Commands; 17 | 18 | use Composer\Console\Application; 19 | use ComposerLink\Commands\UnlinkCommand; 20 | use ComposerLink\LinkManager; 21 | use ComposerLink\Package\LinkedPackage; 22 | use ComposerLink\Plugin; 23 | use ComposerLink\Repository\Repository; 24 | use PHPUnit\Framework\MockObject\MockObject; 25 | use PHPUnit\Framework\TestCase; 26 | use Symfony\Component\Console\Input\StringInput; 27 | use Symfony\Component\Console\Output\OutputInterface; 28 | 29 | class UnlinkCommandTest extends TestCase 30 | { 31 | /** @var Plugin&MockObject */ 32 | protected Plugin $plugin; 33 | 34 | /** @var OutputInterface&MockObject */ 35 | protected OutputInterface $output; 36 | 37 | /** @var LinkManager&MockObject */ 38 | protected LinkManager $linkManager; 39 | 40 | /** @var Repository&MockObject */ 41 | protected Repository $repository; 42 | 43 | /** @var LinkedPackage&MockObject */ 44 | protected LinkedPackage $package; 45 | 46 | protected Application $application; 47 | 48 | protected function setUp(): void 49 | { 50 | parent::setUp(); 51 | 52 | $this->plugin = $this->createMock(Plugin::class); 53 | $this->output = $this->createMock(OutputInterface::class); 54 | $this->linkManager = $this->createMock(LinkManager::class); 55 | $this->repository = $this->createMock(Repository::class); 56 | $this->package = $this->createMock(LinkedPackage::class); 57 | 58 | $this->plugin->method('getRepository')->willReturn($this->repository); 59 | $this->plugin->method('getLinkManager')->willReturn($this->linkManager); 60 | 61 | $this->application = new Application(); 62 | $this->application->setAutoExit(false); 63 | $this->application->setCatchExceptions(false); 64 | $this->application->add(new UnlinkCommand($this->plugin)); 65 | } 66 | 67 | public function test_unlink_command_for_existing_package(): void 68 | { 69 | $this->repository->expects(static::once())->method('findByPath')->willReturn($this->package); 70 | $this->linkManager->expects(static::once())->method('remove')->with($this->package); 71 | 72 | $input = new StringInput('unlink /test-path'); 73 | static::assertSame(0, $this->application->run($input, $this->output)); 74 | } 75 | 76 | public function test_unlink_command_for_existing_package_global(): void 77 | { 78 | $this->plugin->method('isGlobal')->willReturn(true); 79 | $this->repository->expects(static::once())->method('findByPath')->willReturn($this->package); 80 | $this->linkManager->expects(static::once())->method('remove')->with($this->package); 81 | 82 | $input = new StringInput('unlink tests'); 83 | static::assertSame(0, $this->application->run($input, $this->output)); 84 | } 85 | 86 | public function test_unlink_command_for_non_existing_package(): void 87 | { 88 | $this->repository->expects(static::once())->method('findByPath')->willReturn(null); 89 | $this->application->run(new StringInput('unlink /test-path'), $this->output); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/Unit/InstallerFactoryTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use Composer\Composer; 19 | use Composer\IO\IOInterface; 20 | use ComposerLink\InstallerFactory; 21 | 22 | class InstallerFactoryTest extends TestCase 23 | { 24 | public function test_factory(): void 25 | { 26 | $io = $this->createMock(IOInterface::class); 27 | $composer = $this->createMock(Composer::class); 28 | $factory = new InstallerFactory($io, $composer); 29 | $factory->create(); 30 | static::expectNotToPerformAssertions(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/LinkManagerFactoryTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use Composer\Composer; 19 | use Composer\IO\IOInterface; 20 | use ComposerLink\InstallerFactory; 21 | use ComposerLink\LinkManagerFactory; 22 | use ComposerLink\Repository\Repository; 23 | 24 | class LinkManagerFactoryTest extends TestCase 25 | { 26 | public function test_create(): void 27 | { 28 | $factory = new LinkManagerFactory(); 29 | $factory->create( 30 | $this->createMock(Repository::class), 31 | $this->createMock(InstallerFactory::class), 32 | $this->createMock(IOInterface::class), 33 | $this->createMock(Composer::class), 34 | ); 35 | 36 | self::expectNotToPerformAssertions(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Unit/LinkManagerTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use Composer\Composer; 19 | use Composer\DependencyResolver\Request; 20 | use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; 21 | use Composer\Installer; 22 | use Composer\IO\IOInterface; 23 | use Composer\Package\Link; 24 | use Composer\Package\Locker; 25 | use Composer\Package\Package; 26 | use Composer\Package\RootPackageInterface; 27 | use Composer\Repository\LockArrayRepository; 28 | use ComposerLink\InstallerFactory; 29 | use ComposerLink\LinkManager; 30 | use ComposerLink\Repository\Repository; 31 | use PHPUnit\Framework\MockObject\MockObject; 32 | 33 | class LinkManagerTest extends TestCase 34 | { 35 | /** 36 | * @var Repository&MockObject 37 | */ 38 | protected Repository $repository; 39 | 40 | /** 41 | * @var Installer&MockObject 42 | */ 43 | protected Installer $installer; 44 | 45 | /** 46 | * @var IOInterface&MockObject 47 | */ 48 | protected IOInterface $io; 49 | 50 | /** 51 | * @var Composer&MockObject 52 | */ 53 | protected Composer $composer; 54 | 55 | /** 56 | * @var RootPackageInterface&MockObject 57 | */ 58 | protected RootPackageInterface $rootPackage; 59 | 60 | /** 61 | * @var LockArrayRepository&MockObject 62 | */ 63 | protected LockArrayRepository $lockArrayRepository; 64 | 65 | protected LinkManager $linkManager; 66 | 67 | protected function setUp(): void 68 | { 69 | parent::setUp(); 70 | 71 | $this->repository = $this->createMock(Repository::class); 72 | $installerFactory = $this->createMock(InstallerFactory::class); 73 | $this->installer = $this->createMock(Installer::class); 74 | $installerFactory->method('create')->willReturn($this->installer); 75 | $this->io = $this->createMock(IOInterface::class); 76 | $this->composer = $this->createMock(Composer::class); 77 | $this->rootPackage = $this->createMock(RootPackageInterface::class); 78 | $this->composer->method('getPackage')->willReturn($this->rootPackage); 79 | 80 | $locker = $this->createMock(Locker::class); 81 | $this->lockArrayRepository = $this->createMock(LockArrayRepository::class); 82 | $locker->method('getLockedRepository')->willReturn($this->lockArrayRepository); 83 | $this->composer->method('getLocker')->willReturn($locker); 84 | 85 | $this->linkManager = new LinkManager( 86 | $this->repository, 87 | $installerFactory, 88 | $this->io, 89 | $this->composer, 90 | ); 91 | } 92 | 93 | public function test_has_linked_packages(): void 94 | { 95 | static::assertFalse($this->linkManager->hasLinkedPackages()); 96 | $this->linkManager->add($this->mockPackage()); 97 | static::assertTrue($this->linkManager->hasLinkedPackages()); 98 | } 99 | 100 | public function test_loads_active_linked_packages(): void 101 | { 102 | $installerFactory = $this->createMock(InstallerFactory::class); 103 | $installerFactory->method('create')->willReturn($this->installer); 104 | $this->repository->method('all')->willReturn([$this->mockPackage()]); 105 | 106 | $linkManager = new LinkManager( 107 | $this->repository, 108 | $installerFactory, 109 | $this->io, 110 | $this->composer, 111 | ); 112 | static::assertTrue($linkManager->hasLinkedPackages()); 113 | } 114 | 115 | public function test_add_package(): void 116 | { 117 | $package = $this->mockPackage(); 118 | 119 | $this->repository->expects(static::once())->method('store')->with($package); 120 | $this->repository->expects(static::once())->method('persist'); 121 | 122 | $this->linkManager->add($package); 123 | } 124 | 125 | public function test_remove_package(): void 126 | { 127 | $package = $this->mockPackage(); 128 | 129 | $this->repository->expects(static::once())->method('remove')->with($package); 130 | $this->repository->expects(static::once())->method('persist'); 131 | 132 | $this->linkManager->remove($package); 133 | } 134 | 135 | public function test_link_packages_empty(): void 136 | { 137 | $this->installer->expects(static::once())->method('setUpdate')->with(true)->willReturnSelf(); 138 | $this->installer->expects(static::once())->method('setInstall')->with(true)->willReturnSelf(); 139 | $this->installer->expects(static::once())->method('setWriteLock')->with(false)->willReturnSelf(); 140 | $this->installer->expects(static::once())->method('setRunScripts')->with(false)->willReturnSelf(); 141 | $this->installer->expects(static::once())->method('setUpdateAllowList')->with([])->willReturnSelf(); 142 | $this->installer->expects(static::once())->method('setDevMode')->with(false)->willReturnSelf(); 143 | $this->installer->expects(static::once())->method('setPlatformRequirementFilter')->with(new IgnoreAllPlatformRequirementFilter())->willReturnSelf(); 144 | $this->installer->expects(static::once())->method('setUpdateAllowTransitiveDependencies')->with(Request::UPDATE_ONLY_LISTED)->willReturnSelf(); 145 | $this->installer->expects(static::once())->method('run'); 146 | 147 | $this->linkManager->linkPackages(false); 148 | } 149 | 150 | public function test_link_packages(): void 151 | { 152 | $package = $this->mockPackage(); 153 | $link = $this->createMock(Link::class); 154 | $package->method('createLink')->willReturn($link); 155 | $this->linkManager->add($package); 156 | 157 | $this->rootPackage->expects(static::once())->method('setRequires')->with(['test/package' => $link]); 158 | $this->rootPackage->expects(static::once())->method('setDevRequires')->with([]); 159 | $this->installer->expects(static::once())->method('setUpdate')->with(true)->willReturnSelf(); 160 | $this->installer->expects(static::once())->method('setInstall')->with(true)->willReturnSelf(); 161 | $this->installer->expects(static::once())->method('setWriteLock')->with(false)->willReturnSelf(); 162 | $this->installer->expects(static::once())->method('setRunScripts')->with(false)->willReturnSelf(); 163 | $this->installer->expects(static::once())->method('setUpdateAllowList')->with(['test/package'])->willReturnSelf(); 164 | $this->installer->expects(static::once())->method('setDevMode')->with(true)->willReturnSelf(); 165 | $this->installer->expects(static::once())->method('setPlatformRequirementFilter')->with(new IgnoreAllPlatformRequirementFilter())->willReturnSelf(); 166 | $this->installer->expects(static::once())->method('setUpdateAllowTransitiveDependencies')->with(Request::UPDATE_ONLY_LISTED)->willReturnSelf(); 167 | $this->installer->expects(static::once())->method('run'); 168 | 169 | $this->linkManager->linkPackages(true); 170 | } 171 | 172 | public function test_override_from_dev_requirements(): void 173 | { 174 | $package = $this->mockPackage(); 175 | $link = $this->createMock(Link::class); 176 | $package->method('createLink')->willReturn($link); 177 | $this->linkManager->add($package); 178 | 179 | $this->rootPackage->method('getDevRequires')->willReturn(['test/package' => $link]); 180 | $this->rootPackage->expects(static::once())->method('setRequires')->with(['test/package' => $link]); 181 | $this->rootPackage->expects(static::once())->method('setDevRequires')->with([]); 182 | 183 | $this->linkManager->linkPackages(true); 184 | } 185 | 186 | public function test_creates_alias_package(): void 187 | { 188 | $package = $this->mockPackage(); 189 | $locked = $this->createMock(Package::class); 190 | 191 | $this->lockArrayRepository->expects(static::once()) 192 | ->method('findPackage') 193 | ->with($package->getName()) 194 | ->willReturn($locked); 195 | 196 | $this->repository->expects(static::once())->method('store')->with($package); 197 | $this->repository->expects(static::exactly(2))->method('persist'); 198 | 199 | // Add package and remove again 200 | $this->linkManager->add($package); 201 | static::assertTrue($this->linkManager->hasLinkedPackages()); 202 | $this->linkManager->remove($package); 203 | static::assertFalse($this->linkManager->hasLinkedPackages()); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /tests/Unit/Package/LinkedPackageFactoryTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Package; 17 | 18 | use Composer\Installer\InstallationManager; 19 | use Composer\Package\PackageInterface; 20 | use Composer\Repository\InstalledRepositoryInterface; 21 | use ComposerLink\Package\LinkedPackageFactory; 22 | use RuntimeException; 23 | use Tests\Unit\TestCase; 24 | 25 | class LinkedPackageFactoryTest extends TestCase 26 | { 27 | public function test_factory(): void 28 | { 29 | $installationManager = $this->createMock(InstallationManager::class); 30 | $installedRepository = $this->createMock(InstalledRepositoryInterface::class); 31 | $originalPackage = $this->createMock(PackageInterface::class); 32 | $originalPackage->method('getName')->willReturn('test/package'); 33 | $installedRepository->method('getCanonicalPackages')->willReturn([$originalPackage]); 34 | $installationManager->method('getInstallPath')->willReturn('vendor/test/package/'); 35 | file_put_contents($this->tmpAbsoluteDir . 'composer.json', '{"name": "test/package"}'); 36 | 37 | $factory = new LinkedPackageFactory($installationManager, $installedRepository); 38 | $result = $factory->fromPath($this->tmpAbsoluteDir); 39 | 40 | static::assertSame('test/package', $result->getName()); 41 | static::assertSame($originalPackage, $result->getOriginalPackage()); 42 | } 43 | 44 | public function test_no_original_package(): void 45 | { 46 | $installationManager = $this->createMock(InstallationManager::class); 47 | $installedRepository = $this->createMock(InstalledRepositoryInterface::class); 48 | $installedRepository->method('getCanonicalPackages')->willReturn([]); 49 | $installationManager->method('getInstallPath')->willReturn('vendor/test/package/'); 50 | file_put_contents($this->tmpAbsoluteDir . 'composer.json', '{"name": "test/package"}'); 51 | 52 | $factory = new LinkedPackageFactory($installationManager, $installedRepository); 53 | $package = $factory->fromPath($this->tmpAbsoluteDir); 54 | static::assertNull($package->getOriginalPackage()); 55 | } 56 | 57 | public function test_invalid_package(): void 58 | { 59 | $installationManager = $this->createMock(InstallationManager::class); 60 | $installedRepository = $this->createMock(InstalledRepositoryInterface::class); 61 | $installedRepository->method('getCanonicalPackages')->willReturn([]); 62 | file_put_contents($this->tmpAbsoluteDir . 'composer.json', 'null'); 63 | 64 | $this->expectException(RuntimeException::class); 65 | $this->expectExceptionMessage(sprintf('Unable to read composer.json in "%s"', $this->tmpAbsoluteDir)); 66 | 67 | $factory = new LinkedPackageFactory($installationManager, $installedRepository); 68 | $factory->fromPath($this->tmpAbsoluteDir); 69 | } 70 | 71 | public function test_no_composer_file(): void 72 | { 73 | $installationManager = $this->createMock(InstallationManager::class); 74 | $installedRepository = $this->createMock(InstalledRepositoryInterface::class); 75 | $installedRepository->method('getCanonicalPackages')->willReturn([]); 76 | 77 | $this->expectException(RuntimeException::class); 78 | $this->expectExceptionMessage('No composer.json file found in "tests/empty".'); 79 | 80 | $factory = new LinkedPackageFactory($installationManager, $installedRepository); 81 | $factory->fromPath('tests/empty'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Unit/Package/LinkedPackageTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Package; 17 | 18 | use Composer\Package\CompletePackageInterface; 19 | use Composer\Package\Link; 20 | use Composer\Package\PackageInterface; 21 | use Composer\Package\RootPackageInterface; 22 | use Composer\Repository\RepositoryInterface; 23 | use Composer\Semver\Constraint\Constraint; 24 | use ComposerLink\Package\LinkedPackage; 25 | use PHPUnit\Framework\TestCase; 26 | 27 | class LinkedPackageTest extends TestCase 28 | { 29 | public function test_linked_package(): void 30 | { 31 | $package = static::createStub(CompletePackageInterface::class); 32 | $package->method('getName')->willReturn('test/package'); 33 | $originalPackage = static::createStub(PackageInterface::class); 34 | 35 | $linkedPackage = new LinkedPackage( 36 | $package, 37 | '/test-path', 38 | '/test-install-path', 39 | $originalPackage, 40 | ); 41 | 42 | static::assertSame('/test-install-path', $linkedPackage->getInstallationPath()); 43 | static::assertSame('/test-path', $linkedPackage->getPath()); 44 | static::assertSame('/test-path', $linkedPackage->getDistUrl()); 45 | static::assertSame('dist', $linkedPackage->getInstallationSource()); 46 | static::assertSame('path', $linkedPackage->getDistType()); 47 | static::assertSame('stable', $linkedPackage->getStability()); 48 | static::assertSame('dev-linked', $linkedPackage->getVersion()); 49 | static::assertFalse($linkedPackage->isWithoutDependencies()); 50 | 51 | static::assertSame($package, $linkedPackage->getLinkedPackage()); 52 | static::assertSame($originalPackage, $linkedPackage->getOriginalPackage()); 53 | static::assertSame('test/package', $linkedPackage->getName()); 54 | 55 | $newOriginalPackage = $this->createMock(PackageInterface::class); 56 | $linkedPackage->setOriginalPackage($newOriginalPackage); 57 | static::assertSame($newOriginalPackage, $linkedPackage->getOriginalPackage()); 58 | } 59 | 60 | public function test_requires(): void 61 | { 62 | $link = $this->createMock(Link::class); 63 | $package = static::createStub(CompletePackageInterface::class); 64 | $package->method('getRequires')->willReturn(['test' => $link]); 65 | $package->method('getDevRequires')->willReturn(['dev-test' => $link]); 66 | $package->method('getName')->willReturn('test/package'); 67 | $originalPackage = static::createStub(PackageInterface::class); 68 | $originalPackage->method('getRequires')->willReturn(['orig-test' => $link]); 69 | $originalPackage->method('getDevRequires')->willReturn(['orig-dev-test' => $link]); 70 | $linkedPackage = new LinkedPackage( 71 | $package, 72 | '/test-path', 73 | '/test-install-path', 74 | null 75 | ); 76 | 77 | // With dependencies and no original package 78 | static::assertSame(['test' => $link], $linkedPackage->getRequires()); 79 | static::assertSame(['dev-test' => $link], $linkedPackage->getDevRequires()); 80 | 81 | // Without dependencies and no original package 82 | $linkedPackage->setWithoutDependencies(true); 83 | static::assertSame([], $linkedPackage->getRequires()); 84 | static::assertSame([], $linkedPackage->getDevRequires()); 85 | 86 | // Without dependencies and original package 87 | $linkedPackage->setOriginalPackage($originalPackage); 88 | static::assertSame(['orig-test' => $link], $linkedPackage->getRequires()); 89 | static::assertSame(['orig-dev-test' => $link], $linkedPackage->getDevRequires()); 90 | 91 | // With dependencies and original package 92 | $linkedPackage->setWithoutDependencies(false); 93 | static::assertSame(['test' => $link], $linkedPackage->getRequires()); 94 | static::assertSame(['dev-test' => $link], $linkedPackage->getDevRequires()); 95 | 96 | $root = $this->createMock(RootPackageInterface::class); 97 | $root->method('getName')->willReturn('root/package'); 98 | $link = new Link('root/package', 'test/package', new Constraint('=', 'dev-linked'), Link::TYPE_REQUIRE); 99 | static::assertEquals($link, $linkedPackage->createLink($root)); 100 | } 101 | 102 | public function test_decorated_methods(): void 103 | { 104 | $package = static::createStub(CompletePackageInterface::class); 105 | $linkedPackage = new LinkedPackage( 106 | $package, 107 | '/test-path', 108 | '/test-install-path', 109 | null 110 | ); 111 | 112 | $linkedPackage->getScripts(); 113 | $linkedPackage->setScripts([]); 114 | $linkedPackage->getRepositories(); 115 | $linkedPackage->setRepositories([]); 116 | $linkedPackage->getLicense(); 117 | $linkedPackage->setLicense([]); 118 | $linkedPackage->getKeywords(); 119 | $linkedPackage->setKeywords([]); 120 | $linkedPackage->getDescription(); 121 | $linkedPackage->setDescription('description'); 122 | $linkedPackage->getHomepage(); 123 | $linkedPackage->setHomepage('homepage'); 124 | $linkedPackage->getAuthors(); 125 | $linkedPackage->setAuthors([]); 126 | $linkedPackage->getSupport(); 127 | $linkedPackage->setSupport([]); 128 | $linkedPackage->getFunding(); 129 | $linkedPackage->setFunding([]); 130 | $linkedPackage->isAbandoned(); 131 | $linkedPackage->getReplacementPackage(); 132 | $linkedPackage->setAbandoned(true); 133 | $linkedPackage->getArchiveName(); 134 | $linkedPackage->setArchiveName('name'); 135 | $linkedPackage->getArchiveExcludes(); 136 | $linkedPackage->setArchiveExcludes([]); 137 | $linkedPackage->getName(); 138 | $linkedPackage->getPrettyName(); 139 | $linkedPackage->getNames(false); 140 | $linkedPackage->setId(1); 141 | $linkedPackage->getId(); 142 | $linkedPackage->isDev(); 143 | $linkedPackage->getType(); 144 | $linkedPackage->getTargetDir(); 145 | $linkedPackage->getExtra(); 146 | $linkedPackage->setInstallationSource('dist'); 147 | $linkedPackage->getSourceType(); 148 | $linkedPackage->getSourceUrl(); 149 | $linkedPackage->getSourceUrls(); 150 | $linkedPackage->getSourceReference(); 151 | $linkedPackage->getSourceMirrors(); 152 | $linkedPackage->setSourceMirrors([]); 153 | $linkedPackage->getDistUrls(); 154 | $linkedPackage->getDistReference(); 155 | $linkedPackage->getDistSha1Checksum(); 156 | $linkedPackage->getDistMirrors(); 157 | $linkedPackage->setDistMirrors([]); 158 | $linkedPackage->getPrettyVersion(); 159 | $linkedPackage->getFullPrettyVersion(false, CompletePackageInterface::DISPLAY_DIST_REF); 160 | $linkedPackage->getReleaseDate(); 161 | $linkedPackage->getConflicts(); 162 | $linkedPackage->getProvides(); 163 | $linkedPackage->getReplaces(); 164 | $linkedPackage->getSuggests(); 165 | $linkedPackage->getAutoload(); 166 | $linkedPackage->getDevAutoload(); 167 | $linkedPackage->getIncludePaths(); 168 | $linkedPackage->getPhpExt(); 169 | $repository = $this->createMock(RepositoryInterface::class); 170 | $linkedPackage->setRepository($repository); 171 | $linkedPackage->getRepository(); 172 | $linkedPackage->getBinaries(); 173 | $linkedPackage->getUniqueName(); 174 | $linkedPackage->getNotificationUrl(); 175 | $linkedPackage->__toString(); 176 | $linkedPackage->getPrettyString(); 177 | $linkedPackage->isDefaultBranch(); 178 | $linkedPackage->getTransportOptions(); 179 | $linkedPackage->setTransportOptions([]); 180 | $linkedPackage->setSourceReference('reference'); 181 | $linkedPackage->setDistUrl('url'); 182 | $linkedPackage->setDistType('type'); 183 | $linkedPackage->setDistReference('reference'); 184 | $linkedPackage->setSourceDistReferences('reference'); 185 | 186 | static::expectNotToPerformAssertions(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/Unit/PathHelperTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use ComposerLink\PathHelper; 19 | use InvalidArgumentException; 20 | use PHPUnit\Framework\Attributes\DataProvider; 21 | 22 | class PathHelperTest extends TestCase 23 | { 24 | /** 25 | * @param non-empty-string $path 26 | */ 27 | #[DataProvider('provideAbsolutePaths')] 28 | public function test_get_absolute_path(string $path): void 29 | { 30 | $testPath = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..'; 31 | $root = realpath($testPath); 32 | $helper = new PathHelper($path); 33 | static::assertEquals( 34 | $root . DIRECTORY_SEPARATOR . $path, 35 | $helper->toAbsolutePath($testPath) 36 | ->getNormalizedPath() 37 | ); 38 | } 39 | 40 | public function test_absolute_path_to_absolute(): void 41 | { 42 | /** @var non-empty-string $cwd */ 43 | $cwd = getcwd(); 44 | $pathWildcard = new PathHelper($this->tmpAbsoluteDir); 45 | $absolute = $pathWildcard->toAbsolutePath($cwd); 46 | 47 | // We expect a normalized path, so we remove the trailing slash 48 | static::assertSame( 49 | substr($this->tmpAbsoluteDir, 0, -1), 50 | $absolute->getNormalizedPath() 51 | ); 52 | } 53 | 54 | public function test_get_invalid_absolute_path(): void 55 | { 56 | $this->expectException(InvalidArgumentException::class); 57 | $helper = new PathHelper('some-path-non-existing-path'); 58 | $root = PHP_OS_FAMILY === 'Windows' ? 'C:\\' : '/'; 59 | $helper->toAbsolutePath($root); 60 | } 61 | 62 | public function test_paths_considered_equal_without_trailing_separator(): void 63 | { 64 | $path = PHP_OS_FAMILY === 'Windows' ? 'C:\\some\\path' : '/some/path'; 65 | 66 | $helper1 = new PathHelper($path); 67 | $helper2 = new PathHelper($path . DIRECTORY_SEPARATOR); 68 | 69 | static::assertSame($helper1->getNormalizedPath(), $helper2->getNormalizedPath()); 70 | } 71 | 72 | public function test_is_wildcard(): void 73 | { 74 | $pathWildcard = new PathHelper('..' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . '*'); 75 | $pathNonWildcard = new PathHelper('..' . DIRECTORY_SEPARATOR . 'path'); 76 | 77 | static::assertTrue($pathWildcard->isWildCard()); 78 | static::assertFalse($pathNonWildcard->isWildCard()); 79 | } 80 | 81 | public function test_get_paths_from_wildcard(): void 82 | { 83 | mkdir($this->tmpAbsoluteDir . 'test-1'); 84 | touch($this->tmpAbsoluteDir . 'test-1' . DIRECTORY_SEPARATOR . 'composer.json'); 85 | mkdir($this->tmpAbsoluteDir . 'test-2'); 86 | touch($this->tmpAbsoluteDir . 'test-2' . DIRECTORY_SEPARATOR . 'composer.json'); 87 | mkdir($this->tmpAbsoluteDir . 'test-3'); 88 | 89 | $pathWildcard = new PathHelper($this->tmpAbsoluteDir . '*'); 90 | static::assertCount(2, $pathWildcard->getPathsFromWildcard()); 91 | } 92 | 93 | public function test_wildcard_path_to_wildcard_absolute(): void 94 | { 95 | /** @var non-empty-string $cwd */ 96 | $cwd = getcwd(); 97 | $pathWildcard = new PathHelper($this->tmpRelativeDir . '*'); 98 | $absolute = $pathWildcard->toAbsolutePath($cwd); 99 | 100 | static::assertTrue($absolute->isWildCard()); 101 | static::assertSame($this->tmpAbsoluteDir . '*', $absolute->getNormalizedPath()); 102 | } 103 | 104 | /** 105 | * @return string[][] 106 | */ 107 | public static function provideAbsolutePaths(): array 108 | { 109 | return [ 110 | ['tests'], 111 | ['tests' . DIRECTORY_SEPARATOR . 'Unit' . DIRECTORY_SEPARATOR . 'TestCase.php'], 112 | ]; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Unit/PluginTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use Composer\Composer; 19 | use Composer\Config; 20 | use Composer\Installer\InstallationManager; 21 | use Composer\IO\IOInterface; 22 | use Composer\Plugin\Capability\CommandProvider as ComposerCommandProvider; 23 | use Composer\Repository\InstalledRepositoryInterface; 24 | use Composer\Repository\RepositoryManager; 25 | use Composer\Script\Event; 26 | use Composer\Script\ScriptEvents; 27 | use ComposerLink\CommandProvider; 28 | use ComposerLink\LinkManager; 29 | use ComposerLink\LinkManagerFactory; 30 | use ComposerLink\Plugin; 31 | use ComposerLink\Repository\Repository; 32 | use ComposerLink\Repository\RepositoryFactory; 33 | use PHPUnit\Framework\MockObject\MockObject; 34 | use RuntimeException; 35 | use TypeError; 36 | 37 | /** 38 | * @SuppressWarnings(PHPMD.CouplingBetweenObjects) 39 | */ 40 | class PluginTest extends TestCase 41 | { 42 | /** 43 | * @var Config&MockObject 44 | */ 45 | protected Config $config; 46 | 47 | /** 48 | * @var Composer&MockObject 49 | */ 50 | protected Composer $composer; 51 | 52 | /** 53 | * @var IOInterface&MockObject 54 | */ 55 | protected IOInterface $io; 56 | 57 | /** 58 | * @var InstalledRepositoryInterface&MockObject 59 | */ 60 | protected InstalledRepositoryInterface $localRepository; 61 | 62 | /** 63 | * @var Repository&MockObject 64 | */ 65 | protected Repository $repository; 66 | 67 | /** 68 | * @var LinkManager&MockObject 69 | */ 70 | protected LinkManager $linkManager; 71 | 72 | protected Plugin $plugin; 73 | 74 | protected function setUp(): void 75 | { 76 | parent::setUp(); 77 | 78 | $this->io = $this->createMock(IOInterface::class); 79 | $installationManager = $this->createMock(InstallationManager::class); 80 | $this->config = $this->createMock(Config::class); 81 | $repositoryManager = $this->createMock(RepositoryManager::class); 82 | $this->localRepository = $this->createMock(InstalledRepositoryInterface::class); 83 | $repositoryManager->method('getLocalRepository')->willReturn($this->localRepository); 84 | $repositoryFactory = $this->createMock(RepositoryFactory::class); 85 | $this->repository = $this->createMock(Repository::class); 86 | $repositoryFactory->method('create')->willReturn($this->repository); 87 | 88 | $this->composer = $this->createMock(Composer::class); 89 | $this->composer->method('getInstallationManager')->willReturn($installationManager); 90 | $this->composer->method('getConfig')->willReturn($this->config); 91 | $this->composer->method('getRepositoryManager')->willReturn($repositoryManager); 92 | 93 | $this->linkManager = $this->createMock(LinkManager::class); 94 | $linkManagerFactory = $this->createMock(LinkManagerFactory::class); 95 | $linkManagerFactory->method('create')->willReturn($this->linkManager); 96 | 97 | $this->plugin = new Plugin( 98 | $this->filesystem, 99 | $repositoryFactory, 100 | $linkManagerFactory, 101 | ); 102 | } 103 | 104 | /** @SuppressWarnings(PHPMD.StaticAccess) */ 105 | public function test_if_plugin_can_be_utilized(): void 106 | { 107 | $this->config->method('get') 108 | ->willReturnCallback(function ($path) { 109 | return match ($path) { 110 | 'vendor-dir', 'home' => $this->tmpAbsoluteDir, 111 | default => null, 112 | }; 113 | }); 114 | 115 | $this->plugin->activate($this->composer, $this->io); 116 | 117 | $capabilities = $this->plugin->getCapabilities(); 118 | $events = Plugin::getSubscribedEvents(); 119 | 120 | static::assertArrayHasKey(ComposerCommandProvider::class, $capabilities); 121 | static::assertContains(CommandProvider::class, $capabilities); 122 | static::assertArrayHasKey(ScriptEvents::POST_UPDATE_CMD, $events); 123 | static::assertArrayHasKey(ScriptEvents::POST_INSTALL_CMD, $events); 124 | static::assertFalse($this->plugin->isGlobal()); 125 | 126 | $this->plugin->getPackageFactory(); 127 | $this->plugin->getLinkManager(); 128 | $this->plugin->getRepository(); 129 | $this->plugin->deactivate($this->composer, $this->io); 130 | $this->plugin->uninstall($this->composer, $this->io); 131 | } 132 | 133 | public function test_unable_to_activate_plugin(): void 134 | { 135 | $repositoryFactory = $this->createMock(RepositoryFactory::class); 136 | $linkManagerFactory = $this->createMock(LinkManagerFactory::class); 137 | $event = $this->createMock(Event::class); 138 | $event->method('getIO')->willReturn($this->io); 139 | 140 | $repositoryFactory->method('create') 141 | ->willThrowException(new TypeError('test error')); 142 | 143 | $plugin = new Plugin( 144 | $this->filesystem, 145 | $repositoryFactory, 146 | $linkManagerFactory, 147 | ); 148 | 149 | $plugin->activate($this->composer, $this->io); 150 | 151 | $this->io->expects(static::once())->method('warning')->with( 152 | static::stringContains('Composer link couldn\'t be activated') 153 | ); 154 | $plugin->postUpdate($event); 155 | } 156 | 157 | public function test_is_global(): void 158 | { 159 | $this->config->method('get') 160 | ->willReturnCallback(function ($path) { 161 | return match ($path) { 162 | 'vendor-dir' => $this->tmpAbsoluteDir, 163 | 'home' => getcwd(), 164 | default => null, 165 | }; 166 | }); 167 | 168 | $this->plugin->activate($this->composer, $this->io); 169 | 170 | static::assertTrue($this->plugin->isGlobal()); 171 | } 172 | 173 | public function test_post_install(): void 174 | { 175 | $this->plugin->activate($this->composer, $this->io); 176 | $event = $this->createMock(Event::class); 177 | 178 | $this->linkManager->method('hasLinkedPackages')->willReturn(true); 179 | $this->linkManager->expects(static::once())->method('linkPackages'); 180 | $this->plugin->postInstall($event); 181 | } 182 | 183 | public function test_post_update(): void 184 | { 185 | $this->plugin->activate($this->composer, $this->io); 186 | $event = $this->createMock(Event::class); 187 | $package = $this->mockPackage(); 188 | $original = $this->mockPackage('original'); 189 | 190 | $this->localRepository->method('findPackage')->willReturn($original); 191 | $this->linkManager->method('hasLinkedPackages')->willReturn(true); 192 | 193 | $this->repository->method('all')->willReturn([$package]); 194 | $package->expects(static::once())->method('setOriginalPackage')->with($original); 195 | 196 | $this->linkManager->expects(static::once())->method('linkPackages'); 197 | $this->plugin->postUpdate($event); 198 | } 199 | 200 | public function test_plugin_throws_exception_package_factory(): void 201 | { 202 | self::expectException(RuntimeException::class); 203 | $plugin = new Plugin(); 204 | $plugin->getPackageFactory(); 205 | } 206 | 207 | public function test_plugin_throws_exception_link_manager(): void 208 | { 209 | self::expectException(RuntimeException::class); 210 | $plugin = new Plugin(); 211 | $plugin->getLinkManager(); 212 | } 213 | 214 | public function test_plugin_throws_exception_repository(): void 215 | { 216 | self::expectException(RuntimeException::class); 217 | $plugin = new Plugin(); 218 | $plugin->getRepository(); 219 | } 220 | 221 | public function test_plugin_throws_exception_initialize_manager(): void 222 | { 223 | self::expectException(RuntimeException::class); 224 | $plugin = new Plugin(); 225 | $plugin->getLinkManager(); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/Unit/Repository/JsonStorageTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Repository; 17 | 18 | use ComposerLink\Repository\JsonStorage; 19 | use RuntimeException; 20 | use Tests\Unit\TestCase; 21 | 22 | class JsonStorageTest extends TestCase 23 | { 24 | public function test_throws_exception_when_not_data_available(): void 25 | { 26 | $this->expectException(RuntimeException::class); 27 | self::expectExceptionMessage('Cannot read data, no data stored.'); 28 | 29 | $storage = new JsonStorage($this->tmpAbsoluteDir . 'test.json'); 30 | $storage->read(); 31 | } 32 | 33 | public function test_can_write_and_read(): void 34 | { 35 | $storage = new JsonStorage($this->tmpAbsoluteDir . 'test.json'); 36 | $storage->write(['test' => 'data']); 37 | static::assertEquals(['test' => 'data'], $storage->read()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Unit/Repository/RepositoryFactoryTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Repository; 17 | 18 | use ComposerLink\Package\LinkedPackageFactory; 19 | use ComposerLink\Repository\RepositoryFactory; 20 | use Tests\Unit\TestCase; 21 | 22 | class RepositoryFactoryTest extends TestCase 23 | { 24 | public function test_creates_repository(): void 25 | { 26 | $factory = new RepositoryFactory(); 27 | $factory->create($this->tmpAbsoluteDir . 'linked-packages.json', $this->createMock(LinkedPackageFactory::class)); 28 | 29 | static::expectNotToPerformAssertions(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Unit/Repository/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Repository; 17 | 18 | use ComposerLink\Repository\Repository; 19 | use ComposerLink\Repository\StorageInterface; 20 | use ComposerLink\Repository\Transformer; 21 | use PHPUnit\Framework\MockObject\MockObject; 22 | use RuntimeException; 23 | use Tests\Unit\TestCase; 24 | 25 | class RepositoryTest extends TestCase 26 | { 27 | /** @var StorageInterface&MockObject */ 28 | protected StorageInterface $storage; 29 | 30 | /** @var Transformer&MockObject */ 31 | protected Transformer $transformer; 32 | 33 | public function setUp(): void 34 | { 35 | parent::setUp(); 36 | 37 | $this->storage = $this->createMock(StorageInterface::class); 38 | $this->transformer = $this->createMock(Transformer::class); 39 | } 40 | 41 | protected function getRepository(): Repository 42 | { 43 | return new Repository( 44 | $this->storage, 45 | $this->transformer 46 | ); 47 | } 48 | 49 | public function test_if_package_is_stored_and_persisted(): void 50 | { 51 | $package = $this->mockPackage(); 52 | $repository = $this->getRepository(); 53 | 54 | $repository->store($package); 55 | static::assertCount(1, $repository->all()); 56 | static::assertEquals($package, $repository->all()[0]); 57 | static::assertNotSame($package, $repository->findByName('test/package')); 58 | $this->transformer->method('export')->willReturn(['test' => 'exists']); 59 | 60 | $this->storage->expects(static::once()) 61 | ->method('write') 62 | ->with(static::callback(function (array $data) { 63 | self::assertCount(1, $data['packages']); 64 | self::assertSame(['test' => 'exists'], $data['packages'][0]); 65 | 66 | return true; 67 | })); 68 | 69 | $repository->persist(); 70 | } 71 | 72 | public function test_if_package_is_updated_when_stored(): void 73 | { 74 | $package1 = $this->mockPackage(); 75 | $package2 = $this->mockPackage(); 76 | $repository = $this->getRepository(); 77 | 78 | $repository->store($package1); 79 | $repository->store($package2); 80 | 81 | static::assertCount(1, $repository->all()); 82 | static::assertEquals($package2, $repository->findByName('test/package')); 83 | } 84 | 85 | public function test_find_by_path(): void 86 | { 87 | $package = $this->mockPackage(); 88 | $repository = $this->getRepository(); 89 | 90 | $repository->store($package); 91 | static::assertEquals($package, $repository->findByPath('../test-path-package')); 92 | static::assertNotSame($package, $repository->findByName('test/package')); 93 | static::assertNull($repository->findByPath('/test-path-other')); 94 | } 95 | 96 | public function test_find_by_name(): void 97 | { 98 | $package = $this->mockPackage(); 99 | $repository = $this->getRepository(); 100 | 101 | $repository->store($package); 102 | static::assertEquals($package, $repository->findByName('test/package')); 103 | static::assertNotSame($package, $repository->findByName('test/package')); 104 | static::assertNull($repository->findByName('test/package-other')); 105 | } 106 | 107 | public function test_package_is_removed(): void 108 | { 109 | $package = $this->mockPackage(); 110 | $repository = $this->getRepository(); 111 | 112 | $repository->store($package); 113 | $repository->remove($package); 114 | 115 | static::assertCount(0, $repository->all()); 116 | 117 | $this->storage->expects(static::once()) 118 | ->method('write') 119 | ->with(['packages' => []]); 120 | $repository->persist(); 121 | } 122 | 123 | public function test_remove_throws_exception(): void 124 | { 125 | $package = $this->mockPackage(); 126 | $repository = $this->getRepository(); 127 | $this->expectException(RuntimeException::class); 128 | $repository->remove($package); 129 | } 130 | 131 | public function test_if_data_can_be_loaded_from_file(): void 132 | { 133 | $package = $this->mockPackage(); 134 | $this->storage->method('hasData')->willReturn(true); 135 | $this->storage->method('read') 136 | ->willReturn(['packages' => [[]]]); 137 | $repository = $this->getRepository(); 138 | 139 | $this->transformer->method('load')->willReturn($package); 140 | 141 | static::assertCount(1, $repository->all()); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Unit/Repository/TransformerTest.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit\Repository; 17 | 18 | use ComposerLink\Package\LinkedPackageFactory; 19 | use ComposerLink\Repository\Transformer; 20 | use Tests\Unit\TestCase; 21 | 22 | class TransformerTest extends TestCase 23 | { 24 | public function test_load(): void 25 | { 26 | $package = $this->mockPackage(); 27 | $package->expects(static::once())->method('setWithoutDependencies')->with(true); 28 | $packageFactory = $this->createMock(LinkedPackageFactory::class); 29 | $packageFactory->expects(static::once()) 30 | ->method('fromPath') 31 | ->with('../path') 32 | ->willReturn($package); 33 | $transformer = new Transformer($packageFactory); 34 | $transformer->load( 35 | [ 36 | 'path' => '../path', 37 | ] 38 | ); 39 | } 40 | 41 | public function test_load_without_dependencies(): void 42 | { 43 | $package = $this->mockPackage('package', false); 44 | $package->expects(static::once())->method('setWithoutDependencies')->with(false); 45 | 46 | $packageFactory = $this->createMock(LinkedPackageFactory::class); 47 | $packageFactory->expects(static::once()) 48 | ->method('fromPath') 49 | ->with('../path') 50 | ->willReturn($package); 51 | $transformer = new Transformer($packageFactory); 52 | $transformer->load( 53 | [ 54 | 'path' => '../path', 55 | 'withoutDependencies' => false, 56 | ] 57 | ); 58 | } 59 | 60 | public function test_export(): void 61 | { 62 | $packageFactory = $this->createMock(LinkedPackageFactory::class); 63 | $transformer = new Transformer($packageFactory); 64 | 65 | $data = $transformer->export($this->mockPackage()); 66 | static::assertEquals([ 67 | 'path' => '../test-path-package', 68 | 'withoutDependencies' => false, 69 | ], $data); 70 | 71 | $package = $this->mockPackage(); 72 | $package->method('isWithoutDependencies')->willReturn(true); 73 | $data = $transformer->export($package); 74 | static::assertEquals([ 75 | 'path' => '../test-path-package', 76 | 'withoutDependencies' => true, 77 | ], $data); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Unit/TestCase.php: -------------------------------------------------------------------------------- 1 | . 9 | * 10 | * For the full copyright and license information, please view the LICENSE.md 11 | * file that was distributed with this source code. 12 | * 13 | * @link https://github.com/SanderSander/composer-link 14 | */ 15 | 16 | namespace Tests\Unit; 17 | 18 | use Composer\Package\PackageInterface; 19 | use ComposerLink\Package\LinkedPackage; 20 | use PHPUnit\Framework\MockObject\MockObject; 21 | use Tests\TestCase as BaseTest; 22 | 23 | abstract class TestCase extends BaseTest 24 | { 25 | /** 26 | * @SuppressWarnings(PHPMD.BooleanArgumentFlag) 27 | * 28 | * @return LinkedPackage&MockObject 29 | */ 30 | protected function mockPackage(string $name = 'package', bool $withOriginalPackage = true): MockObject 31 | { 32 | $package = $this->createMock(LinkedPackage::class); 33 | $package->method('getName')->willReturn('test/' . $name); 34 | $package->method('getPath')->willReturn('../test-path-' . $name); 35 | $package->method('getInstallationPath')->willReturn('../install-path-' . $name); 36 | if ($withOriginalPackage) { 37 | $package->method('getOriginalPackage') 38 | ->willReturn($this->createMock(PackageInterface::class)); 39 | } 40 | 41 | return $package; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/mock/package-1/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test/package-1" 3 | } 4 | -------------------------------------------------------------------------------- /tests/mock/package-2/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test/package-2", 3 | "require": { 4 | "psr/container": "2.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/mock/package-3/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test/package-2", 3 | "require": { 4 | "psr/container": "2.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/mock/psr-container/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psr/container" 3 | } 4 | --------------------------------------------------------------------------------