Repository: Letudiant/composer-shared-package-plugin Branch: master Commit: 55c47cfdd7fa Files: 27 Total size: 117.6 KB Directory structure: gitextract_uxe5wr2i/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docs/ │ ├── all-available-configurations.md │ └── how-to-use/ │ ├── disable-this-plugin-in-development-environment.md │ ├── update-only-your-own-packages.md │ └── work-with-satis.md ├── phpunit.xml.dist ├── src/ │ └── LEtudiant/ │ └── Composer/ │ ├── Data/ │ │ └── Package/ │ │ ├── PackageDataManagerInterface.php │ │ └── SharedPackageDataManager.php │ ├── Installer/ │ │ ├── Config/ │ │ │ └── SharedPackageInstallerConfig.php │ │ ├── SharedPackageInstaller.php │ │ └── Solver/ │ │ ├── SharedPackageInstallerSolver.php │ │ └── SharedPackageSolver.php │ ├── SharedPackagePlugin.php │ └── Util/ │ └── SymlinkFilesystem.php └── tests/ ├── bootstrap.php └── unit/ └── Test/ └── Unit/ └── LEtudiant/ └── Composer/ ├── Data/ │ └── Package/ │ └── SharedPackageDataManagerTest.php ├── Installer/ │ ├── Config/ │ │ └── SharedPackageInstallerConfigTest.php │ ├── SharedPackageInstallerTest.php │ └── Solver/ │ ├── SharedPackageInstallerSolverNotSharedTest.php │ ├── SharedPackageInstallerSolverSharedTest.php │ └── SharedPackageSolverTest.php ├── SharedPackagePluginTest.php └── Util/ └── SymlinkFilesystemTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea tests/build vendor composer.lock composer.phar phpunit.xml ================================================ FILE: .travis.yml ================================================ language: php php: - 5.5 - 5.6 - 7.0 before_script: - curl -s http://getcomposer.org/installer | php - php composer.phar install --no-interaction --prefer-source script: - ./vendor/bin/phpunit after_script: - ./vendor/bin/test-reporter --coverage-report=tests/build/logs/clover.xml --stdout > codeclimate.json - "curl -X POST -d @codeclimate.json -H 'Content-Type: application/json' -H 'User-Agent: Code Climate (PHP Test Reporter v0.1.1)' https://codeclimate.com/test_reports" addons: code_climate: repo_token: secure: "J5qoMlEWXejZ7F3/9oH6jnXPg7eUdB982jczrcCQWYYCCMmQTqTuKBPXRM+HtaE6UyMvtk5um/WXWwbDCmrb7NmOpgfIaPtjyhGvmg/q71stCkt8QapMEwQKRB7HSzIjsFcsc2kCPVew93x5lTEFWV+Hu+apFUOGVr/mn/1j/3g=" ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 L'Etudiant 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: README.md ================================================ # Composer - Shared Package Plugin [![Code Climate](https://codeclimate.com/github/Letudiant/composer-shared-package-plugin/badges/gpa.svg)](https://codeclimate.com/github/Letudiant/composer-shared-package-plugin) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Letudiant/composer-shared-package-plugin/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Letudiant/composer-shared-package-plugin/?branch=master) [![Build Status](https://travis-ci.org/Letudiant/composer-shared-package-plugin.svg?branch=master)](https://travis-ci.org/Letudiant/composer-shared-package-plugin) [![Test Coverage](https://codeclimate.com/github/Letudiant/composer-shared-package-plugin/badges/coverage.svg)](https://codeclimate.com/github/Letudiant/composer-shared-package-plugin) This composer plugin allows you to share **your selected packages between your projects by creating symlinks**. All shared packages will be in the same dedicated directory for all of your projects (ordered by versions) and a symlink directory container will be created on your projects (`vendor-shared` by default). **This plugin will improve your work process** to avoid to work into the `vendor` folder or to avoid to force you to push your package to work/test it with another project. * [How it works](#how-it-works) * [Installation](#installation) * [Structure generation example](#structure-generation-example) * [How to use (known issues)](#how-to-use-known-issues) * [Update only your own packages](./docs/how-to-use/update-only-your-own-packages.md) * [Disable this plugin in development environment (for CI purpose, for example)](./docs/how-to-use/disable-this-plugin-in-development-environment.md) * [Work with Satis : increase the Composer speed](./docs/how-to-use/work-with-satis.md) * [All available configurations](#all-available-configurations) * [Reporting an issue or a feature request](#reporting-an-issue-or-a-feature-request) * [ChangeLog](#changelog) * [Credit](#credit) * [License](#license) ## How it works A shared package is flagged by two ways : * By setting the root project `composer.json` extra configuration `package-list` with the selected package name *(works only with the `>= 2.x` version)*. * By setting the `composer.json` package `type` to `shared-package` *(the `<= 1.x` version way, still works on `2.x`)*. If this composer plugin is required in the root project `composer.json` : the package will be downloaded in the dedicated dependencies directory that you provided and a symlink will be created in the project `vendor-shared` directory *(by default)*. This plugin allows you to work with many versions at the same time for a package by creating a sub-directory named by the version of your package *(dev-master, dev-develop, 1.0.x-dev, etc)*. A `packages.json` file is created in the dependencies sources directory to know which projects use a package version and be able to ask you if you want to delete the version directory during the Composer uninstall process, if no project seems to use it. ## Installation ### Step 1 : edit your root composer.json Add, to your root project `composer.json`, this require **(in dev only)** : ``` json // composer.json (project) { "require-dev": { "letudiant/composer-shared-package-plugin": "~2.0" } } ``` **Note:** this plugin works fine in production mode, but it has been created for development purpose. ### Step 2 : set your dependencies vendor path Your dependencies vendor path is the path where all your shared packages will be downloaded. This path should be at the same level (or above) of all your projects. If you IDE doesn't handle symlinks, you may use this directory to work on your development packages. Otherwise, you'll be able to work directly on your symlinks with modern IDE (PHP Storm, SublimeText, ...). Add, to your root project `composer.json`, this extra configuration : ``` json // composer.json { "extra": { "shared-package": { "vendor-dir": "/path/to/your/dependencies/directory" } } } ``` **Note:** you can pass a relative path (`foo/bar`) or absolute path (starts with "/" : `/foo/bar`). If your path is relative, your symlink directory base path will be relative too. **Note for VM users:** you can manually override the symlink directory base path with the configuration `symlink-base-path` if your host machine dependencies directory path is not the same as your guest machine, see [all available configurations](./docs/all-available-configurations.md) page for more information. ### Step 3 : select your shared packages Add, in your own package `composer.json`, which one you want to share between your projects : ``` json // composer.json { "extra": { "shared-package": { "vendor-dir": "/path/to/your/dependencies/directory", "package-list": [ "foo/bar", "bar/*" ] } } } ``` **Note:** as you can see, you can pass a wild card `*` to the package name. So, in this example, all packages that starts with `bar/` will be shared. **Note²:** you can set a package name to `*` to share **all packages**. ### Step 4 : (re)install your dependencies *If you already have installed your project dependencies, you have to fully delete your `vendor/` directory and your `composer.lock` file.* Run the `composer install` command. You should see a new `vendor-shared` folder with all shared packages symlinks. ### Step 5 : play with require-dev : You can avoid to have two project `composer.json` by setting your `require` dependencies on a stable version (`~x.x.x`) and work on dev environement with a your working in progress version by setting, in your `require-dev` with your development version, like this : ``` json // composer.json (project) { "require": { "acme/foo-bar": "~1.0" }, "require-dev": { "acme/foo-bar": "dev-develop as 1.0" } } ``` Thanks to that, you will be able to work with development version in dev environement and have stable version in production. **Note:** the alias `* as 1.0` may avoid a Composer version solver error, because this behavior is not handled by default. **Note²:** Composer has not been created to work with development version/branch, so when you run a `composer install`, the current package branch `HEAD` commit will flagged in your `composer.lock`. So, the next time you'll run this command on dev environement, and if you already have a `composer.lock` file, Composer will checkout the flagged commit and not the new `HEAD` *(if you made new commit)* of your branch : **your shared packages won't be up to date**. To avoid this behavior, please read "[How to use - Update only your own packages](./docs/how-to-use/update-only-your-own-packages.md)". ## Structure generation example Here, a complete example. Our own shared package is called `acme/foo-bar`. ``` json // composer.json (project) { "require": { "letudiant/composer-shared-package-plugin": "~1.0", "symfony/console": "~2.6", "acme/foo-bar": "~1.0" }, "require-dev": { "acme/foo-bar": "dev-develop as 1.0" }, "extra": { "shared-package": { "vendor-dir": "../composer-dependencies", "package-list": [ "acme/foo-bar" ] } } } ``` With this `composer.json`, the structure will look like : ``` bash |-- packages.json |-- composer-dependencies/ | +-- acme/ | +-- foo-bar/ | +-- dev-develop/ | |-- src/ | |-- composer.json | +-- ... +-- project/ +-- src/ +-- vendor/ | +-- symfony/ | +-- console/ | +-- ... |-- vendor-shared/ | +-- acme/ | +-- foo-bar/ (symlink to "../../../composer-dependencies/acme/foo-bar/dev-develop/") +-- ... ``` ## How to use (and known issues) This plugin implement a new behavior which is not handled by Composer, so there are a few known issues. Here, the way to fix them : * [Update only your own packages](./docs/how-to-use/update-only-your-own-packages.md) * [Disable this plugin in development environment (for CI purpose, for example)](./docs/how-to-use/disable-this-plugin-in-development-environment.md) * [Work with Satis : increase the Composer speed](./docs/how-to-use/work-with-satis.md) ## All available configurations See the [all available configurations documentation](./docs/all-available-configurations.md). ## Reporting an issue or a feature request Feel free to open an issue, fork this project or suggest an awesome new feature in [the issue tracker](https://github.com/Letudiant/composer-shared-package-plugin/issues). ## ChangeLog ### 3.1.0 * Implement environment variables, as suggested by [babwar](https://github.com/babwar), to allow to override the default configuration in `composer.json` file - [More information](./docs/all-available-configurations.md). ### 3.0.0 : * Fix the BC update in Composer with `getInstallPath` method. ### 2.0.0 : * Implement the possibility to choice each package you want to share with the configuration `package-list` - [More information](./docs/all-available-configurations.md). * Delete conditions on stable/dev version. Now a shared package is shared on stable version too (tag). ### 1.2.0 : * Rewrite installer, the installer choice process is now in a dedicated class. * Implement new `symlink-enabled` configuration to allow to enable/disable the symlink creation process - [More information](./docs/all-available-configurations.md). ### 1.1.0 : * Implement new `symlink-base-path` configuration, as suggested by [philbates35](https://github.com/philbates35), to allow VM users to override the symlink directory base path, see [issue](https://github.com/Letudiant/composer-shared-package-plugin/issues/1) - [More information](./docs/all-available-configurations.md). ## Credit ![L'Étudiant](http://www.letudiant.fr/etucmsEtuPlugin/images/header/logo.png) This plugin project is maintained by **[L'Etudiant](https://github.com/Letudiant)**. The Composer project is maintained by Nils Adermann & Jordi Boggiano, see https://github.com/composer/composer#authors for more information. ## License This plugin is licensed under MIT license, see the [LICENSE file](./LICENSE) for more information. You can also read the [Composer license](https://github.com/composer/composer/blob/master/LICENSE) for more information. ================================================ FILE: composer.json ================================================ { "name": "letudiant/composer-shared-package-plugin", "description": "This composer plugin allows you to share selected packages between your projects with symlinks.", "type": "composer-plugin", "license": "MIT", "authors": [ { "name": "Sylvain Lorinet", "email": "sylvain.lorinet@gmail.com" } ], "support": { "issues": "https://github.com/letudiant/composer-shared-package-plugin/issues" }, "require": { "php": ">=5.3.2", "composer-plugin-api": "~1.0" }, "require-dev": { "phpunit/phpunit": "~4.7", "composer/composer": "dev-master", "codeclimate/php-test-reporter": "dev-master", "symfony/var-dumper": "~2.7" }, "autoload": { "psr-0": { "LEtudiant\\Composer\\": "src/" } }, "autoload-dev": { "Test\\Unit\\LEtudiant\\Composer\\": "tests/unit" }, "extra": { "class": "LEtudiant\\Composer\\SharedPackagePlugin", "branch-alias": { "dev-master": "3.1-dev" } } } ================================================ FILE: docs/all-available-configurations.md ================================================ # Composer - Shared Package Plugin ## All available configurations All these configuration should set in the `extra` `shared-package`configuration key in your project `composer.json`. * `vendor-dir` *(required)* : your shared packages sources directory. This folder is used by the project Composer autoloader. Relative or absolute path are allowed. **Note:** you can use the environment variable `COMPOSER_SPP_VENDOR_DIR` to override the default value. * `package-list` : the shared packages list. You can pass the entire package name, or add the wildcard `*` in the name (e.g. `bar/*`). If you want to share all your project packages, set a package name to `*`. * `symlink-dir` : your symlinks container directory on your project *(default: vendor-shared)*. Relative or absolute path are allowed. * `symlink-base-path` : the source base path for all of your symlinks. By default, it's the `vendor-dir` path, but you can override this configuration. It's useful if you use a Virtual Machine and if your dependency directory path is not the same on both machines. Relative or absolute path are allowed. If you choose to set a relative path, it should start from your project root directory (where your project `composer.json` file is located). **Note:** you can use the environment variable `COMPOSER_SPP_SYMLINK_BASE_PATH` to override the default value. * `symlink-enabled` : *(boolean, default: true)* enable or not the symlink directory creation process. Useful if you work directly with the sources directory and you don't want to have the symlink directory in your project. ### Example ``` json // composer.json (project) { "extra": { "shared-package": { "vendor-dir": "/var/projects/composer-dependencies", "symlink-dir": "symlinks-folder", "symlink-base-path": "/home/www/projects/composer-dependencies", "symlink-enabled": true, "package-list": [ "foo/bar", "bar/*", "*" ] } } } ``` ================================================ FILE: docs/how-to-use/disable-this-plugin-in-development-environment.md ================================================ # Composer - Shared Package Plugin ## How to use ### Disable this plugin in development environment (for CI purpose, for example) Just add the `--no-plugins` flag to your `install/update` command. Composer will disable all plugins, including this one. `composer install --no-plugins` If you run this command on a Continuous Integration, don't forget to [update your own packages](./update-only-your-own-packages.md) after. ### Next See [Work with Satis : increase the Composer speed](./work-with-satis.md). ================================================ FILE: docs/how-to-use/update-only-your-own-packages.md ================================================ # Composer - Shared Package Plugin ## How to use ### Update only your own packages You can easily update your own package, without updating stable packages. Indeed, Composer allows the wildcard `*` with the `update` command. Imagine that your packages are prefixed by `acme` (`acme/cache`, `acme/awesome-component`, ...), you should execute this command : `composer update "acme*"` If you have packages without the same prefix, no worry, just execute this command : `composer update "acme*" "other-prefix*" "yet-another-prefix*"` So, when you run a `composer install`, don't forget to run the `composer update "prefix*"` command after to update your own shared packages because the `composer.lock` keeps only the installation commit reference *(after the first `composer install` command)*, and not your branches `HEAD`. ### Next See [Disable this plugin in development environment (for CI purpose, for example)](./disable-this-plugin-in-development-environment.md). ================================================ FILE: docs/how-to-use/work-with-satis.md ================================================ # Composer - Shared Package Plugin ## How to use ### Work with Satis : increase Composer speed As we know, Composer isn't the fastest tool in the world. But it does its job well. Unfortunately, **with custom repositories**, Composer is very slow because it reads each repository `composer.json` file (run a `composer update -v`, you'll see). The only way to increase the Composer speed, is to install a proxy to host our custom repositories : Satis can do this, and it's open-source. For more information about the Satis installation/configuration, please read [the official documentation](https://getcomposer.org/doc/articles/handling-private-packages-with-satis.md#satis). With 10 customs repositories, Composer took about 2/3 minutes to proceed the `update` command and only ~10 seconds after than Satis was installed. ### Next See [Update only your own packages](./update-only-your-own-packages.md). ================================================ FILE: phpunit.xml.dist ================================================ tests/unit src ================================================ FILE: src/LEtudiant/Composer/Data/Package/PackageDataManagerInterface.php ================================================ */ interface PackageDataManagerInterface { /** * Add a row in the "packages.json" file, with the project name for the "package/version" key * * @param PackageInterface $package */ public function addPackageUsage(PackageInterface $package); /** * Remove the row in the "packages.json" file * * @param PackageInterface $package */ public function removePackageUsage(PackageInterface $package); /** * Return usage of the current package * * @param PackageInterface $package * * @return array */ public function getPackageUsage(PackageInterface $package); /** * @param PackageInterface $package */ public function setPackageInstallationSource(PackageInterface $package); /** * Set the vendor directory to save the "packages.json" file * * @param string $vendorDir */ public function setVendorDir($vendorDir); } ================================================ FILE: src/LEtudiant/Composer/Data/Package/SharedPackageDataManager.php ================================================ */ class SharedPackageDataManager implements PackageDataManagerInterface { const PACKAGE_DATA_FILENAME = 'packages.json'; const PACKAGE_INSTALLATION_SOURCE = 'source'; /** * @var Composer */ protected $composer; /** * @var string */ protected $vendorDir; /** * @var array */ protected $packagesData; /** * @param Composer $composer */ public function __construct(Composer $composer) { $this->composer = $composer; } /** * @param string $vendorDir */ public function setVendorDir($vendorDir) { $this->vendorDir = $vendorDir; } /** * @param PackageInterface $package * @param array $packageData */ protected function updatePackageUsageFile(PackageInterface $package, array $packageData) { $packageKey = $package->getPrettyName() . '/' . $package->getPrettyVersion(); // Remove the row if there is no data anymore if (!isset($packageData[0])) { if (isset($this->packagesData[$packageKey])) { unset($this->packagesData[$packageKey]); } } elseif (!isset($this->packagesData[$packageKey])) { $this->packagesData[$packageKey] = array( // Force "source" installation to ensure that we download VCS files 'project-usage' => $packageData ); } else { $this->packagesData[$packageKey]['project-usage'] = $packageData; } file_put_contents( $this->vendorDir . DIRECTORY_SEPARATOR . self::PACKAGE_DATA_FILENAME, json_encode($this->packagesData) ); } /** * Add a row in the "packages.json" file, with the project name for the "package/version" key * * @param PackageInterface $package */ public function addPackageUsage(PackageInterface $package) { $usageData = $this->getPackageUsage($package); $packageName = $this->composer->getPackage()->getName(); if (!in_array($packageName, $usageData)) { $usageData[] = $packageName; } $this->updatePackageUsageFile($package, $usageData); } /** * Remove the row in the "packages.json" file * * @param PackageInterface $package */ public function removePackageUsage(PackageInterface $package) { $usageData = $this->getPackageUsage($package); $newUsageData = array(); $projectName = $this->composer->getPackage()->getName(); foreach ($usageData as $usage) { if ($projectName !== $usage) { $newUsageData[] = $usage; } } $this->updatePackageUsageFile($package, $newUsageData); } /** * Return usage of the current package * * @param PackageInterface $package * * @return array */ public function getPackageUsage(PackageInterface $package) { return $this->getPackageDataKey($package, 'project-usage', array()); } /** * Initialize the package data array if not set */ protected function initializePackageData() { $filePath = $this->vendorDir . DIRECTORY_SEPARATOR . self::PACKAGE_DATA_FILENAME; if (!is_file($filePath)) { $this->packagesData = array(); } else { $this->packagesData = json_decode(file_get_contents($filePath), true); } } /** * @param PackageInterface $package * @param string $key * @param mixed $defaultValue * * @return mixed */ protected function getPackageDataKey(PackageInterface $package, $key, $defaultValue = null) { if (!isset($this->packagesData)) { $this->initializePackageData(); } $packageKey = $package->getPrettyName() . '/' . $package->getPrettyVersion(); if (!isset($this->packagesData[$packageKey]) || !isset($this->packagesData[$packageKey][$key])) { return $defaultValue; } return $this->packagesData[$packageKey][$key]; } /** * @param PackageInterface $package */ public function setPackageInstallationSource(PackageInterface $package) { $package->setInstallationSource(static::PACKAGE_INSTALLATION_SOURCE); } } ================================================ FILE: src/LEtudiant/Composer/Installer/Config/SharedPackageInstallerConfig.php ================================================ * * @see https://github.com/Letudiant/composer-shared-package-plugin/blob/master/docs/all-available-configurations.md */ class SharedPackageInstallerConfig { const ENV_PARAMETER_VENDOR_DIR = 'COMPOSER_SPP_VENDOR_DIR'; const ENV_PARAMETER_SYMLINK_BASE_PATH = 'COMPOSER_SPP_SYMLINK_BASE_PATH'; /** * @var string */ protected $originalVendorDir; /** * @var string */ protected $symlinkDir; /** * @var string */ protected $vendorDir; /** * @var string|null */ protected $symlinkBasePath; /** * @var bool */ protected $isSymlinkEnabled = true; /** * @var array */ protected $packageList = array(); /** * @param string $originalRelativeVendorDir * @param string $originalAbsoluteVendorDir * @param array|null $extraConfigs */ public function __construct($originalRelativeVendorDir, $originalAbsoluteVendorDir, $extraConfigs) { if (!isset($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['vendor-dir'])) { throw new \InvalidArgumentException( 'The "vendor-dir" parameter for "' . SharedPackageInstaller::PACKAGE_TYPE . '" configuration ' . 'should be provided in your project composer.json ("extra" key)' ); } $this->originalVendorDir = $originalRelativeVendorDir; $baseDir = substr($originalAbsoluteVendorDir, 0, -strlen($this->originalVendorDir)); $this->setVendorDir($baseDir, $extraConfigs); $this->setSymlinkDirectory($baseDir, $extraConfigs); $this->setSymlinkBasePath($extraConfigs); $this->setIsSymlinkEnabled($extraConfigs); $this->setPackageList($extraConfigs); } /** * @param string $baseDir * @param array $extraConfigs */ protected function setSymlinkDirectory($baseDir, array $extraConfigs) { $this->symlinkDir = $baseDir . 'vendor-shared'; if (isset($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-dir'])) { $this->symlinkDir = $extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-dir']; if ('/' != $this->symlinkDir[0]) { $this->symlinkDir = $baseDir . $this->symlinkDir; } } } /** * @param string $baseDir * @param array $extraConfigs * * @throws \InvalidArgumentException */ protected function setVendorDir($baseDir, array $extraConfigs) { $this->vendorDir = $extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['vendor-dir']; if (false !== getenv(static::ENV_PARAMETER_VENDOR_DIR)) { $this->vendorDir = getenv(static::ENV_PARAMETER_VENDOR_DIR); } if ('/' != $this->vendorDir[0]) { $this->vendorDir = $baseDir . $this->vendorDir; } } /** * Allow to override symlinks base path. * This is useful for a Virtual Machine environment, where directories can be different * on the host machine and the guest machine. * * @param array $extraConfigs */ protected function setSymlinkBasePath(array $extraConfigs) { if (isset($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-base-path'])) { $this->symlinkBasePath = $extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-base-path']; if (false !== getenv(static::ENV_PARAMETER_SYMLINK_BASE_PATH)) { $this->symlinkBasePath = getenv(static::ENV_PARAMETER_SYMLINK_BASE_PATH); } // Remove the ending slash if exists if ('/' === $this->symlinkBasePath[strlen($this->symlinkBasePath) - 1]) { $this->symlinkBasePath = substr($this->symlinkBasePath, 0, -1); } } elseif (0 < strpos($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['vendor-dir'], '/')) { $this->symlinkBasePath = $extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['vendor-dir']; } // Up to the project root directory if (0 < strpos($this->symlinkBasePath, '/')) { $this->symlinkBasePath = '../../' . $this->symlinkBasePath; } } /** * The symlink directory creation process can be disabled. * This may mean that you work directly with the sources directory so the symlink directory is useless. * * @param array $extraConfigs */ protected function setIsSymlinkEnabled(array $extraConfigs) { if (isset($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-enabled'])) { if (!is_bool($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-enabled'])) { throw new \UnexpectedValueException('The configuration "symlink-enabled" should be a boolean'); } $this->isSymlinkEnabled = $extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['symlink-enabled']; } } /** * @return array */ public function getPackageList() { return $this->packageList; } /** * @param array $extraConfigs */ public function setPackageList(array $extraConfigs) { if (isset($extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['package-list'])) { $packageList = $extraConfigs[SharedPackageInstaller::PACKAGE_TYPE]['package-list']; if (!is_array($packageList)) { throw new \UnexpectedValueException('The configuration "package-list" should be a JSON object'); } $this->packageList = $packageList; } } /** * @return bool */ public function isSymlinkEnabled() { return $this->isSymlinkEnabled; } /** * @return string */ public function getVendorDir() { return $this->vendorDir; } /** * @return string */ public function getSymlinkDir() { return $this->symlinkDir; } /** * @param bool $endingSlash * * @return string */ public function getOriginalVendorDir($endingSlash = false) { if ($endingSlash && null != $this->originalVendorDir) { return $this->originalVendorDir . '/'; } return $this->originalVendorDir; } /** * @return string|null */ public function getSymlinkBasePath() { return $this->symlinkBasePath; } } ================================================ FILE: src/LEtudiant/Composer/Installer/SharedPackageInstaller.php ================================================ */ class SharedPackageInstaller extends LibraryInstaller { const PACKAGE_TYPE = 'shared-package'; const PACKAGE_PRETTY_NAME = 'letudiant/composer-shared-package-plugin'; /** * @var SharedPackageInstallerConfig */ protected $config; /** * @var PackageDataManagerInterface */ protected $packageDataManager; /** * @var SymlinkFilesystem */ protected $filesystem; /** * @param IOInterface $io * @param Composer $composer * @param SymlinkFilesystem $filesystem * @param PackageDataManagerInterface $dataManager * @param SharedPackageInstallerConfig $config */ public function __construct( IOInterface $io, Composer $composer, SymlinkFilesystem $filesystem, PackageDataManagerInterface $dataManager, SharedPackageInstallerConfig $config ) { $this->filesystem = $filesystem; parent::__construct($io, $composer, 'library', $this->filesystem); $this->config = $config; $this->vendorDir = $this->config->getVendorDir(); $this->packageDataManager = $dataManager; $this->packageDataManager->setVendorDir($this->vendorDir); } /** * @inheritdoc */ public function getInstallPath(PackageInterface $package) { $this->initializeVendorDir(); $basePath = $this->vendorDir . DIRECTORY_SEPARATOR . $package->getPrettyName() . DIRECTORY_SEPARATOR . $package->getPrettyVersion() ; $targetDir = $package->getTargetDir(); return $basePath . ($targetDir ? '/' . $targetDir : ''); } /** * @param PackageInterface $package * * @return string */ protected function getPackageVendorSymlink(PackageInterface $package) { return $this->config->getSymlinkDir() . DIRECTORY_SEPARATOR . $package->getPrettyName(); } /** * @param InstalledRepositoryInterface $repo * @param PackageInterface $package */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!is_readable($this->getInstallPath($package))) { parent::install($repo, $package); } elseif (!$repo->hasPackage($package)) { $this->binaryInstaller->installBinaries($package, $this->getInstallPath($package)); $repo->addPackage(clone $package); } $this->createPackageVendorSymlink($package); $this->packageDataManager->addPackageUsage($package); } /** * @param InstalledRepositoryInterface $repo * @param PackageInterface $package * * @return bool */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { // Just check if the sources folder and the link exist return $repo->hasPackage($package) && is_readable($this->getInstallPath($package)) && is_link($this->getPackageVendorSymlink($package)) ; } /** * {@inheritdoc} */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { $this->packageDataManager->setPackageInstallationSource($initial); $this->packageDataManager->setPackageInstallationSource($target); // The package need only a code update because the version (branch), only the commit changed if ($this->getInstallPath($initial) === $this->getInstallPath($target)) { $this->createPackageVendorSymlink($target); parent::update($repo, $initial, $target); } else { // If the initial package sources folder exists, uninstall it $this->composer->getInstallationManager()->uninstall($repo, new UninstallOperation($initial)); // Install the target package $this->composer->getInstallationManager()->install($repo, new InstallOperation($target)); } } /** * @param InstalledRepositoryInterface $repo * @param PackageInterface $package * * @throws FilesystemException */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if ($this->isSourceDirUnused($package) && $this->io->askConfirmation( "The package version " . $package->getPrettyName() . " " . "(" . $package->getPrettyVersion() . ") seems to be unused." . PHP_EOL . 'Do you want to delete the source folder ? [y/n] (default: no) : ', false )) { $this->packageDataManager->setPackageInstallationSource($package); parent::uninstall($repo, $package); } else { $this->binaryInstaller->removeBinaries($package); $repo->removePackage($package); } $this->packageDataManager->removePackageUsage($package); $this->removePackageVendorSymlink($package); } /** * Detect if other project use the dependency by using the "packages.json" file * * @param PackageInterface $package * * @return bool */ protected function isSourceDirUnused(PackageInterface $package) { $usageData = $this->packageDataManager->getPackageUsage($package); return sizeof($usageData) <= 1; } /** * @param PackageInterface $package */ protected function createPackageVendorSymlink(PackageInterface $package) { if ($this->config->isSymlinkEnabled() && $this->filesystem->ensureSymlinkExists( $this->getSymlinkSourcePath($package), $this->getPackageVendorSymlink($package) ) ) { $this->io->write(array( ' - Creating symlink for ' . $package->getPrettyName() . ' (' . $package->getPrettyVersion() . ')', '' )); } } /** * @param PackageInterface $package * * @return string */ protected function getSymlinkSourcePath(PackageInterface $package) { if (null != $this->config->getSymlinkBasePath()) { $targetDir = $package->getTargetDir(); $sourcePath = $this->config->getSymlinkBasePath() . '/' . $package->getPrettyName() . '/' . $package->getPrettyVersion() . ($targetDir ? '/' . $targetDir : '') ; } else { $sourcePath = $this->getInstallPath($package); } return $sourcePath; } /** * @param PackageInterface $package * * @throws FilesystemException */ protected function removePackageVendorSymlink(PackageInterface $package) { if ( $this->config->isSymlinkEnabled() && $this->filesystem->removeSymlink($this->getPackageVendorSymlink($package)) ) { $this->io->write(array( ' - Deleting symlink for ' . $package->getPrettyName() . ' ' . '(' . $package->getPrettyVersion() . ')', '' )); $symlinkParentDirectory = dirname($this->getPackageVendorSymlink($package)); $this->filesystem->removeEmptyDirectory($symlinkParentDirectory); } } /** * @param string $packageType * * @return bool */ public function supports($packageType) { return true; } } ================================================ FILE: src/LEtudiant/Composer/Installer/Solver/SharedPackageInstallerSolver.php ================================================ */ class SharedPackageInstallerSolver implements InstallerInterface { /** * @var SymlinkFilesystem */ protected $filesystem; /** * @var SharedPackageSolver */ protected $solver; /** * @var SharedPackageInstaller */ protected $symlinkInstaller; /** * @var LibraryInstaller */ protected $defaultInstaller; /** * @param SharedPackageSolver $solver * @param SharedPackageInstaller $symlinkInstaller * @param LibraryInstaller $defaultInstaller */ public function __construct( SharedPackageSolver $solver, SharedPackageInstaller $symlinkInstaller, LibraryInstaller $defaultInstaller ) { $this->solver = $solver; $this->symlinkInstaller = $symlinkInstaller; $this->defaultInstaller = $defaultInstaller; } /** * Returns the installation path of a package * * @param PackageInterface $package * * @return string */ public function getInstallPath(PackageInterface $package) { if ($this->solver->isSharedPackage($package)) { return $this->symlinkInstaller->getInstallPath($package); } return $this->defaultInstaller->getInstallPath($package); } /** * @param InstalledRepositoryInterface $repo * @param PackageInterface $package */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { if ($this->solver->isSharedPackage($package)) { $this->symlinkInstaller->install($repo, $package); } else { $this->defaultInstaller->install($repo, $package); } } /** * @param InstalledRepositoryInterface $repo * @param PackageInterface $package * * @return bool */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { if ($this->solver->isSharedPackage($package)) { return $this->symlinkInstaller->isInstalled($repo, $package); } return $this->defaultInstaller->isInstalled($repo, $package); } /** * {@inheritdoc} */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { // If both packages are not shared if (!$this->solver->isSharedPackage($initial) && !$this->solver->isSharedPackage($target)) { $this->defaultInstaller->update($repo, $initial, $target); } else { if (!$repo->hasPackage($initial)) { throw new \InvalidArgumentException('Package is not installed : ' . $initial->getPrettyName()); } $this->symlinkInstaller->update($repo, $initial, $target); } } /** * @param InstalledRepositoryInterface $repo * @param PackageInterface $package * * @throws FilesystemException */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if ($this->solver->isSharedPackage($package)) { if (!$repo->hasPackage($package)) { throw new \InvalidArgumentException('Package is not installed : ' . $package->getPrettyName()); } $this->symlinkInstaller->uninstall($repo, $package); } else { $this->defaultInstaller->uninstall($repo, $package); } } /** * @param string $packageType * * @return bool */ public function supports($packageType) { // The solving process is in SharedPackageSolver::isSharedPackage() method return true; } } ================================================ FILE: src/LEtudiant/Composer/Installer/Solver/SharedPackageSolver.php ================================================ */ class SharedPackageSolver { /** * @var array */ protected $packageCallbacks = array(); /** * @var bool */ protected $areAllShared = false; /** * @param SharedPackageInstallerConfig $config */ public function __construct(SharedPackageInstallerConfig $config) { $packageList = $config->getPackageList(); foreach ($packageList as $packageName) { if ('*' === $packageName) { $this->areAllShared = true; } } if (!$this->areAllShared) { $this->packageCallbacks = $this->createCallbacks($packageList); } } /** * @param PackageInterface $package * * @return bool */ public function isSharedPackage(PackageInterface $package) { $prettyName = $package->getPrettyName(); // Avoid putting this package into dependencies folder, because on the first installation the package won't be // installed in dependencies folder but in the vendor folder. // So I prefer keeping this behavior for further installs. if (SharedPackageInstaller::PACKAGE_PRETTY_NAME === $prettyName) { return false; } if ($this->areAllShared || SharedPackageInstaller::PACKAGE_TYPE === $package->getType()) { return true; } foreach ($this->packageCallbacks as $equalityCallback) { if ($equalityCallback($prettyName)) { return true; } } return false; } /** * @param array $packageList * * @return array */ protected function createCallbacks(array $packageList) { $callbacks = array(); foreach ($packageList as $packageName) { // Has wild card (*) if (false !== strpos($packageName, '*')) { $pattern = str_replace('*', '[a-zA-Z0-9-_]+', str_replace('/', '\/', $packageName)); $callbacks[] = function ($packagePrettyName) use ($pattern) { return 1 === preg_match('/' . $pattern . '/', $packagePrettyName); }; // Raw package name } else { $callbacks[] = function ($packagePrettyName) use ($packageName) { return $packageName === $packagePrettyName; }; } } return $callbacks; } } ================================================ FILE: src/LEtudiant/Composer/SharedPackagePlugin.php ================================================ */ class SharedPackagePlugin implements PluginInterface { /** * @param Composer $composer * @param IOInterface $io */ public function activate(Composer $composer, IOInterface $io) { $config = $this->setConfig($composer); $composer->getInstallationManager()->addInstaller(new SharedPackageInstallerSolver( new SharedPackageSolver($config), new SharedPackageInstaller( $io, $composer, new SymlinkFilesystem(), new SharedPackageDataManager($composer), $config ), new LibraryInstaller($io, $composer) )); } /** * @param Composer $composer * * @return SharedPackageInstallerConfig */ protected function setConfig(Composer $composer) { return new SharedPackageInstallerConfig( $composer->getConfig()->get('vendor-dir'), $composer->getConfig()->get('vendor-dir', 1), $composer->getPackage()->getExtra() ); } } ================================================ FILE: src/LEtudiant/Composer/Util/SymlinkFilesystem.php ================================================ */ class SymlinkFilesystem extends Filesystem { /** * Create a symlink * * @param string $sourcePath * @param string $symlinkPath * * @return bool */ public function ensureSymlinkExists($sourcePath, $symlinkPath) { if (!is_link($symlinkPath)) { $this->ensureDirectoryExists(dirname($symlinkPath)); return symlink($sourcePath, $symlinkPath); } return false; } /** * @param string $symlinkPath * * @return bool * * @throws \RuntimeException */ public function removeSymlink($symlinkPath) { if (is_link($symlinkPath)) { if (!$this->unlink($symlinkPath)) { // @codeCoverageIgnoreStart throw new \RuntimeException('Unable to remove the symlink : ' . $symlinkPath); // @codeCoverageIgnoreEnd } return true; } return false; } /** * @param string $directoryPath * * @return bool * * @throws \RuntimeException */ public function removeEmptyDirectory($directoryPath) { if (is_dir($directoryPath) && $this->isDirEmpty($directoryPath)) { if (!$this->removeDirectory($directoryPath)) { // @codeCoverageIgnoreStart throw new \RuntimeException('Unable to remove the directory : ' . $directoryPath); // @codeCoverageIgnoreEnd } return true; } return false; } } ================================================ FILE: tests/bootstrap.php ================================================ * * @covers \LEtudiant\Composer\Data\Package\SharedPackageDataManager */ class SharedPackageDataManagerTest extends \PHPUnit_Framework_TestCase { /** * @var string */ protected $vendorDir; /** * @var RootPackageInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $rootPackage; /** * @var Composer */ protected $composer; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->vendorDir = sys_get_temp_dir() . '/composer-test-vendor-shared'; if (!is_dir($this->vendorDir)) { if (!mkdir($this->vendorDir)) { throw new \RuntimeException('Cannot create the temporary vendor dir'); } } else { $this->clearPackageData(); } $this->composer = new Composer(); /** @var RootPackageInterface|\PHPUnit_Framework_MockObject_MockObject $rootPackage */ $this->rootPackage = $this->getMock('Composer\Package\RootPackageInterface'); $this->composer->setPackage($this->rootPackage); } /** * @inheritdoc */ protected function tearDown() { $this->clearPackageData(); parent::tearDown(); } /** * @test */ public function getPackageUsageWithoutFile() { $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $this->assertEquals(array(), $dataManager->getPackageUsage($this->createPackage())); } /** * @test */ public function getPackageUsageWithFile() { $this->initializePackageData(); $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $this->assertEquals(array( 'letudiant/root-package' ), $dataManager->getPackageUsage($this->createPackage())); } /** * @test */ public function addPackageUsageWithoutData() { $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $this->rootPackage ->expects($this->once()) ->method('getName') ->willReturn('letudiant/root-package') ; $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->exactly(2)) ->method('getPrettyName') ->willReturn('letudiant/foo-bar') ; $package ->expects($this->exactly(2)) ->method('getPrettyVersion') ->willReturn('dev-develop') ; $package ->expects($this->never()) ->method('getInstallationSource') ; $dataManager->addPackageUsage($package); $this->assertFileExists($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $content = file_get_contents($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $this->assertJson($content); $this->assertEquals(array( 'letudiant/foo-bar/dev-develop' => array( 'project-usage' => array( 'letudiant/root-package' ) ) ), json_decode($content, true)); } /** * @test */ public function addPackageUsageWithHasData() { $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $this->initializePackageData(); $this->rootPackage ->expects($this->once()) ->method('getName') ->willReturn('letudiant/root-package2') ; $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->exactly(2)) ->method('getPrettyName') ->willReturn('letudiant/foo-bar') ; $package ->expects($this->exactly(2)) ->method('getPrettyVersion') ->willReturn('dev-develop') ; $package ->expects($this->exactly(0)) ->method('getInstallationSource') ; $dataManager->addPackageUsage($package); $this->assertFileExists($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $content = file_get_contents($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $this->assertJson($content); $this->assertEquals(array( 'letudiant/foo-bar/dev-develop' => array( 'installation-source' => SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE, 'project-usage' => array( 'letudiant/root-package', 'letudiant/root-package2' ) ) ), json_decode($content, true)); } /** * @test */ public function removePackageUsageWithoutData() { $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $this->rootPackage ->expects($this->once()) ->method('getName') ->willReturn('letudiant/root-package') ; $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->exactly(2)) ->method('getPrettyName') ->willReturn('letudiant/foo-bar') ; $package ->expects($this->exactly(2)) ->method('getPrettyVersion') ->willReturn('dev-develop') ; $package ->expects($this->exactly(0)) ->method('getInstallationSource') ; $dataManager->removePackageUsage($package); $this->assertFileExists($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $content = file_get_contents($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $this->assertJson($content); $this->assertEquals(array(), json_decode($content, true)); } /** * @test */ public function removePackageUsageWithData() { $this->initializePackageData(); $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $this->rootPackage ->expects($this->exactly(2)) ->method('getName') ->will($this->onConsecutiveCalls( 'letudiant/root-package', 'letudiant/root-package2' )) ; $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->exactly(4)) ->method('getPrettyName') ->willReturn('letudiant/foo-bar') ; $package ->expects($this->exactly(4)) ->method('getPrettyVersion') ->willReturn('dev-develop') ; $package ->expects($this->exactly(0)) ->method('getInstallationSource') ; // Remove the right package $this->initializePackageData(); $dataManager->removePackageUsage($package); $this->assertFileExists($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $content = file_get_contents($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $this->assertJson($content); $this->assertEquals(array(), json_decode($content, true)); // Remove another package, should not remove the initial package $this->initializePackageData(); $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $dataManager->removePackageUsage($package); $this->assertFileExists($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $content = file_get_contents($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); $this->assertJson($content); $this->assertEquals(array( 'letudiant/foo-bar/dev-develop' => array( 'installation-source' => SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE, 'project-usage' => array( 'letudiant/root-package' ) ) ), json_decode($content, true)); } /** * @test */ public function setPackageInstallationSourceWhenNotNull() { $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->once()) ->method('setInstallationSource') ; // With already provided installation source $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $dataManager->setPackageInstallationSource($package); } /** * @test */ public function setPackageInstallationSourceWhenNull() { $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->once()) ->method('setInstallationSource') ->with(SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE) ; $this->initializePackageData(); $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $dataManager->setPackageInstallationSource($package); } /** * @test */ public function setPackageInstallationSourceWhenNullAndNoData() { $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->once()) ->method('setInstallationSource') ->with(SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE) ; $dataManager = new SharedPackageDataManager($this->composer); $dataManager->setVendorDir($this->vendorDir); $dataManager->setPackageInstallationSource($package); } /** * Initialize fake data file */ protected function initializePackageData() { file_put_contents($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME, json_encode(array( 'letudiant/foo-bar/dev-develop' => array( 'installation-source' => SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE, 'project-usage' => array( 'letudiant/root-package' ) ) ))); $this->assertFileExists($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME); } /** * Remove the fake data file */ protected function clearPackageData() { if (is_file($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME) && !@unlink($this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME)) { throw new \RuntimeException('Cannot delete the file "' . $this->vendorDir . '/' . SharedPackageDataManager::PACKAGE_DATA_FILENAME . '"'); } } /** * @return PackageInterface|\PHPUnit_Framework_MockObject_MockObject */ protected function createPackage() { $package = $this->getMock('Composer\Package\PackageInterface'); $package ->expects($this->once()) ->method('getPrettyName') ->willReturn('letudiant/foo-bar') ; $package ->expects($this->once()) ->method('getPrettyVersion') ->willReturn('dev-develop') ; return $package; } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/Installer/Config/SharedPackageInstallerConfigTest.php ================================================ * * @covers \LEtudiant\Composer\Installer\Config\SharedPackageInstallerConfig */ class SharedPackageInstallerConfigTest extends \PHPUnit_Framework_TestCase { /** * Delete both env vars */ protected function tearDown() { putenv(SharedPackageInstallerConfig::ENV_PARAMETER_VENDOR_DIR); putenv(SharedPackageInstallerConfig::ENV_PARAMETER_SYMLINK_BASE_PATH); parent::tearDown(); } /** * @test * * @expectedException \InvalidArgumentException * @expectedExceptionMessage The "vendor-dir" parameter for "shared-package" configuration should be provided in your project composer.json ("extra" key) */ public function noVendorDirConfigured() { $this->createInstallerConfig(array()); } /** * @test * * @dataProvider getVendorDirDataProvider * * @param string $vendorDirPath */ public function getVendorDir($vendorDirPath) { $this->assertEquals(sys_get_temp_dir() . '/composer-test-dependencies-dir', $this->createInstallerConfig(array( 'vendor-dir' => $vendorDirPath ))->getVendorDir()); } /** * @test * * @dataProvider getVendorDirFromEnvVarDataProvider * * @param string $vendorDirPath * @param string $envVar */ public function getVendorDirFromEnvVar($vendorDirPath, $envVar) { putenv(SharedPackageInstallerConfig::ENV_PARAMETER_VENDOR_DIR . '=' . $envVar); $this->assertEquals(sys_get_temp_dir() . '/composer-test-dependencies-dir-env-var', $this->createInstallerConfig(array( 'vendor-dir' => $vendorDirPath ))->getVendorDir()); } /** * @return array */ public function getVendorDirFromEnvVarDataProvider() { return array( array( sys_get_temp_dir() . '/composer-test-dependencies-dir', sys_get_temp_dir() . '/composer-test-dependencies-dir-env-var' ), array( 'composer-test-dependencies-dir', 'composer-test-dependencies-dir-env-var' ) ); } /** * @return array */ public function getVendorDirDataProvider() { return array( array( sys_get_temp_dir() . '/composer-test-dependencies-dir' ), array( 'composer-test-dependencies-dir' ) ); } /** * @test * * @dataProvider getSymlinkDirDataProvider * * @param string $symlinkDirPath */ public function getSymlinkDir($symlinkDirPath) { $this->assertEquals(sys_get_temp_dir() . '/composer-test-vendor-shared-dir', $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-dir' => $symlinkDirPath ))->getSymlinkDir()); } /** * @return array */ public function getSymlinkDirDataProvider() { return array( array( sys_get_temp_dir() . '/composer-test-vendor-shared-dir' ), array( 'composer-test-vendor-shared-dir' ) ); } /** * @test */ public function getSymlinkDirWithEmptyConfiguration() { $this->assertEquals(sys_get_temp_dir() . '/vendor-shared', $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir' ))->getSymlinkDir()); } /** * @test * * @dataProvider getOriginalVendorDirDataProvider */ public function getOriginalVendorDir($expectedValue, $nullable) { $this->assertEquals($expectedValue, $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir' ))->getOriginalVendorDir($nullable)); } /** * @return array */ public function getOriginalVendorDirDataProvider() { return array( array( 'composer-test-vendor-dir/', true ), array( 'composer-test-vendor-dir', false ) ); } /** * @test */ public function getSymlinkBasePathWhenNull() { $this->assertNull($this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir' ))->getSymlinkBasePath()); } /** * @test */ public function getSymlinkBasePathWhenNotNull() { $this->assertEquals('/foo/bar', $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-base-path' => '/foo/bar' ))->getSymlinkBasePath()); } /** * @test */ public function getSymlinkBasePathWhenNotNullAndEndingSlash() { $this->assertEquals('/foo/bar', $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-base-path' => '/foo/bar/' ))->getSymlinkBasePath()); } /** * @test */ public function getSymlinkBasePathWhenVendorDirIsRelative() { $this->assertEquals('../../../composer-test-dependencies-dir', $this->createInstallerConfig(array( 'vendor-dir' => '../composer-test-dependencies-dir' ))->getSymlinkBasePath()); } /** * @test */ public function getSymlinkBasePathWhenRelative() { $this->assertEquals('../../../composer-test-dependencies-dir', $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-base-path' => '../composer-test-dependencies-dir' ))->getSymlinkBasePath()); } /** * @test */ public function getSymlinkBasePathFromEnvVar() { putenv(SharedPackageInstallerConfig::ENV_PARAMETER_SYMLINK_BASE_PATH . '=/composer-test-dependencies-dir-env-var'); $this->assertEquals('/composer-test-dependencies-dir-env-var', $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-base-path' => '../composer-test-dependencies-dir' ))->getSymlinkBasePath()); } /** * @test */ public function isSymlinkEnabledDefaultValue() { $this->assertTrue($this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', ))->isSymlinkEnabled()); } /** * @test */ public function setIsSymlinkEnabled() { $this->assertFalse($this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-enabled' => false ))->isSymlinkEnabled()); } /** * @test * * @expectedException \UnexpectedValueException * @expectedExceptionMessage The configuration "symlink-enabled" should be a boolean */ public function setIsSymlinkEnabledWithString() { $this->assertTrue($this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'symlink-enabled' => 'foo' ))->isSymlinkEnabled()); } /** * @test * * @expectedException \UnexpectedValueException * @expectedExceptionMessage The configuration "package-list" should be a JSON object */ public function setPackageListWrongTypeException() { $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'package-list' => 'foo' )); } /** * @test */ public function setPackageList() { $this->assertEquals(array('foo'), $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir', 'package-list' => array( 'foo' ) ))->getPackageList()); } /** * @test */ public function setPackageListEmpty() { $this->assertEquals(array(), $this->createInstallerConfig(array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-dependencies-dir' ))->getPackageList()); } /** * @param array $extra * @param string $relativeDir * @param null|string $absoluteDir * * @return SharedPackageInstallerConfig */ protected function createInstallerConfig(array $extra, $relativeDir = 'composer-test-vendor-dir', $absoluteDir = null) { if (null == $absoluteDir) { $absoluteDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $relativeDir; } return new SharedPackageInstallerConfig( $relativeDir, $absoluteDir, array( SharedPackageInstaller::PACKAGE_TYPE => $extra ) ); } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/Installer/SharedPackageInstallerTest.php ================================================ * * @covers \LEtudiant\Composer\Installer\SharedPackageInstaller */ class SharedPackageInstallerTest extends TestCase { /** * @var Composer */ protected $composer; /** * @var SharedPackageInstallerConfig */ protected $config; /** * @var string */ protected $vendorDir; /** * @var string */ protected $binDir; /** * @var string */ protected $symlinkDir; /** * @var string */ protected $dependenciesDir; /** * @var DownloadManager|\PHPUnit_Framework_MockObject_MockObject */ protected $dm; /** * @var InstalledRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $repository; /** * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $io; /** * @var SymlinkFilesystem */ protected $fs; /** * @var PackageDataManagerInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $dataManager; /** * @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject */ protected $im; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->fs = new SymlinkFilesystem(); $this->composer = new Composer(); $composerConfig = new Config(); $this->composer->setConfig($composerConfig); $this->im = $this->getMock('Composer\Installer\InstallationManager'); $this->composer->setInstallationManager($this->im); $this->vendorDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-vendor'; $this->ensureDirectoryExistsAndClear($this->vendorDir); $this->binDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'composer-test-bin'; $this->ensureDirectoryExistsAndClear($this->binDir); $this->dependenciesDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'composer-test-dependencies'; $this->ensureDirectoryExistsAndClear($this->dependenciesDir); $this->symlinkDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'composer-test-vendor-shared'; $composerConfig->merge(array( 'config' => array( 'vendor-dir' => $this->vendorDir, 'bin-dir' => $this->binDir, ), )); $this->dm = $this->getMockBuilder('Composer\Downloader\DownloadManager') ->disableOriginalConstructor() ->getMock() ; $this->composer->setDownloadManager($this->dm); $extraConfig = array( SharedPackageInstaller::PACKAGE_TYPE => array( 'vendor-dir' => $this->dependenciesDir, 'symlink-dir' => $this->symlinkDir ) ); /** @var RootPackage|\PHPUnit_Framework_MockObject_MockObject $package */ $package = $this->getMock('Composer\Package\RootPackageInterface'); $package ->expects($this->any()) ->method('getExtra') ->willReturn($extraConfig) ; $this->composer->setPackage($package); $this->repository = $this->getMock('Composer\Repository\InstalledRepositoryInterface'); $this->io = $this->getMock('Composer\IO\IOInterface'); $this->dataManager = $this->getMockBuilder('LEtudiant\Composer\Data\Package\SharedPackageDataManager') ->disableOriginalConstructor() ->getMock() ; $vendorDirParams = explode(DIRECTORY_SEPARATOR, $this->vendorDir); $this->config = new SharedPackageInstallerConfig( end($vendorDirParams), $this->vendorDir, $extraConfig ); } /** * @inheritdoc */ protected function tearDown() { $this->fs->removeDirectory($this->vendorDir); $this->fs->removeDirectory($this->binDir); $this->fs->removeDirectory($this->symlinkDir); $this->fs->removeDirectory($this->dependenciesDir); parent::tearDown(); } /** * @test */ public function testInstallerCreationShouldNotCreateVendorDirectory() { $this->fs->removeDirectory($this->vendorDir); $this->createInstaller(); $this->assertFileNotExists($this->vendorDir); } /** * @test */ public function testInstallerCreationShouldNotCreateBinDirectory() { $this->fs->removeDirectory($this->binDir); $this->createInstaller(); $this->assertFileNotExists($this->binDir); } /** * @test */ public function isInstalled() { $installer = $this->createInstaller(); $package = $this->createPackageMock(); $this->repository ->expects($this->exactly(2)) ->method('hasPackage') ->with($package) ->will($this->onConsecutiveCalls(false, true)) ; $this->assertFalse($installer->isInstalled($this->repository, $package)); $this->fs->ensureDirectoryExists($installer->getInstallPath($package)); $reflection = new \ReflectionObject($installer); $method = $reflection->getMethod('createPackageVendorSymlink'); $method->setAccessible(true); $method->invokeArgs($installer, array($package)); $this->assertTrue($installer->isInstalled($this->repository, $package)); } /** * @test */ public function install() { $installer = $this->createInstaller(); $package = $this->createPackageMock(); $this->dm ->expects($this->exactly(1)) ->method('download') ->with($package, $this->dependenciesDir . '/letudiant/foo-bar/dev-develop') ; $this->repository ->expects($this->exactly(2)) ->method('addPackage') ->with($package) ; $this->dataManager ->expects($this->exactly(2)) ->method('addPackageUsage') ->willReturn($package) ; $installer->install($this->repository, $package); $this->assertFileExists($this->vendorDir, 'Vendor dir should be created'); $this->assertFileExists($this->binDir, 'Bin dir should be created'); $this->assertFileExists($this->symlinkDir, 'Symlink dir should be created'); $this->assertFileExists($this->symlinkDir . '/letudiant', 'Symlink package prefix dir should be created'); $this->assertTrue(is_link($this->symlinkDir . '/letudiant/foo-bar'), 'Symlink should be created'); $this->assertFileExists($this->dependenciesDir, 'Dependencies dir should be created'); // Install another time with already created directory $this->fs->ensureDirectoryExists($installer->getInstallPath($package)); $this->repository ->expects($this->once()) ->method('hasPackage') ->with($package) ->willReturn(false) ; $installer->install($this->repository, $package); } /** * @test * * @depends install */ public function installWithSymlinkBasePath() { $symlinkBasePath = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'composer-test-symlink-base-path'; $config = $this->createConfigMock($this->dependenciesDir, $this->symlinkDir, $symlinkBasePath); $installer = $this->createInstaller($config); $package = $this->createPackageMock(); $installer->install($this->repository, $package); $this->assertFileExists($this->symlinkDir, 'Symlink dir should be created'); $this->assertFileExists($this->symlinkDir . '/letudiant', 'Symlink package prefix dir should be created'); $this->assertTrue(is_link($this->symlinkDir . '/letudiant/foo-bar'), 'Symlink should be created'); $this->assertEquals($symlinkBasePath . '/letudiant/foo-bar/dev-develop', readlink($this->symlinkDir . '/letudiant/foo-bar'), 'Symlink should have a custom base path'); } /** * @test * * @depends install */ public function installWithSymlinkBasePathAndTargetDir() { $symlinkBasePath = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'composer-test-symlink-base-path'; $config = $this->createConfigMock($this->dependenciesDir, $this->symlinkDir, $symlinkBasePath); $installer = $this->createInstaller($config); $package = $this->createPackageMock(); $package ->expects($this->exactly(5)) ->method('getTargetDir') ->willReturn('target-dir') ; $installer->install($this->repository, $package); $this->assertFileExists($this->symlinkDir, 'Symlink dir should be created'); $this->assertFileExists($this->symlinkDir . '/letudiant', 'Symlink package prefix dir should be created'); $this->assertTrue(is_link($this->symlinkDir . '/letudiant/foo-bar'), 'Symlink should be created'); $this->assertEquals($symlinkBasePath . '/letudiant/foo-bar/dev-develop/target-dir', readlink($this->symlinkDir . '/letudiant/foo-bar'), 'Symlink should have a custom base path'); } /** * @test * * @depends install */ public function installWithSymlinkDisabled() { $config = $this->createConfigMock($this->dependenciesDir, $this->symlinkDir, null, false); $installer = $this->createInstaller($config); $package = $this->createPackageMock(); $installer->install($this->repository, $package); $this->assertFileNotExists($this->symlinkDir, 'Symlink dir should be created'); } /** * @test * * @depends testInstallerCreationShouldNotCreateVendorDirectory * @depends testInstallerCreationShouldNotCreateBinDirectory */ public function updateCode() { $installer = $this->createInstaller(); $initial = $this->createPackageMock(); $target = $this->createPackageMock(); $this->dm ->expects($this->never()) ->method('download') ; $this->repository ->expects($this->exactly(2)) ->method('hasPackage') ->will($this->onConsecutiveCalls(true, true, false)) ; $this->dataManager ->expects($this->never()) ->method('addPackageUsage') ; $this->dataManager ->expects($this->never()) ->method('removePackageUsage') ; $installer->update($this->repository, $initial, $target); $this->assertFileExists($this->vendorDir, 'Vendor dir should be created'); $this->assertFileExists($this->binDir, 'Bin dir should be created'); $this->assertFileExists($this->dependenciesDir, 'Dependencies dir should be created'); $this->assertFileExists($this->symlinkDir, 'Symlink dir should be created'); $this->assertTrue(is_link($this->symlinkDir . '/letudiant/foo-bar')); } /** * @test * * @depends testInstallerCreationShouldNotCreateVendorDirectory * @depends testInstallerCreationShouldNotCreateBinDirectory */ public function updateFull() { /** @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject $im */ $this->im ->expects($this->once()) ->method('uninstall') ; $this->im ->expects($this->once()) ->method('install') ; $installer = $this->createInstaller(); $initial = $this->createPackageMock('letudiant/bar-foo'); $target = $this->createPackageMock(); $installer->update($this->repository, $initial, $target); } /** * @test */ public function uninstall() { $this->io ->expects($this->once()) ->method('askConfirmation') ->willReturn(true) ; /** @var SymlinkFilesystem|\PHPUnit_Framework_MockObject_MockObject $filesystem */ $filesystem = $this->getMock('\LEtudiant\Composer\Util\SymlinkFilesystem'); $filesystem ->expects($this->once()) ->method('removeSymlink') ->willReturn(true) ; $installer = $this->createInstaller($this->config, $filesystem); $package = $this->createPackageMock(); $this->repository ->expects($this->exactly(1)) ->method('hasPackage') ->with($package) ->will($this->onConsecutiveCalls(true, true)) ; $this->repository ->expects($this->once()) ->method('removePackage') ->with($package) ; $this->dm ->expects($this->once()) ->method('remove') ->with($package, $this->dependenciesDir . '/letudiant/foo-bar/dev-develop') ; $this->dataManager ->expects($this->once()) ->method('removePackageUsage') ->with($package) ; $installer->uninstall($this->repository, $package); } /** * @test */ public function uninstallKeepSources() { $this->io ->expects($this->once()) ->method('askConfirmation') ->willReturn(false) ; /** @var SymlinkFilesystem|\PHPUnit_Framework_MockObject_MockObject $filesystem */ $filesystem = $this->getMock('\LEtudiant\Composer\Util\SymlinkFilesystem'); $filesystem ->expects($this->once()) ->method('removeSymlink') ->willReturn(true) ; $installer = $this->createInstaller($this->config, $filesystem); $package = $this->createPackageMock(); $this->repository ->expects($this->once()) ->method('removePackage') ->with($package) ; $this->dm ->expects($this->never()) ->method('remove') ; $this->dataManager ->expects($this->once()) ->method('removePackageUsage') ->with($package) ; $installer->uninstall($this->repository, $package); } /** * @test */ public function getInstallPath() { $installer = $this->createInstaller(); $package = $this->createPackageMock(); $package ->expects($this->once()) ->method('getTargetDir') ->will($this->returnValue(null)); $this->assertEquals($this->dependenciesDir . '/letudiant/foo-bar/dev-develop', $installer->getInstallPath($package)); } /** * @test */ public function getInstallPathWithTargetDir() { $installer = $this->createInstaller(); $package = $this->createPackageMock(); $package ->expects($this->once()) ->method('getTargetDir') ->will($this->returnValue('Some/Namespace')) ; $package ->expects($this->any()) ->method('getPrettyName') ->will($this->returnValue('foo/bar')) ; $this->assertEquals($this->dependenciesDir . '/letudiant/foo-bar/dev-develop/Some/Namespace', $installer->getInstallPath($package)); } /** * @test */ public function supports() { $installer = $this->createInstaller(); $this->assertTrue($installer->supports('library')); $this->assertTrue($installer->supports(SharedPackageInstaller::PACKAGE_TYPE)); } /** * @param string|null $vendorDir * @param string|null $symlinkDir * @param string|null $symlinkBasePath * @param bool $isSymlinkEnabled * * @return SharedPackageInstallerConfig|\PHPUnit_Framework_MockObject_MockObject */ protected function createConfigMock($vendorDir = null, $symlinkDir = null, $symlinkBasePath = null, $isSymlinkEnabled = true) { if (null == $vendorDir) { $vendorDir = $this->dependenciesDir; } if (null == $symlinkDir) { $symlinkDir = $this->symlinkDir; } $config = $this->getMockBuilder('LEtudiant\Composer\Installer\Config\SharedPackageInstallerConfig') ->disableOriginalConstructor() ->getMock() ; $config ->expects($this->any()) ->method('getVendorDir') ->willReturn($vendorDir) ; $config ->expects($this->any()) ->method('getSymlinkDir') ->willReturn($symlinkDir) ; $config ->expects($this->any()) ->method('getSymlinkBasePath') ->willReturn($symlinkBasePath) ; $config ->expects($this->any()) ->method('isSymlinkEnabled') ->willReturn($isSymlinkEnabled) ; $config ->expects($this->any()) ->method('getOriginalVendorDir') ->with(array(true)) ->willReturn($this->vendorDir . '/') ; $config ->expects($this->any()) ->method('getOriginalVendorDir') ->with(array(false)) ->willReturn($this->vendorDir) ; return $config; } /** * @param string $prettyName * * @return Package|\PHPUnit_Framework_MockObject_MockObject */ protected function createPackageMock($prettyName = 'letudiant/foo-bar') { /** @var Package|\PHPUnit_Framework_MockObject_MockObject $package */ $package = $this->getMockBuilder('Composer\Package\Package') ->setConstructorArgs(array(md5(mt_rand()), 'dev-develop', 'dev-develop')) ->getMock() ; $package ->expects($this->any()) ->method('getType') ->willReturn(SharedPackageInstaller::PACKAGE_TYPE) ; $package ->expects($this->any()) ->method('getPrettyName') ->willReturn($prettyName) ; $package ->expects($this->any()) ->method('getPrettyVersion') ->willReturn('dev-develop') ; $package ->expects($this->any()) ->method('getVersion') ->willReturn('dev-develop') ; $package ->expects($this->any()) ->method('getInstallationSource') ->willReturn(SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE) ; return $package; } /** * @param null|SharedPackageInstallerConfig $config * @param null|Filesystem $filesystem * * @return SharedPackageInstaller */ protected function createInstaller($config = null, $filesystem = null) { if (null == $filesystem) { $filesystem = $this->fs; } if (null == $config) { $config = $this->config; } return new SharedPackageInstaller($this->io, $this->composer, $filesystem, $this->dataManager, $config); } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/Installer/Solver/SharedPackageInstallerSolverNotSharedTest.php ================================================ * * @covers \LEtudiant\Composer\Installer\Solver\SharedPackageInstallerSolver */ class SharedPackageInstallerSolverNotSharedTest extends \PHPUnit_Framework_TestCase { /** * @var LibraryInstaller|\PHPUnit_Framework_MockObject_MockObject */ protected $installer; /** * @var InstalledRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $repository; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->installer = $this->getMockBuilder('\Composer\Installer\LibraryInstaller') ->disableOriginalConstructor() ->getMock() ; $this->repository = $this->getMock('\Composer\Repository\InstalledRepositoryInterface'); } /** * @test */ public function getInstallPath() { $package = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('getInstallPath') ->with($package) ; $this->createSolver()->getInstallPath($package); } /** * @test */ public function install() { $package = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('install') ->with($this->repository, $package) ; $this->createSolver()->install($this->repository, $package); } /** * @test */ public function isInstalled() { $package = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('isInstalled') ->with($this->repository, $package) ; $this->createSolver()->isInstalled($this->repository, $package); } /** * @test */ public function update() { $initial = $this->createPackageMock(); $target = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('update') ->with($this->repository, $initial, $target) ; $this->createSolver()->update($this->repository, $initial, $target); } /** * @test */ public function uninstall() { $package = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('uninstall') ->with($this->repository, $package) ; $this->createSolver()->uninstall($this->repository, $package); } /** * @test */ public function supports() { $this->assertTrue($this->createSolver()->supports('library')); } /** * @return SharedPackageInstallerSolver */ protected function createSolver() { /** @var SharedPackageInstaller|\PHPUnit_Framework_MockObject_MockObject $symlinkInstaller */ $symlinkInstaller = $this->getMockBuilder('\LEtudiant\Composer\Installer\SharedPackageInstaller') ->disableOriginalConstructor() ->getMock() ; $config = new SharedPackageInstallerConfig('foo', 'bar', array( SharedPackageInstaller::PACKAGE_TYPE => array( 'vendor-dir' => 'foo' ) )); return new SharedPackageInstallerSolver(new SharedPackageSolver($config), $symlinkInstaller, $this->installer); } /** * @return Package|\PHPUnit_Framework_MockObject_MockObject */ protected function createPackageMock() { return $this->getMockBuilder('Composer\Package\Package') ->setConstructorArgs(array(md5(mt_rand()), '1.0.0.0', '1.0.0')) ->getMock() ; } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/Installer/Solver/SharedPackageInstallerSolverSharedTest.php ================================================ * * @covers \LEtudiant\Composer\Installer\Solver\SharedPackageInstallerSolver */ class SharedPackageInstallerSolverSharedTest extends SharedPackageInstallerSolverNotSharedTest { /** * @var SharedPackageInstaller|\PHPUnit_Framework_MockObject_MockObject */ protected $installer; /** * @var InstalledRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $repository; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->installer = $this->getMockBuilder('\LEtudiant\Composer\Installer\SharedPackageInstaller') ->disableOriginalConstructor() ->getMock() ; $this->repository = $this->getMock('\Composer\Repository\InstalledRepositoryInterface'); } /** * @return SharedPackageInstallerSolver */ protected function createSolver() { /** @var LibraryInstaller|\PHPUnit_Framework_MockObject_MockObject $defaultInstaller */ $defaultInstaller = $this->getMockBuilder('\Composer\Installer\LibraryInstaller') ->disableOriginalConstructor() ->getMock() ; $config = new SharedPackageInstallerConfig('foo', 'bar', array( SharedPackageInstaller::PACKAGE_TYPE => array( 'vendor-dir' => 'foo' ) )); return new SharedPackageInstallerSolver(new SharedPackageSolver($config), $this->installer, $defaultInstaller); } /** * @test * * @expectedException \InvalidArgumentException * @expectedExceptionMessage Package is not installed : letudiant/foo-bar */ public function updateWhenInitialNotInstalledException() { $initial = $this->createPackageMock(); $target = $this->createPackageMock(); $this->installer ->expects($this->never()) ->method('update') ->with($this->repository, $initial, $target) ; $this->createSolver()->update($this->repository, $initial, $target); } /** * @test */ public function uninstall() { $package = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('uninstall') ->with($this->repository, $package) ; $this->repository ->expects($this->once()) ->method('hasPackage') ->with($package) ->willReturn(true) ; $this->createSolver()->uninstall($this->repository, $package); } /** * @test */ public function update() { $initial = $this->createPackageMock(); $target = $this->createPackageMock(); $this->installer ->expects($this->once()) ->method('update') ->with($this->repository, $initial, $target) ; $this->repository ->expects($this->once()) ->method('hasPackage') ->with($initial) ->willReturn(true) ; $this->createSolver()->update($this->repository, $initial, $target); } /** * @test * * @expectedException \InvalidArgumentException * @expectedExceptionMessage Package is not installed : letudiant/foo-bar */ public function uninstallWhenPackageNotInstalledException() { $package = $this->createPackageMock(); $this->installer ->expects($this->never()) ->method('uninstall') ->with($this->repository, $package) ; $this->createSolver()->uninstall($this->repository, $package); } /** * @test */ public function supports() { $this->assertTrue($this->createSolver()->supports(SharedPackageInstaller::PACKAGE_TYPE)); } /** * @return Package|\PHPUnit_Framework_MockObject_MockObject */ protected function createPackageMock() { /** @var Package|\PHPUnit_Framework_MockObject_MockObject $package */ $package = $this->getMockBuilder('Composer\Package\Package') ->setConstructorArgs(array(md5(mt_rand()), 'dev-develop', 'dev-develop')) ->getMock() ; $package ->expects($this->any()) ->method('getType') ->willReturn(SharedPackageInstaller::PACKAGE_TYPE) ; $package ->expects($this->any()) ->method('isDev') ->willReturn(true) ; $package ->expects($this->any()) ->method('getPrettyName') ->willReturn('letudiant/foo-bar') ; $package ->expects($this->any()) ->method('getPrettyVersion') ->willReturn('dev-develop') ; $package ->expects($this->any()) ->method('getVersion') ->willReturn('dev-develop') ; $package ->expects($this->any()) ->method('getInstallationSource') ->willReturn(SharedPackageDataManager::PACKAGE_INSTALLATION_SOURCE) ; return $package; } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/Installer/Solver/SharedPackageSolverTest.php ================================================ * * @covers \LEtudiant\Composer\Installer\Solver\SharedPackageSolver */ class SharedPackageSolverTest extends \PHPUnit_Framework_TestCase { /** * @var SharedPackageInstallerConfig|\PHPUnit_Framework_MockObject_MockObject */ protected $config; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->config = $this->getMockBuilder('LEtudiant\Composer\Installer\Config\SharedPackageInstallerConfig') ->disableOriginalConstructor() ->getMock() ; } /** * @test */ public function constructAllShared() { $this->config ->expects($this->once()) ->method('getPackageList') ->willReturn(array( '*' )) ; $solver = $this->createSolver(); $this->assertAttributeEquals(true, 'areAllShared', $solver, 'All packages should be shared'); $this->assertAttributeCount(0, 'packageCallbacks', $solver, 'Package callbacks should be empty'); } /** * @test */ public function constructWithPackageList() { $this->config ->expects($this->once()) ->method('getPackageList') ->willReturn(array( 'foo/bar', 'bar/*' )) ; $solver = $this->createSolver(); $this->assertAttributeEquals(false, 'areAllShared', $solver, 'All packages should not be shared'); $this->assertAttributeCount(2, 'packageCallbacks', $solver, 'Package callbacks should be filled'); $callbacks = $this->getObjectAttribute($solver, 'packageCallbacks'); $this->assertArrayHasKey(0, $callbacks); $this->assertInternalType('callable', $callbacks[0]); $this->assertInternalType('callable', $callbacks[1]); } /** * @test */ public function constructWhenNull() { $this->config ->expects($this->once()) ->method('getPackageList') ->willReturn(array()) ; $solver = $this->createSolver(); $this->assertAttributeEquals(false, 'areAllShared', $solver, 'All packages should not be shared'); $this->assertAttributeCount(0, 'packageCallbacks', $solver, 'Package callbacks should be empty'); } /** * @param int $i * @param bool $expectedValue * @param string $packagePrettyName * * @test * @dataProvider createCallbacksDataProvider */ public function createCallbacks($i, $expectedValue, $packagePrettyName) { $this->config ->expects($this->once()) ->method('getPackageList') ->willReturn(array( 'foo/bar', 'bar/*' )) ; $solver = $this->createSolver(); $callbacks = $this->getObjectAttribute($solver, 'packageCallbacks'); $this->assertEquals($expectedValue, $callbacks[$i]($packagePrettyName)); } /** * @return array */ public function createCallbacksDataProvider() { return array( // Raw equality (foo/bar) array(0, false, 'foo/bar2'), array(0, false, 'foo2/bar2'), array(0, false, 'foo2/bar'), array(0, false, 'foo/'), array(0, false, 'foo'), array(0, false, ''), array(0, true, 'foo/bar'), // Regex equality (bar/*) array(1, false, 'foo/bar'), array(1, false, 'bar2/foo'), array(1, false, 'bar/'), array(1, false, 'bar'), array(1, true, 'bar/foo'), array(1, true, 'bar/foo2'), array(1, true, 'bar/foo-2'), array(1, true, 'bar/foo_2'), array(1, true, 'bar/foO_2'), ); } /** * @test */ public function isSharedPackageWithAllShared() { $this->config ->expects($this->once()) ->method('getPackageList') ->willReturn(array( '*' )) ; $solver = $this->createSolver(); // False $this->assertFalse($solver->isSharedPackage($this->createPackageMock(SharedPackageInstaller::PACKAGE_PRETTY_NAME))); // True $this->assertTrue($solver->isSharedPackage($this->createPackageMock())); $this->assertTrue($solver->isSharedPackage($this->createPackageMock('foo/bar'))); $this->assertTrue($solver->isSharedPackage($this->createPackageMock('bar/foo'))); $this->assertTrue($solver->isSharedPackage($this->createPackageMock('unknown/unknown'))); } /** * @test */ public function isSharedPackageWithPackageList() { $this->config ->expects($this->once()) ->method('getPackageList') ->willReturn(array( 'foo/bar', 'bar/*' )) ; $solver = $this->createSolver(); // False $this->assertFalse($solver->isSharedPackage($this->createPackageMock('unknown/unknown'))); // True // Package with "shared-package" type $this->assertTrue($solver->isSharedPackage($this->createPackageMock('unknown/unknown', SharedPackageInstaller::PACKAGE_TYPE))); // Packages in the package list $this->assertTrue($solver->isSharedPackage($this->createPackageMock('foo/bar'))); $this->assertTrue($solver->isSharedPackage($this->createPackageMock('bar/foo'))); } /** * @param null|string $prettyName * @param null|string $type * * @return PackageInterface|\PHPUnit_Framework_MockObject_MockObject */ public function createPackageMock($prettyName = null, $type = null) { $package = $this->getMock('Composer\Package\PackageInterface'); if (null != $prettyName) { $package ->expects($this->once()) ->method('getPrettyName') ->willReturn($prettyName) ; } if (null != $type) { $package ->expects($this->once()) ->method('getType') ->willReturn($type) ; } return $package; } /** * @return SharedPackageSolver */ protected function createSolver() { return new SharedPackageSolver($this->config); } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/SharedPackagePluginTest.php ================================================ * * @covers \LEtudiant\Composer\SharedPackagePlugin */ class SharedPackagePluginTest extends \PHPUnit_Framework_TestCase { /** * @var Composer */ protected $composer; /** * @var IOInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $io; /** * @var InstallationManager|\PHPUnit_Framework_MockObject_MockObject */ protected $im; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->composer = new Composer(); $config = new Config(); $this->composer->setConfig($config); /** @var RootPackageInterface|\PHPUnit_Framework_MockObject_MockObject $package */ $package = $this->getMock('Composer\Package\RootPackageInterface'); $package ->expects($this->any()) ->method('getExtra') ->willReturn(array( SharedPackageInstaller::PACKAGE_TYPE => array( 'vendor-dir' => sys_get_temp_dir() . '/composer-test-vendor-shared' ) )) ; $this->composer->setPackage($package); $this->im = $this->getMock('Composer\Installer\InstallationManager'); $this->composer->setInstallationManager($this->im); $this->io = $this->getMock('Composer\IO\IOInterface'); } /** * @test */ public function active() { $this->im->expects($this->once()) ->method('addInstaller') ; $plugin = new SharedPackagePlugin(); $plugin->activate($this->composer, $this->io); } } ================================================ FILE: tests/unit/Test/Unit/LEtudiant/Composer/Util/SymlinkFilesystemTest.php ================================================ * * @covers \LEtudiant\Composer\Util\SymlinkFilesystem */ class SymlinkFilesystemTest extends \PHPUnit_Framework_TestCase { /** * @var string */ protected $testDir; /** * @inheritdoc */ protected function setUp() { parent::setUp(); $this->testDir = sys_get_temp_dir() . '/composer-filesystem-test'; mkdir($this->testDir); } /** * @inheritdoc */ protected function tearDown() { if (is_link($this->testDir . '/foo')) { unlink($this->testDir . '/foo'); } if (is_dir($this->testDir)) { rmdir($this->testDir); } } /** * @test */ public function ensureSymlinkExistsWhenExists() { symlink(sys_get_temp_dir(), $this->testDir . '/foo'); $filesystem = new SymlinkFilesystem(); $this->assertFalse($filesystem->ensureSymlinkExists(sys_get_temp_dir(), $this->testDir . '/foo')); } /** * @test */ public function ensureSymlinkExistsWhenNotExists() { $filesystem = new SymlinkFilesystem(); $this->assertTrue($filesystem->ensureSymlinkExists(sys_get_temp_dir(), $this->testDir . '/foo')); $this->assertFileExists($this->testDir . '/foo'); } /** * @test */ public function removeSymlinkWhenExists() { symlink(sys_get_temp_dir(), $this->testDir . '/foo'); $filesystem = new SymlinkFilesystem(); $this->assertTrue($filesystem->removeSymlink($this->testDir . '/foo')); $this->assertFileNotExists($this->testDir . '/foo'); } /** * @test */ public function removeSymlinkWhenNotExists() { $filesystem = new SymlinkFilesystem(); $this->assertFalse($filesystem->removeSymlink($this->testDir . '/foo')); } /** * @test */ public function removeEmptyDirectoryWhenExists() { $filesystem = new SymlinkFilesystem(); $this->assertTrue($filesystem->removeEmptyDirectory($this->testDir)); $this->assertFileNotExists($this->testDir); } /** * @test */ public function removeEmptyDirectoryWhenNotEmpty() { symlink(sys_get_temp_dir(), $this->testDir . '/foo'); $filesystem = new SymlinkFilesystem(); $this->assertFalse($filesystem->removeEmptyDirectory($this->testDir)); $this->assertFileExists($this->testDir); } /** * @test */ public function removeEmptyDirectoryWhenNotExists() { $filesystem = new SymlinkFilesystem(); $this->assertFalse($filesystem->removeEmptyDirectory($this->testDir . '/bar')); } }