├── 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 | [](https://travis-ci.org/Cotya/magento-composer-installer)
2 | [](https://ci.appveyor.com/project/Flyingmana/magento-composer-installer-396)
3 | [](https://scrutinizer-ci.com/g/Cotya/magento-composer-installer/)
4 | [](https://scrutinizer-ci.com/g/Cotya/magento-composer-installer/)
5 | [](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 | [](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 |
--------------------------------------------------------------------------------