├── .scrutinizer.yml ├── .travis.yml ├── README.md ├── appveyor.yml ├── composer.json ├── res └── target.xml └── src └── MagentoHackathon └── Composer └── Magento ├── Event ├── EventManager.php ├── InstallEvent.php ├── PackagePostInstallEvent.php ├── PackagePreInstallEvent.php └── PackageUnInstallEvent.php ├── Factory ├── InstallStrategyFactory.php ├── InstallerFactory.php ├── ModuleManagerFactory.php ├── ParserFactory.php ├── ParserFactoryInterface.php └── PathTranslationParserFactory.php ├── GitIgnore.php ├── InstallStrategy ├── Copy.php ├── Exception │ ├── SourceNotExistsException.php │ └── TargetExistsException.php ├── InstallStrategyInterface.php ├── Link.php ├── None.php └── Symlink.php ├── InstalledPackage.php ├── InstalledPackageDumper.php ├── Installer ├── GlobResolver.php ├── Installer.php ├── InstallerInterface.php └── TargetFilter.php ├── Listener ├── CheckAndCreateMagentoRootDirListener.php ├── GitIgnoreListener.php └── PackagePrioritySortListener.php ├── Map ├── Map.php └── MapCollection.php ├── ModuleManager.php ├── Parser ├── MapParser.php ├── ModmanParser.php ├── PackageXmlParser.php ├── Parser.php ├── ParserInterface.php └── PathTranslationParser.php ├── Plugin.php ├── ProjectConfig.php ├── Repository ├── InstalledPackageFileSystemRepository.php └── InstalledPackageRepositoryInterface.php ├── UnInstallStrategy ├── UnInstallStrategy.php └── UnInstallStrategyInterface.php └── Util └── FileSystem.php /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 5.5 6 | - 5.4 7 | - 5.3 8 | - hhvm 9 | 10 | matrix: 11 | allow_failures: 12 | - php: 5.3 13 | - php: hhvm 14 | 15 | install: 16 | - composer self-update 17 | - composer install --dev --prefer-source 18 | 19 | before_script: 20 | - mkdir -p build/logs 21 | 22 | script: 23 | - ./vendor/bin/phpunit --coverage-clover ./build/logs/clover.xml 24 | - ./vendor/bin/phpcs --standard=PSR2 ./src/ 25 | - ./vendor/bin/phpcs --standard=PSR2 ./tests/MagentoHackathon 26 | 27 | after_script: 28 | - wget https://scrutinizer-ci.com/ocular.phar 29 | - php ocular.phar code-coverage:upload --format=php-clover ./build/logs/clover.xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/AydinHassan/magento-module-composer-installer.svg?branch=master)](https://travis-ci.org/AydinHassan/magento-module-composer-installer) 2 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/AydinHassan/magento-module-composer-installer/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/AydinHassan/magento-module-composer-installer/?branch=master) 3 | [![Code Coverage](https://scrutinizer-ci.com/g/AydinHassan/magento-module-composer-installer/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/AydinHassan/magento-module-composer-installer/?branch=master) 4 | 5 | # Magento Composer Installer 6 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/magento-hackathon/magento-composer-installer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | Dart Mosaic 9 | 10 | The purpose of this project is to 11 | enable [composer](https://github.com/composer/composer) to install Magento modules, 12 | and automatically integrate them into a Magento installation. 13 | 14 | We strongly recommend you to also read the general composer documentations on [getcomposer.org](http://getcomposer.org) 15 | 16 | Also you should see [Using composer correctly (confoo) by Igor Wiedler](https://speakerdeck.com/igorw/using-composer-correctly-confoo) 17 | 18 | 19 | ## Project Details 20 | 21 | This project only covers the custom installer for composer. If you have problems with outdated versions, 22 | need to install magento connect modules or similar, you need to look for [packages.firegento.com](http://packages.firegento.com/) 23 | 24 | 25 | ### support contacts 26 | 27 | If you have problems please have patience, as normal support is done during free time. 28 | If you are willing to pay to get your problem fixed, communicate this from the start to get faster responses. 29 | 30 | 31 | If you need consulting, support, training or help regarding Magento and Composer, 32 | you have the chance to hire one of the following people/companies. 33 | 34 | * Daniel Fahlke aka Flyingmana (Maintainer): flyingmana@googlemail.com [@Flyingmana](https://twitter.com/Flyingmana) 35 | * brandung - Magento Team: magento-team@brandung.de (http://brandung.de) 36 | * Your Name here 37 | * Your Name here 38 | * Your Company Name here 39 | * Your Company Name here 40 | 41 | other support contacts 42 | 43 | * irc: freenode the channels #magento-composer #magento-reddit and for german speaking people #magento-de 44 | * twitter: [@firegento](https://twitter.com/firegento) 45 | 46 | ## Known issue 47 | - Error message: `Fatal error: Call to undefined method MagentoHackathon\Composer\Magento\Installer::setDeployManager()` happens when you update from 1.x to 2.x, as we switched from pure installer to plugin. 48 | 49 | Solution: remove the `vendor` directory and the `composer.lock` and do a fresh install. 50 | ======= 51 | ## Known issues 52 | 53 | ### When upgrading from 1.x to 2.x 54 | 55 | The update from 1.x to 2.x has to be done with no plugins as otherwise a fatal error will be triggered (which does not hurt, just run the update again and it runs through). 56 | 57 | - Error message: `Fatal error: Call to undefined method MagentoHackathon\Composer\Magento\Installer::setDeployManager()` 58 | 59 | To prevent this error, upgrade only *magento-composer-installer* first: 60 | 61 | ```composer update --no-plugins --no-dev "magento-hackathon/magento-composer-installer"``` 62 | 63 | Fallback Solutions: 64 | 65 | 1. execute `composer install` two times. 66 | 2. remove the `vendor` directory and `composer.lock` and do a fresh install. 67 | 68 | ### Timeouts and slow downloading. 69 | 70 | Mostly caused by outtages of Github, Repositories or the Internet. This is a common problem with having all 71 | packges remote. 72 | 73 | For all of this Issues you can make use of the commercial [Toran Proxy](https://toranproxy.com/). 74 | It also allows hosting of private packages and speeds up the whole downloading process. 75 | 76 | Another alternative is to look into [Satis](https://github.com/composer/satis), bare git mirrors and repository aliasing. 77 | 78 | Another way to speedup downloads over ssh(also interesting for satis users) is to improve your ssh configs. 79 | At least for newer versions of openSSH you can add the following to your ```.ssh/config``` to reuse previous connections. 80 | ``` 81 | Host * 82 | ControlPath ~/.ssh/controlmasters/%r@%h:%p 83 | ControlMaster auto 84 | ControlPersist 10m 85 | ``` 86 | 87 | also you need to create the ```controlmasters``` directory: 88 | ```sh 89 | mkdir ~/.ssh/controlmasters 90 | chmod go-xr ~/.ssh/controlmasters 91 | ``` 92 | 93 | further information can be found on [wikibooks](http://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing) 94 | 95 | ## Usage 96 | 97 | See below for a [generic instruction on how to install composer](#installation-of-composer) if you aren't familiar with it. 98 | 99 | 100 | ### Install a module in your project 101 | 102 | If you want to use [the public Magento module repository](http://packages.firegento.com), 103 | set up your root ```composer.json``` in your project like this: 104 | 105 | ```json 106 | { 107 | "require": { 108 | "your-vendor-name/module-name": "*", 109 | "magento-hackathon/magento-composer-installer": "*" 110 | }, 111 | "repositories": [ 112 | { 113 | "type": "composer", 114 | "url": "http://packages.firegento.com" 115 | } 116 | ], 117 | "extra":{ 118 | "magento-root-dir": "htdocs/" 119 | } 120 | } 121 | ``` 122 | 123 | If you want to use a github/git/svn/etc repository, 124 | set up your root ```composer.json``` in your project like this: 125 | 126 | ```json 127 | { 128 | "require": { 129 | "magento-hackathon/magento-composer-installer":"*", 130 | "the-vendor-name/the-module-name": "*" 131 | }, 132 | "repositories": [ 133 | { 134 | "type": "vcs", 135 | "url": "https://github.com/magento-hackathon/magento-composer-installer" 136 | }, 137 | { 138 | "type": "vcs", 139 | "url": "the/github/or/git/or/svn/etc/repository/uri-of-the-module" 140 | } 141 | ], 142 | "extra":{ 143 | "magento-root-dir": "htdocs/" 144 | } 145 | } 146 | ``` 147 | Notes: 148 | 149 | 1. More information about VCS repositories can be found 150 | at [getcomposer.org](http://getcomposer.org/doc/05-repositories.md#vcs) 151 | 152 | 153 | 154 | ### Change the Vendor/Name of your Module 155 | 156 | sometimes it will happen, that you change the name or the vendor of a package. 157 | For example you developed a module in your own namespace and later moved it to an organization, or you moved it 158 | from one to another organization. 159 | In this cases you should change your ```composer.json``` a bit to make it for users easier. 160 | Look for the ```replace``` statement 161 | 162 | 163 | ```json 164 | { 165 | "name": "your-new-vendor-name/module-name", 166 | "type": "magento-module", 167 | "license":"OSL-3.0", 168 | "description":"A short one line description of your module", 169 | "authors":[ 170 | { 171 | "name":"Author Name", 172 | "email":"author@example.com" 173 | } 174 | ], 175 | "replace": { 176 | "your-vendor-name/module-name":"*" 177 | } 178 | } 179 | ``` 180 | 181 | 182 | 183 | ### Auto add files to .gitignore 184 | 185 | If you want to have the deployed files automatically added to your .gitignore file, then you can just set the `auto-append-gitignore` key to true: 186 | 187 | ```json 188 | { 189 | ... 190 | "extra":{ 191 | "magento-root-dir": "htdocs/", 192 | "auto-append-gitignore": true 193 | } 194 | ... 195 | } 196 | ``` 197 | 198 | The `.gitignore` file will be loaded from the current directory, and if it does not exist, it will be created. Every set of module files, will have a comment above them 199 | describing the module name for clarity. 200 | 201 | Multiple deploys will not add additional lines to your .gitignore, they will only ever be added once. 202 | 203 | 204 | ### Testing 205 | 206 | First clone the magento-composer-installer, then install the dev-stuff: 207 | 208 | ``` 209 | ./bin/composer.phar install --dev 210 | ``` 211 | 212 | then run ```vendor/bin/phpunit``` in project-root directory. 213 | 214 | Note: Windows users please run ```phpunit``` with Administrator permissions. 215 | 216 | 217 | ### How to overwrite dependencies 218 | 219 | We don't want to use always the official repositories for specific dependencies. 220 | For example for development purpose or use versions with custom patches. 221 | 222 | For this case you have the _repositories_ section inside your project composer.json 223 | Here you can define own package composer.json for specific dependencies by the package name. 224 | 225 | This example shows how to use a local git projects local-master branch which even works without a composer.json inside 226 | and a project in VCS with existing composer.json, which is not yet on packagist. 227 | 228 | ```json 229 | { 230 | ... 231 | "repositories": [ 232 | { 233 | "type": "package", 234 | "package": { 235 | "name": "magento-hackathon/magento-composer-installer", 236 | "version": "dev-master", 237 | "type": "composer-installer", 238 | "source": { 239 | "type": "git", 240 | "url": "/public_projects/magento/magento-composer-installer/", 241 | "reference": "local-master" 242 | }, 243 | "autoload": { 244 | "psr-0": {"MagentoHackathon\\Composer\\Magento": "src/"} 245 | }, 246 | "extra": { 247 | "class": "\\MagentoHackathon\\Composer\\Magento\\Installer" 248 | } 249 | } 250 | } 251 | ] 252 | } 253 | ``` 254 | 255 | ## Installation of composer 256 | 257 | ### 1. Install PHP-Composer 258 | 259 | #### On Linux/Mac 260 | 261 | Go to your project root directory and run: 262 | 263 | ``` 264 | mkdir bin 265 | curl -s https://getcomposer.org/installer | php -- --install-dir=bin 266 | ``` 267 | 268 | #### On Windows 269 | Please take a look at http://getcomposer.org/doc/00-intro.md#installation-windows 270 | 271 | Creation of symbolic links requires the SeCreateSymbolicLinkPrivilege (“Create symbolic links”), which is granted only 272 | to administrators by default (but you can change that using security policy). 273 | 274 | To change the policies: 275 | - Launch secpol.msc via Start or Start → Run. 276 | - Open Security Settings → Local Policies → User Rights Assignment. 277 | - In the list, find the "Create symbolic links" item, which represents SeCreateSymbolicLinkPrivilege. 278 | Double-click on the item and add yourself (or the whole Users group) to the list. 279 | 280 | (Seen at http://superuser.com/questions/124679/how-do-i-create-an-mklink-in-windows-7-home-premium-as-a-regular-user#125981) 281 | 282 | 283 | ### 2. Download composer.json template 284 | 285 | See [Usage](#usage). 286 | 287 | 288 | ### 3. Install Magento modules via composer 289 | 290 | ``` 291 | php bin/composer.phar install 292 | ``` 293 | 294 | 295 | 296 | 297 | 298 | ## Further Information 299 | 300 | * [FAQ](doc/FAQ.md) 301 | * [Make a Magento module installable with composer](doc/MakeAModuleInstallableWithComposer.md) 302 | * [About File Mapping like for example modman](doc/Mapping.md) 303 | * [About Deploying files into your Magento root and possible configs](doc/Deploy.md) 304 | 305 | ### External Links 306 | 307 | * [Composer How to Screencast](http://www.youtube.com/watch?v=m_yprtQiFgk) 308 | * [Introducing Composer Blog on Magebase.com](http://magebase.com/magento-tutorials/composer-with-magento/) 309 | * [Magento, Composer and Symfonys Dependency Injection](http://www.piotrbelina.com/magento-composer-and-dependency-injection/) 310 | * [Using Composer for Magento(at engineyard)](https://blog.engineyard.com/2014/composer-for-magento) 311 | 312 | ### Core Contributors 313 | 314 | * Daniel Fahlke aka Flyingmana (Maintainer) 315 | * Jörg Weller 316 | * Karl Spies 317 | * Tobias Vogt 318 | * David Fuhr 319 | * Amir Tchavoshinia 320 | * Vinai Kopp (Maintainer) -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | shallow_clone: true 3 | platform: 'x86' 4 | clone_folder: C:\projects\magento-composer-installer 5 | branches: 6 | except: 7 | - gh-pages 8 | 9 | install: 10 | - SET PATH=C:\Program Files\OpenSSL;%PATH% 11 | - cinst php 12 | - cd c:\tools\php 13 | - copy php.ini-production php.ini 14 | - echo date.timezone="UTC" >> php.ini 15 | - echo extension_dir=ext >> php.ini 16 | - echo extension=php_openssl.dll >> php.ini 17 | - SET PATH=C:\tools\php;%PATH% 18 | - cd C:\projects\magento-composer-installer 19 | - php -r "readfile('http://getcomposer.org/installer');" | php 20 | - php composer.phar install --prefer-dist --no-interaction 21 | 22 | test_script: 23 | - cd C:\projects\magento-composer-installer 24 | - vendor\bin\phpunit.bat tests -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"magento-hackathon/magento-composer-installer", 3 | "description":"Composer installer for Magento modules", 4 | "keywords":[ "composer-installer", "magento"], 5 | "minimum-stability":"stable", 6 | "type":"composer-plugin", 7 | "license":"OSL-3.0", 8 | "homepage":"https://github.com/magento-hackathon/magento-composer-installer", 9 | "repositories": [ 10 | { 11 | "type": "composer", 12 | "url": "http://packages.firegento.com" 13 | } 14 | ], 15 | "authors":[ 16 | { 17 | "name":"Aydin Hassan", 18 | "email":"aydin@hotmail.co.uk" 19 | }, 20 | { 21 | "name":"Daniel Fahlke aka Flyingmana", 22 | "email":"flyingmana@googlemail.com" 23 | }, 24 | { 25 | "name":"Jörg Weller", 26 | "email":"weller@flagbit.de" 27 | }, 28 | { 29 | "name":"Karl Spies", 30 | "email":"karl.spies@gmx.net" 31 | }, 32 | { 33 | "name":"Tobias Vogt", 34 | "email":"tobi@webguys.de" 35 | }, 36 | { 37 | "name":"David Fuhr", 38 | "email":"fuhr@flagbit.de" 39 | }, 40 | { 41 | "name":"Vinai Kopp", 42 | "email":"vinai@netzarbeiter.com" 43 | } 44 | ], 45 | "require":{ 46 | "php": ">=5.3.2", 47 | "symfony/console": "~2.5", 48 | "composer-plugin-api": "1.0.0" 49 | }, 50 | "require-dev":{ 51 | "phpunit/phpunit":"~4.4", 52 | "squizlabs/php_codesniffer": "~2.1", 53 | "composer/composer":"1.0.*@dev", 54 | "mikey179/vfsStream":"~1.4" 55 | }, 56 | "autoload":{ 57 | "psr-0":{ 58 | "MagentoHackathon\\Composer":"src/" 59 | } 60 | }, 61 | "autoload-dev":{ 62 | "psr-0":{ 63 | "MagentoHackathon\\Composer\\Magento":"tests/" 64 | } 65 | }, 66 | "archive": { 67 | "exclude": [ 68 | "vendor" 69 | ] 70 | }, 71 | "extra":{ 72 | "class":"MagentoHackathon\\Composer\\Magento\\Plugin" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /res/target.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/EventManager.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class EventManager 13 | { 14 | /** 15 | * @var array 16 | */ 17 | private $listeners = []; 18 | 19 | /** 20 | * @param string $event 21 | * @param callable $callback 22 | */ 23 | public function listen($event, $callback) 24 | { 25 | if (!is_callable($callback)) { 26 | throw new \InvalidArgumentException(sprintf( 27 | 'Second argument should be a callable. Got: "%s"', 28 | is_object($callback) ? get_class($callback) : gettype($callback) 29 | )); 30 | } 31 | 32 | if (!isset($this->listeners[$event])) { 33 | $this->listeners[$event] = [$callback]; 34 | } else { 35 | $this->listeners[$event][] = $callback; 36 | } 37 | } 38 | 39 | /** 40 | * @param Event $event 41 | */ 42 | public function dispatch(Event $event) 43 | { 44 | if (!isset($this->listeners[$event->getName()])) { 45 | return; 46 | } 47 | 48 | foreach ($this->listeners[$event->getName()] as $listener) { 49 | call_user_func_array($listener, [$event]); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/InstallEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class InstallEvent extends Event 14 | { 15 | /** 16 | * @var ArrayObject 17 | */ 18 | protected $packages; 19 | 20 | /** 21 | * @param string $name 22 | * @param ArrayObject $packages 23 | */ 24 | public function __construct($name, ArrayObject $packages) 25 | { 26 | parent::__construct($name); 27 | $this->packages = $packages; 28 | } 29 | 30 | /** 31 | * @return ArrayObject 32 | */ 33 | public function getPackages() 34 | { 35 | return $this->packages; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/PackagePostInstallEvent.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class PackagePostInstallEvent extends Event 16 | { 17 | /** 18 | * @var PackageInterface 19 | */ 20 | protected $composerPackage; 21 | 22 | /** 23 | * @var InstalledPackage 24 | */ 25 | protected $installedPackage; 26 | 27 | /** 28 | * @param PackageInterface $composerPackage 29 | * @param InstalledPackage $installedPackage 30 | */ 31 | public function __construct(PackageInterface $composerPackage, InstalledPackage $installedPackage) 32 | { 33 | parent::__construct('package-post-install'); 34 | $this->composerPackage = $composerPackage; 35 | $this->installedPackage = $installedPackage; 36 | } 37 | 38 | /** 39 | * @return PackageInterface 40 | */ 41 | public function getPackage() 42 | { 43 | return $this->composerPackage; 44 | } 45 | 46 | /** 47 | * @return MapCollection 48 | */ 49 | public function getMappings() 50 | { 51 | return $this->installedPackage->getMappings(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/PackagePreInstallEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PackagePreInstallEvent extends Event 14 | { 15 | /** 16 | * @var PackageInterface 17 | */ 18 | protected $package; 19 | 20 | /** 21 | * @param PackageInterface $package 22 | */ 23 | public function __construct(PackageInterface $package) 24 | { 25 | parent::__construct('package-pre-install'); 26 | $this->package = $package; 27 | } 28 | 29 | /** 30 | * @return PackageInterface 31 | */ 32 | public function getPackage() 33 | { 34 | return $this->package; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/PackageUnInstallEvent.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PackageUnInstallEvent extends Event 15 | { 16 | /** 17 | * @var InstalledPackage 18 | */ 19 | protected $package; 20 | 21 | /** 22 | * @param string $name 23 | * @param InstalledPackage $package 24 | */ 25 | public function __construct($name, InstalledPackage $package) 26 | { 27 | parent::__construct($name); 28 | $this->package = $package; 29 | } 30 | 31 | /** 32 | * @return InstalledPackage 33 | */ 34 | public function getPackage() 35 | { 36 | return $this->package; 37 | } 38 | 39 | /** 40 | * @return MapCollection 41 | */ 42 | public function getMappings() 43 | { 44 | return $this->package->getMappings(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/InstallStrategyFactory.php: -------------------------------------------------------------------------------- 1 | 100, 33 | 'link' => 100, 34 | 'copy' => 101, 35 | ]; 36 | 37 | /** 38 | * @var array 39 | */ 40 | protected static $strategies = [ 41 | 'copy' => '\MagentoHackathon\Composer\Magento\InstallStrategy\Copy', 42 | 'symlink' => '\MagentoHackathon\Composer\Magento\InstallStrategy\Symlink', 43 | 'link' => '\MagentoHackathon\Composer\Magento\InstallStrategy\Link', 44 | 'none' => '\MagentoHackathon\Composer\Magento\InstallStrategy\None', 45 | ]; 46 | 47 | /** 48 | * @param ProjectConfig $config 49 | */ 50 | public function __construct(ProjectConfig $config) 51 | { 52 | $this->config = $config; 53 | } 54 | 55 | /** 56 | * @param PackageInterface $package 57 | * @return InstallStrategyInterface 58 | */ 59 | public function make(PackageInterface $package) 60 | { 61 | $strategyName = $this->determineStrategy($package); 62 | if (!isset(static::$strategies[$strategyName])) { 63 | $className = static::$strategies['symlink']; 64 | } else { 65 | $className = static::$strategies[$strategyName]; 66 | } 67 | 68 | if ($className === static::$strategies['none']) { 69 | $instance = new $className; 70 | } else { 71 | $instance = new $className(new FileSystem); 72 | } 73 | 74 | return $instance; 75 | } 76 | 77 | /** 78 | * @param PackageInterface $package 79 | * @return string 80 | */ 81 | public function determineStrategy(PackageInterface $package) 82 | { 83 | $strategyName = $this->config->getDeployStrategy(); 84 | if ($this->config->hasDeployStrategyOverwrite()) { 85 | $moduleSpecificDeployStrategies = $this->config->getDeployStrategyOverwrite(); 86 | 87 | if (isset($moduleSpecificDeployStrategies[$package->getName()])) { 88 | $strategyName = $moduleSpecificDeployStrategies[$package->getName()]; 89 | } 90 | } 91 | 92 | return $strategyName; 93 | } 94 | 95 | /** 96 | * @param PackageInterface $package 97 | * @return int 98 | */ 99 | public function getDefaultPriority(PackageInterface $package) 100 | { 101 | if (isset(static::$priorityMap[$this->determineStrategy($package)])) { 102 | return static::$priorityMap[$this->determineStrategy($package)]; 103 | } 104 | 105 | return 100; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/InstallerFactory.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class InstallerFactory 19 | { 20 | /** 21 | * @param ProjectConfig $config 22 | * @param EventManager $eventManager 23 | * 24 | * @return Installer 25 | */ 26 | public function make(ProjectConfig $config, EventManager $eventManager) 27 | { 28 | $installStrategyFactory = new InstallStrategyFactory($config); 29 | $fileSystem = new FileSystem; 30 | $globResolver = new GlobResolver; 31 | $targetFilter = new TargetFilter($config->getMagentoDeployIgnore()); 32 | $parser = new Parser(new ParserFactory($config)); 33 | 34 | return new Installer( 35 | $installStrategyFactory, 36 | $fileSystem, 37 | $config, 38 | $globResolver, 39 | $targetFilter, 40 | $parser, 41 | $eventManager 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/ModuleManagerFactory.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ModuleManagerFactory 25 | { 26 | /** 27 | * @param ProjectConfig $config 28 | * @param EventManager $eventManager 29 | * @param IOInterface $io 30 | * 31 | * @return ModuleManager 32 | */ 33 | public function make(ProjectConfig $config, EventManager $eventManager, IOInterface $io) 34 | { 35 | $installStrategyFactory = new InstallStrategyFactory($config); 36 | 37 | if ($config->hasAutoAppendGitignore()) { 38 | $this->addGitIgnoreListener($eventManager, $config); 39 | } 40 | 41 | if ($io->isDebug()) { 42 | $this->addDebugListener($eventManager, $io); 43 | } 44 | 45 | $eventManager->listen( 46 | 'pre-install', 47 | new CheckAndCreateMagentoRootDirListener($config->getMagentoRootDir(false)) 48 | ); 49 | 50 | $eventManager->listen( 51 | 'pre-install', 52 | new PackagePrioritySortListener($installStrategyFactory, $config) 53 | ); 54 | 55 | $installerFactory = new InstallerFactory; 56 | return new ModuleManager( 57 | new InstalledPackageFileSystemRepository( 58 | $config->getModuleRepositoryLocation(), 59 | new InstalledPackageDumper 60 | ), 61 | $eventManager, 62 | $config, 63 | new UnInstallStrategy(new FileSystem, $config->getMagentoRootDir()), 64 | $installerFactory->make($config, $eventManager), 65 | $installStrategyFactory 66 | ); 67 | } 68 | 69 | /** 70 | * @param EventManager $eventManager 71 | * @param ProjectConfig $config 72 | */ 73 | protected function addGitIgnoreListener(EventManager $eventManager, ProjectConfig $config) 74 | { 75 | $gitIgnoreLocation = sprintf('%s/.gitignore', $config->getMagentoRootDir()); 76 | $gitIgnore = new GitIgnoreListener(new GitIgnore($gitIgnoreLocation)); 77 | 78 | $eventManager->listen('package-post-install', [$gitIgnore, 'addNewInstalledFiles']); 79 | $eventManager->listen('package-post-uninstall', [$gitIgnore, 'removeUnInstalledFiles']); 80 | } 81 | 82 | /** 83 | * @param EventManager $eventManager 84 | * @param IOInterface $io 85 | */ 86 | protected function addDebugListener(EventManager $eventManager, IOInterface $io) 87 | { 88 | $eventManager->listen('package-pre-install', function (PackagePreInstallEvent $event) use ($io) { 89 | $io->write('Start magento deploy for ' . $event->getPackage()->getName()); 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/ParserFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ParserFactory implements ParserFactoryInterface 18 | { 19 | 20 | /** 21 | * @var ProjectConfig 22 | */ 23 | protected $config; 24 | 25 | /** 26 | */ 27 | public function __construct(ProjectConfig $config) 28 | { 29 | $this->config = $config; 30 | } 31 | 32 | /** 33 | * @param PackageInterface $package 34 | * @param string $sourceDir 35 | * @return ParserInterface 36 | * @throws \ErrorException 37 | */ 38 | public function make(PackageInterface $package, $sourceDir) 39 | { 40 | $moduleSpecificMap = $this->config->getMagentoMapOverwrite(); 41 | if (isset($moduleSpecificMap[$package->getName()])) { 42 | $map = $moduleSpecificMap[$package->getName()]; 43 | return new MapParser($map); 44 | } 45 | 46 | $extra = $package->getExtra(); 47 | if (isset($extra['map'])) { 48 | return new MapParser($extra['map']); 49 | } 50 | 51 | if (isset($extra['package-xml'])) { 52 | return new PackageXmlParser(sprintf('%s/%s', $sourceDir, $extra['package-xml'])); 53 | } 54 | 55 | $modmanFile = sprintf('%s/modman', $sourceDir); 56 | if (file_exists($modmanFile)) { 57 | return new ModmanParser($modmanFile); 58 | } 59 | 60 | throw new \ErrorException( 61 | sprintf('Unable to find deploy strategy for module: "%s" no known mapping', $package->getName()) 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/ParserFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ParserFactoryInterface 14 | { 15 | 16 | /** 17 | * @param PackageInterface $package 18 | * @param string $sourceDir 19 | * @return ParserInterface 20 | */ 21 | public function make(PackageInterface $package, $sourceDir); 22 | } 23 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/PathTranslationParserFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class PathTranslationParserFactory implements ParserFactoryInterface 16 | { 17 | /** 18 | * @var ParserFactoryInterface 19 | */ 20 | protected $parserFactory; 21 | 22 | /** 23 | * @var ProjectConfig 24 | */ 25 | protected $config; 26 | 27 | /** 28 | * @param ParserFactoryInterface $parserFactory 29 | */ 30 | public function __construct(ParserFactoryInterface $parserFactory, ProjectConfig $config) 31 | { 32 | $this->parserFactory = $parserFactory; 33 | $this->config = $config; 34 | } 35 | 36 | /** 37 | * @param PackageInterface $package 38 | * @param string $sourceDir 39 | * @return ParserInterface 40 | * @throws \ErrorException 41 | */ 42 | public function make(PackageInterface $package, $sourceDir) 43 | { 44 | $parser = $this->parserFactory->make($package, $sourceDir); 45 | 46 | if ($this->config->hasPathMappingTranslations()) { 47 | $translations = $this->config->getPathMappingTranslations(); 48 | return new PathTranslationParser($parser, $translations); 49 | } 50 | 51 | return $parser; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/GitIgnore.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class GitIgnore 11 | { 12 | /** 13 | * @var array 14 | */ 15 | protected $lines = []; 16 | 17 | /** 18 | * @var string|null 19 | */ 20 | protected $gitIgnoreLocation; 21 | 22 | /** 23 | * @var bool 24 | */ 25 | protected $hasChanges = false; 26 | 27 | /** 28 | * @param string $fileLocation 29 | */ 30 | public function __construct($fileLocation) 31 | { 32 | $this->gitIgnoreLocation = $fileLocation; 33 | if (file_exists($fileLocation)) { 34 | $this->lines = array_flip(file($fileLocation, FILE_IGNORE_NEW_LINES)); 35 | } 36 | } 37 | 38 | /** 39 | * @param string $file 40 | */ 41 | public function addEntry($file) 42 | { 43 | if (!isset($this->lines[$file])) { 44 | $this->lines[$file] = $file; 45 | } 46 | $this->hasChanges = true; 47 | } 48 | 49 | /** 50 | * @param array $files 51 | */ 52 | public function addMultipleEntries(array $files) 53 | { 54 | foreach ($files as $file) { 55 | $this->addEntry($file); 56 | } 57 | } 58 | 59 | /** 60 | * @param string $file 61 | */ 62 | public function removeEntry($file) 63 | { 64 | if (isset($this->lines[$file])) { 65 | unset($this->lines[$file]); 66 | $this->hasChanges = true; 67 | } 68 | } 69 | 70 | /** 71 | * @param array $files 72 | */ 73 | public function removeMultipleEntries(array $files) 74 | { 75 | foreach ($files as $file) { 76 | $this->removeEntry($file); 77 | } 78 | } 79 | 80 | /** 81 | * @return array 82 | */ 83 | public function getEntries() 84 | { 85 | return array_values(array_flip($this->lines)); 86 | } 87 | 88 | /** 89 | * Write the file 90 | */ 91 | public function write() 92 | { 93 | if ($this->hasChanges) { 94 | file_put_contents($this->gitIgnoreLocation, implode("\n", array_flip($this->lines))); 95 | } 96 | $this->hasChanges = false; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstallStrategy/Copy.php: -------------------------------------------------------------------------------- 1 | fileSystem = $fileSystem; 27 | } 28 | 29 | /** 30 | * Resolve the mappings. If source is a folder, create mappings for every file inside it. 31 | * Also if destination dir is an existing folder and its base does not match the source base, 32 | * source should be placed inside destination. 33 | * 34 | * @param string $source 35 | * @param string $destination 36 | * @param string $absoluteSource 37 | * @param string $absoluteDestination 38 | * 39 | * @return array Resolved Mappings 40 | */ 41 | public function resolve($source, $destination, $absoluteSource, $absoluteDestination) 42 | { 43 | if (is_dir($absoluteDestination) 44 | && !$this->fileSystem->sourceAndDestinationBaseMatch($source, $destination) 45 | ) { 46 | // If the destination exists and is a directory 47 | // and basename of source and destination are not equal that means we want to copy 48 | // source into destination, not to destination 49 | // eg. src: code/Some_Module.xml dest: app/etc/modules 50 | // would result in Some_Module.xml being placed inside: app/etc/modules 51 | // - so: app/etc/modules/Some_Module.xml 52 | // 53 | $destination = sprintf('%s/%s', $destination, basename($source)); 54 | } 55 | 56 | //dir - dir 57 | if (is_dir($absoluteSource)) { 58 | return $this->resolveDirectory($source, $destination, $absoluteSource, $absoluteDestination); 59 | } 60 | 61 | //file - to - file 62 | return [[$source, $destination]]; 63 | } 64 | 65 | /** 66 | * Build an array of mappings which should be created 67 | * eg. Every file in the directory 68 | * 69 | * @param string $source 70 | * @param string $destination 71 | * @param string $absoluteSource 72 | * @param string $absoluteDestination 73 | * 74 | * @return array Array of resolved mappings 75 | */ 76 | protected function resolveDirectory($source, $destination, $absoluteSource, $absoluteDestination) 77 | { 78 | $iterator = new \RecursiveIteratorIterator( 79 | new \RecursiveDirectoryIterator($absoluteSource, \RecursiveDirectoryIterator::SKIP_DOTS), 80 | \RecursiveIteratorIterator::SELF_FIRST 81 | ); 82 | 83 | $resolvedMappings = []; 84 | foreach ($iterator as $item) { 85 | /** @var SplFileinfo $item */ 86 | 87 | if ($item->isFile()) { 88 | $resolvedMappings[] = [ 89 | sprintf('%s/%s', $source, $iterator->getSubPathname()), 90 | sprintf('%s/%s', $destination, $iterator->getSubPathname()), 91 | ]; 92 | } 93 | } 94 | return $resolvedMappings; 95 | } 96 | 97 | /** 98 | * @param Map $map 99 | * @param bool $force 100 | * @throws TargetExistsException 101 | */ 102 | public function create(Map $map, $force) 103 | { 104 | // If file exists and force is not specified, throw exception 105 | if (file_exists($map->getAbsoluteDestination())) { 106 | if (!$force) { 107 | throw new TargetExistsException($map->getAbsoluteDestination()); 108 | } 109 | $this->fileSystem->remove($map->getAbsoluteDestination()); 110 | } 111 | 112 | copy($map->getAbsoluteSource(), $map->getAbsoluteDestination()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstallStrategy/Exception/SourceNotExistsException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class SourceNotExistsException extends RuntimeException 13 | { 14 | /** 15 | * @var string 16 | */ 17 | protected $sourceFilePath; 18 | 19 | /** 20 | * @param string $source 21 | */ 22 | public function __construct($source) 23 | { 24 | $this->sourceFilePath = $source; 25 | $message = sprintf('Source "%s" does not exist', $source); 26 | parent::__construct($message); 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function getSourceFilePath() 33 | { 34 | return $this->sourceFilePath; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstallStrategy/Exception/TargetExistsException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class TargetExistsException extends RuntimeException 13 | { 14 | /** 15 | * @var string 16 | */ 17 | protected $targetFilePath; 18 | 19 | /** 20 | * @param string $target 21 | */ 22 | public function __construct($target) 23 | { 24 | $this->targetFilePath = $target; 25 | $message = sprintf('Target "%s" already exists (set extra.magento-force to override)', $target); 26 | parent::__construct($message); 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function getTargetFilePath() 33 | { 34 | return $this->targetFilePath; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstallStrategy/InstallStrategyInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface InstallStrategyInterface 14 | { 15 | 16 | /** 17 | * Resolve the mappings. Should return an array of mappings which 18 | * should be created. Eg in the case of copy, if source is a directory, then each file inside that directory 19 | * should be returned as a mapping. 20 | * 21 | * @param string $source 22 | * @param string $destination 23 | * @param string $absoluteSource 24 | * @param string $absoluteDestination 25 | * 26 | * @return array Resolved Mappings 27 | */ 28 | public function resolve($source, $destination, $absoluteSource, $absoluteDestination); 29 | 30 | /** 31 | * @param Map $map Map contains the relative and absolute source and destination 32 | * @param bool $force Whether the creation should be forced (eg if it exists already) 33 | * @throws TargetExistsException If a 34 | * 35 | */ 36 | public function create(Map $map, $force); 37 | } 38 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstallStrategy/Link.php: -------------------------------------------------------------------------------- 1 | fileSystem = $fileSystem; 27 | } 28 | 29 | /** 30 | * Resolve the mappings. If source is a folder, create mappings for every file inside it. 31 | * Also if destination dir is an existing folder and its base does not match the source base, 32 | * source should be placed inside destination. 33 | * 34 | * @param string $source 35 | * @param string $destination 36 | * @param string $absoluteSource 37 | * @param string $absoluteDestination 38 | * 39 | * @return array Resolved Mappings 40 | */ 41 | public function resolve($source, $destination, $absoluteSource, $absoluteDestination) 42 | { 43 | if (is_dir($absoluteDestination) 44 | && !$this->fileSystem->sourceAndDestinationBaseMatch($source, $destination) 45 | ) { 46 | // If the destination exists and is a directory 47 | // and basename of source and destination are not equal that means we want to copy 48 | // source into destination, not to destination 49 | // eg. src: code/Some_Module.xml dest: app/etc/modules 50 | // would result in Some_Module.xml being placed inside: app/etc/modules 51 | // - so: app/etc/modules/Some_Module.xml 52 | // 53 | 54 | $destination = sprintf('%s/%s', $destination, basename($source)); 55 | } 56 | 57 | //dir - dir 58 | if (is_dir($absoluteSource)) { 59 | return $this->resolveDirectory($source, $destination, $absoluteSource, $absoluteDestination); 60 | } 61 | 62 | //file - to - file 63 | return [[$source, $destination]]; 64 | } 65 | 66 | /** 67 | * Build an array of mappings which should be created 68 | * eg. Every file in the directory 69 | * 70 | * @param string $source 71 | * @param string $destination 72 | * @param string $absoluteSource 73 | * @param string $absoluteDestination 74 | * 75 | * @return array Array of resolved mappings 76 | */ 77 | protected function resolveDirectory($source, $destination, $absoluteSource, $absoluteDestination) 78 | { 79 | $iterator = new \RecursiveIteratorIterator( 80 | new \RecursiveDirectoryIterator($absoluteSource, \RecursiveDirectoryIterator::SKIP_DOTS), 81 | \RecursiveIteratorIterator::SELF_FIRST 82 | ); 83 | 84 | $resolvedMappings = []; 85 | foreach ($iterator as $item) { 86 | /** @var SplFileinfo $item */ 87 | if ($item->isFile()) { 88 | $resolvedMappings[] = [ 89 | sprintf('%s/%s', $source, $iterator->getSubPathname()), 90 | sprintf('%s/%s', $destination, $iterator->getSubPathname()), 91 | ]; 92 | } 93 | } 94 | return $resolvedMappings; 95 | } 96 | 97 | /** 98 | * @param Map $map 99 | * @param bool $force 100 | * @throws TargetExistsException 101 | */ 102 | public function create(Map $map, $force) 103 | { 104 | // If file exists and force is not specified, throw exception 105 | if (file_exists($map->getAbsoluteDestination())) { 106 | if (!$force) { 107 | throw new TargetExistsException($map->getAbsoluteDestination()); 108 | } 109 | $this->fileSystem->remove($map->getAbsoluteDestination()); 110 | } 111 | 112 | link($map->getAbsoluteSource(), $map->getAbsoluteDestination()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstallStrategy/None.php: -------------------------------------------------------------------------------- 1 | fileSystem = $fileSystem; 28 | } 29 | 30 | /** 31 | * Resolve the mappings. 32 | * 33 | * For symlinks, if the destination exists and is a directory, the symlink 34 | * should be created *inside* destination 35 | * 36 | * @param string $source 37 | * @param string $destination 38 | * @param string $absoluteSource 39 | * @param string $absoluteDestination 40 | * 41 | * @return array Resolved Mappings 42 | */ 43 | public function resolve($source, $destination, $absoluteSource, $absoluteDestination) 44 | { 45 | if (is_dir($absoluteDestination)) { 46 | $destination = sprintf('%s/%s', $destination, basename($source)); 47 | } 48 | 49 | return [[$source, $destination]]; 50 | } 51 | 52 | /** 53 | * @param Map $map 54 | * @param bool $force 55 | * @throws TargetExistsException 56 | */ 57 | public function create(Map $map, $force) 58 | { 59 | //if destination exists should we overwrite it? 60 | if (is_dir($map->getAbsoluteDestination()) 61 | && $this->fileSystem->sourceAndDestinationBaseMatch($map->getSource(), $map->getDestination()) 62 | ) { 63 | if (!$force) { 64 | throw new TargetExistsException($map->getAbsoluteDestination()); 65 | } 66 | $this->fileSystem->remove($map->getAbsoluteDestination()); 67 | } 68 | 69 | return $this->symlink($map, $force); 70 | } 71 | 72 | /** 73 | * @param Map $map 74 | * @param bool $force 75 | * @throws TargetExistsException 76 | */ 77 | protected function symlink(Map $map, $force) 78 | { 79 | if (is_link($map->getAbsoluteDestination())) { 80 | $symLinkCorrect = $this->fileSystem->symLinkPointsToCorrectLocation( 81 | $map->getAbsoluteDestination(), 82 | $map->getAbsoluteSource() 83 | ); 84 | 85 | if ($symLinkCorrect) { 86 | return; 87 | } 88 | $this->fileSystem->remove($map->getAbsoluteDestination()); 89 | } 90 | 91 | // If file exists and force is not specified, throw exception unless FORCE is set 92 | // existing symlinks are already handled 93 | if (file_exists($map->getAbsoluteDestination())) { 94 | if (!$force) { 95 | throw new TargetExistsException($map->getAbsoluteDestination()); 96 | } 97 | $this->fileSystem->remove($map->getAbsoluteDestination()); 98 | } 99 | 100 | $this->fileSystem->createSymlink($map->getAbsoluteSource(), $map->getAbsoluteDestination()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstalledPackage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InstalledPackage 13 | { 14 | /** 15 | * @var string 16 | */ 17 | protected $name; 18 | 19 | /** 20 | * @var string 21 | */ 22 | protected $version; 23 | 24 | /** 25 | * @var MapCollection 26 | */ 27 | protected $mappings; 28 | 29 | /** 30 | * @param string $name 31 | * @param string $version 32 | * @param MapCollection $mappings 33 | */ 34 | public function __construct($name, $version, MapCollection $mappings) 35 | { 36 | $this->name = $name; 37 | $this->version = $version; 38 | $this->mappings = $mappings; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getName() 45 | { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getVersion() 53 | { 54 | return $this->version; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getUniqueName() 61 | { 62 | return sprintf('%s-%s', $this->getName(), $this->getVersion()); 63 | } 64 | 65 | /** 66 | * @return MapCollection 67 | */ 68 | public function getMappings() 69 | { 70 | return $this->mappings; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstalledPackageDumper.php: -------------------------------------------------------------------------------- 1 | $installedPackage->getName(), 22 | 'version' => $installedPackage->getVersion(), 23 | 'mappings' => $this->dumpMappings($installedPackage->getMappings()), 24 | ]; 25 | } 26 | 27 | /** 28 | * @param array $data 29 | * @return InstalledPackage 30 | */ 31 | public function restore(array $data) 32 | { 33 | return new InstalledPackage($data['packageName'], $data['version'], $this->restoreMappings($data['mappings'])); 34 | } 35 | 36 | /** 37 | * @param MapCollection $mappings 38 | * 39 | * @return array 40 | */ 41 | private function dumpMappings(MapCollection $mappings) 42 | { 43 | return array_map( 44 | function (Map $map) { 45 | return [ 46 | 'source' => $map->getSource(), 47 | 'destination' => $map->getDestination(), 48 | 'source_root' => $map->getSourceRoot(), 49 | 'destination_root' => $map->getDestinationRoot() 50 | ]; 51 | }, 52 | $mappings->all() 53 | ); 54 | } 55 | 56 | /** 57 | * @param array $mappings 58 | * 59 | * @return MapCollection 60 | */ 61 | private function restoreMappings(array $mappings) 62 | { 63 | return new MapCollection(array_map( 64 | function (array $row) { 65 | return new Map( 66 | $row['source'], 67 | $row['destination'], 68 | $row['source_root'], 69 | $row['destination_root'] 70 | ); 71 | }, 72 | $mappings 73 | )); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Installer/GlobResolver.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class GlobResolver 19 | { 20 | 21 | /** 22 | * Resolve glob mappings to file -> file mappings 23 | * 24 | * @param MapCollection $maps 25 | * 26 | * @return MapCollection 27 | */ 28 | public function resolve(MapCollection $maps) 29 | { 30 | 31 | $updatedMappings = []; 32 | foreach ($maps as $mapping) { 33 | /** @var Map $mapping */ 34 | if (file_exists($mapping->getAbsoluteSource())) { 35 | //file is a file, we don't care about this 36 | $updatedMappings[] = $mapping; 37 | continue; 38 | } 39 | 40 | //not a file, is it a glob? 41 | $iterator = new GlobIterator($mapping->getAbsoluteSource()); 42 | 43 | try { 44 | if (!$iterator->count()) { 45 | //we just skip this if there are no results 46 | //if there are no results should we just remove this mapping? 47 | //it kind of makes sense to as it is not necessarily an error 48 | //if a glob produces no results 49 | $updatedMappings[] = $mapping; 50 | continue; 51 | } 52 | } catch (LogicException $e) { 53 | /** 54 | * This a PHP bug where a LogicException is thrown if no files exist 55 | * @link https://bugs.php.net/bug.php?id=55701 56 | */ 57 | $updatedMappings[] = $mapping; 58 | continue; 59 | } 60 | 61 | //add each glob as a separate mapping 62 | foreach ($iterator as $globResult) { 63 | $updatedMappings[] = $this->processMapping($globResult, $mapping); 64 | } 65 | } 66 | 67 | return new MapCollection($updatedMappings); 68 | } 69 | 70 | /** 71 | * @param \SplFileInfo $globMatch 72 | * @param Map $map 73 | * 74 | * @return Map 75 | */ 76 | protected function processMapping(\SplFileInfo $globMatch, Map $map) 77 | { 78 | $absolutePath = $globMatch->getPathname(); 79 | 80 | //get the relative path to this file/dir - strip of the source path 81 | //+1 to strip leading slash 82 | $source = substr($absolutePath, strlen($map->getSourceRoot()) + 1); 83 | $destination = sprintf('%s/%s', $map->getDestination(), $globMatch->getFilename()); 84 | 85 | return new Map($source, $destination, $map->getSourceRoot(), $map->getDestinationRoot()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Installer/Installer.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Installer implements InstallerInterface 22 | { 23 | 24 | /** 25 | * @var InstallStrategyFactory 26 | */ 27 | protected $installStrategyFactory; 28 | 29 | /** 30 | * @var FileSystem 31 | */ 32 | protected $fileSystem; 33 | 34 | /** 35 | * @var ProjectConfig 36 | */ 37 | protected $config; 38 | 39 | /** 40 | * @var GlobResolver 41 | */ 42 | protected $globResolver; 43 | 44 | /** 45 | * @var TargetFilter 46 | */ 47 | protected $targetFilter; 48 | 49 | /** 50 | * @var Parser 51 | */ 52 | protected $parser; 53 | 54 | /** 55 | * @var EventManager 56 | */ 57 | protected $eventManager; 58 | 59 | /** 60 | * @param InstallStrategyFactory $installStrategyFactory 61 | * @param FileSystem $fileSystem 62 | * @param ProjectConfig $config 63 | * @param GlobResolver $globResolver 64 | * @param TargetFilter $targetFilter 65 | * @param Parser $parser 66 | * @param EventManager $eventManager 67 | */ 68 | public function __construct( 69 | InstallStrategyFactory $installStrategyFactory, 70 | FileSystem $fileSystem, 71 | ProjectConfig $config, 72 | GlobResolver $globResolver, 73 | TargetFilter $targetFilter, 74 | Parser $parser, 75 | EventManager $eventManager 76 | ) { 77 | $this->installStrategyFactory = $installStrategyFactory; 78 | $this->fileSystem = $fileSystem; 79 | $this->config = $config; 80 | $this->globResolver = $globResolver; 81 | $this->targetFilter = $targetFilter; 82 | $this->parser = $parser; 83 | $this->eventManager = $eventManager; 84 | } 85 | 86 | /** 87 | * Delegate installation to the particular strategy 88 | * 89 | * @param PackageInterface $package 90 | * @param string $packageSourceDirectory 91 | * @return MapCollection 92 | * @throws \ErrorException 93 | */ 94 | public function install(PackageInterface $package, $packageSourceDirectory) 95 | { 96 | $installStrategy = $this->installStrategyFactory->make($package); 97 | $force = $this->config->getMagentoForceByPackageName($package->getName()); 98 | 99 | $mappings = $this->parser->getMappings( 100 | $package, 101 | $packageSourceDirectory, 102 | $this->config->getMagentoRootDir() 103 | ); 104 | 105 | //lets expand glob mappings first 106 | $mappings = $this->globResolver->resolve($mappings); 107 | 108 | $this->createMissingDirectories($mappings); 109 | 110 | $mappings = $this->resolveMappings($mappings, $installStrategy); 111 | $mappings = $this->removeIgnoredMappings($package, $mappings); 112 | $mappings = $this->removeNonExistingSourceMappings($mappings); 113 | 114 | foreach ($mappings as $map) { 115 | /** @var Map $map */ 116 | $this->fileSystem->ensureDirectoryExists(dirname($map->getAbsoluteDestination())); 117 | try { 118 | $installStrategy->create($map, $force); 119 | } catch (TargetExistsException $e) { 120 | //dispath event so console can log? 121 | //re-throw for now 122 | throw $e; 123 | } 124 | } 125 | 126 | return $mappings; 127 | } 128 | 129 | /** 130 | * If raw Map destination ends with a directory separator, 131 | * source is not a directory and the destination file does not exist 132 | * create destination s a directory 133 | * 134 | * @param MapCollection $mappings 135 | */ 136 | protected function createMissingDirectories(MapCollection $mappings) 137 | { 138 | foreach ($mappings as $map) { 139 | /** @var Map $map */ 140 | 141 | // Create target directory if it ends with a directory separator 142 | if ($this->fileSystem->endsWithDirectorySeparator($map->getRawDestination()) 143 | && !is_dir($map->getAbsoluteSource()) 144 | && !file_exists($map->getAbsoluteDestination()) 145 | ) { 146 | $this->fileSystem->ensureDirectoryExists($map->getAbsoluteDestination()); 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * @param MapCollection $mappings 153 | * @param InstallStrategyInterface $installStrategy 154 | * @return MapCollection 155 | */ 156 | protected function resolveMappings(MapCollection $mappings, InstallStrategyInterface $installStrategy) 157 | { 158 | $replacementItems = []; 159 | foreach ($mappings as $map) { 160 | /** @var Map $map */ 161 | $resolvedMappings = $installStrategy->resolve( 162 | $map->getSource(), 163 | $map->getDestination(), 164 | $map->getAbsoluteSource(), 165 | $map->getAbsoluteDestination() 166 | ); 167 | 168 | $maps = array_map( 169 | function (array $mapping) use ($map) { 170 | return new Map($mapping[0], $mapping[1], $map->getSourceRoot(), $map->getDestinationRoot()); 171 | }, 172 | $resolvedMappings 173 | ); 174 | 175 | $replacementItems = array_merge($replacementItems, $maps); 176 | } 177 | 178 | return new MapCollection($replacementItems); 179 | } 180 | 181 | /** 182 | * @param MapCollection $mappings 183 | * @return MapCollection 184 | */ 185 | protected function removeNonExistingSourceMappings(MapCollection $mappings) 186 | { 187 | //remove mappings where source doesn't exist 188 | return $mappings->filter(function (Map $map) { 189 | //throw exceptions for missing source? 190 | //trigger event and log? 191 | return file_exists($map->getAbsoluteSource()); 192 | }); 193 | } 194 | 195 | /** 196 | * @param PackageInterface $package 197 | * @param MapCollection $mappings 198 | * @return MapCollection 199 | */ 200 | protected function removeIgnoredMappings(PackageInterface $package, MapCollection $mappings) 201 | { 202 | $targetFilter = $this->targetFilter; 203 | return $mappings->filter(function (Map $map) use ($package, $targetFilter) { 204 | return !$targetFilter->isTargetIgnored($package, $map->getDestination()); 205 | }); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Installer/InstallerInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface InstallerInterface 14 | { 15 | /** 16 | * @param PackageInterface $package 17 | * @param string $packageSourceDirectory 18 | * 19 | * @return MapCollection 20 | */ 21 | public function install(PackageInterface $package, $packageSourceDirectory); 22 | } 23 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Installer/TargetFilter.php: -------------------------------------------------------------------------------- 1 | ignores = $ignores; 24 | } 25 | 26 | /** 27 | * @param PackageInterface $package 28 | * @param string $target 29 | * @return bool 30 | */ 31 | public function isTargetIgnored(PackageInterface $package, $target) 32 | { 33 | $moduleSpecificDeployIgnores = []; 34 | 35 | if (isset($this->ignores['*'])) { 36 | $moduleSpecificDeployIgnores = $this->ignores['*']; 37 | } 38 | 39 | if (isset($this->ignores[$package->getName()])) { 40 | $moduleSpecificDeployIgnores = array_merge( 41 | $moduleSpecificDeployIgnores, 42 | $this->ignores[$package->getName()] 43 | ); 44 | } 45 | 46 | //prepend all ignores with '/' if they do not have it already 47 | $moduleSpecificDeployIgnores = array_map( 48 | function ($ignore) { 49 | return sprintf('/%s', ltrim($ignore, '\\/')); 50 | }, 51 | $moduleSpecificDeployIgnores 52 | ); 53 | 54 | $target = sprintf('/%s', ltrim($target, '\\/')); 55 | $target = str_replace('/./', '/', $target); 56 | $target = str_replace('//', '/', $target); 57 | 58 | return count( 59 | array_filter( 60 | $moduleSpecificDeployIgnores, 61 | function ($ignore) use ($target) { 62 | return $target === $ignore; 63 | } 64 | ) 65 | ) > 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Listener/CheckAndCreateMagentoRootDirListener.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class CheckAndCreateMagentoRootDirListener 13 | { 14 | 15 | /** 16 | * @var string 17 | */ 18 | protected $magentoRootDir; 19 | 20 | /** 21 | * @param string $magentoRootDir 22 | */ 23 | public function __construct($magentoRootDir) 24 | { 25 | $this->magentoRootDir = $magentoRootDir; 26 | } 27 | 28 | /** 29 | * @param InstallEvent $event 30 | */ 31 | public function __invoke(InstallEvent $event) 32 | { 33 | if (!file_exists($this->magentoRootDir)) { 34 | mkdir($this->magentoRootDir); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Listener/GitIgnoreListener.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class GitIgnoreListener 18 | { 19 | 20 | /** 21 | * @var GitIgnore 22 | */ 23 | protected $gitIgnore; 24 | 25 | /** 26 | * @param GitIgnore $gitIgnore 27 | */ 28 | public function __construct(GitIgnore $gitIgnore) 29 | { 30 | $this->gitIgnore = $gitIgnore; 31 | } 32 | 33 | /** 34 | * Add any files which were installed to the .gitignore 35 | * 36 | * @param PackagePostInstallEvent $e 37 | */ 38 | public function addNewInstalledFiles(PackagePostInstallEvent $e) 39 | { 40 | $files = $this->processMappings($e->getMappings()); 41 | $this->gitIgnore->addMultipleEntries($files); 42 | $this->gitIgnore->write(); 43 | } 44 | 45 | /** 46 | * Remove any files which were removed to the .gitignore 47 | * 48 | * @param PackageUnInstallEvent $e 49 | */ 50 | public function removeUnInstalledFiles(PackageUnInstallEvent $e) 51 | { 52 | $files = $this->processMappings($e->getMappings()); 53 | $this->gitIgnore->removeMultipleEntries($files); 54 | $this->gitIgnore->write(); 55 | } 56 | 57 | /** 58 | * @param MapCollection $maps 59 | * @return array 60 | */ 61 | private function processMappings(MapCollection $maps) 62 | { 63 | return array_map( 64 | function (Map $map) { 65 | return sprintf('/%s', $map->getDestination()); 66 | }, 67 | $maps->all() 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Listener/PackagePrioritySortListener.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class PackagePrioritySortListener 16 | { 17 | /** 18 | * @var InstallStrategyFactory 19 | */ 20 | protected $installStrategyFactory; 21 | 22 | /** 23 | * @var ProjectConfig 24 | */ 25 | protected $config; 26 | 27 | /** 28 | * @param InstallStrategyFactory $installStrategyFactory 29 | * @param ProjectConfig $config 30 | */ 31 | public function __construct( 32 | InstallStrategyFactory $installStrategyFactory, 33 | ProjectConfig $config 34 | ) { 35 | 36 | $this->installStrategyFactory = $installStrategyFactory; 37 | $this->config = $config; 38 | } 39 | 40 | /** 41 | * @param InstallEvent $event 42 | */ 43 | public function __invoke(InstallEvent $event) 44 | { 45 | $packagesToInstall = $event->getPackages(); 46 | $userPriorities = $this->config->getSortPriorities(); 47 | $priorities = []; 48 | foreach ($packagesToInstall as $package) { 49 | /** @var PackageInterface $package */ 50 | if (isset($userPriorities[$package->getName()])) { 51 | $priority = $userPriorities[$package->getName()]; 52 | } else { 53 | $priority = $this->installStrategyFactory->getDefaultPriority($package); 54 | } 55 | 56 | $priorities[$package->getName()] = $priority; 57 | } 58 | 59 | $packagesToInstall->uasort( 60 | function (PackageInterface $a, PackageInterface $b) use ($priorities) { 61 | $aVal = $priorities[$a->getName()]; 62 | $bVal = $priorities[$b->getName()]; 63 | 64 | if ($aVal === $bVal) { 65 | return 0; 66 | } 67 | return ($aVal > $bVal) ? -1 : 1; 68 | } 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Map/Map.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class Map 11 | { 12 | /** 13 | * @var string 14 | */ 15 | protected $source; 16 | 17 | /** 18 | * @var string 19 | */ 20 | protected $destination; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $sourceRoot; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $destinationRoot; 31 | 32 | /** 33 | * @param string $source 34 | * @param string $destination 35 | * @param string $sourceRoot 36 | * @param string $destinationRoot 37 | */ 38 | public function __construct($source, $destination, $sourceRoot, $destinationRoot) 39 | { 40 | $this->source = $source; 41 | $this->destination = $destination; 42 | $this->sourceRoot = $sourceRoot; 43 | $this->destinationRoot = $destinationRoot; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getRawSource() 50 | { 51 | return $this->source; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getRawDestination() 58 | { 59 | return $this->destination; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getSource() 66 | { 67 | return trim($this->source, '\\/'); 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getDestination() 74 | { 75 | return trim($this->destination, '\\/'); 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getAbsoluteSource() 82 | { 83 | return sprintf('%s/%s', rtrim($this->sourceRoot, '\\/'), $this->getSource()); 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | public function getAbsoluteDestination() 90 | { 91 | return sprintf('%s/%s', rtrim($this->destinationRoot, '\\/'), $this->getDestination()); 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getSourceRoot() 98 | { 99 | return rtrim($this->sourceRoot, '\\/'); 100 | } 101 | 102 | /** 103 | * @return string 104 | */ 105 | public function getDestinationRoot() 106 | { 107 | return rtrim($this->destinationRoot, '\\/'); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Map/MapCollection.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MapCollection implements IteratorAggregate, Countable 15 | { 16 | 17 | /** 18 | * @var Map[] 19 | */ 20 | protected $maps; 21 | 22 | /** 23 | * @param array $maps 24 | */ 25 | public function __construct(array $maps) 26 | { 27 | //enforce type safety 28 | array_map(function ($map) { 29 | if (!$map instanceof Map) { 30 | throw new \InvalidArgumentException('Input must be an array of "Map"'); 31 | } 32 | }, $maps); 33 | $this->maps = $maps; 34 | } 35 | 36 | /** 37 | * @return ArrayIterator 38 | */ 39 | public function getIterator() 40 | { 41 | return new ArrayIterator($this->maps); 42 | } 43 | 44 | /** 45 | * @return Map[] 46 | */ 47 | public function all() 48 | { 49 | return $this->maps; 50 | } 51 | 52 | /** 53 | * @return int 54 | */ 55 | public function count() 56 | { 57 | return count($this->maps); 58 | } 59 | 60 | /** 61 | * @param Map $mapToRemove 62 | * 63 | * @return static 64 | */ 65 | public function remove(Map $mapToRemove) 66 | { 67 | return new static(array_values(array_filter( 68 | $this->maps, 69 | function (Map $map) use ($mapToRemove) { 70 | return $mapToRemove !== $map; 71 | } 72 | ))); 73 | } 74 | 75 | /** 76 | * @param Map $mapToReplace 77 | * @param Map[] $replacementMaps 78 | * 79 | * @return static 80 | */ 81 | public function replace(Map $mapToReplace, array $replacementMaps) 82 | { 83 | //enforce type safety 84 | array_map(function ($map) { 85 | if (!$map instanceof Map) { 86 | throw new \InvalidArgumentException('Input must be an array of "Map"'); 87 | } 88 | }, $replacementMaps); 89 | 90 | $key = array_search($mapToReplace, $this->maps); 91 | if (false === $key) { 92 | throw new \InvalidArgumentException('Map does not belong to this collection'); 93 | } 94 | 95 | $maps = $this->maps; 96 | array_splice($maps, $key, 1, $replacementMaps); 97 | return new static($maps); 98 | } 99 | 100 | /** 101 | * @param $callback 102 | * @return static 103 | */ 104 | public function filter($callback) 105 | { 106 | if (!is_callable($callback)) { 107 | throw new \InvalidArgumentException( 108 | sprintf('Expected callable, got "%s"', is_object($callback) ? get_class($callback) : gettype($callback)) 109 | ); 110 | } 111 | 112 | return new static(array_filter($this->maps, $callback)); 113 | } 114 | 115 | /** 116 | * @return array 117 | */ 118 | public function getAllDestinations() 119 | { 120 | return array_map( 121 | function (Map $map) { 122 | return $map->getAbsoluteDestination(); 123 | }, 124 | $this->maps 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/ModuleManager.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ModuleManager 23 | { 24 | /** 25 | * @var InstalledPackageRepositoryInterface 26 | */ 27 | protected $installedPackageRepository; 28 | 29 | /** 30 | * @var EventManager 31 | */ 32 | protected $eventManager; 33 | 34 | /** 35 | * @var ProjectConfig 36 | */ 37 | protected $config; 38 | 39 | /** 40 | * @var UnInstallStrategyInterface 41 | */ 42 | protected $unInstallStrategy; 43 | 44 | /** 45 | * @var InstallerInterface 46 | */ 47 | protected $installer; 48 | 49 | /** 50 | * @var InstallStrategyFactory 51 | */ 52 | protected $installStrategyFactory; 53 | 54 | /** 55 | * @param InstalledPackageRepositoryInterface $installedRepository 56 | * @param EventManager $eventManager 57 | * @param ProjectConfig $config 58 | * @param UnInstallStrategyInterface $unInstallStrategy 59 | * @param InstallerInterface $installer 60 | * @param InstallStrategyFactory $installStrategyFactory 61 | */ 62 | public function __construct( 63 | InstalledPackageRepositoryInterface $installedRepository, 64 | EventManager $eventManager, 65 | ProjectConfig $config, 66 | UnInstallStrategyInterface $unInstallStrategy, 67 | InstallerInterface $installer, 68 | InstallStrategyFactory $installStrategyFactory 69 | ) { 70 | $this->installedPackageRepository = $installedRepository; 71 | $this->eventManager = $eventManager; 72 | $this->config = $config; 73 | $this->unInstallStrategy = $unInstallStrategy; 74 | $this->installer = $installer; 75 | $this->installStrategyFactory = $installStrategyFactory; 76 | } 77 | 78 | /** 79 | * @param array $currentComposerInstalledPackages 80 | * @return array 81 | */ 82 | public function updateInstalledPackages(array $currentComposerInstalledPackages) 83 | { 84 | $packagesToRemove = $this->getRemoves( 85 | $currentComposerInstalledPackages, 86 | $this->installedPackageRepository->findAll() 87 | ); 88 | 89 | $packagesToInstall = $this->getInstalls( 90 | $currentComposerInstalledPackages 91 | ); 92 | 93 | $this->doRemoves($packagesToRemove); 94 | $this->doInstalls($packagesToInstall); 95 | } 96 | 97 | /** 98 | * @param ArrayObject $packagesToRemove 99 | */ 100 | public function doRemoves(ArrayObject $packagesToRemove) 101 | { 102 | foreach ($packagesToRemove as $package) { 103 | $mappings = $package->getMappings(); 104 | 105 | $this->eventManager->dispatch( 106 | new PackageUnInstallEvent('package-pre-uninstall', $package, $mappings) 107 | ); 108 | 109 | $this->unInstallStrategy->unInstall($mappings); 110 | $this->installedPackageRepository->remove($package); 111 | 112 | $this->eventManager->dispatch( 113 | new PackageUnInstallEvent('package-post-uninstall', $package, $mappings) 114 | ); 115 | } 116 | } 117 | 118 | /** 119 | * @param ArrayObject $packagesToInstall 120 | */ 121 | protected function doInstalls(ArrayObject $packagesToInstall) 122 | { 123 | $this->eventManager->dispatch(new InstallEvent('pre-install', $packagesToInstall)); 124 | 125 | foreach ($packagesToInstall as $package) { 126 | $this->eventManager->dispatch(new PackagePreInstallEvent($package)); 127 | 128 | $mappings = $this->installer->install($package, $this->getPackageSourceDirectory($package)); 129 | 130 | $installedPackaged = new InstalledPackage( 131 | $package->getName(), 132 | $package->getVersion(), 133 | $mappings 134 | ); 135 | 136 | $this->installedPackageRepository->add($installedPackaged); 137 | 138 | $this->eventManager->dispatch(new PackagePostInstallEvent($package, $installedPackaged)); 139 | } 140 | 141 | $this->eventManager->dispatch(new InstallEvent('post-install', $packagesToInstall)); 142 | } 143 | 144 | /** 145 | * @param PackageInterface[] $currentComposerInstalledPackages 146 | * @param InstalledPackage[] $magentoInstalledPackages 147 | * @return ArrayObject 148 | */ 149 | protected function getRemoves(array $currentComposerInstalledPackages, array $magentoInstalledPackages) 150 | { 151 | //make the package names as the array keys 152 | $currentComposerInstalledPackages = array_combine( 153 | array_map( 154 | function (PackageInterface $package) { 155 | return $package->getName(); 156 | }, 157 | $currentComposerInstalledPackages 158 | ), 159 | $currentComposerInstalledPackages 160 | ); 161 | 162 | return new ArrayObject( 163 | array_filter( 164 | $magentoInstalledPackages, 165 | function (InstalledPackage $package) use ($currentComposerInstalledPackages) { 166 | if (!isset($currentComposerInstalledPackages[$package->getName()])) { 167 | return true; 168 | } 169 | 170 | $composerPackage = $currentComposerInstalledPackages[$package->getName()]; 171 | return $package->getUniqueName() !== $composerPackage->getUniqueName(); 172 | } 173 | ) 174 | ); 175 | } 176 | 177 | /** 178 | * @param PackageInterface[] $currentComposerInstalledPackages 179 | * @return ArrayObject 180 | */ 181 | protected function getInstalls(array $currentComposerInstalledPackages) 182 | { 183 | $repo = $this->installedPackageRepository; 184 | return new ArrayObject( 185 | array_filter($currentComposerInstalledPackages, function (PackageInterface $package) use ($repo) { 186 | return !$repo->has($package->getName(), $package->getVersion()); 187 | }) 188 | ); 189 | } 190 | 191 | /** 192 | * @param PackageInterface $package 193 | * @return string 194 | */ 195 | private function getPackageSourceDirectory(PackageInterface $package) 196 | { 197 | $path = sprintf("%s/%s", realpath($this->config->getVendorDir()), $package->getPrettyName()); 198 | $targetDir = $package->getTargetDir(); 199 | 200 | if ($targetDir) { 201 | $path = sprintf("%s/%s", $path, $targetDir); 202 | } 203 | 204 | return $path; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/MapParser.php: -------------------------------------------------------------------------------- 1 | mappings = $mappings; 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function getMappings() 32 | { 33 | return $this->mappings; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/ModmanParser.php: -------------------------------------------------------------------------------- 1 | file = new \SplFileObject($modManFile); 26 | } 27 | 28 | /** 29 | * @return array 30 | * @throws \ErrorException 31 | */ 32 | public function getMappings() 33 | { 34 | if (!$this->file->isReadable()) { 35 | throw new \ErrorException(sprintf('modman file "%s" not readable', $this->file->getPathname())); 36 | } 37 | 38 | $map = $this->parseMappings(); 39 | return $map; 40 | } 41 | 42 | /** 43 | * @throws \ErrorException 44 | * @return array 45 | */ 46 | protected function parseMappings() 47 | { 48 | $map = []; 49 | foreach ($this->file as $line => $row) { 50 | $row = trim($row); 51 | if ('' === $row || in_array($row[0], ['#', '@'])) { 52 | continue; 53 | } 54 | $parts = preg_split('/\s+/', $row, 2, PREG_SPLIT_NO_EMPTY); 55 | if (count($parts) != 2) { 56 | throw new \ErrorException( 57 | sprintf('Invalid row on line %d has %d parts, expected 2', $line, count($row)) 58 | ); 59 | } 60 | $map[] = $parts; 61 | } 62 | return $map; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/PackageXmlParser.php: -------------------------------------------------------------------------------- 1 | file = new \SplFileObject($packageXmlFile); 33 | } 34 | 35 | /** 36 | * @return array 37 | * @throws \ErrorException 38 | */ 39 | public function getMappings() 40 | { 41 | if (!$this->file->isReadable()) { 42 | throw new \ErrorException(sprintf('Package file "%s" not readable', $this->file->getPathname())); 43 | } 44 | 45 | $map = $this->parseMappings(); 46 | return $map; 47 | } 48 | 49 | /** 50 | * @throws \RuntimeException 51 | * @return array 52 | */ 53 | protected function parseMappings() 54 | { 55 | $map = []; 56 | 57 | /** @var $package SimpleXMLElement */ 58 | $package = simplexml_load_file($this->file->getPathname()); 59 | if (isset($package)) { 60 | foreach ($package->xpath('//contents/target') as $target) { 61 | try { 62 | $basePath = $this->getTargetPath($target); 63 | 64 | foreach ($target->children() as $child) { 65 | foreach ($this->getElementPaths($child) as $elementPath) { 66 | $relativePath = $basePath . '/' . $elementPath; 67 | $map[] = [$relativePath, $relativePath]; 68 | } 69 | } 70 | 71 | } catch (\RuntimeException $e) { 72 | // Skip invalid targets 73 | continue; 74 | } 75 | } 76 | } 77 | return $map; 78 | } 79 | 80 | /** 81 | * @param \SimpleXMLElement $target 82 | * @return string 83 | * @throws \RuntimeException 84 | */ 85 | protected function getTargetPath(\SimpleXMLElement $target) 86 | { 87 | $name = (string) $target->attributes()->name; 88 | $targets = $this->getTargetsDefinitions(); 89 | if (! isset($targets[$name])) { 90 | throw new \RuntimeException('Invalid target type ' . $name); 91 | } 92 | return $targets[$name]; 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | protected function getTargetsDefinitions() 99 | { 100 | if (!$this->targets) { 101 | $targets = simplexml_load_file(__DIR__ . '/../../../../../res/target.xml'); 102 | foreach ($targets as $target) { 103 | $attributes = $target->attributes(); 104 | $this->targets["{$attributes->name}"] = "{$attributes->uri}"; 105 | } 106 | } 107 | return $this->targets; 108 | } 109 | 110 | /** 111 | * @param \SimpleXMLElement $element 112 | * @return array 113 | * @throws \RuntimeException 114 | */ 115 | protected function getElementPaths(\SimpleXMLElement $element) 116 | { 117 | $type = $element->getName(); 118 | $name = (string) $element->attributes()->name; 119 | $elementPaths = []; 120 | 121 | switch ($type) { 122 | case 'dir': 123 | if ($element->count()) { 124 | foreach ($element->children() as $child) { 125 | foreach ($this->getElementPaths($child) as $elementPath) { 126 | $elementPaths[] = $name == '.' ? $elementPath : $name . '/' . $elementPath; 127 | } 128 | } 129 | } else { 130 | $elementPaths[] = $name; 131 | } 132 | break; 133 | 134 | case 'file': 135 | $elementPaths[] = $name; 136 | break; 137 | 138 | default: 139 | throw new \RuntimeException('Unknown path type: ' . $type); 140 | } 141 | 142 | return $elementPaths; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/Parser.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Parser 16 | { 17 | 18 | /** 19 | * @var ParserFactoryInterface 20 | */ 21 | protected $parserFactory; 22 | 23 | /** 24 | * @param ParserFactoryInterface $parserFactory 25 | */ 26 | public function __construct(ParserFactoryInterface $parserFactory) 27 | { 28 | $this->parserFactory = $parserFactory; 29 | } 30 | 31 | /** 32 | * @param PackageInterface $package 33 | * @param string $packageSourceDirectory 34 | * @param string $installDirectory 35 | * 36 | * @return MapCollection 37 | */ 38 | public function getMappings(PackageInterface $package, $packageSourceDirectory, $installDirectory) 39 | { 40 | $mapParser = $this->parserFactory->make($package, $packageSourceDirectory); 41 | 42 | $maps = array_map( 43 | function (array $map) use ($packageSourceDirectory, $installDirectory) { 44 | return new Map($map[0], $map[1], $packageSourceDirectory, $installDirectory); 45 | }, 46 | $mapParser->getMappings() 47 | ); 48 | 49 | return new MapCollection($maps); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/ParserInterface.php: -------------------------------------------------------------------------------- 1 | pathPrefixTranslations = $this->createPrefixVariants($translations); 40 | $this->parser = $parser; 41 | } 42 | 43 | /** 44 | * Given an array of path mapping translations, combine them with a list 45 | * of starting variations. This is so that a translation for 'js' will 46 | * also match path mappings beginning with './js'. 47 | * 48 | * @param $translations 49 | * @return array 50 | */ 51 | protected function createPrefixVariants($translations) 52 | { 53 | $newTranslations = []; 54 | foreach ($translations as $key => $value) { 55 | foreach ($this->pathPrefixVariants as $variant) { 56 | $newTranslations[$variant . $key] = $value; 57 | } 58 | } 59 | 60 | return $newTranslations; 61 | } 62 | 63 | /** 64 | * loop the mappings for the wrapped parser, check if any of the targets are for 65 | * directories that have been moved under the public directory. If so, 66 | * update the target paths to include 'public/'. As no standard Magento 67 | * path mappings should ever start with 'public/', and path mappings 68 | * that already include the public directory should always have 69 | * js/skin/media paths starting with 'public/', it should be safe to call 70 | * multiple times on either. 71 | * 72 | * @return array Updated path mappings 73 | */ 74 | public function getMappings() 75 | { 76 | $translatedMappings = []; 77 | foreach ($this->parser->getMappings() as $index => $mapping) { 78 | $translatedMappings[$index] = $mapping; 79 | foreach ($this->pathPrefixTranslations as $prefix => $translate) { 80 | if (strpos($mapping[1], $prefix) === 0) { 81 | // replace the old prefix with the translated version 82 | $translatedMappings[$index][1] = $translate . substr($mapping[1], strlen($prefix)); 83 | // should never need to translate a prefix more than once 84 | // per path mapping 85 | break; 86 | } 87 | } 88 | } 89 | 90 | return $translatedMappings; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Plugin.php: -------------------------------------------------------------------------------- 1 | io = $io; 76 | $this->composer = $composer; 77 | $composerConfig = $composer->getConfig()->all(); 78 | $this->config = new ProjectConfig($composer->getPackage()->getExtra(), $composerConfig['config']); 79 | $this->eventManager = new EventManager; 80 | $moduleManagerFactory = new ModuleManagerFactory; 81 | $this->moduleManager = $moduleManagerFactory->make($this->config, $this->eventManager, $io); 82 | 83 | if ($io->isDebug()) { 84 | $io->write('Activate Magento plugin'); 85 | } 86 | } 87 | 88 | /** 89 | * @return array 90 | */ 91 | public static function getSubscribedEvents() 92 | { 93 | return [ 94 | ScriptEvents::POST_INSTALL_CMD => [ 95 | ['onNewCodeEvent', 0], 96 | ], 97 | ScriptEvents::POST_UPDATE_CMD => [ 98 | ['onNewCodeEvent', 0], 99 | ], 100 | ]; 101 | } 102 | 103 | /** 104 | * This event is triggered after installing or updating composer 105 | * 106 | * @param CommandEvent $event 107 | */ 108 | public function onNewCodeEvent(CommandEvent $event) 109 | { 110 | $magentoModules = array_filter( 111 | $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(), 112 | function (PackageInterface $package) { 113 | return $package->getType() === static::PACKAGE_TYPE; 114 | } 115 | ); 116 | 117 | $this->moduleManager->updateInstalledPackages($magentoModules); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/ProjectConfig.php: -------------------------------------------------------------------------------- 1 | extra = $extra; 55 | $this->composerConfig = $composerConfig; 56 | } 57 | 58 | /** 59 | * @param array $array 60 | * @param string $key 61 | * @param mixed $default 62 | * 63 | * @return mixed 64 | */ 65 | protected function fetchVarFromConfigArray(array $array, $key, $default = null) 66 | { 67 | $result = $default; 68 | if (isset($array[$key])) { 69 | $result = $array[$key]; 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | /** 76 | * @param string $key 77 | * @param mixed $default 78 | * 79 | * @return mixed 80 | */ 81 | protected function fetchVarFromExtraConfig($key, $default = null) 82 | { 83 | return $this->fetchVarFromConfigArray($this->extra, $key, $default); 84 | } 85 | 86 | /** 87 | * @param bool $realPath 88 | * @return string 89 | */ 90 | public function getMagentoRootDir($realPath = true) 91 | { 92 | $rootDir = rtrim( 93 | trim( 94 | $this->fetchVarFromExtraConfig( 95 | self::MAGENTO_ROOT_DIR_KEY, 96 | self::DEFAULT_MAGENTO_ROOT_DIR 97 | ) 98 | ), 99 | DIRECTORY_SEPARATOR 100 | ); 101 | 102 | if ($realPath) { 103 | return realpath($rootDir); 104 | } 105 | 106 | return $rootDir; 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function getDeployStrategy() 113 | { 114 | return trim((string) $this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_STRATEGY_KEY)); 115 | } 116 | 117 | /** 118 | * @return bool 119 | */ 120 | public function hasDeployStrategy() 121 | { 122 | return $this->hasExtraField(self::MAGENTO_DEPLOY_STRATEGY_KEY); 123 | } 124 | 125 | /** 126 | * @return array 127 | */ 128 | public function getDeployStrategyOverwrite() 129 | { 130 | return $this->transformArrayKeysToLowerCase( 131 | $this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_STRATEGY_OVERWRITE_KEY, []) 132 | ); 133 | } 134 | 135 | /** 136 | * @return bool 137 | */ 138 | public function hasDeployStrategyOverwrite() 139 | { 140 | return $this->hasExtraField(self::MAGENTO_DEPLOY_STRATEGY_OVERWRITE_KEY); 141 | } 142 | 143 | /** 144 | * @return array 145 | */ 146 | public function getMagentoDeployIgnore() 147 | { 148 | return $this->transformArrayKeysToLowerCase( 149 | $this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_IGNORE_KEY, []) 150 | ); 151 | } 152 | 153 | /** 154 | * @return bool 155 | */ 156 | public function hasMagentoDeployIgnore() 157 | { 158 | return $this->hasExtraField(self::MAGENTO_DEPLOY_IGNORE_KEY); 159 | } 160 | 161 | /** 162 | * @return bool 163 | */ 164 | public function getMagentoForce() 165 | { 166 | return (bool) $this->fetchVarFromExtraConfig(self::MAGENTO_FORCE_KEY); 167 | } 168 | 169 | /** 170 | * @param string $packagename 171 | * @return string 172 | */ 173 | public function getMagentoForceByPackageName($packagename) 174 | { 175 | return $this->getMagentoForce(); 176 | } 177 | 178 | /** 179 | * @return bool 180 | */ 181 | public function hasAutoAppendGitignore() 182 | { 183 | return $this->hasExtraField(self::AUTO_APPEND_GITIGNORE_KEY); 184 | } 185 | 186 | /** 187 | * @return array 188 | */ 189 | public function getPathMappingTranslations() 190 | { 191 | return $this->fetchVarFromExtraConfig(self::PATH_MAPPINGS_TRANSLATIONS_KEY, []); 192 | } 193 | 194 | /** 195 | * @return bool 196 | */ 197 | public function hasPathMappingTranslations() 198 | { 199 | return $this->hasExtraField(self::PATH_MAPPINGS_TRANSLATIONS_KEY); 200 | } 201 | 202 | /** 203 | * @return array 204 | */ 205 | public function getMagentoMapOverwrite() 206 | { 207 | return $this->transformArrayKeysToLowerCase( 208 | $this->fetchVarFromExtraConfig(self::MAGENTO_MAP_OVERWRITE_KEY, []) 209 | ); 210 | } 211 | 212 | /** 213 | * @param string $key 214 | * @return bool 215 | */ 216 | protected function hasExtraField($key) 217 | { 218 | return !is_null($this->fetchVarFromExtraConfig($key)); 219 | } 220 | 221 | /** 222 | * @param array $array 223 | * 224 | * @return array 225 | */ 226 | public function transformArrayKeysToLowerCase(array $array) 227 | { 228 | return array_change_key_case($array, CASE_LOWER); 229 | } 230 | 231 | /** 232 | * Get Composer vendor directory 233 | * 234 | * @return string 235 | */ 236 | public function getVendorDir() 237 | { 238 | return $this->fetchVarFromConfigArray($this->composerConfig, 'vendor-dir'); 239 | } 240 | 241 | /** 242 | * Get Package Sort Order 243 | * 244 | * @return array 245 | */ 246 | public function getSortPriorities() 247 | { 248 | return $this->fetchVarFromConfigArray($this->extra, self::SORT_PRIORITY_KEY, []); 249 | } 250 | 251 | /** 252 | * @return string 253 | */ 254 | public function getModuleRepositoryLocation() 255 | { 256 | $moduleRepoDir = $this->fetchVarFromExtraConfig( 257 | self::MODULE_REPOSITORY_LOCATION_KEY, 258 | $this->fetchVarFromConfigArray( 259 | $this->composerConfig, 260 | 'vendor-dir' 261 | ) 262 | ); 263 | 264 | return sprintf('%s/magento-installed.json', $moduleRepoDir); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Repository/InstalledPackageFileSystemRepository.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class InstalledPackageFileSystemRepository implements InstalledPackageRepositoryInterface 14 | { 15 | 16 | /** 17 | * @var string Path to state file 18 | */ 19 | protected $filePath; 20 | 21 | /** 22 | * @var array 23 | */ 24 | protected $packages = []; 25 | 26 | /** 27 | * @var bool Flag to indicate if we have read the existing file 28 | */ 29 | protected $isLoaded = false; 30 | 31 | /** 32 | * @var bool Flag to indicate if we need to write once we are finished 33 | */ 34 | protected $hasChanges = false; 35 | 36 | /** 37 | * @var InstalledPackageDumper 38 | */ 39 | protected $dumper; 40 | 41 | /** 42 | * If file exists, check its readable 43 | * Check in any case that it's writeable 44 | * 45 | * @param string $filePath 46 | * @param InstalledPackageDumper $dumper 47 | */ 48 | public function __construct($filePath, InstalledPackageDumper $dumper) 49 | { 50 | if (file_exists($filePath) && !is_writable($filePath)) { 51 | throw new \InvalidArgumentException(sprintf('File "%s" is not writable', $filePath)); 52 | } 53 | 54 | if (file_exists($filePath) && !is_readable($filePath)) { 55 | throw new \InvalidArgumentException(sprintf('File "%s" is not readable', $filePath)); 56 | } 57 | 58 | if (!file_exists($filePath) && !is_writable(dirname($filePath))) { 59 | throw new \InvalidArgumentException(sprintf('Directory "%s" is not writable', dirname($filePath))); 60 | } 61 | 62 | $this->filePath = $filePath; 63 | $this->dumper = $dumper; 64 | } 65 | 66 | /** 67 | * @return array 68 | */ 69 | public function findAll() 70 | { 71 | $this->load(); 72 | return $this->packages; 73 | } 74 | 75 | /** 76 | * @param string $packageName 77 | * @return InstalledPackage 78 | * @throws \Exception 79 | */ 80 | public function findByPackageName($packageName) 81 | { 82 | $this->load(); 83 | foreach ($this->packages as $package) { 84 | if ($package->getName() === $packageName) { 85 | return $package; 86 | } 87 | } 88 | 89 | throw new \Exception(sprintf('Package Installed Files for: "%s" not found', $packageName)); 90 | } 91 | 92 | /** 93 | * If version specified, perform a strict check, 94 | * which only returns true if repository has the package in the specified version 95 | * 96 | * @param string $packageName 97 | * @param string $version 98 | * @return bool 99 | */ 100 | public function has($packageName, $version = null) 101 | { 102 | $this->load(); 103 | try { 104 | $package = $this->findByPackageName($packageName); 105 | 106 | if (null === $version) { 107 | return true; 108 | } 109 | 110 | return $package->getVersion() === $version; 111 | } catch (\Exception $e) { 112 | return false; 113 | } 114 | } 115 | 116 | /** 117 | * @param InstalledPackage $package 118 | * @throws \Exception 119 | */ 120 | public function add(InstalledPackage $package) 121 | { 122 | $this->load(); 123 | 124 | try { 125 | $this->findByPackageName($package->getName()); 126 | } catch (\Exception $e) { 127 | $this->packages[] = $package; 128 | $this->hasChanges = true; 129 | return; 130 | } 131 | 132 | throw new \Exception(sprintf('Package: "%s" is already installed', $package->getName())); 133 | } 134 | 135 | /** 136 | * @param InstalledPackage $package 137 | * @throws \Exception 138 | */ 139 | public function remove(InstalledPackage $package) 140 | { 141 | $this->load(); 142 | 143 | foreach ($this->packages as $key => $installedPackage) { 144 | if ($installedPackage->getName() === $package->getName()) { 145 | array_splice($this->packages, $key, 1); 146 | $this->hasChanges = true; 147 | return; 148 | } 149 | } 150 | 151 | throw new \Exception(sprintf('Package: "%s" not found', $package->getName())); 152 | } 153 | 154 | /** 155 | * Load the Mappings File 156 | * 157 | * @return array 158 | */ 159 | private function load() 160 | { 161 | if (!$this->isLoaded && file_exists($this->filePath)) { 162 | $data = json_decode(file_get_contents($this->filePath), true); 163 | 164 | foreach ($data as $installedPackageData) { 165 | $this->packages[] = $this->dumper->restore($installedPackageData); 166 | } 167 | } 168 | 169 | $this->isLoaded = true; 170 | } 171 | 172 | /** 173 | * Do the write on destruct, we shouldn't have to do this manually 174 | * - you don't call save after adding an entry to the database 175 | * and at the same time, do want to perform IO for each package addition/removal. 176 | * 177 | * Also I don't like enforcing the consumer to call save and load. 178 | */ 179 | public function __destruct() 180 | { 181 | if ($this->hasChanges) { 182 | $data = []; 183 | foreach ($this->packages as $installedPackage) { 184 | $data[] = $this->dumper->dump($installedPackage); 185 | } 186 | 187 | $flags = JSON_UNESCAPED_SLASHES; 188 | if (version_compare(PHP_VERSION, '5.4.0') >= 0) { 189 | $flags |= JSON_PRETTY_PRINT; 190 | } 191 | 192 | file_put_contents($this->filePath, json_encode($data, $flags)); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Repository/InstalledPackageRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface InstalledPackageRepositoryInterface 13 | { 14 | /** 15 | * Get all installed packages 16 | * 17 | * @return InstalledPackage[] 18 | */ 19 | public function findAll(); 20 | 21 | /** 22 | * @param string $packageName 23 | * @return InstalledPackage|null 24 | */ 25 | public function findByPackageName($packageName); 26 | 27 | /** 28 | * @param InstalledPackage $package 29 | */ 30 | public function remove(InstalledPackage $package); 31 | 32 | /** 33 | * @param InstalledPackage $package 34 | */ 35 | public function add(InstalledPackage $package); 36 | 37 | /** 38 | * @param string $packageName 39 | * @param string $version 40 | * @return bool 41 | */ 42 | public function has($packageName, $version = null); 43 | } 44 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/UnInstallStrategy/UnInstallStrategy.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class UnInstallStrategy implements UnInstallStrategyInterface 15 | { 16 | 17 | /** 18 | * @var FileSystem 19 | */ 20 | protected $fileSystem; 21 | 22 | /** 23 | * The root dir for uninstalling from. Should be project root. 24 | * 25 | * @var string 26 | */ 27 | protected $rootDir; 28 | 29 | /** 30 | * @param FileSystem $fileSystem 31 | * @param string $rootDir 32 | */ 33 | public function __construct(FileSystem $fileSystem, $rootDir) 34 | { 35 | $this->fileSystem = $fileSystem; 36 | $this->rootDir = $rootDir; 37 | } 38 | 39 | /** 40 | * UnInstall the extension given the list of install files 41 | * 42 | * @param MapCollection $collection 43 | */ 44 | public function unInstall(MapCollection $collection) 45 | { 46 | foreach ($collection as $map) { 47 | /** @var Map $map */ 48 | $this->fileSystem->unlink($map->getAbsoluteDestination()); 49 | 50 | if ($this->fileSystem->isDirEmpty(dirname($map->getAbsoluteDestination()))) { 51 | $this->fileSystem->removeEmptyDirectoriesUpToRoot( 52 | dirname($map->getAbsoluteDestination()), 53 | $this->rootDir 54 | ); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/UnInstallStrategy/UnInstallStrategyInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface UnInstallStrategyInterface 13 | { 14 | /** 15 | * UnInstall the extension given the list of install files 16 | * @param MapCollection $collection 17 | */ 18 | public function unInstall(MapCollection $collection); 19 | } 20 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Util/FileSystem.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FileSystem extends ComposerFs 13 | { 14 | /** 15 | * Returns the relative path from $from to $to 16 | * 17 | * This is utility method for symlink creation. 18 | * Orig Source: http://stackoverflow.com/a/2638272/485589 19 | * 20 | * @param string $from 21 | * @param string $to 22 | * 23 | * @return string 24 | */ 25 | public function getRelativePath($from, $to) 26 | { 27 | $from = str_replace('\\', '/', $from); 28 | $to = str_replace('\\', '/', $to); 29 | 30 | $from = str_replace(['/./', '//'], '/', $from); 31 | $to = str_replace(['/./', '//'], '/', $to); 32 | 33 | $from = explode('/', $from); 34 | $to = explode('/', $to); 35 | 36 | $relPath = $to; 37 | 38 | foreach ($from as $depth => $dir) { 39 | // find first non-matching dir 40 | if ($dir === $to[$depth]) { 41 | // ignore this directory 42 | array_shift($relPath); 43 | } else { 44 | // get number of remaining dirs to $from 45 | $remaining = count($from) - $depth; 46 | if ($remaining > 1) { 47 | // add traversals up to first matching dir 48 | $padLength = (count($relPath) + $remaining - 1) * -1; 49 | $relPath = array_pad($relPath, $padLength, '..'); 50 | break; 51 | } else { 52 | $relPath[0] = './' . $relPath[0]; 53 | } 54 | } 55 | } 56 | return implode('/', $relPath); 57 | } 58 | 59 | /** 60 | * @param string $absoluteSource 61 | * @param string $rootDirectory 62 | * 63 | * @return string 64 | */ 65 | public function makePathRelative($absoluteSource, $rootDirectory) 66 | { 67 | return substr($absoluteSource, strlen($rootDirectory) + 1); 68 | } 69 | 70 | /** 71 | * Check if the basename of source and directory match 72 | * 73 | * @param string $source 74 | * @param string $destination 75 | * 76 | * @return bool 77 | */ 78 | public function sourceAndDestinationBaseMatch($source, $destination) 79 | { 80 | $sourceBase = basename($source); 81 | $destinationBase = basename($destination); 82 | 83 | return $sourceBase === $destinationBase; 84 | } 85 | 86 | /** 87 | * If destination to correct location 88 | * return true 89 | * 90 | * @param string $destination 91 | * @param string $source 92 | * 93 | * @return bool 94 | */ 95 | public function symLinkPointsToCorrectLocation($destination, $source) 96 | { 97 | $destinationAbsolutePath = realpath(readlink($destination)); 98 | $sourceAbsolutePath = realpath($source); 99 | 100 | return $destinationAbsolutePath === $sourceAbsolutePath; 101 | } 102 | 103 | /** 104 | * Create a symlink fixes various quirks on different OS's 105 | * 106 | * @param string $source 107 | * @param string $destination 108 | * 109 | * @throws \ErrorException 110 | */ 111 | public function createSymlink($source, $destination) 112 | { 113 | $relativeSourcePath = $this->getRelativePath($destination, $source); 114 | if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { 115 | $relativeSourcePath = str_replace('/', '\\', $relativeSourcePath); 116 | $param = is_dir($source) ? '/D' : ''; 117 | exec(sprintf('mklink %s %s %s', $param, $destination, $relativeSourcePath), $output, $return); 118 | } else { 119 | $result = @symlink($relativeSourcePath, $destination); 120 | 121 | if (false === $result) { 122 | throw new \ErrorException(sprintf('An error occurred while creating symlink: %s', $relativeSourcePath)); 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * @param string $path 129 | * @return bool 130 | */ 131 | public function endsWithDirectorySeparator($path) 132 | { 133 | return in_array(substr($path, -1), ['/', '\\']); 134 | } 135 | 136 | /** 137 | * Extend to unlink symlinks incase they are 138 | * directories. 139 | * 140 | * @param $file 141 | * @return bool 142 | */ 143 | public function remove($file) 144 | { 145 | if (is_link($file)) { 146 | return $this->unlink($file); 147 | } 148 | 149 | if (is_dir($file)) { 150 | return $this->removeDirectory($file); 151 | } 152 | 153 | if (file_exists($file)) { 154 | return $this->unlink($file); 155 | } 156 | 157 | return false; 158 | } 159 | 160 | /** 161 | * @param string $dir 162 | * @param string $root 163 | */ 164 | public function removeEmptyDirectoriesUpToRoot($dir, $root) 165 | { 166 | if (!$this->isDirEmpty($dir)) { 167 | return; 168 | } 169 | 170 | if ($dir === $root) { 171 | return; 172 | } 173 | 174 | $this->removeDirectory($dir); 175 | $this->removeEmptyDirectoriesUpToRoot(dirname($dir), $root); 176 | } 177 | } 178 | --------------------------------------------------------------------------------