├── src └── MagentoHackathon │ └── Composer │ ├── Magento │ ├── Util │ │ └── FileSystem.php │ ├── UnInstallStrategy │ │ ├── UnInstallStrategyInterface.php │ │ └── UnInstallStrategy.php │ ├── Parser │ │ ├── Parser.php │ │ ├── MapParser.php │ │ ├── ModmanParser.php │ │ ├── PathTranslationParser.php │ │ └── PackageXmlParser.php │ ├── Factory │ │ ├── ParserFactoryInterface.php │ │ ├── PathTranslationParserFactory.php │ │ ├── InstallStrategyFactory.php │ │ ├── EntryFactory.php │ │ ├── ParserFactory.php │ │ └── DeploystrategyFactory.php │ ├── Deploystrategy │ │ ├── AbsoluteSymlink.php │ │ ├── None.php │ │ ├── Move.php │ │ ├── Link.php │ │ ├── Copy.php │ │ ├── Symlink.php │ │ └── DeploystrategyAbstract.php │ ├── Event │ │ ├── PackageDeployEvent.php │ │ ├── PackagePreInstallEvent.php │ │ ├── PackageUnInstallEvent.php │ │ └── EventManager.php │ ├── InstalledPackageDumper.php │ ├── Repository │ │ ├── InstalledPackageRepositoryInterface.php │ │ └── InstalledPackageFileSystemRepository.php │ ├── Deploy │ │ └── Manager │ │ │ └── Entry.php │ ├── InstalledPackage.php │ ├── GitIgnoreListener.php │ ├── DeployManager.php │ ├── GitIgnore.php │ ├── Patcher │ │ └── Bootstrap.php │ ├── ModuleManager.php │ ├── ProjectConfig.php │ └── Plugin.php │ └── Helper.php ├── SECURITY.md ├── sonar-project.properties ├── .github ├── workflows │ ├── php.yml │ └── integration.yml └── FUNDING.yml ├── appveyor.yml ├── bin └── magento-composer-installer.php ├── res └── target.xml ├── .travis.yml ├── CONTRIBUTING.md ├── composer.json ├── CHANGELOG.md └── README.md /src/MagentoHackathon/Composer/Magento/Util/FileSystem.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FileSystem extends ComposerFs 13 | { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 3.2.x | :white_check_mark: | 11 | | 3.1.2 | :white_check_mark: | 12 | | < 3.1.2 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | You can send an e-mail to flyingmana@googlemail.com 17 | 18 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/UnInstallStrategy/UnInstallStrategyInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface UnInstallStrategyInterface 11 | { 12 | /** 13 | * UnInstall the extension given the list of install files 14 | * 15 | * @param array $files 16 | */ 17 | public function unInstall(array $files); 18 | } 19 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Cotya_magento-composer-installer 2 | sonar.organization=cotya 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=magento-composer-installer 6 | #sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | sonar.sources=./src/ 10 | sonar.tests=./tests/ 11 | 12 | # Encoding of the source code. Default is default system encoding 13 | sonar.sourceEncoding=UTF-8 14 | 15 | sonar.language=php 16 | sonar.php.tests.reportPath=junit.xml 17 | sonar.php.coverage.reportPaths=coverage.clover 18 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/Parser.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ParserFactoryInterface 14 | { 15 | 16 | /** 17 | * @param PackageInterface $package 18 | * @param string $sourceDir 19 | * @return Parser 20 | */ 21 | public function make(PackageInterface $package, $sourceDir); 22 | } 23 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Deploystrategy/AbsoluteSymlink.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/Deploystrategy/None.php: -------------------------------------------------------------------------------- 1 | > php.ini 16 | - echo extension_dir=ext >> php.ini 17 | - echo extension=php_openssl.dll >> php.ini 18 | - SET PATH=C:\tools\php;%PATH% 19 | - cd C:\projects\random\customer\magento 20 | - php -r "readfile('http://getcomposer.org/installer');" | php 21 | - php composer.phar install --prefer-dist --no-interaction 22 | test_script: 23 | - cd C:\projects\random\customer\magento 24 | - vendor\bin\phpunit.bat --testsuite Unit 25 | -------------------------------------------------------------------------------- /bin/magento-composer-installer.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new \MagentoHackathon\Composer\Magento\Command\DeployCommand()); 23 | $application->add(new \MagentoHackathon\Composer\Magento\Command\DeployAllCommand()); 24 | $application->run(); 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/PackageDeployEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PackageDeployEvent extends Event 14 | { 15 | /** 16 | * @var Entry 17 | */ 18 | protected $deployEntry; 19 | 20 | /** 21 | * @param string $name 22 | * @param Entry $deployEntry 23 | */ 24 | public function __construct($name, Entry $deployEntry) 25 | { 26 | parent::__construct($name); 27 | $this->deployEntry = $deployEntry; 28 | } 29 | 30 | /** 31 | * @return Entry 32 | */ 33 | public function getDeployEntry() 34 | { 35 | return $this->deployEntry; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstalledPackageDumper.php: -------------------------------------------------------------------------------- 1 | $installedPackage->getName(), 19 | 'version' => $installedPackage->getVersion(), 20 | 'installedFiles' => $installedPackage->getInstalledFiles(), 21 | ); 22 | } 23 | 24 | /** 25 | * @param array $data 26 | * @return InstalledPackage 27 | */ 28 | public function restore(array $data) 29 | { 30 | return new InstalledPackage($data['packageName'], $data['version'], $data['installedFiles']); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/PackagePreInstallEvent.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PackagePreInstallEvent extends Event 15 | { 16 | /** 17 | * @var PackageInterface 18 | */ 19 | protected $package; 20 | 21 | /** 22 | * @param string $name 23 | * @param PackageInterface $package 24 | */ 25 | public function __construct($name, PackageInterface $package) 26 | { 27 | parent::__construct($name); 28 | $this->package = $package; 29 | } 30 | 31 | /** 32 | * @return PackageInterface 33 | */ 34 | public function getPackage() 35 | { 36 | return $this->package; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /res/target.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 array 41 | */ 42 | public function getInstalledFiles() 43 | { 44 | return $this->package->getInstalledFiles(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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/Deploy/Manager/Entry.php: -------------------------------------------------------------------------------- 1 | packageName = $packageName; 27 | } 28 | 29 | /** 30 | * @return mixed 31 | */ 32 | public function getPackageName() 33 | { 34 | return $this->packageName; 35 | } 36 | 37 | /** 38 | * @param \MagentoHackathon\Composer\Magento\Deploystrategy\DeploystrategyAbstract $deployStrategy 39 | */ 40 | public function setDeployStrategy($deployStrategy) 41 | { 42 | $this->deployStrategy = $deployStrategy; 43 | } 44 | 45 | /** 46 | * @return \MagentoHackathon\Composer\Magento\Deploystrategy\DeploystrategyAbstract 47 | */ 48 | public function getDeployStrategy() 49 | { 50 | return $this->deployStrategy; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Event/EventManager.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class EventManager 13 | { 14 | /** 15 | * @var array 16 | */ 17 | private $listeners = array(); 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] = array($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, array($event)); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/InstalledPackage.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InstalledPackage 11 | { 12 | /** 13 | * @var string 14 | */ 15 | protected $name; 16 | 17 | /** 18 | * @var string 19 | */ 20 | protected $version; 21 | 22 | /** 23 | * @var array 24 | */ 25 | protected $installedFiles; 26 | 27 | /** 28 | * @param string $name 29 | * @param string $version 30 | * @param array $files 31 | */ 32 | public function __construct($name, $version, array $files) 33 | { 34 | $this->name = $name; 35 | $this->installedFiles = $files; 36 | $this->version = $version; 37 | } 38 | 39 | /** 40 | * @return string 41 | */ 42 | public function getName() 43 | { 44 | return $this->name; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getVersion() 51 | { 52 | return $this->version; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getUniqueName() 59 | { 60 | return sprintf('%s-%s', $this->getName(), $this->getVersion()); 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function getInstalledFiles() 67 | { 68 | return $this->installedFiles; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/GitIgnoreListener.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class GitIgnoreListener 14 | { 15 | 16 | /** 17 | * @var GitIgnore 18 | */ 19 | protected $gitIgnore; 20 | 21 | /** 22 | * @param GitIgnore $gitIgnore 23 | */ 24 | public function __construct(GitIgnore $gitIgnore) 25 | { 26 | $this->gitIgnore = $gitIgnore; 27 | } 28 | 29 | /** 30 | * Add any files which were installed to the .gitignore 31 | * 32 | * @param PackageDeployEvent $packageDeployEvent 33 | */ 34 | public function addNewInstalledFiles(PackageDeployEvent $packageDeployEvent) 35 | { 36 | $this->gitIgnore->addMultipleEntries( 37 | $packageDeployEvent->getDeployEntry()->getDeployStrategy()->getDeployedFiles() 38 | ); 39 | $this->gitIgnore->write(); 40 | } 41 | 42 | /** 43 | * Remove any files which were removed to the .gitignore 44 | * 45 | * @param PackageUnInstallEvent $e 46 | */ 47 | public function removeUnInstalledFiles(PackageUnInstallEvent $e) 48 | { 49 | $this->gitIgnore->removeMultipleEntries($e->getInstalledFiles()); 50 | $this->gitIgnore->write(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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 Parser 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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | os: 3 | - linux 4 | php: 5 | - 7.4 6 | - 7.3 7 | - 7.2 8 | - 5.6 9 | 10 | env: 11 | - TEST_SUITE=Unit 12 | - TEST_SUITE=Fullstack 13 | 14 | matrix: 15 | fast_finish: true 16 | include: 17 | - php: 5.6 18 | env: TEST_SUITE=Static 19 | - php: 7.0 20 | env: TEST_SUITE=Static 21 | - php: 7.4 22 | os: windows 23 | - php: 7.4 24 | env: COMPOSER_VERSION=dev 25 | allow_failures: 26 | - os: windows 27 | - env: TEST_SUITE=Static 28 | - env: COMPOSER_VERSION=beta 29 | - env: COMPOSER_VERSION=dev 30 | 31 | cache: 32 | directories: 33 | - $HOME/.composer/cache 34 | 35 | install: 36 | - php ./tests/prepare_composer.php 37 | - chmod +x ./composer.phar 38 | - ./composer.phar --version 39 | - ./composer.phar install -n --prefer-source 40 | 41 | script: 42 | - > 43 | echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini 44 | - > 45 | sh -c "if [ '$TEST_SUITE' = 'Unit' ] || [ '$TEST_SUITE' = 'Fullstack' ]; then 46 | ./vendor/bin/phpunit --coverage-clover=coverage.clover --testsuite=$TEST_SUITE; 47 | fi" 48 | - > 49 | sh -c "if [ '$TEST_SUITE' = 'Static' ]; then 50 | ./vendor/bin/phpcs --standard=PSR2 ./src/; 51 | ./vendor/bin/phpcs --standard=PSR2 ./tests/MagentoHackathon; 52 | fi" 53 | after_script: 54 | - wget https://scrutinizer-ci.com/ocular.phar 55 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 56 | 57 | notifications: 58 | webhooks: 59 | on_success: change # options: [always|never|change] default: always 60 | on_failure: always # options: [always|never|change] default: always 61 | on_start: false # default: false 62 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/InstallStrategyFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 33 | $this->parserFactory = $parserFactory; 34 | } 35 | 36 | /** 37 | * @param PackageInterface $package 38 | * @param string $packageSourcePath 39 | * @return DeploystrategyAbstract 40 | */ 41 | public function make(PackageInterface $package, $packageSourcePath) 42 | { 43 | $strategyName = $this->config->getModuleSpecificDeployStrategy($package->getName()); 44 | 45 | $ns = '\MagentoHackathon\Composer\Magento\Deploystrategy\\'; 46 | $className = $ns . ucfirst($strategyName); 47 | if (!class_exists($className)) { 48 | $className = $ns . 'Symlink'; 49 | } 50 | 51 | $strategy = new $className($packageSourcePath, realpath($this->config->getMagentoRootDir())); 52 | $strategy->setIgnoredMappings($this->config->getModuleSpecificDeployIgnores($package->getName())); 53 | $strategy->setIsForced($this->config->getMagentoForceByPackageName($package->getName())); 54 | 55 | $mappingParser = $this->parserFactory->make($package, $packageSourcePath); 56 | $strategy->setMappings($mappingParser->getMappings()); 57 | 58 | return $strategy; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/EntryFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class EntryFactory 16 | { 17 | 18 | /** 19 | * @var ProjectConfig 20 | */ 21 | protected $config; 22 | 23 | /** 24 | * @var DeploystrategyFactory 25 | */ 26 | protected $deploystrategyFactory; 27 | 28 | /** 29 | * @var ParserFactoryInterface 30 | */ 31 | protected $parserFactory; 32 | 33 | /** 34 | * @param ProjectConfig $config 35 | * @param DeploystrategyFactory $deploystrategyFactory 36 | * @param ParserFactoryInterface $parserFactory 37 | */ 38 | public function __construct( 39 | ProjectConfig $config, 40 | DeploystrategyFactory $deploystrategyFactory, 41 | ParserFactoryInterface $parserFactory 42 | ) { 43 | $this->config = $config; 44 | $this->deploystrategyFactory = $deploystrategyFactory; 45 | $this->parserFactory = $parserFactory; 46 | } 47 | 48 | /** 49 | * @param PackageInterface $package 50 | * @param string $packageSourceDirectory 51 | * @return Entry 52 | */ 53 | public function make(PackageInterface $package, $packageSourceDirectory) 54 | { 55 | $entry = new Entry(); 56 | $entry->setPackageName($package->getName()); 57 | 58 | $strategy = $this->deploystrategyFactory->make($package, $packageSourceDirectory); 59 | $mappingParser = $this->parserFactory->make($package, $packageSourceDirectory); 60 | 61 | $strategy->setMappings($mappingParser->getMappings()); 62 | $entry->setDeployStrategy($strategy); 63 | 64 | return $entry; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Deploystrategy/Move.php: -------------------------------------------------------------------------------- 1 | sourceDir)) { 34 | $this->removeDir($this->sourceDir); 35 | } 36 | } 37 | 38 | /** 39 | * Recursively remove files and folders from given path 40 | * 41 | * @param $path 42 | * @return void 43 | * @throws \Exception 44 | */ 45 | private function removeDir($path) 46 | { 47 | $iterator = new \RecursiveIteratorIterator( 48 | new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), 49 | \RecursiveIteratorIterator::CHILD_FIRST 50 | ); 51 | foreach ($iterator as $fileInfo) { 52 | $filename = $fileInfo->getFilename(); 53 | if ($filename != '..' || $filename != '.') { 54 | $removeAction = ($fileInfo->isDir() ? 'rmdir' : 'unlink'); 55 | try { 56 | $removeAction($fileInfo->getRealPath()); 57 | } catch (\Exception $e) { 58 | if (strpos($e->getMessage(), 'Directory not empty')) { 59 | $this->removeDir($fileInfo->getRealPath()); 60 | } else { 61 | throw new Exception(sprintf('%s could not be removed.', $fileInfo->getRealPath())); 62 | } 63 | } 64 | } 65 | } 66 | rmdir($path); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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 = array(); 49 | foreach ($this->file as $line => $row) { 50 | $row = trim($row); 51 | if ('' === $row || in_array($row[0], array('#', '@'))) { 52 | continue; 53 | } 54 | $parts = preg_split('/\s+/', $row, -1, PREG_SPLIT_NO_EMPTY); 55 | if (count($parts) === 1) { 56 | $part = reset($parts); 57 | $map[] = array($part, $part); 58 | } elseif (is_int(count($parts) / 2)) { 59 | $partCountSplit = count($parts) / 2; 60 | $map[] = array( 61 | implode(' ', array_slice($parts, 0, $partCountSplit)), 62 | implode(' ', array_slice($parts, $partCountSplit)), 63 | ); 64 | } else { 65 | throw new \ErrorException( 66 | sprintf('Invalid row on line %d has %d parts, expected 2', $line, count($parts)) 67 | ); 68 | } 69 | } 70 | return $map; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/UnInstallStrategy/UnInstallStrategy.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UnInstallStrategy implements UnInstallStrategyInterface 13 | { 14 | 15 | /** 16 | * @var Filesystem 17 | */ 18 | protected $fileSystem; 19 | 20 | /** 21 | * The root dir for uninstalling from. Should be project root. 22 | * 23 | * @var string 24 | */ 25 | protected $rootDir; 26 | 27 | /** 28 | * @param Filesystem $fileSystem 29 | * @param string $rootDir 30 | */ 31 | public function __construct(Filesystem $fileSystem, $rootDir) 32 | { 33 | $this->fileSystem = $fileSystem; 34 | $this->rootDir = $rootDir; 35 | } 36 | 37 | /** 38 | * UnInstall the extension given the list of install files 39 | * 40 | * @param array $files 41 | */ 42 | public function unInstall(array $files) 43 | { 44 | foreach ($files as $file) { 45 | $file = $this->rootDir . $file; 46 | 47 | /* 48 | because of different reasons the file can be already gone. 49 | example: 50 | - file got deployed by multiple modules(should only happen with copy force) 51 | - user did things 52 | 53 | when the file is a symlink, but the target is already gone, file_exists returns false 54 | */ 55 | 56 | if (is_link($file)) { 57 | if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { 58 | @unlink($file) || @rmdir($file); 59 | } else { 60 | $this->fileSystem->unlink($file); 61 | } 62 | } 63 | 64 | if (file_exists($file)) { 65 | $this->fileSystem->remove($file); 66 | } 67 | 68 | $parentDir = dirname($file); 69 | while (is_dir($parentDir) 70 | && $this->fileSystem->isDirEmpty($parentDir) 71 | && $parentDir !== $this->rootDir 72 | ) { 73 | $this->fileSystem->removeDirectory($parentDir); 74 | $parentDir = dirname($parentDir); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /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 Parser 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 | $connectPackageXmlFile = sprintf('%s/package.xml', $sourceDir); 61 | if (file_exists($connectPackageXmlFile)) { 62 | return new PackageXmlParser($connectPackageXmlFile); 63 | } 64 | 65 | throw new \ErrorException( 66 | sprintf( 67 | 'Unable to find deploy strategy for module: "%s" no known mapping'.PHP_EOL 68 | .'sourceDir: "%s"', 69 | $package->getName(), 70 | $sourceDir 71 | ) 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Factory/DeploystrategyFactory.php: -------------------------------------------------------------------------------- 1 | '\MagentoHackathon\Composer\Magento\Deploystrategy\Copy', 26 | 'symlink' => '\MagentoHackathon\Composer\Magento\Deploystrategy\Symlink', 27 | 'absoluteSymlink' => '\MagentoHackathon\Composer\Magento\Deploystrategy\AbsoluteSymlink', 28 | 'link' => '\MagentoHackathon\Composer\Magento\Deploystrategy\Link', 29 | 'none' => '\MagentoHackathon\Composer\Magento\Deploystrategy\None', 30 | ); 31 | 32 | /** 33 | * @param ProjectConfig $config 34 | */ 35 | public function __construct(ProjectConfig $config) 36 | { 37 | $this->config = $config; 38 | } 39 | 40 | /** 41 | * @param PackageInterface $package 42 | * @param string $packageSourcePath 43 | * @return DeploystrategyAbstract 44 | */ 45 | public function make(PackageInterface $package, $packageSourcePath) 46 | { 47 | $strategyName = $this->config->getDeployStrategy(); 48 | if ($this->config->hasDeployStrategyOverwrite()) { 49 | $moduleSpecificDeployStrategies = $this->config->getDeployStrategyOverwrite(); 50 | 51 | if (isset($moduleSpecificDeployStrategies[$package->getName()])) { 52 | $strategyName = $moduleSpecificDeployStrategies[$package->getName()]; 53 | } 54 | } 55 | 56 | if (!isset(static::$strategies[$strategyName])) { 57 | $className = static::$strategies['symlink']; 58 | } else { 59 | $className = static::$strategies[$strategyName]; 60 | } 61 | 62 | $strategy = new $className($packageSourcePath, realpath($this->config->getMagentoRootDir())); 63 | $strategy->setIgnoredMappings($this->config->getModuleSpecificDeployIgnores($package->getName())); 64 | $strategy->setIsForced($this->config->getMagentoForceByPackageName($package->getName())); 65 | return $strategy; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Which Branch to submit my PR to 2 | 3 | We use a little different branching strategy then other projects, so finding the right branch to contribute to is a bit confusing for some. 4 | Instead of the usual dev and master branches, our focus is on version branches. The reason is, that people use different versions and usually 5 | keep them to dont break their deployment workflow. We respect this, and so we still support older versions to some extent. 6 | 7 | Most PRs are to fix bugs, the best target to submit this fix is to the branch referencing the major.minor version you use. 8 | 9 | If you want to submit a new Feature, we prefer the default branch or the highest version branch for this. 10 | But if you use an older version, you can target this. We will care about porting your patch upstream. 11 | 12 | This should not be necessary, but if you have a patch which is introducing a backwards compatible break, 13 | then dont submit your Branch as PR, but open an issue with a link to the branch. 14 | We may then say which branch would be best suited to target for a PR, 15 | or even create a new major version Branch for this. 16 | It would also be possible, that we merge it without the PR workflow. 17 | 18 | ## Keeping the change log up to date 19 | You **must** update the `CHANGELOG.md` file (in the `Unreleased` section) if your change is significant in this sense. 20 | Keep in mind that people are reading the change log to check for new or removed features, backward incompatibilities ("BC breaks") 21 | or security fixes. Do not change the change log for very minor changes. 22 | If you're unsure, update the change log file. 23 | 24 | ## Refactoring 25 | 26 | Refactoring as part of your PRs may slow down the merge process, as refactoring makes reviewing patches harder. 27 | 28 | Refactoring only PRs will usually be postponed to the the next Major release, as they make merging and porting 29 | between branches a lot harder. 30 | There may only be a few cases, where an exception will be made. 31 | 32 | ## Submitting Bugs 33 | 34 | A lot of bugs are very hard to track down, as they often depend on specific combinations of packages and versions. 35 | To make debugging issues easier, always also post the used Version of the Installer. 36 | Even better, if you can show the used composer.json so we can reproduce the Issue based on it. 37 | 38 | # Afterword 39 | 40 | dont be afraind, we are open for every kind of contribution, regardless how little it is or how much work it will be for us. 41 | A good prepared contributions will most times get faster into the project, but we will never decline a contribution because 42 | it does not meet our standards, it will only take time till we are able to patch it enough. 43 | Also you can get valuable feedback, how to improve contributions for the next time. :) 44 | 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magento-hackathon/magento-composer-installer", 3 | "description": "Composer installer for Magento modules", 4 | "keywords": [ 5 | "composer-installer", 6 | "magento", 7 | "openmage" 8 | ], 9 | "minimum-stability": "stable", 10 | "type": "composer-plugin", 11 | "license": "OSL-3.0", 12 | "homepage": "https://github.com/magento-hackathon/magento-composer-installer", 13 | "repositories": [ 14 | { 15 | "type": "composer", 16 | "url": "https://packages.firegento.com" 17 | } 18 | ], 19 | "config": { 20 | "platform": { 21 | "php": "5.6" 22 | } 23 | }, 24 | "authors": [ 25 | { 26 | "name": "Daniel Fahlke aka Flyingmana", 27 | "email": "flyingmana@googlemail.com" 28 | }, 29 | { 30 | "name": "Jörg Weller", 31 | "email": "weller@flagbit.de" 32 | }, 33 | { 34 | "name": "Karl Spies", 35 | "email": "karl.spies@gmx.net" 36 | }, 37 | { 38 | "name": "Tobias Vogt", 39 | "email": "tobi@webguys.de" 40 | }, 41 | { 42 | "name": "David Fuhr", 43 | "email": "fuhr@flagbit.de" 44 | }, 45 | { 46 | "name": "Vinai Kopp", 47 | "email": "vinai@netzarbeiter.com" 48 | } 49 | ], 50 | "funding": [ 51 | { 52 | "type": "patreon", 53 | "url": "https://www.patreon.com/Flyingmana" 54 | }, 55 | { 56 | "type": "github", 57 | "url": "https://github.com/sponsors/Flyingmana" 58 | } 59 | ], 60 | "require": { 61 | "php": ">=5.5", 62 | "flyingmana/composer-config-reader": "*", 63 | "symfony/console": "^2.5|^3.0|^4.0|^5.0|^6.0", 64 | "composer-plugin-api": "^2.0" 65 | }, 66 | "require-dev": { 67 | "phpunit/phpunit": "~4.3", 68 | "phpunit/phpunit-mock-objects": "~2.3", 69 | "squizlabs/php_codesniffer": "~2.1", 70 | "composer/composer": "2.*", 71 | "symfony/process": "~2.5", 72 | "mikey179/vfsstream": "~1.4", 73 | "ext-json": "*", 74 | "cotya/composer-test-framework": "~2.0" 75 | }, 76 | "suggest": { 77 | "theseer/autoload": "~1.14", 78 | "colinmollenhour/modman": "*" 79 | }, 80 | "autoload": { 81 | "psr-0": { 82 | "MagentoHackathon\\Composer": "src/" 83 | } 84 | }, 85 | "autoload-dev": { 86 | "psr-0": { 87 | "MagentoHackathon\\Composer\\Magento": "tests/" 88 | } 89 | }, 90 | "bin": [ 91 | "bin/magento-composer-installer.php" 92 | ], 93 | "archive": { 94 | "exclude": [ 95 | "vendor", 96 | "/tests/FullStackTest/" 97 | ] 98 | }, 99 | "extra": { 100 | "class": "MagentoHackathon\\Composer\\Magento\\Plugin" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: "Integration Tests" 2 | on: 3 | push: 4 | pull_request: 5 | 6 | 7 | jobs: 8 | unit: 9 | name: Unit Tests on ${{ matrix.php-versions }} 10 | runs-on: ${{ matrix.operating-system }} 11 | strategy: 12 | max-parallel: 5 13 | matrix: 14 | operating-system: [ubuntu-latest] 15 | php-versions: [ '7.4', '7.2', '7.3' ] 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@master 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extension-csv: mbstring #optional, setup extensions 23 | ini-values-csv: post_max_size=256M, short_open_tag=On #optional, setup php.ini configuration 24 | coverage: xdebug #optional, setup coverage driver 25 | pecl: true #optional, setup PECL 26 | - name: Prepare 27 | run: | 28 | php -v 29 | php ./tests/prepare_composer.php 30 | chmod +x ./composer.phar 31 | ./composer.phar --version 32 | ./composer.phar install -n --prefer-source 33 | - name: Run Unit Tests 34 | run: ./vendor/bin/phpunit --coverage-clover=coverage.clover --log-junit=junit.xml --testsuite=Unit; 35 | - name: prepare SonarCloud Scan Data 36 | if: ${{ matrix.php-versions == '7.4' }} 37 | continue-on-error: true 38 | run: | 39 | ls -la 40 | sed -i 's@'$GITHUB_WORKSPACE'/@/github/workspace/@g' junit.xml 41 | sed -i 's@'$GITHUB_WORKSPACE'/@/github/workspace/@g' coverage.clover 42 | ls -la 43 | - name: SonarCloud Scan 44 | uses: SonarSource/sonarcloud-github-action@master 45 | if: ${{ matrix.php-versions == '7.4' }} 46 | continue-on-error: true 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 49 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 50 | 51 | 52 | fullstack: 53 | name: Fullstack Tests on ${{ matrix.php-versions }} 54 | runs-on: ${{ matrix.operating-system }} 55 | needs: unit 56 | strategy: 57 | max-parallel: 1 58 | matrix: 59 | operating-system: [ ubuntu-latest ] 60 | php-versions: [ '7.4', '7.2', '7.3' ] 61 | steps: 62 | - uses: actions/checkout@v1 63 | - name: Setup PHP 64 | uses: shivammathur/setup-php@master 65 | with: 66 | php-version: ${{ matrix.php-versions }} 67 | extension-csv: mbstring #optional, setup extensions 68 | ini-values-csv: post_max_size=256M, short_open_tag=On #optional, setup php.ini configuration 69 | coverage: none #optional, setup coverage driver 70 | pecl: true #optional, setup PECL 71 | - name: Prepare 72 | run: | 73 | php -v 74 | php ./tests/prepare_composer.php 75 | chmod +x ./composer.phar 76 | ./composer.phar --version 77 | ./composer.phar install -n --prefer-source 78 | - name: Run Fullstack Tests 79 | run: ./vendor/bin/phpunit --coverage-clover=coverage.clover --testsuite=Fullstack; 80 | 81 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Parser/PathTranslationParser.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 = array(); 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 = array(); 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/DeployManager.php: -------------------------------------------------------------------------------- 1 | eventManager = $eventManager; 51 | } 52 | 53 | /** 54 | * @param Entry $package 55 | */ 56 | public function addPackage(Entry $package) 57 | { 58 | $this->packages[] = $package; 59 | } 60 | 61 | /** 62 | * @param $priorities 63 | */ 64 | public function setSortPriority($priorities) 65 | { 66 | $this->sortPriority = $priorities; 67 | } 68 | 69 | /** 70 | * uses the sortPriority Array to sort the packages. 71 | * Highest priority first. 72 | * Copy gets per default higher priority then others 73 | */ 74 | protected function sortPackages() 75 | { 76 | $sortPriority = $this->sortPriority; 77 | $getPriorityValue = function (Entry $object) use ($sortPriority) { 78 | $result = 100; 79 | if (isset($sortPriority[$object->getPackageName()])) { 80 | $result = $sortPriority[$object->getPackageName()]; 81 | } elseif ($object->getDeployStrategy() instanceof Copy || $object->getDeployStrategy() instanceof Move) { 82 | $result = 101; 83 | } 84 | return $result; 85 | }; 86 | usort( 87 | $this->packages, 88 | function ($a, $b) use ($getPriorityValue) { 89 | /** @var Entry $a */ 90 | /** @var Entry $b */ 91 | $aVal = $getPriorityValue($a); 92 | $bVal = $getPriorityValue($b); 93 | if ($aVal == $bVal) { 94 | return 0; 95 | } 96 | return ($aVal > $bVal) ? -1 : 1; 97 | } 98 | ); 99 | } 100 | 101 | /** 102 | * Deploy all the queued packages 103 | */ 104 | public function doDeploy() 105 | { 106 | $this->sortPackages(); 107 | /** @var Entry $package */ 108 | foreach ($this->packages as $package) { 109 | $this->eventManager->dispatch(new PackageDeployEvent('pre-package-deploy', $package)); 110 | $package->getDeployStrategy()->deploy(); 111 | $this->eventManager->dispatch(new PackageDeployEvent('post-package-deploy', $package)); 112 | } 113 | } 114 | 115 | /** 116 | * @return Deploy\Manager\Entry[] 117 | */ 118 | public function getEntries() 119 | { 120 | return $this->packages; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Deploystrategy/Link.php: -------------------------------------------------------------------------------- 1 | getSourceDir() . '/' . $this->removeTrailingSlash($source); 24 | $destPath = $this->getDestDir() . '/' . $this->removeTrailingSlash($dest); 25 | 26 | 27 | // Create all directories up to one below the target if they don't exist 28 | $destDir = dirname($destPath); 29 | if (!file_exists($destDir)) { 30 | mkdir($destDir, 0777, true); 31 | } 32 | 33 | // Handle source to dir link, 34 | // e.g. Namespace_Module.csv => app/locale/de_DE/ 35 | if (file_exists($destPath) && is_dir($destPath)) { 36 | if (basename($sourcePath) === basename($destPath)) { 37 | // copy/link each child of $sourcePath into $destPath 38 | foreach (new \DirectoryIterator($sourcePath) as $item) { 39 | $item = (string) $item; 40 | if (!strcmp($item, '.') || !strcmp($item, '..')) { 41 | continue; 42 | } 43 | $childSource = $source . '/' . $item; 44 | $this->create($childSource, substr($destPath, strlen($this->getDestDir())+1)); 45 | } 46 | return true; 47 | } else { 48 | $destPath .= '/' . basename($source); 49 | return $this->create($source, substr($destPath, strlen($this->getDestDir())+1)); 50 | } 51 | } 52 | 53 | // From now on $destPath can't be a directory, that case is already handled 54 | 55 | // If file exists and force is not specified, throw exception unless FORCE is set 56 | if (file_exists($destPath)) { 57 | if ($this->isForced()) { 58 | unlink($destPath); 59 | } else { 60 | throw new \ErrorException("Target $dest already exists (set extra.magento-force to override)"); 61 | } 62 | } 63 | 64 | // File to file 65 | if (!is_dir($sourcePath)) { 66 | if (is_dir($destPath)) { 67 | $destPath .= '/' . basename($sourcePath); 68 | } 69 | return link($sourcePath, $destPath); 70 | } 71 | 72 | // Copy dir to dir 73 | // First create destination folder if it doesn't exist 74 | if (file_exists($destPath)) { 75 | $destPath .= '/' . basename($sourcePath); 76 | } 77 | mkdir($destPath, 0777, true); 78 | 79 | $iterator = new \RecursiveIteratorIterator( 80 | new \RecursiveDirectoryIterator($sourcePath), 81 | \RecursiveIteratorIterator::SELF_FIRST 82 | ); 83 | 84 | foreach ($iterator as $item) { 85 | $subDestPath = $destPath . '/' . $iterator->getSubPathName(); 86 | if ($item->isDir()) { 87 | if (! file_exists($subDestPath)) { 88 | mkdir($subDestPath, 0777, true); 89 | } 90 | } else { 91 | link($item, $subDestPath); 92 | $this->addDeployedFile($subDestPath); 93 | } 94 | if (!is_readable($subDestPath)) { 95 | throw new \ErrorException("Could not create $subDestPath"); 96 | } 97 | } 98 | 99 | return true; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/GitIgnore.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class GitIgnore 11 | { 12 | /** 13 | * @var array 14 | */ 15 | protected $lines = array(); 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 = $this->removeDuplicates(file($fileLocation, FILE_IGNORE_NEW_LINES)); 35 | } 36 | } 37 | 38 | /** 39 | * @param string $file 40 | */ 41 | public function addEntry($file) 42 | { 43 | $file = $this->prependSlashIfNotExist($file); 44 | if (!in_array($file, $this->lines)) { 45 | $this->lines[] = $file; 46 | } 47 | $this->hasChanges = true; 48 | } 49 | 50 | /** 51 | * @param array $files 52 | */ 53 | public function addMultipleEntries(array $files) 54 | { 55 | foreach ($files as $file) { 56 | $this->addEntry($file); 57 | } 58 | } 59 | 60 | /** 61 | * @param string $file 62 | */ 63 | public function removeEntry($file) 64 | { 65 | $file = $this->prependSlashIfNotExist($file); 66 | $key = array_search($file, $this->lines); 67 | if (false !== $key) { 68 | unset($this->lines[$key]); 69 | $this->hasChanges = true; 70 | 71 | // renumber array 72 | $this->lines = array_values($this->lines); 73 | } 74 | } 75 | 76 | /** 77 | * @param array $files 78 | */ 79 | public function removeMultipleEntries(array $files) 80 | { 81 | foreach ($files as $file) { 82 | $this->removeEntry($file); 83 | } 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function getEntries() 90 | { 91 | return $this->lines; 92 | } 93 | 94 | /** 95 | * Write the file 96 | */ 97 | public function write() 98 | { 99 | if ($this->hasChanges) { 100 | file_put_contents($this->gitIgnoreLocation, implode("\n", $this->lines)); 101 | } 102 | } 103 | 104 | /** 105 | * Prepend a forward slash to a path 106 | * if it does not already start with one. 107 | * 108 | * @param string $file 109 | * @return string 110 | */ 111 | private function prependSlashIfNotExist($file) 112 | { 113 | return sprintf('/%s', ltrim($file, '/')); 114 | } 115 | 116 | /** 117 | * Removes duplicate patterns from the input array, without touching comments, line breaks etc. 118 | * Will remove the last duplicate pattern. 119 | * 120 | * @param array $lines 121 | * @return array 122 | */ 123 | private function removeDuplicates($lines) 124 | { 125 | // remove empty lines 126 | $duplicates = array_filter($lines); 127 | 128 | // remove comments 129 | $duplicates = array_filter($duplicates, function ($line) { 130 | return strpos($line, '#') !== 0; 131 | }); 132 | 133 | // check if duplicates exist 134 | if (count($duplicates) !== count(array_unique($duplicates))) { 135 | $duplicates = array_filter(array_count_values($duplicates), function ($count) { 136 | return $count > 1; 137 | }); 138 | 139 | // search from bottom to top 140 | $lines = array_reverse($lines); 141 | foreach ($duplicates as $duplicate => $count) { 142 | // remove all duplicates, except the first one 143 | for ($i = 1; $i < $count; $i++) { 144 | $key = array_search($duplicate, $lines); 145 | unset($lines[$key]); 146 | } 147 | } 148 | 149 | // restore original order 150 | $lines = array_values(array_reverse($lines)); 151 | } 152 | 153 | return $lines; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Helper.php: -------------------------------------------------------------------------------- 1 | getPathname().'/composer.json')) { 31 | throw new \InvalidArgumentException('no composer.json found in project root'); 32 | } 33 | $this->projectRoot = $projectRoot; 34 | 35 | $reader = new \Eloquent\Composer\Configuration\ConfigurationReader; 36 | $composerJsonObject = $reader->read($this->projectRoot.'/composer.json'); 37 | $this->magentoProjectConfig = new ProjectConfig( 38 | (array)$composerJsonObject->extra(), 39 | (array)$composerJsonObject 40 | ); 41 | } 42 | 43 | public function getVendorDirectory() 44 | { 45 | /* 46 | $reader = new \Eloquent\Composer\Configuration\ConfigurationReader; 47 | $composerJsonObject = $reader->read($this->projectRoot.'/composer.json'); 48 | return $composerJsonObject->vendorName(); 49 | */ 50 | return new \SplFileInfo($this->projectRoot.'/vendor'); 51 | } 52 | 53 | public function getInstalledPackages() 54 | { 55 | 56 | $installedJsonObject = json_decode(file_get_contents( 57 | $this->getVendorDirectory()->getPathname().'/composer/installed.json' 58 | ), true); 59 | return $installedJsonObject; 60 | } 61 | 62 | /** 63 | * @return ProjectConfig 64 | */ 65 | public function getMagentoProjectConfig() 66 | { 67 | return $this->magentoProjectConfig; 68 | } 69 | 70 | public function getPackageByName($name) 71 | { 72 | $result = null; 73 | foreach ($this->getInstalledPackages() as $package) { 74 | if ($package['name'] == $name) { 75 | $result = $package; 76 | break; 77 | } 78 | } 79 | return $result; 80 | } 81 | 82 | public static function initMagentoRootDir( 83 | ProjectConfig $projectConfig, 84 | \Composer\IO\IOInterface $io, 85 | \Composer\Util\Filesystem $filesystem, 86 | $vendorDir 87 | ) { 88 | if (false === $projectConfig->hasMagentoRootDir()) { 89 | $projectConfig->setMagentoRootDir( 90 | $io->ask( 91 | sprintf('please define your magento root dir [%s]', ProjectConfig::DEFAULT_MAGENTO_ROOT_DIR), 92 | ProjectConfig::DEFAULT_MAGENTO_ROOT_DIR 93 | ) 94 | ); 95 | } 96 | 97 | $magentoRootDirPath = $projectConfig->getMagentoRootDir(); 98 | $magentoRootDir = new \SplFileInfo($magentoRootDirPath); 99 | 100 | if (!is_dir($magentoRootDirPath) 101 | && $io->askConfirmation( 102 | 'magento root dir "' . $magentoRootDirPath . '" missing! create now? [Y,n] ' 103 | ) 104 | ) { 105 | $filesystem->ensureDirectoryExists($magentoRootDir); 106 | $io->write('magento root dir "' . $magentoRootDirPath . '" created'); 107 | } 108 | 109 | if (!is_dir($magentoRootDirPath)) { 110 | $dir = self::joinFilePath($vendorDir, $magentoRootDirPath); 111 | } 112 | } 113 | 114 | /** 115 | * join 2 paths 116 | * 117 | * @param $path1 118 | * @param $path2 119 | * @param $delimiter 120 | * @param bool $prependDelimiter 121 | * @param string $additionalPrefix 122 | * 123 | * @internal param $url1 124 | * @internal param $url2 125 | * 126 | * @return string 127 | */ 128 | public static function joinPath($path1, $path2, $delimiter, $prependDelimiter = false, $additionalPrefix = '') 129 | { 130 | $prefix = $additionalPrefix . $prependDelimiter ? $delimiter : ''; 131 | 132 | return $prefix . join( 133 | $delimiter, 134 | array( 135 | explode($path1, $delimiter), 136 | explode($path2, $delimiter) 137 | ) 138 | ); 139 | } 140 | 141 | /** 142 | * @param $path1 143 | * @param $path2 144 | * 145 | * @return string 146 | */ 147 | public static function joinFilePath($path1, $path2) 148 | { 149 | return self::joinPath($path1, $path2, DIRECTORY_SEPARATOR, true); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /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 = array(); 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 | if (pathinfo($elementPath, PATHINFO_EXTENSION) == 'txt') { 67 | continue; 68 | } 69 | $relativePath = str_replace('//', '/', $basePath . '/' . $elementPath); 70 | //remove the any trailing './' or '.' from the targets base-path. 71 | if (strpos($relativePath, './') === 0) { 72 | $relativePath = substr($relativePath, 2); 73 | } 74 | $map[] = array($relativePath, $relativePath); 75 | } 76 | } 77 | } catch (\RuntimeException $e) { 78 | // Skip invalid targets 79 | continue; 80 | } 81 | } 82 | } 83 | return $map; 84 | } 85 | 86 | /** 87 | * @param \SimpleXMLElement $target 88 | * @return string 89 | * @throws \RuntimeException 90 | */ 91 | protected function getTargetPath(\SimpleXMLElement $target) 92 | { 93 | $name = (string) $target->attributes()->name; 94 | $targets = $this->getTargetsDefinitions(); 95 | if (! isset($targets[$name])) { 96 | throw new \RuntimeException('Invalid target type ' . $name); 97 | } 98 | return $targets[$name]; 99 | } 100 | 101 | /** 102 | * @return array 103 | */ 104 | protected function getTargetsDefinitions() 105 | { 106 | if (empty($this->targets)) { 107 | /** @var $targets \SimpleXMLElement */ 108 | $targets = simplexml_load_file(__DIR__ . '/../../../../../res/target.xml'); 109 | foreach ($targets as $target) { 110 | /** @var $target \SimpleXMLElement */ 111 | $attributes = $target->attributes(); 112 | $this->targets["{$attributes->name}"] = "{$attributes->uri}"; 113 | } 114 | } 115 | return $this->targets; 116 | } 117 | 118 | /** 119 | * @param \SimpleXMLElement $element 120 | * @return array 121 | * @throws \RuntimeException 122 | */ 123 | protected function getElementPaths(\SimpleXMLElement $element) 124 | { 125 | $type = $element->getName(); 126 | $name = $element->attributes()->name; 127 | $elementPaths = array(); 128 | 129 | switch ($type) { 130 | case 'dir': 131 | if ($element->children()) { 132 | foreach ($element->children() as $child) { 133 | foreach ($this->getElementPaths($child) as $elementPath) { 134 | $elementPaths[] = $name == '.' ? $elementPath : $name . '/' . $elementPath; 135 | } 136 | } 137 | } else { 138 | $elementPaths[] = $name; 139 | } 140 | break; 141 | 142 | case 'file': 143 | $elementPaths[] = $name; 144 | break; 145 | 146 | default: 147 | throw new \RuntimeException('Unknown path type: ' . $type); 148 | } 149 | 150 | return $elementPaths; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Deploystrategy/Copy.php: -------------------------------------------------------------------------------- 1 | getCurrentMapping(); 24 | $mapSource = $this->removeTrailingSlash($mapSource); 25 | $mapDest = $this->removeTrailingSlash($mapDest); 26 | $cleanDest = $this->removeTrailingSlash($dest); 27 | 28 | $sourcePath = $this->getSourceDir() . '/' . $this->removeTrailingSlash($source); 29 | $destPath = $this->getDestDir() . '/' . $this->removeTrailingSlash($dest); 30 | 31 | 32 | // Create all directories up to one below the target if they don't exist 33 | $destDir = dirname($destPath); 34 | if (!file_exists($destDir)) { 35 | mkdir($destDir, 0777, true); 36 | } 37 | 38 | // Handle source to dir copy, 39 | // e.g. Namespace_Module.csv => app/locale/de_DE/ 40 | // Namespace/ModuleDir => Namespace/ 41 | // Namespace/ModuleDir => Namespace/, but Namespace/ModuleDir may exist 42 | // Namespace/ModuleDir => Namespace/ModuleDir, but ModuleDir may exist 43 | 44 | // first iteration through, we need to update the mappings to correctly handle mismatch globs 45 | if ($mapSource == $this->removeTrailingSlash($source) && $mapDest == $this->removeTrailingSlash($dest)) { 46 | if (basename($sourcePath) !== basename($destPath)) { 47 | $this->setCurrentMapping(array($mapSource, $mapDest . '/' . basename($source))); 48 | $cleanDest = $cleanDest . '/' . basename($source); 49 | } 50 | } 51 | 52 | if (file_exists($destPath) && is_dir($destPath)) { 53 | $mapSource = rtrim($mapSource, '*'); 54 | if (strcmp(substr($cleanDest, strlen($mapDest)+1), substr($source, strlen($mapSource)+1)) === 0) { 55 | // copy each child of $sourcePath into $destPath 56 | foreach (new \DirectoryIterator($sourcePath) as $item) { 57 | $item = (string) $item; 58 | if (!strcmp($item, '.') || !strcmp($item, '..')) { 59 | continue; 60 | } 61 | $childSource = $this->removeTrailingSlash($source) . '/' . $item; 62 | $this->create($childSource, substr($destPath, strlen($this->getDestDir())+1)); 63 | } 64 | return true; 65 | } else { 66 | $destPath = $this->removeTrailingSlash($destPath) . '/' . basename($source); 67 | return $this->create($source, substr($destPath, strlen($this->getDestDir())+1)); 68 | } 69 | } 70 | 71 | // From now on $destPath can't be a directory, that case is already handled 72 | 73 | // If file exists and force is not specified, throw exception unless FORCE is set 74 | if (file_exists($destPath)) { 75 | if ($this->isForced()) { 76 | unlink($destPath); 77 | } else { 78 | throw new \ErrorException("Target $dest already exists (set extra.magento-force to override)"); 79 | } 80 | } 81 | 82 | // File to file 83 | if (!is_dir($sourcePath)) { 84 | if (is_dir($destPath)) { 85 | $destPath .= '/' . basename($sourcePath); 86 | } 87 | $destPath = str_replace('\\', '/', $destPath); 88 | $this->addDeployedFile($destPath); 89 | return $this->transfer($sourcePath, $destPath); 90 | } 91 | 92 | // Copy dir to dir 93 | // First create destination folder if it doesn't exist 94 | if (file_exists($destPath)) { 95 | $destPath .= '/' . basename($sourcePath); 96 | } 97 | mkdir($destPath, 0777, true); 98 | 99 | $iterator = new \RecursiveIteratorIterator( 100 | new \RecursiveDirectoryIterator($sourcePath), 101 | \RecursiveIteratorIterator::SELF_FIRST 102 | ); 103 | 104 | foreach ($iterator as $item) { 105 | $subDestPath = $destPath . '/' . $iterator->getSubPathName(); 106 | if ($item->isDir()) { 107 | if (! file_exists($subDestPath)) { 108 | mkdir($subDestPath, 0777, true); 109 | } 110 | } else { 111 | $subDestPath = str_replace('\\', '/', $subDestPath); 112 | $this->transfer($item, $subDestPath); 113 | $this->addDeployedFile($subDestPath); 114 | } 115 | if (!is_readable($subDestPath)) { 116 | throw new \ErrorException("Could not create $subDestPath"); 117 | } 118 | } 119 | 120 | return true; 121 | } 122 | 123 | /** 124 | * transfer by copy files 125 | * 126 | * @param string $item 127 | * @param string $subDestPath 128 | * @return bool 129 | */ 130 | 131 | protected function transfer($item, $subDestPath) 132 | { 133 | return copy($item, $subDestPath); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /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 = array(); 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 = array(); 183 | foreach ($this->packages as $installedPackage) { 184 | $data[] = $this->dumper->dump($installedPackage); 185 | } 186 | 187 | file_put_contents($this->filePath, json_encode($data, JSON_PRETTY_PRINT)); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Patcher/Bootstrap.php: -------------------------------------------------------------------------------- 1 | setMageClassFilePath($mageClassFilePath); 38 | $this->config = $config; 39 | } 40 | 41 | /** 42 | * @param ProjectConfig $config 43 | * @return $this 44 | */ 45 | public static function fromConfig(ProjectConfig $config) 46 | { 47 | return new self($config->getMagentoRootDir() . '/app/Mage.php', $config); 48 | } 49 | 50 | /** 51 | * @return ProjectConfig 52 | */ 53 | private function getConfig() 54 | { 55 | return $this->config; 56 | } 57 | 58 | /** 59 | * @return string 60 | * @throws \DomainException 61 | */ 62 | private function getMageClassFilePath() 63 | { 64 | $mageFileCheck = true; 65 | 66 | if (!is_file($this->mageClassFilePath)) { 67 | $message = "{$this->mageClassFilePath} is not a file."; 68 | $mageFileCheck = false; 69 | } elseif (!is_readable($this->mageClassFilePath)) { 70 | $message = "{$this->mageClassFilePath} is not readable."; 71 | $mageFileCheck = false; 72 | } elseif (!is_writable($this->mageClassFilePath)) { 73 | $message = "{$this->mageClassFilePath} is not writable."; 74 | $mageFileCheck = false; 75 | } 76 | 77 | if (!$mageFileCheck) { 78 | throw new \DomainException($message); 79 | } 80 | 81 | return $this->mageClassFilePath; 82 | } 83 | 84 | /** 85 | * Path to the Mage.php file which the patch will be applied on. 86 | * 87 | * @param string $mageClassFilePath 88 | */ 89 | private function setMageClassFilePath($mageClassFilePath) 90 | { 91 | $this->mageClassFilePath = $mageClassFilePath; 92 | } 93 | 94 | /** 95 | * @return bool 96 | */ 97 | private function isPatchAlreadyApplied() 98 | { 99 | return strpos(file_get_contents($this->getMageClassFilePath()), self::PATCH_MARK) !== false; 100 | } 101 | 102 | /** 103 | * @return bool 104 | */ 105 | public function canApplyPatch() 106 | { 107 | // check the config first 108 | if (!$this->getConfig()->mustApplyBootstrapPatch()) { 109 | $message = "Magento autoloader patching skipped because of configuration flag"; 110 | $result = false; 111 | } elseif ($this->isPatchAlreadyApplied()) { 112 | $message = "{$this->getMageClassFilePath()} was already patched"; 113 | $result = false; 114 | } else { 115 | $result = true; 116 | $message = "Autoloader patch to {$this->getMageClassFilePath()} was applied successfully"; 117 | } 118 | 119 | $this->getIo()->write($message); 120 | 121 | return $result; 122 | } 123 | 124 | /** 125 | * @return bool 126 | */ 127 | public function patch() 128 | { 129 | return $this->canApplyPatch() ? $this->writeComposerAutoloaderPatch() : false; 130 | } 131 | 132 | /** 133 | * @return string 134 | */ 135 | protected function getAppPath() 136 | { 137 | return $this->getConfig()->getMagentoRootDir() . '/app'; 138 | } 139 | 140 | /** 141 | * @return bool 142 | */ 143 | protected function writeComposerAutoloaderPatch() 144 | { 145 | $mageFileContent = file($this->getMageClassFilePath()); 146 | 147 | $mageFileBootstrapPart = ''; 148 | $mageFileClassDeclarationPart = ''; 149 | $isBootstrapPart = true; 150 | 151 | foreach ($mageFileContent as $row) { 152 | if ($isBootstrapPart) { 153 | $mageFileBootstrapPart .= $row; 154 | } else { 155 | $mageFileClassDeclarationPart .= $row; 156 | } 157 | if (strpos($row, 'Varien_Autoload') === 0) { 158 | $isBootstrapPart = false; 159 | } 160 | } 161 | 162 | $mageFileReplacement = $mageFileBootstrapPart . PHP_EOL 163 | . $this->getAutoloaderPatchString() . PHP_EOL 164 | . $mageFileClassDeclarationPart; 165 | 166 | return file_put_contents($this->getMageClassFilePath(), $mageFileReplacement) !== false; 167 | } 168 | 169 | /** 170 | * @param IOInterface $io 171 | */ 172 | public function setIo(IOInterface $io) 173 | { 174 | $this->io = $io; 175 | } 176 | 177 | /** 178 | * @return IOInterface 179 | */ 180 | public function getIo() 181 | { 182 | if (!$this->io) { 183 | $this->io = new NullIO; 184 | } 185 | return $this->io; 186 | } 187 | 188 | /** 189 | * @return string 190 | */ 191 | private function getAutoloaderPatchString() 192 | { 193 | $patchMark = self::PATCH_MARK; 194 | 195 | // get the vendor folder name from Config, in case it's changed 196 | $vendorFolderName = basename($this->getConfig()->getVendorDir()); 197 | 198 | $autoloadPhp = $vendorFolderName . '/autoload.php'; 199 | 200 | return <<getSourceDir() . '/' . $this->removeTrailingSlash($source); 24 | $destPath = $this->getDestDir() . '/' . $this->removeTrailingSlash($dest); 25 | 26 | if (!is_file($sourcePath) && !is_dir($sourcePath)) { 27 | throw new \ErrorException("Could not find path '$sourcePath'"); 28 | } 29 | 30 | /* 31 | 32 | Assume app/etc exists, app/etc/a does not exist unless specified differently 33 | 34 | OK dir app/etc/a --> link app/etc/a to dir 35 | OK dir app/etc/ --> link app/etc/dir to dir 36 | OK dir app/etc --> link app/etc/dir to dir 37 | 38 | OK dir/* app/etc --> for each dir/$file create a target link in app/etc 39 | OK dir/* app/etc/ --> for each dir/$file create a target link in app/etc 40 | OK dir/* app/etc/a --> for each dir/$file create a target link in app/etc/a 41 | OK dir/* app/etc/a/ --> for each dir/$file create a target link in app/etc/a 42 | 43 | OK file app/etc --> link app/etc/file to file 44 | OK file app/etc/ --> link app/etc/file to file 45 | OK file app/etc/a --> link app/etc/a to file 46 | OK file app/etc/a --> if app/etc/a is a file throw exception unless force is set, in that case rm and see above 47 | OK file app/etc/a/ --> link app/etc/a/file to file regardless if app/etc/a existst or not 48 | 49 | */ 50 | 51 | // Symlink already exists 52 | if (is_link($destPath)) { 53 | if (realpath(readlink($destPath)) == realpath($sourcePath)) { 54 | // .. and is equal to current source-link 55 | return true; 56 | } 57 | unlink($destPath); 58 | } 59 | 60 | // Create all directories up to one below the target if they don't exist 61 | $destDir = dirname($destPath); 62 | if (!file_exists($destDir)) { 63 | mkdir($destDir, 0777, true); 64 | } 65 | 66 | // Handle source to dir linking, 67 | // e.g. Namespace_Module.csv => app/locale/de_DE/ 68 | // Namespace/ModuleDir => Namespace/ 69 | // Namespace/ModuleDir => Namespace/, but Namespace/ModuleDir may exist 70 | // Namespace/ModuleDir => Namespace/ModuleDir, but ModuleDir may exist 71 | 72 | if (file_exists($destPath) && is_dir($destPath)) { 73 | if (basename($sourcePath) === basename($destPath)) { 74 | if ($this->isForced()) { 75 | $this->filesystem->remove($destPath); 76 | } else { 77 | throw new \ErrorException("Target $dest already exists (set extra.magento-force to override)"); 78 | } 79 | } else { 80 | $destPath .= '/' . basename($source); 81 | } 82 | return $this->create($source, substr($destPath, strlen($this->getDestDir()) + 1)); 83 | } 84 | 85 | // From now on $destPath can't be a directory, that case is already handled 86 | 87 | // If file exists and force is not specified, throw exception unless FORCE is set 88 | // existing symlinks are already handled 89 | if (file_exists($destPath)) { 90 | if ($this->isForced()) { 91 | unlink($destPath); 92 | } else { 93 | throw new \ErrorException( 94 | "Target $dest already exists and is not a symlink (set extra.magento-force to override)" 95 | ); 96 | } 97 | } 98 | 99 | $relSourcePath = $this->getRelativePath($destPath, $sourcePath); 100 | 101 | // Create symlink 102 | $destPath = str_replace('\\', '/', $destPath); 103 | if (false === $this->symlink($relSourcePath, $destPath, $sourcePath)) { 104 | $msg = "An error occured while creating symlink\n" . $relSourcePath . " -> " . $destPath; 105 | if ('\\' === DIRECTORY_SEPARATOR) { 106 | $msg .= "\nDo you have admin privileges?"; 107 | } 108 | throw new \ErrorException($msg); 109 | } 110 | 111 | // Check we where able to create the symlink 112 | // if (false === $destPath = @readlink($destPath)) { 113 | // throw new \ErrorException("Symlink $destPath points to target $destPath"); 114 | // } 115 | $this->addDeployedFile($destPath); 116 | 117 | return true; 118 | } 119 | 120 | /** 121 | * @param $relSourcePath 122 | * @param $destPath 123 | * @param $absSourcePath 124 | * 125 | * @return bool 126 | */ 127 | protected function symlink($relSourcePath, $destPath, $absSourcePath) 128 | { 129 | $sourcePath = $relSourcePath; 130 | // use console native windows tool to create relative windows symlinks 131 | if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { 132 | $sourcePath = str_replace('/', '\\', $sourcePath); 133 | $symlinkDir = dirname($destPath); 134 | $currentDir = getcwd(); 135 | chdir($symlinkDir); 136 | 137 | $flag = is_dir($symlinkDir . '\\' . $sourcePath) ? '/D ' : ''; 138 | $pathInfo = pathinfo($destPath); 139 | $res = exec('mklink ' . $flag . $pathInfo['basename'] .' '. $sourcePath); 140 | 141 | chdir($currentDir); 142 | return $res; 143 | } else { 144 | return symlink($sourcePath, $destPath); 145 | } 146 | } 147 | 148 | /** 149 | * Returns the relative path from $from to $to 150 | * 151 | * This is utility method for symlink creation. 152 | * Orig Source: http://stackoverflow.com/a/2638272/485589 153 | */ 154 | public function getRelativePath($from, $to) 155 | { 156 | $from = str_replace('\\', '/', $from); 157 | $to = str_replace('\\', '/', $to); 158 | 159 | $from = str_replace(array('/./', '//'), '/', $from); 160 | $to = str_replace(array('/./', '//'), '/', $to); 161 | 162 | // calculate relative link from realpath $from, to handle cases where $from folder is already inside another symlink 163 | // e.g. when symlinking files from one module to another 164 | if (\file_exists($from)) { 165 | $from = \realpath($from); 166 | } elseif(\file_exists(dirname($from))) { 167 | $from = \realpath(dirname($from)) . '/' . \basename($from); 168 | } 169 | 170 | $from = explode('/', $from); 171 | $to = explode('/', $to); 172 | 173 | $relPath = $to; 174 | 175 | foreach ($from as $depth => $dir) { 176 | // find first non-matching dir 177 | if ($dir === $to[$depth]) { 178 | // ignore this directory 179 | array_shift($relPath); 180 | } else { 181 | // get number of remaining dirs to $from 182 | $remaining = count($from) - $depth; 183 | if ($remaining > 1) { 184 | // add traversals up to first matching dir 185 | $padLength = (count($relPath) + $remaining - 1) * -1; 186 | $relPath = array_pad($relPath, $padLength, '..'); 187 | break; 188 | } else { 189 | $relPath[0] = './' . $relPath[0]; 190 | } 191 | } 192 | } 193 | return implode('/', $relPath); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/ModuleManager.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class ModuleManager 21 | { 22 | /** 23 | * @var InstalledPackageRepositoryInterface 24 | */ 25 | protected $installedPackageRepository; 26 | 27 | /** 28 | * @var EventManager 29 | */ 30 | protected $eventManager; 31 | 32 | /** 33 | * @var ProjectConfig 34 | */ 35 | protected $config; 36 | 37 | /** 38 | * @var UnInstallStrategyInterface 39 | */ 40 | protected $unInstallStrategy; 41 | 42 | /** 43 | * @var InstallStrategyFactory 44 | */ 45 | protected $installStrategyFactory; 46 | 47 | /** 48 | * @param InstalledPackageRepositoryInterface $installedRepository 49 | * @param EventManager $eventManager 50 | * @param ProjectConfig $config 51 | * @param UnInstallStrategyInterface $unInstallStrategy 52 | * @param InstallStrategyFactory $installStrategyFactory 53 | */ 54 | public function __construct( 55 | InstalledPackageRepositoryInterface $installedRepository, 56 | EventManager $eventManager, 57 | ProjectConfig $config, 58 | UnInstallStrategyInterface $unInstallStrategy, 59 | InstallStrategyFactory $installStrategyFactory 60 | ) { 61 | $this->installedPackageRepository = $installedRepository; 62 | $this->eventManager = $eventManager; 63 | $this->config = $config; 64 | $this->unInstallStrategy = $unInstallStrategy; 65 | $this->installStrategyFactory = $installStrategyFactory; 66 | } 67 | 68 | /** 69 | * @param array $currentComposerInstalledPackages 70 | * @return array 71 | */ 72 | public function updateInstalledPackages(array $currentComposerInstalledPackages) 73 | { 74 | $packagesToRemove = $this->getRemoves( 75 | $currentComposerInstalledPackages, 76 | $this->installedPackageRepository->findAll() 77 | ); 78 | 79 | $packagesToInstall = $this->getInstalls($currentComposerInstalledPackages); 80 | 81 | $this->doRemoves($packagesToRemove); 82 | $this->doInstalls($packagesToInstall); 83 | 84 | return array( 85 | $packagesToRemove, 86 | $packagesToInstall 87 | ); 88 | } 89 | 90 | /** 91 | * @param PackageInterface[] $packagesToInstall 92 | */ 93 | public function doInstalls(array $packagesToInstall) 94 | { 95 | foreach ($packagesToInstall as $install) { 96 | $installStrategy = $this->installStrategyFactory->make( 97 | $install, 98 | $this->getPackageSourceDirectory($install) 99 | ); 100 | 101 | $deployEntry = new Entry(); 102 | $deployEntry->setPackageName($install->getPrettyName()); 103 | $deployEntry->setDeployStrategy($installStrategy); 104 | $this->eventManager->dispatch( 105 | new PackageDeployEvent('pre-package-deploy', $deployEntry) 106 | ); 107 | $files = $installStrategy->deploy()->getDeployedFiles(); 108 | $this->eventManager->dispatch( 109 | new PackageDeployEvent('post-package-deploy', $deployEntry) 110 | ); 111 | $this->installedPackageRepository->add(new InstalledPackage( 112 | $install->getName(), 113 | $this->createVersion($install), 114 | $files 115 | )); 116 | } 117 | } 118 | 119 | /** 120 | * @param InstalledPackage[] $packagesToRemove 121 | */ 122 | public function doRemoves(array $packagesToRemove) 123 | { 124 | foreach ($packagesToRemove as $remove) { 125 | $this->eventManager->dispatch(new PackageUnInstallEvent('pre-package-uninstall', $remove)); 126 | $this->unInstallStrategy->unInstall($remove->getInstalledFiles()); 127 | $this->eventManager->dispatch(new PackageUnInstallEvent('post-package-uninstall', $remove)); 128 | $this->installedPackageRepository->remove($remove); 129 | } 130 | } 131 | 132 | /** 133 | * @param PackageInterface[] $currentComposerInstalledPackages 134 | * @param InstalledPackage[] $magentoInstalledPackages 135 | * @return InstalledPackage[] 136 | */ 137 | public function getRemoves(array $currentComposerInstalledPackages, array $magentoInstalledPackages) 138 | { 139 | //make the package names as the array keys 140 | if (count($currentComposerInstalledPackages)) { 141 | $currentComposerInstalledPackages = array_combine( 142 | array_map( 143 | function (PackageInterface $package) { 144 | return $package->getName(); 145 | }, 146 | $currentComposerInstalledPackages 147 | ), 148 | $currentComposerInstalledPackages 149 | ); 150 | } 151 | return array_filter( 152 | $magentoInstalledPackages, 153 | function (InstalledPackage $package) use ($currentComposerInstalledPackages) { 154 | if (!isset($currentComposerInstalledPackages[$package->getName()])) { 155 | return true; 156 | } 157 | 158 | $composerPackage = $currentComposerInstalledPackages[$package->getName()]; 159 | return $package->getVersion() !== $this->createVersion($composerPackage); 160 | } 161 | ); 162 | } 163 | 164 | /** 165 | * @param PackageInterface[] $currentComposerInstalledPackages 166 | * @return PackageInterface[] 167 | */ 168 | public function getInstalls(array $currentComposerInstalledPackages) 169 | { 170 | $repo = $this->installedPackageRepository; 171 | $packages = array_filter($currentComposerInstalledPackages, function (PackageInterface $package) use ($repo) { 172 | return !$repo->has($package->getName(), $this->createVersion($package)); 173 | }); 174 | 175 | $config = $this->config; 176 | usort($packages, function (PackageInterface $aObject, PackageInterface $bObject) use ($config) { 177 | $a = $config->getModuleSpecificSortValue($aObject->getName()); 178 | $b = $config->getModuleSpecificSortValue($bObject->getName()); 179 | if ($a == $b) { 180 | return strcmp($aObject->getName(), $bObject->getName()); 181 | /** 182 | * still changes sort order and breaks a test, so for now strcmp as workaround 183 | * to keep the test working. 184 | */ 185 | // return 0; 186 | } 187 | return ($a < $b) ? -1 : 1; 188 | }); 189 | 190 | return $packages; 191 | } 192 | 193 | /** 194 | * @param PackageInterface $package 195 | * @return string 196 | */ 197 | private function getPackageSourceDirectory(PackageInterface $package) 198 | { 199 | if ($package instanceof RootPackage) { 200 | $path = sprintf("%s/..", $this->config->getVendorDir()); 201 | } else { 202 | $path = sprintf("%s/%s", $this->config->getVendorDir(), $package->getPrettyName()); 203 | } 204 | 205 | $targetDir = $package->getTargetDir(); 206 | 207 | if ($targetDir) { 208 | $path = sprintf("%s/%s", $path, $targetDir); 209 | } 210 | 211 | $path = realpath($path); 212 | return $path; 213 | } 214 | 215 | /** 216 | * Create a version string which is unique. dev-master 217 | * packages report a version of 9999999-dev. We need a unique version 218 | * so we can detect changes. here we use the source reference which 219 | * in the case of git is the commit hash 220 | * 221 | * @param PackageInterface $package 222 | * 223 | * @return string 224 | */ 225 | private function createVersion(PackageInterface $package) 226 | { 227 | $version = $package->getVersion(); 228 | 229 | if (null !== $package->getSourceReference()) { 230 | $version = sprintf('%s-%s', $version, $package->getSourceReference()); 231 | } 232 | 233 | return $version; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Cotya/magento-composer-installer.svg)](https://travis-ci.org/Cotya/magento-composer-installer) 2 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/1bm54s9jv3603xl5?svg=true)](https://ci.appveyor.com/project/Flyingmana/magento-composer-installer-396) 3 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Cotya/magento-composer-installer/badges/quality-score.png)](https://scrutinizer-ci.com/g/Cotya/magento-composer-installer/) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/Cotya/magento-composer-installer/badges/coverage.png)](https://scrutinizer-ci.com/g/Cotya/magento-composer-installer/) 5 | [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=284872)](https://www.bountysource.com/trackers/284872-magento-hackathon-magento-composer-installer?utm_source=284872&utm_medium=shield&utm_campaign=TRACKER_BADGE) 6 | 7 | !!! support the maintainer of this project via Patreon: [https://www.patreon.com/Flyingmana](https://www.patreon.com/Flyingmana) 8 | 9 | [![Become a Patreon](doc/become_a_patron_button.png)](https://www.patreon.com/Flyingmana) 10 | 11 | # Magento Composer Installer 12 | 13 | The purpose of this project is to 14 | enable [composer](https://github.com/composer/composer) to install Magento modules, 15 | and automatically integrate them into a Magento installation and add Composer's vendor autoloader 16 | ability to Magento's so that Composer-compatible 3rd party tools can be used. 17 | 18 | If you want to install the Magento Core, you should try 19 | [AydinHassan/magento-core-composer-installer](https://github.com/AydinHassan/magento-core-composer-installer) 20 | as an additional plugin. 21 | 22 | We strongly recommend you to also read the general composer documentation at [getcomposer.org](https://getcomposer.org) 23 | 24 | Also you should see: 25 | 26 | * [Using composer correctly (confoo) by Igor Wiedler](https://speakerdeck.com/igorw/using-composer-correctly-confoo) 27 | 28 | 29 | ## Magento 2 30 | 31 | Congratulation to be working with Magento 2. Don't try to use it together with this project. 32 | Your princess is in [another Castle](http://devdocs.magento.com/guides/v2.0/install-gde/prereq/integrator_install.html#integrator-first-composer-ce) 33 | 34 | ## Project Details 35 | 36 | This project only covers the custom installer for composer. If you have problems with outdated versions, 37 | need to install magento connect modules or similar, you need to look for [packages.firegento.com](https://packages.firegento.com/) 38 | which you probably should add as composer repository (globally) 39 | 40 | ```composer config -g repositories.firegento composer https://packages.firegento.com``` 41 | 42 | ### supported PHP Versions 43 | 44 | We don't officially support PHP versions which are [End of Life](https://secure.php.net/eol.php) means which they don't get [security patches](https://secure.php.net/supported-versions.php) anymore. Even if the install requirement still allows them. 45 | This will change, as soon as someone is willing to pay for supporting them. 46 | 47 | ### support contacts 48 | 49 | If you have problems please have patience, as normal support is done during free time. 50 | If you are willing to pay to get your problem fixed, communicate this from the start to get faster responses. 51 | 52 | If you need consulting, support, training or help regarding Magento and Composer, 53 | you have the chance to hire one of the following people/companies. 54 | 55 | * Daniel Fahlke aka Flyingmana (Maintainer): flyingmana@googlemail.com [@Flyingmana](https://twitter.com/Flyingmana) 56 | * brandung - Magento Team: magento-team@brandung.de (http://brandung.de) 57 | 58 | other support contacts 59 | 60 | * irc: freenode the channels #magento-composer #magento-reddit and for german speaking people #magento-de 61 | * twitter: [@firegento](https://twitter.com/firegento) 62 | 63 | ### changelog 64 | 65 | See [CHANGELOG.md](CHANGELOG.md). 66 | 67 | ======= 68 | ## Known issues 69 | 70 | ### need to redeploy packages 71 | 72 | earlier we suggested the use of the command integrator package, that is not needed anymore. 73 | ```composer.phar run-script post-install-cmd -vvv -- --redeploy``` 74 | This does remove all deployed files and redeploys every module 75 | 76 | ### using non default autoloading 77 | 78 | we handle this topic in our [FAQ](doc/FAQ.md). 79 | 80 | ### Timeouts and slow downloading. 81 | 82 | Mostly caused by outages of Github, Repositories or the Internet. This is a common problem with having all 83 | packages remote. 84 | 85 | For all of this issues you can make use of the commercial [Toran Proxy](https://toranproxy.com/). 86 | It also allows hosting of private packages and speeds up the whole downloading process. 87 | 88 | Another alternative is to look into [Satis](https://github.com/composer/satis), bare git mirrors and repository aliasing. 89 | 90 | Another way to speedup downloads over ssh (also interesting for satis users) is to improve your ssh configs. 91 | At least for newer versions of openSSH you can add the following to your ```.ssh/config``` to reuse previous connections. 92 | ``` 93 | Host * 94 | ControlPath ~/.ssh/controlmasters/%r@%h:%p 95 | ControlMaster auto 96 | ControlPersist 10m 97 | ``` 98 | 99 | also you need to create the ```controlmasters``` directory: 100 | ```sh 101 | mkdir ~/.ssh/controlmasters 102 | chmod go-xr ~/.ssh/controlmasters 103 | ``` 104 | 105 | further information can be found on [wikibooks](https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing) 106 | 107 | ## Usage 108 | 109 | ### Update the Installer 110 | 111 | as this is a composer plugin, you should only use these two commands to update the installer 112 | 113 | ``` 114 | composer require --no-update magento-hackathon/magento-composer-installer="3.2.*" 115 | composer update --no-plugins --no-scripts magento-hackathon/magento-composer-installer 116 | ``` 117 | 118 | the second command needs maybe a `--with-dependencies` 119 | Depending on your workflow with composer, you may want to use more explicit versions 120 | 121 | ### Install a module in your project 122 | 123 | make sure to use [the public Magento module repository](https://packages.firegento.com) as composer repository: 124 | 125 | ```composer config -g repositories.firegento composer https://packages.firegento.com``` 126 | 127 | configure your `magento root dir`, the directory where your magento resides: 128 | ```composer config extra.magento-root-dir "htdocs/"``` 129 | 130 | an example how your project ```composer.json``` could look like: 131 | 132 | ```json 133 | { 134 | "repositories": [ 135 | { 136 | "type": "composer", 137 | "url": "https://packages.firegento.com" 138 | } 139 | ], 140 | "extra":{ 141 | "magento-root-dir": "htdocs/" 142 | } 143 | } 144 | ``` 145 | 146 | ### Auto add files to .gitignore 147 | 148 | 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: 149 | 150 | ```json 151 | { 152 | "extra":{ 153 | "magento-root-dir": "htdocs/", 154 | "auto-append-gitignore": true 155 | } 156 | } 157 | ``` 158 | 159 | 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 160 | describing the module name for clarity. 161 | 162 | Multiple deploys will not add additional lines to your `.gitignore`, they will only ever be added once. 163 | 164 | 165 | ### Adding Composer's autoloader to Magento 166 | 167 | Documentation available [here](doc/Autoloading.md). 168 | 169 | ### Overwriting a production setting (DevMode) 170 | 171 | ```json 172 | { 173 | "extra":{ 174 | "magento-deploystrategy": "copy", 175 | "magento-deploystrategy-dev": "symlink" 176 | } 177 | } 178 | ``` 179 | 180 | Example in [devmode doc](doc/DevMode.md). 181 | 182 | 183 | ### Include your project in deployment 184 | 185 | When the magento-composer-installer is run, it only looks for magento-modules among your project's dependencies. Thus, if 186 | your project is a magento-module and you want to test it, you will need a second `composer.json` for deployment, 187 | where your project is configured as a required package. 188 | 189 | If you wish to deploy your project's files (a.k.a. root package), too, you need to setup your `composer.json` as follows: 190 | 191 | ``` 192 | { 193 | "type": "magento-module", 194 | ... 195 | "extra": { 196 | "magento-root-dir": "htdocs/", 197 | "include-root-package": true 198 | } 199 | } 200 | ``` 201 | 202 | ### Testing 203 | 204 | First clone the magento-composer-installer, then install the dev-stuff (installed by default): 205 | 206 | ``` 207 | ./bin/composer.phar install 208 | ``` 209 | 210 | then run ```vendor/bin/phpunit``` in project-root directory. 211 | 212 | Note: Windows users please run ```phpunit``` with Administrator permissions. 213 | 214 | 215 | ## Further Information 216 | 217 | * [FAQ](doc/FAQ.md) 218 | * [Make a Magento module installable with composer](doc/MakeAModuleInstallableWithComposer.md) 219 | * [About File Mapping like for example modman](doc/Mapping.md) 220 | * [About Deploying files into your Magento root and possible configs](doc/Deploy.md) 221 | 222 | ### External Links 223 | 224 | * [Composer How to Screencast](http://www.youtube.com/watch?v=m_yprtQiFgk) 225 | * [Introducing Composer Blog on Magebase.com](http://magebase.com/magento-tutorials/composer-with-magento/) 226 | * [Magento, Composer and Symfonys Dependency Injection](http://www.piotrbelina.com/magento-composer-and-dependency-injection/) 227 | * [Using Composer for Magento(at engineyard)](https://blog.engineyard.com/2014/composer-for-magento) 228 | 229 | ### Core Contributors 230 | 231 | * Daniel Fahlke aka Flyingmana (Maintainer) 232 | * Jörg Weller 233 | * Karl Spies 234 | * Tobias Vogt 235 | * David Fuhr 236 | * Amir Tchavoshinia 237 | * Vinai Kopp (Maintainer) 238 | 239 | ## Thank You 240 | 241 | There are a few companies we want to thank for supporting this project in one way or another. 242 | 243 | #####[digital.manufaktur GmbH](https://www.digitalmanufaktur.com/) 244 | 245 | Teached me (Flyingmana) most I know about Magento and 246 | paid my participation for the hackathon were the installer got created. 247 | 248 | #####[melovely](http://www.melovely.de/) 249 | 250 | Support me (Flyingmana) as my current employer very much in my work on everything composer related. 251 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/ProjectConfig.php: -------------------------------------------------------------------------------- 1 | extra = $extra; 66 | $this->composerConfig = $composerConfig; 67 | 68 | $this->isDevMode = false; 69 | 70 | if (!is_null($projectConfig = $this->fetchVarFromConfigArray($this->extra, self::MAGENTO_PROJECT_KEY))) { 71 | $this->applyMagentoConfig($projectConfig); 72 | } 73 | } 74 | 75 | /** 76 | * @param array $array 77 | * @param string|integer $key 78 | * @param mixed $default 79 | * 80 | * @return mixed 81 | */ 82 | protected function fetchVarFromConfigArray($array, $key, $default = null) 83 | { 84 | $array = (array)$array; 85 | $result = $default; 86 | 87 | if ($this->isDevMode && isset($array[$key . self::EXTRA_DEV_MODE_APPEND])) { 88 | $result = $array[$key . self::EXTRA_DEV_MODE_APPEND]; 89 | } elseif (isset($array[$key])) { 90 | $result = $array[$key]; 91 | } 92 | 93 | return $result; 94 | } 95 | 96 | /** 97 | * @param $key 98 | * @param null $default 99 | * 100 | * @return null 101 | */ 102 | protected function fetchVarFromExtraConfig($key, $default = null) 103 | { 104 | return $this->fetchVarFromConfigArray($this->extra, $key, $default); 105 | } 106 | 107 | /** 108 | * @param $config 109 | */ 110 | protected function applyMagentoConfig($config) 111 | { 112 | $this->libraryPath = $this->fetchVarFromConfigArray($config, 'libraryPath'); 113 | $this->libraryPackages = $this->fetchVarFromConfigArray($config, 'libraries'); 114 | } 115 | 116 | /** 117 | * @return mixed 118 | */ 119 | public function getLibraryPath() 120 | { 121 | return $this->libraryPath; 122 | } 123 | 124 | /** 125 | * @param $packagename 126 | * 127 | * @return null 128 | */ 129 | public function getLibraryConfigByPackagename($packagename) 130 | { 131 | return $this->fetchVarFromConfigArray($this->libraryPackages, $packagename); 132 | } 133 | 134 | /** 135 | * @return string 136 | */ 137 | public function getMagentoRootDir() 138 | { 139 | return rtrim( 140 | trim( 141 | $this->fetchVarFromExtraConfig( 142 | self::MAGENTO_ROOT_DIR_KEY, 143 | self::DEFAULT_MAGENTO_ROOT_DIR 144 | ) 145 | ), 146 | DIRECTORY_SEPARATOR 147 | ); 148 | } 149 | 150 | /** 151 | * @param $rootDir 152 | */ 153 | public function setMagentoRootDir($rootDir) 154 | { 155 | $this->updateExtraConfig(self::MAGENTO_ROOT_DIR_KEY, rtrim(trim($rootDir), DIRECTORY_SEPARATOR)); 156 | } 157 | 158 | /** 159 | * @return bool 160 | */ 161 | public function hasMagentoRootDir() 162 | { 163 | return $this->hasExtraField(self::MAGENTO_ROOT_DIR_KEY); 164 | } 165 | 166 | public function getMagentoVarDir() 167 | { 168 | return $this->getMagentoRootDir().'var'.DIRECTORY_SEPARATOR; 169 | } 170 | 171 | /** 172 | * @param $deployStrategy 173 | */ 174 | public function setDeployStrategy($deployStrategy) 175 | { 176 | $this->updateExtraConfig(self::MAGENTO_DEPLOY_STRATEGY_KEY, trim($deployStrategy)); 177 | } 178 | 179 | /** 180 | * @return string 181 | */ 182 | public function getDeployStrategy() 183 | { 184 | return trim((string)$this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_STRATEGY_KEY)); 185 | } 186 | 187 | /** 188 | * @return bool 189 | */ 190 | public function hasDeployStrategy() 191 | { 192 | return $this->hasExtraField(self::MAGENTO_DEPLOY_STRATEGY_KEY); 193 | } 194 | 195 | /** 196 | * @return array 197 | */ 198 | public function getDeployStrategyOverwrite() 199 | { 200 | return (array)$this->transformArrayKeysToLowerCase( 201 | $this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_STRATEGY_OVERWRITE_KEY, array()) 202 | ); 203 | } 204 | 205 | /** 206 | * @return bool 207 | */ 208 | public function hasDeployStrategyOverwrite() 209 | { 210 | return $this->hasExtraField(self::MAGENTO_DEPLOY_STRATEGY_OVERWRITE_KEY); 211 | } 212 | 213 | /** 214 | * @param $packagename 215 | * 216 | * @return integer 217 | */ 218 | public function getModuleSpecificDeployStrategy($packagename) 219 | { 220 | $moduleSpecificDeployStrategies = $this->getDeployStrategyOverwrite(); 221 | 222 | $strategyName = $this->getDeployStrategy(); 223 | if (isset($moduleSpecificDeployStrategies[$packagename])) { 224 | $strategyName = $moduleSpecificDeployStrategies[$packagename]; 225 | } 226 | return $strategyName; 227 | } 228 | 229 | /** 230 | * @param $packagename 231 | * 232 | * @return integer 233 | */ 234 | public function getModuleSpecificSortValue($packagename) 235 | { 236 | $sortPriorityArray = $this->fetchVarFromExtraConfig(self::SORT_PRIORITY_KEY, array()); 237 | if (isset($sortPriorityArray[$packagename])) { 238 | $sortValue = $sortPriorityArray[$packagename]; 239 | } else { 240 | $sortValue = 100; 241 | if ($this->getModuleSpecificDeployStrategy($packagename) === 'copy' 242 | || $this->getModuleSpecificDeployStrategy($packagename) === 'move') { 243 | $sortValue++; 244 | } 245 | } 246 | return $sortValue; 247 | } 248 | 249 | /** 250 | * @return array 251 | */ 252 | public function getMagentoDeployIgnore() 253 | { 254 | return (array)$this->transformArrayKeysToLowerCase( 255 | $this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_IGNORE_KEY) 256 | ); 257 | } 258 | 259 | /** 260 | * @param $packagename 261 | * 262 | * @return array 263 | */ 264 | public function getModuleSpecificDeployIgnores($packagename) 265 | { 266 | $moduleSpecificDeployIgnores = array(); 267 | if ($this->hasMagentoDeployIgnore()) { 268 | $magentoDeployIgnore = $this->getMagentoDeployIgnore(); 269 | if (isset($magentoDeployIgnore['*'])) { 270 | $moduleSpecificDeployIgnores = $magentoDeployIgnore['*']; 271 | } 272 | if (isset($magentoDeployIgnore[$packagename])) { 273 | $moduleSpecificDeployIgnores = array_merge( 274 | $moduleSpecificDeployIgnores, 275 | $magentoDeployIgnore[$packagename] 276 | ); 277 | } 278 | } 279 | return $moduleSpecificDeployIgnores; 280 | } 281 | 282 | /** 283 | * @return bool 284 | */ 285 | public function hasMagentoDeployIgnore() 286 | { 287 | return $this->hasExtraField(self::MAGENTO_DEPLOY_IGNORE_KEY); 288 | } 289 | 290 | /** 291 | * @param $magentoForce 292 | */ 293 | public function setMagentoForce($magentoForce) 294 | { 295 | $this->updateExtraConfig(self::MAGENTO_FORCE_KEY, trim($magentoForce)); 296 | } 297 | 298 | /** 299 | * @return string 300 | */ 301 | public function getMagentoForce() 302 | { 303 | return (bool)$this->fetchVarFromExtraConfig(self::MAGENTO_FORCE_KEY); 304 | } 305 | 306 | /** 307 | * @return bool 308 | */ 309 | public function hasMagentoForce() 310 | { 311 | return $this->hasExtraField(self::MAGENTO_FORCE_KEY); 312 | } 313 | 314 | public function getMagentoForceByPackageName($packagename) 315 | { 316 | return $this->getMagentoForce(); 317 | } 318 | 319 | /** 320 | * @return bool 321 | */ 322 | public function hasAutoAppendGitignore() 323 | { 324 | return $this->hasExtraField(self::AUTO_APPEND_GITIGNORE_KEY); 325 | } 326 | 327 | /** 328 | * @return array 329 | */ 330 | public function getPathMappingTranslations() 331 | { 332 | return (array)$this->fetchVarFromExtraConfig(self::PATH_MAPPINGS_TRANSLATIONS_KEY); 333 | } 334 | 335 | /** 336 | * @return bool 337 | */ 338 | public function hasPathMappingTranslations() 339 | { 340 | return $this->hasExtraField(self::PATH_MAPPINGS_TRANSLATIONS_KEY); 341 | } 342 | 343 | /** 344 | * @return array 345 | */ 346 | public function getMagentoDeployOverwrite() 347 | { 348 | return (array)$this->transformArrayKeysToLowerCase( 349 | $this->fetchVarFromExtraConfig(self::MAGENTO_DEPLOY_STRATEGY_OVERWRITE_KEY) 350 | ); 351 | } 352 | 353 | public function getMagentoMapOverwrite() 354 | { 355 | return $this->transformArrayKeysToLowerCase( 356 | (array)$this->fetchVarFromExtraConfig(self::MAGENTO_MAP_OVERWRITE_KEY) 357 | ); 358 | } 359 | protected function hasExtraField($key) 360 | { 361 | return (bool)!is_null($this->fetchVarFromExtraConfig($key)); 362 | } 363 | 364 | /** 365 | * @param $key 366 | * @param $value 367 | */ 368 | protected function updateExtraConfig($key, $value) 369 | { 370 | $this->extra[$key] = $value; 371 | $this->updateExtraJson(); 372 | } 373 | 374 | /** 375 | * @throws \Exception 376 | */ 377 | protected function updateExtraJson() 378 | { 379 | $composerFile = Factory::getComposerFile(); 380 | 381 | if (!file_exists($composerFile) && !file_put_contents($composerFile, "{\n}\n")) { 382 | throw new Exception(sprintf('%s could not be created', $composerFile)); 383 | } 384 | 385 | if (!is_readable($composerFile)) { 386 | throw new Exception(sprintf('%s is not readable', $composerFile)); 387 | } 388 | 389 | if (!is_writable($composerFile)) { 390 | throw new Exception(sprintf('%s is not writable', $composerFile)); 391 | } 392 | 393 | $json = new JsonFile($composerFile); 394 | $composer = $json->read(); 395 | 396 | $baseExtra = array_key_exists(self::EXTRA_KEY, $composer) 397 | ? $composer[self::EXTRA_KEY] 398 | : array(); 399 | 400 | if (!$this->updateFileCleanly($json, $baseExtra, $this->extra, self::EXTRA_KEY)) { 401 | foreach ($this->extra as $key => $value) { 402 | $baseExtra[$key] = $value; 403 | } 404 | 405 | $composer[self::EXTRA_KEY] = $baseExtra; 406 | $json->write($composer); 407 | } 408 | } 409 | 410 | /** 411 | * @param JsonFile $json 412 | * @param array $base 413 | * @param array $new 414 | * @param $rootKey 415 | * 416 | * @return bool 417 | */ 418 | private function updateFileCleanly(JsonFile $json, array $base, array $new, $rootKey) 419 | { 420 | $contents = file_get_contents($json->getPath()); 421 | 422 | $manipulator = new JsonManipulator($contents); 423 | 424 | foreach ($new as $childKey => $childValue) { 425 | if (!$manipulator->addProperty($rootKey . '.' . $childKey, $childValue)) { 426 | return false; 427 | } 428 | } 429 | 430 | file_put_contents($json->getPath(), $manipulator->getContents()); 431 | 432 | return true; 433 | } 434 | 435 | /** 436 | * @param array $array 437 | * 438 | * @return array 439 | */ 440 | public function transformArrayKeysToLowerCase(array $array) 441 | { 442 | return array_change_key_case($array, CASE_LOWER); 443 | } 444 | 445 | public function getComposerRepositories() 446 | { 447 | return $this->fetchVarFromConfigArray($this->composerConfig, 'repositories', array()); 448 | } 449 | 450 | /** 451 | * Get Composer vendor directory 452 | * 453 | * @return string 454 | */ 455 | public function getVendorDir() 456 | { 457 | return $this->fetchVarFromConfigArray( 458 | isset($this->composerConfig['config']) ? $this->composerConfig['config'] : array(), 459 | 'vendor-dir', 460 | getcwd() . '/vendor' 461 | ); 462 | } 463 | 464 | /** 465 | * @return boolean 466 | */ 467 | public function mustApplyBootstrapPatch() 468 | { 469 | return (bool) $this->fetchVarFromExtraConfig(self::EXTRA_WITH_BOOTSTRAP_PATCH_KEY, true); 470 | } 471 | 472 | /** 473 | * @return boolean 474 | */ 475 | public function skipSuggestComposerRepositories() 476 | { 477 | return (bool) $this->fetchVarFromExtraConfig(self::EXTRA_WITH_SKIP_SUGGEST_KEY, false); 478 | } 479 | 480 | /** 481 | * @param $includeRootPackage 482 | */ 483 | public function setIncludeRootPackage($includeRootPackage) 484 | { 485 | $this->updateExtraConfig(self::INCLUDE_ROOT_PACKAGE_KEY, trim($includeRootPackage)); 486 | } 487 | 488 | /** 489 | * @return bool 490 | */ 491 | public function getIncludeRootPackage() 492 | { 493 | return (bool)$this->fetchVarFromExtraConfig(self::INCLUDE_ROOT_PACKAGE_KEY); 494 | } 495 | 496 | /** 497 | * Get dev mode 498 | * 499 | * @return bool 500 | */ 501 | public function isDevMode() 502 | { 503 | return $this->isDevMode; 504 | } 505 | 506 | /** 507 | * Dev mode 508 | */ 509 | public function setDevMode() 510 | { 511 | $this->isDevMode = true; 512 | } 513 | 514 | /** 515 | * No dev mode 516 | */ 517 | public function setNoDevMode() 518 | { 519 | $this->isDevMode = false; 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Deploystrategy/DeploystrategyAbstract.php: -------------------------------------------------------------------------------- 1 | destDir = $destDir; 88 | $this->sourceDir = $sourceDir; 89 | $this->filesystem = new Filesystem; 90 | } 91 | 92 | /** 93 | * Executes the deployment strategy for each mapping 94 | * 95 | * @return \MagentoHackathon\Composer\Magento\Deploystrategy\DeploystrategyAbstract 96 | */ 97 | public function deploy() 98 | { 99 | $this->beforeDeploy(); 100 | foreach ($this->getMappings() as $data) { 101 | list ($source, $dest) = $data; 102 | $this->setCurrentMapping($data); 103 | $this->create($source, $dest); 104 | } 105 | $this->afterDeploy(); 106 | return $this; 107 | } 108 | 109 | /** 110 | * beforeDeploy 111 | * 112 | * @return void 113 | */ 114 | protected function beforeDeploy() 115 | { 116 | } 117 | 118 | /** 119 | * afterDeploy 120 | * 121 | * @return void 122 | */ 123 | protected function afterDeploy() 124 | { 125 | } 126 | 127 | /** 128 | * Removes the module's files in the given path from the target dir 129 | * 130 | * @return \MagentoHackathon\Composer\Magento\Deploystrategy\DeploystrategyAbstract 131 | */ 132 | public function clean() 133 | { 134 | $this->beforeClean(); 135 | foreach ($this->getMappings() as $data) { 136 | list ($source, $dest) = $data; 137 | $this->remove($source, $dest); 138 | $this->rmEmptyDirsRecursive(dirname($dest), $this->getDestDir()); 139 | } 140 | $this->afterClean(); 141 | return $this; 142 | } 143 | 144 | /** 145 | * beforeClean 146 | * 147 | * @return void 148 | */ 149 | protected function beforeClean() 150 | { 151 | } 152 | 153 | /** 154 | * afterClean 155 | * 156 | * @return void 157 | */ 158 | protected function afterClean() 159 | { 160 | } 161 | 162 | /** 163 | * Returns the destination dir of the magento module 164 | * 165 | * @return string 166 | */ 167 | protected function getDestDir() 168 | { 169 | return $this->destDir; 170 | } 171 | 172 | /** 173 | * Returns the current path of the extension 174 | * 175 | * @return mixed 176 | */ 177 | protected function getSourceDir() 178 | { 179 | return $this->sourceDir; 180 | } 181 | 182 | /** 183 | * If set overrides existing files 184 | * 185 | * @return bool 186 | */ 187 | public function isForced() 188 | { 189 | return $this->isForced; 190 | } 191 | 192 | /** 193 | * Setter for isForced property 194 | * 195 | * @param bool $forced 196 | */ 197 | public function setIsForced($forced = true) 198 | { 199 | $this->isForced = (bool)$forced; 200 | } 201 | 202 | /** 203 | * Returns the path mappings to map project's directories to magento's directory structure 204 | * 205 | * @return array 206 | */ 207 | public function getMappings() 208 | { 209 | return $this->mappings; 210 | } 211 | 212 | /** 213 | * Sets path mappings to map project's directories to magento's directory structure 214 | * 215 | * @param array $mappings 216 | */ 217 | public function setMappings(array $mappings) 218 | { 219 | $this->mappings = $mappings; 220 | } 221 | 222 | /** 223 | * Gets the current mapping used on the deployment iteration 224 | * 225 | * @return array 226 | */ 227 | public function getCurrentMapping() 228 | { 229 | return $this->currentMapping; 230 | } 231 | 232 | /** 233 | * Sets the current mapping used on the deployment iteration 234 | * 235 | * @param array $mapping 236 | */ 237 | public function setCurrentMapping($mapping) 238 | { 239 | $this->currentMapping = $mapping; 240 | } 241 | 242 | 243 | /** 244 | * sets the current ignored mappings 245 | * 246 | * @param $ignoredMappings 247 | */ 248 | public function setIgnoredMappings($ignoredMappings) 249 | { 250 | $this->ignoredMappings = $ignoredMappings; 251 | } 252 | 253 | /** 254 | * gets the current ignored mappings 255 | * 256 | * @return array 257 | */ 258 | public function getIgnoredMappings() 259 | { 260 | return $this->ignoredMappings; 261 | } 262 | 263 | 264 | /** 265 | * @param string $destination 266 | * 267 | * @return bool 268 | */ 269 | protected function isDestinationIgnored($destination) 270 | { 271 | $destination = '/' . $destination; 272 | $destination = str_replace('/./', '/', $destination); 273 | $destination = str_replace('//', '/', $destination); 274 | foreach ($this->ignoredMappings as $ignored) { 275 | if (0 === strpos($ignored, $destination)) { 276 | return true; 277 | } 278 | } 279 | return false; 280 | } 281 | 282 | /** 283 | * Add a key value pair to mapping 284 | */ 285 | public function addMapping($key, $value) 286 | { 287 | $this->mappings[] = array($key, $value); 288 | } 289 | 290 | /** 291 | * @param string $path 292 | * @return string 293 | */ 294 | protected function removeLeadingSlash($path) 295 | { 296 | return ltrim($path, '\\/'); 297 | } 298 | 299 | /** 300 | * @param string $path 301 | * @return string 302 | */ 303 | protected function removeTrailingSlash($path) 304 | { 305 | return rtrim($path, '\\/'); 306 | } 307 | 308 | /** 309 | * @param string $path 310 | * @return string 311 | */ 312 | protected function removeLeadingAndTrailingSlash($path) 313 | { 314 | return trim($path, '\\/'); 315 | } 316 | 317 | /** 318 | * Normalize mapping parameters using a glob wildcard. 319 | * 320 | * Delegate the creation of the module's files in the given destination. 321 | * 322 | * @param string $source 323 | * @param string $dest 324 | * 325 | * @throws \ErrorException 326 | * @return bool 327 | */ 328 | public function create($source, $dest) 329 | { 330 | if ($this->isDestinationIgnored($dest)) { 331 | return; 332 | } 333 | 334 | $sourcePath = $this->getSourceDir() . '/' . $this->removeLeadingSlash($source); 335 | $destPath = $this->getDestDir() . '/' . $this->removeLeadingSlash($dest); 336 | 337 | /* List of possible cases, keep around for now, might come in handy again 338 | 339 | Assume app/etc exists, app/etc/a does not exist unless specified differently 340 | 341 | dir app/etc/a/ --> link app/etc/a to dir 342 | dir app/etc/a --> link app/etc/a to dir 343 | dir app/etc/ --> link app/etc/dir to dir 344 | dir app/etc --> link app/etc/dir to dir 345 | 346 | dir/* app/etc --> for each dir/$file create a target link in app/etc 347 | dir/* app/etc/ --> for each dir/$file create a target link in app/etc 348 | dir/* app/etc/a --> for each dir/$file create a target link in app/etc/a 349 | dir/* app/etc/a/ --> for each dir/$file create a target link in app/etc/a 350 | 351 | file app/etc --> link app/etc/file to file 352 | file app/etc/ --> link app/etc/file to file 353 | file app/etc/a --> link app/etc/a to file 354 | file app/etc/a --> if app/etc/a is a file throw exception unless force is set, in that case rm and see above 355 | file app/etc/a/ --> link app/etc/a/file to file regardless if app/etc/a exists or not 356 | 357 | */ 358 | 359 | // Create target directory if it ends with a directory separator 360 | if (!file_exists($destPath) && in_array(substr($destPath, -1), array('/', '\\')) && !is_dir($sourcePath)) { 361 | mkdir($destPath, 0777, true); 362 | $destPath = $this->removeTrailingSlash($destPath); 363 | } 364 | 365 | // If source doesn't exist, check if it's a glob expression, otherwise we have nothing we can do 366 | if (!file_exists($sourcePath)) { 367 | // Handle globing 368 | $matches = glob($sourcePath); 369 | if (!empty($matches)) { 370 | foreach ($matches as $match) { 371 | $absolutePath = sprintf('%s/%s', $this->removeTrailingSlash($destPath), basename($match)); 372 | $relativeDestination = substr($absolutePath, strlen($this->getDestDir())); //strip off dest dir 373 | $relativeDestination = $this->removeLeadingSlash($relativeDestination); 374 | $relativeSource = substr($match, strlen($this->getSourceDir()) + 1); 375 | 376 | $this->create($relativeSource, $relativeDestination); 377 | } 378 | return true; 379 | } 380 | 381 | // Source file isn't a valid file or glob 382 | throw new \ErrorException("Source $sourcePath does not exist"); 383 | } 384 | return $this->createDelegate($source, $dest); 385 | } 386 | 387 | /** 388 | * Remove (unlink) the destination file 389 | * 390 | * @param string $source 391 | * @param string $dest 392 | * 393 | * @throws \ErrorException 394 | */ 395 | public function remove($source, $dest) 396 | { 397 | $sourcePath = $this->getSourceDir() . '/' . ltrim($this->removeTrailingSlash($source), '\\/'); 398 | $destPath = $this->getDestDir() . '/' . ltrim($dest, '\\/'); 399 | 400 | // If source doesn't exist, check if it's a glob expression, otherwise we have nothing we can do 401 | if (!file_exists($sourcePath)) { 402 | // Handle globing 403 | $matches = glob($sourcePath); 404 | if (!empty($matches)) { 405 | foreach ($matches as $match) { 406 | $newDest = substr($destPath . '/' . basename($match), strlen($this->getDestDir())); 407 | $newDest = ltrim($newDest, ' \\/'); 408 | $this->remove(substr($match, strlen($this->getSourceDir()) + 1), $newDest); 409 | } 410 | } 411 | return; 412 | } 413 | 414 | // MP Avoid removing whole folders in case the modman file is not 100% well-written 415 | // e.g. app/etc/modules/Testmodule.xml app/etc/modules/ installs correctly, 416 | // but would otherwise delete the whole app/etc/modules folder! 417 | if (basename($sourcePath) !== basename($destPath)) { 418 | $destPath .= '/' . basename($source); 419 | } 420 | $this->filesystem->remove($destPath); 421 | $this->addRemovedFile($destPath); 422 | } 423 | 424 | /** 425 | * Remove an empty directory branch up to $stopDir, or stop at the first non-empty parent. 426 | * 427 | * @param string $dir 428 | * @param string $stopDir 429 | */ 430 | public function rmEmptyDirsRecursive($dir, $stopDir = null) 431 | { 432 | $absoluteDir = $this->getDestDir() . '/' . $dir; 433 | if (is_dir($absoluteDir)) { 434 | $iterator = new \RecursiveIteratorIterator( 435 | new \RecursiveDirectoryIterator($absoluteDir, \RecursiveDirectoryIterator::SKIP_DOTS), 436 | \RecursiveIteratorIterator::CHILD_FIRST 437 | ); 438 | 439 | if (iterator_count($iterator) > 0) { 440 | // The directory contains something, do not remove 441 | return; 442 | } 443 | 444 | // RecursiveIteratorIterator have opened handle on $absoluteDir 445 | // that cause Windows to block the directory and not remove it until 446 | // the iterator will be destroyed. 447 | unset($iterator); 448 | 449 | // The specified directory is empty 450 | if (@rmdir($absoluteDir)) { 451 | // If the parent directory doesn't match the $stopDir and it's empty, remove it, too 452 | $parentDir = dirname($dir); 453 | $absoluteParentDir = $this->getDestDir() . '/' . $parentDir; 454 | if (!isset($stopDir) || (realpath($stopDir) !== realpath($absoluteParentDir))) { 455 | // Remove the parent directory if it is empty 456 | $this->rmEmptyDirsRecursive($parentDir); 457 | } 458 | } 459 | } 460 | } 461 | 462 | /** 463 | * Create the module's files in the given destination. 464 | * 465 | * NOTE: source and dest have to be passed as relative directories, like they are listed in the mapping 466 | * 467 | * @param string $source 468 | * @param string $dest 469 | * 470 | * @return bool 471 | */ 472 | abstract protected function createDelegate($source, $dest); 473 | 474 | /** 475 | * Add a file/folder to the list of deployed files 476 | * @param string $file 477 | */ 478 | public function addDeployedFile($file) 479 | { 480 | $destination = str_replace('\\', '/', $this->getDestDir()); 481 | //strip of destination deploy 482 | $quoted = preg_quote($destination, '/'); 483 | $file = preg_replace(sprintf('/^%s/', $quoted), '', $file); 484 | $this->deployedFiles[] = $file; 485 | } 486 | 487 | /** 488 | * Add a file/folder to the list of removed files 489 | * @param string $file 490 | */ 491 | public function addRemovedFile($file) 492 | { 493 | //strip of destination deploy location 494 | $file = preg_replace(sprintf('/^%s/', preg_quote($this->getDestDir(), '/')), '', $file); 495 | $this->removedFiles[] = $file; 496 | } 497 | 498 | /** 499 | * Get all the deployed files 500 | * 501 | * @return array 502 | */ 503 | public function getDeployedFiles() 504 | { 505 | return array_unique($this->deployedFiles); 506 | } 507 | 508 | /** 509 | * Get all the removed files 510 | * 511 | * @return array 512 | */ 513 | public function getRemovedFiles() 514 | { 515 | return $this->removedFiles; 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /src/MagentoHackathon/Composer/Magento/Plugin.php: -------------------------------------------------------------------------------- 1 | deployManager = new DeployManager($eventManager); 104 | $this->deployManager->setSortPriority($this->getSortPriority($composer)); 105 | 106 | $this->applyEvents($eventManager); 107 | } 108 | 109 | protected function applyEvents(EventManager $eventManager) 110 | { 111 | 112 | if ($this->config->hasAutoAppendGitignore()) { 113 | $gitIgnoreLocation = sprintf('%s/.gitignore', $this->config->getMagentoRootDir()); 114 | $gitIgnore = new GitIgnoreListener(new GitIgnore($gitIgnoreLocation)); 115 | 116 | $eventManager->listen('post-package-deploy', [$gitIgnore, 'addNewInstalledFiles']); 117 | $eventManager->listen('post-package-uninstall', [$gitIgnore, 'removeUnInstalledFiles']); 118 | } 119 | 120 | $io = $this->io; 121 | if ($this->io->isDebug()) { 122 | $eventManager->listen('pre-package-deploy', function (PackageDeployEvent $event) use ($io) { 123 | $io->write('Start magento deploy for ' . $event->getDeployEntry()->getPackageName()); 124 | }); 125 | } 126 | } 127 | 128 | /** 129 | * get Sort Priority from extra Config 130 | * 131 | * @param \Composer\Composer $composer 132 | * 133 | * @return array 134 | */ 135 | private function getSortPriority(Composer $composer) 136 | { 137 | $extra = $composer->getPackage()->getExtra(); 138 | 139 | return isset($extra[ProjectConfig::SORT_PRIORITY_KEY]) 140 | ? $extra[ProjectConfig::SORT_PRIORITY_KEY] 141 | : array(); 142 | } 143 | 144 | /** 145 | * Apply plugin modifications to composer 146 | * 147 | * @param Composer $composer 148 | * @param IOInterface $io 149 | */ 150 | public function activate(Composer $composer, IOInterface $io) 151 | { 152 | $this->io = $io; 153 | $this->composer = $composer; 154 | 155 | $this->filesystem = new Filesystem(); 156 | $this->config = new ProjectConfig($composer->getPackage()->getExtra(), $composer->getConfig()->all()); 157 | 158 | if (!$this->config->skipSuggestComposerRepositories()) { 159 | $this->suggestComposerRepositories(); 160 | } 161 | 162 | $this->entryFactory = new EntryFactory( 163 | $this->config, 164 | new DeploystrategyFactory($this->config), 165 | new PathTranslationParserFactory(new ParserFactory($this->config), $this->config) 166 | ); 167 | 168 | $this->initDeployManager($composer, $io, $this->getEventManager()); 169 | $this->writeDebug('activate magento plugin'); 170 | } 171 | 172 | /** 173 | * Deactivate plugin 174 | * 175 | * @param Composer $composer 176 | * @param IOInterface $io 177 | */ 178 | public function deactivate(Composer $composer, IOInterface $io) 179 | { 180 | 181 | } 182 | 183 | /** 184 | * Uninstall plugin 185 | * 186 | * @param Composer $composer 187 | * @param IOInterface $io 188 | */ 189 | public function uninstall(Composer $composer, IOInterface $io) 190 | { 191 | 192 | } 193 | 194 | /** 195 | * Returns an array of event names this subscriber wants to listen to. 196 | * 197 | * The array keys are event names and the value can be: 198 | * 199 | * * The method name to call (priority defaults to 0) 200 | * * An array composed of the method name to call and the priority 201 | * * An array of arrays composed of the method names to call and respective 202 | * priorities, or 0 if unset 203 | * 204 | * For instance: 205 | * 206 | * * array('eventName' => 'methodName') 207 | * * array('eventName' => array('methodName', $priority)) 208 | * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) 209 | * 210 | * @return array The event names to listen to 211 | */ 212 | public static function getSubscribedEvents() 213 | { 214 | return array( 215 | Installer\PackageEvents::PRE_PACKAGE_UPDATE => array( 216 | array('onPackageUpdate', 0), 217 | ), 218 | ScriptEvents::POST_INSTALL_CMD => array( 219 | array('onNewCodeEvent', 0), 220 | ), 221 | ScriptEvents::POST_UPDATE_CMD => array( 222 | array('onNewCodeEvent', 0), 223 | ), 224 | ); 225 | } 226 | 227 | /** 228 | * event listener is named this way, as it listens for events leading to changed code files 229 | * 230 | * @param Event $event 231 | */ 232 | public function onNewCodeEvent(Event $event) 233 | { 234 | 235 | $packageTypeToMatch = static::PACKAGE_TYPE; 236 | $magentoModules = array_filter( 237 | $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(), 238 | function (PackageInterface $package) use ($packageTypeToMatch) { 239 | if ($package instanceof AliasPackage) { 240 | return false; 241 | } 242 | return $package->getType() === $packageTypeToMatch; 243 | } 244 | ); 245 | 246 | if ($this->composer->getPackage()->getType() === static::PACKAGE_TYPE 247 | && $this->config->getIncludeRootPackage() === true 248 | ) { 249 | $magentoModules[] = $this->composer->getPackage(); 250 | } 251 | 252 | $vendorDir = rtrim($this->composer->getConfig()->get(self::VENDOR_DIR_KEY), '/'); 253 | 254 | Helper::initMagentoRootDir( 255 | $this->config, 256 | $this->io, 257 | $this->filesystem, 258 | $vendorDir 259 | ); 260 | 261 | if ($event->isDevMode()) { 262 | $this->config->setDevMode(); 263 | } 264 | 265 | $this->applyEvents($this->getEventManager()); 266 | 267 | if (in_array('--redeploy', $event->getArguments())) { 268 | $this->writeDebug('remove all deployed modules'); 269 | $this->getModuleManager()->updateInstalledPackages(array()); 270 | } 271 | $this->writeDebug('start magento module deploy via moduleManager'); 272 | $magentoModules = array_values($magentoModules); 273 | $this->getModuleManager()->updateInstalledPackages($magentoModules); 274 | $this->deployLibraries(); 275 | 276 | $patcher = Bootstrap::fromConfig($this->config); 277 | $patcher->setIo($this->io); 278 | try { 279 | $patcher->patch(); 280 | } catch (\DomainException $e) { 281 | $this->io->write(''.$e->getMessage().''); 282 | } 283 | } 284 | 285 | public function onPackageUpdate(PackageEvent $event) 286 | { 287 | /** @var UpdateOperation $operation */ 288 | $operation = $event->getOperation(); 289 | if ( 290 | $operation->getInitialPackage() && 291 | $operation->getInitialPackage()->getName() === 'magento-hackathon/magento-composer-installer' 292 | ) { 293 | throw new \Exception( 294 | 'Dont update the "magento-hackathon/magento-composer-installer" with active plugins.' 295 | . PHP_EOL . 296 | 'Consult the documentation on how to update the Installer' . PHP_EOL . 297 | 'https://github.com/Cotya/magento-composer-installer#update-the-installer' . PHP_EOL 298 | ); 299 | } 300 | } 301 | 302 | /** 303 | * test configured repositories and give message about adding recommended ones 304 | */ 305 | protected function suggestComposerRepositories() 306 | { 307 | $foundFiregento = false; 308 | $foundMagento = false; 309 | 310 | foreach ($this->config->getComposerRepositories() as $repository) { 311 | if (!isset($repository["type"]) || $repository["type"] !== "composer") { 312 | continue; 313 | } 314 | if (strpos($repository["url"], "packages.firegento.com") !== false) { 315 | $foundFiregento = true; 316 | } 317 | }; 318 | $message1 = "you may want to add the %s repository to composer."; 319 | $message2 = "add it with: composer.phar config -g repositories.%s composer %s"; 320 | if (!$foundFiregento) { 321 | $this->io->write(sprintf($message1, 'packages.firegento.com')); 322 | $this->io->write(sprintf($message2, 'firegento', 'https://packages.firegento.com')); 323 | } 324 | } 325 | 326 | /** 327 | * deploy Libraries 328 | */ 329 | protected function deployLibraries() 330 | { 331 | $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getPackages(); 332 | $autoloadDirectories = array(); 333 | 334 | $libraryPath = $this->config->getLibraryPath(); 335 | 336 | if ($libraryPath === null) { 337 | $this->writeDebug('jump over deployLibraries as no Magento libraryPath is set'); 338 | 339 | return; 340 | } 341 | 342 | $vendorDir = rtrim($this->composer->getConfig()->get(self::VENDOR_DIR_KEY), '/'); 343 | 344 | $this->filesystem->removeDirectory($libraryPath); 345 | $this->filesystem->ensureDirectoryExists($libraryPath); 346 | 347 | foreach ($packages as $package) { 348 | /** @var PackageInterface $package */ 349 | $packageConfig = $this->config->getLibraryConfigByPackagename($package->getName()); 350 | if ($packageConfig === null) { 351 | continue; 352 | } 353 | if (!isset($packageConfig['autoload'])) { 354 | $packageConfig['autoload'] = array('/'); 355 | } 356 | foreach ($packageConfig['autoload'] as $path) { 357 | $autoloadDirectories[] = $libraryPath . '/' . $package->getName() . "/" . $path; 358 | } 359 | $this->writeDebug(sprintf('Magento deployLibraries executed for %s', $package->getName())); 360 | 361 | $libraryTargetPath = $libraryPath . '/' . $package->getName(); 362 | $this->filesystem->removeDirectory($libraryTargetPath); 363 | $this->filesystem->ensureDirectoryExists($libraryTargetPath); 364 | $this->copyRecursive($vendorDir . '/' . $package->getPrettyName(), $libraryTargetPath); 365 | } 366 | 367 | if (false !== ($executable = $this->getTheseerAutoloadExecutable())) { 368 | $this->writeDebug('Magento deployLibraries executes autoload generator'); 369 | 370 | $params = $this->getTheseerAutoloadParams($libraryPath, $autoloadDirectories); 371 | 372 | $process = new Process([$executable, $params]); 373 | $process->run(); 374 | } 375 | } 376 | 377 | /** 378 | * return the autoload generator binary path or false if not found 379 | * 380 | * @return bool|string 381 | */ 382 | protected function getTheseerAutoloadExecutable() 383 | { 384 | $executable = $this->composer->getConfig()->get(self::BIN_DIR_KEY) 385 | . self::THESEER_AUTOLOAD_EXEC_BIN_PATH; 386 | 387 | if (!file_exists($executable)) { 388 | $executable = $this->composer->getConfig()->get(self::VENDOR_DIR_KEY) 389 | . self::THESEER_AUTOLOAD_EXEC_REL_PATH; 390 | } 391 | 392 | if (!file_exists($executable)) { 393 | $this->writeDebug( 394 | 'Magento deployLibraries autoload generator not available, you should require "theseer/autoload"', 395 | $executable 396 | ); 397 | 398 | return false; 399 | } 400 | 401 | return $executable; 402 | } 403 | 404 | /** 405 | * get Theseer Autoload Generator Params 406 | * 407 | * @param string $libraryPath 408 | * @param array $autoloadDirectories 409 | * 410 | * @return string 411 | */ 412 | protected function getTheseerAutoloadParams($libraryPath, $autoloadDirectories) 413 | { 414 | // @todo --blacklist 'test\\\\*' 415 | return " -b {$libraryPath} -o {$libraryPath}/autoload.php " . implode(' ', $autoloadDirectories); 416 | } 417 | 418 | /** 419 | * Copy then delete is a non-atomic version of {@link rename}. 420 | * 421 | * Some systems can't rename and also don't have proc_open, 422 | * which requires this solution. 423 | * 424 | * copied from \Composer\Util\Filesystem::copyThenRemove and removed the remove part 425 | * 426 | * @param string $source 427 | * @param string $target 428 | */ 429 | protected function copyRecursive($source, $target) 430 | { 431 | $it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS); 432 | $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); 433 | $this->filesystem->ensureDirectoryExists($target); 434 | 435 | foreach ($ri as $file) { 436 | $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathName(); 437 | if ($file->isDir()) { 438 | $this->filesystem->ensureDirectoryExists($targetPath); 439 | } else { 440 | copy($file->getPathname(), $targetPath); 441 | } 442 | } 443 | } 444 | 445 | /** 446 | * print Debug Message 447 | * 448 | * @param $message 449 | */ 450 | private function writeDebug($message, $varDump = null) 451 | { 452 | if ($this->io->isDebug()) { 453 | $this->io->write($message); 454 | 455 | if (!is_null($varDump)) { 456 | var_dump($varDump); 457 | } 458 | } 459 | } 460 | 461 | /** 462 | * @param PackageInterface $package 463 | * @return string 464 | */ 465 | public function getPackageInstallPath(PackageInterface $package) 466 | { 467 | $vendorDir = realpath(rtrim($this->composer->getConfig()->get('vendor-dir'), '/')); 468 | return sprintf('%s/%s', $vendorDir, $package->getPrettyName()); 469 | } 470 | 471 | /** 472 | * @return EventManager 473 | */ 474 | protected function getEventManager() 475 | { 476 | if (null === $this->eventManager) { 477 | $this->eventManager = new EventManager; 478 | } 479 | 480 | return $this->eventManager; 481 | } 482 | 483 | /** 484 | * @return ModuleManager 485 | */ 486 | protected function getModuleManager() 487 | { 488 | if (null === $this->moduleManager) { 489 | $this->moduleManager = new ModuleManager( 490 | new InstalledPackageFileSystemRepository( 491 | rtrim($this->composer->getConfig()->get(self::VENDOR_DIR_KEY), '/') . '/installed.json', 492 | new InstalledPackageDumper() 493 | ), 494 | $this->getEventManager(), 495 | $this->config, 496 | new UnInstallStrategy($this->filesystem, $this->config->getMagentoRootDir()), 497 | new InstallStrategyFactory($this->config, new ParserFactory($this->config)) 498 | ); 499 | } 500 | 501 | return $this->moduleManager; 502 | } 503 | } 504 | --------------------------------------------------------------------------------