diff --git a/src/Symfony/Installer/DemoCommand.php b/src/Symfony/Installer/DemoCommand.php index 8944c31..ed8fe87 100644 --- a/src/Symfony/Installer/DemoCommand.php +++ b/src/Symfony/Installer/DemoCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Installer\Exception\AbortException; +use Symfony\Installer\Manager\ComposerManager; /** * This command creates a full-featured Symfony demo application. @@ -60,6 +61,8 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->projectDir = $this->fs->isAbsolutePath($directory) ? $directory : getcwd().DIRECTORY_SEPARATOR.$directory; $this->projectName = basename($directory); } + + $this->composerManager = new ComposerManager($this->projectDir); } /** @@ -75,7 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ->download() ->extract() ->cleanUp() - ->updateComposerJson() + ->updateComposerConfig() ->createGitIgnore() ->checkSymfonyRequirements() ->displayInstallationResult() diff --git a/src/Symfony/Installer/DownloadCommand.php b/src/Symfony/Installer/DownloadCommand.php index 2ac118d..a88b07b 100644 --- a/src/Symfony/Installer/DownloadCommand.php +++ b/src/Symfony/Installer/DownloadCommand.php @@ -28,6 +28,7 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; use Symfony\Installer\Exception\AbortException; +use Symfony\Installer\Manager\ComposerManager; /** * Abstract command used by commands which download and extract compressed Symfony files. @@ -82,6 +83,9 @@ abstract class DownloadCommand extends Command */ protected $requirementsErrors = array(); + /** @var ComposerManager */ + protected $composerManager; + /** * Returns the type of the downloaded application in a human readable format. * It's mainly used to display readable error messages. @@ -363,23 +367,9 @@ private function getSymfonyRequirementsFilePath() * * @return $this */ - protected function updateComposerJson() + protected function updateComposerConfig() { - $composerConfig = $this->getProjectComposerConfig(); - - if (isset($composerConfig['config']['platform']['php'])) { - unset($composerConfig['config']['platform']['php']); - - if (empty($composerConfig['config']['platform'])) { - unset($composerConfig['config']['platform']); - } - - if (empty($composerConfig['config'])) { - unset($composerConfig['config']); - } - } - - $this->saveProjectComposerConfig($composerConfig); + $this->composerManager->initializeProjectConfig(); return $this; } @@ -418,17 +408,13 @@ protected function createGitIgnore() */ protected function getInstalledSymfonyVersion() { - $composer = json_decode(file_get_contents($this->projectDir.'/composer.lock'), true); + $symfonyVersion = $this->composerManager->getPackageVersion('symfony/symfony'); - foreach ($composer['packages'] as $package) { - if ('symfony/symfony' === $package['name']) { - if ('v' === substr($package['version'], 0, 1)) { - return substr($package['version'], 1); - }; + if (!empty($symfonyVersion) && 'v' === substr($symfonyVersion, 0, 1)) { + return substr($symfonyVersion, 1); + }; - return $package['version']; - } - } + return $symfonyVersion; } /** @@ -600,107 +586,6 @@ protected function getUrlContents($url) return $client->get($url)->getBody()->getContents(); } - /** - * It returns the project's Composer config as a PHP array. - * - * @return $this|array - */ - protected function getProjectComposerConfig() - { - $composerJsonFilepath = $this->projectDir.'/composer.json'; - - if (!is_writable($composerJsonFilepath)) { - if ($this->output->isVerbose()) { - $this->output->writeln(sprintf( - " [WARNING] Project's Composer config cannot be updated because\n". - " the %s file is not writable.\n", - $composerJsonFilepath - )); - } - - return $this; - } - - return json_decode(file_get_contents($composerJsonFilepath), true); - } - - /** - * It saves the given PHP array as the project's Composer config. In addition - * to JSON-serializing the contents, it synchronizes the composer.lock file to - * avoid out-of-sync Composer errors. - * - * @param array $config - */ - protected function saveProjectComposerConfig(array $config) - { - $composerJsonFilepath = $this->projectDir.'/composer.json'; - $this->fs->dumpFile($composerJsonFilepath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"); - - $this->syncComposerLockFile(); - } - - /** - * Updates the hash values stored in composer.lock to avoid out-of-sync - * problems when the composer.json file contents are changed. - */ - private function syncComposerLockFile() - { - $composerJsonFileContents = file_get_contents($this->projectDir.'/composer.json'); - $composerLockFileContents = json_decode(file_get_contents($this->projectDir.'/composer.lock'), true); - - if (array_key_exists('hash', $composerLockFileContents)) { - $composerLockFileContents['hash'] = md5($composerJsonFileContents); - } - - if (array_key_exists('content-hash', $composerLockFileContents)) { - $composerLockFileContents['content-hash'] = $this->getComposerContentHash($composerJsonFileContents); - } - - $this->fs->dumpFile($this->projectDir.'/composer.lock', json_encode($composerLockFileContents, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"); - } - - /** - * Returns the md5 hash of the sorted content of the composer file. - * - * @see https://github.com/composer/composer/blob/master/src/Composer/Package/Locker.php (getContentHash() method) - * - * @param string $composerJsonFileContents The contents of the composer.json file. - * - * @return string The hash of the composer file content. - */ - private function getComposerContentHash($composerJsonFileContents) - { - $composerConfig = json_decode($composerJsonFileContents, true); - - $relevantKeys = array( - 'name', - 'version', - 'require', - 'require-dev', - 'conflict', - 'replace', - 'provide', - 'minimum-stability', - 'prefer-stable', - 'repositories', - 'extra', - ); - - $relevantComposerConfig = array(); - - foreach (array_intersect($relevantKeys, array_keys($composerConfig)) as $key) { - $relevantComposerConfig[$key] = $composerConfig[$key]; - } - - if (isset($composerConfig['config']['platform'])) { - $relevantComposerConfig['config']['platform'] = $composerConfig['config']['platform']; - } - - ksort($relevantComposerConfig); - - return md5(json_encode($relevantComposerConfig)); - } - /** * Enables the signal handler. * diff --git a/src/Symfony/Installer/Manager/ComposerManager.php b/src/Symfony/Installer/Manager/ComposerManager.php new file mode 100644 index 0000000..e7c9266 --- /dev/null +++ b/src/Symfony/Installer/Manager/ComposerManager.php @@ -0,0 +1,193 @@ +projectDir = $projectDir; + $this->fs = new Filesystem(); + } + + public function initializeProjectConfig() + { + $composerConfig = $this->getProjectConfig(); + + if (isset($composerConfig['config']['platform']['php'])) { + unset($composerConfig['config']['platform']['php']); + + if (empty($composerConfig['config']['platform'])) { + unset($composerConfig['config']['platform']); + } + + if (empty($composerConfig['config'])) { + unset($composerConfig['config']); + } + } + + $this->saveProjectConfig($composerConfig); + } + + public function updateProjectConfig(array $newConfig) + { + $oldConfig = $this->getProjectConfig(); + $projectConfig = array_replace_recursive($oldConfig, $newConfig); + + // remove null values from project's config + $projectConfig = array_filter($projectConfig, function($value) { return !is_null($value); }); + + $this->saveProjectConfig($projectConfig); + } + + public function getPackageVersion($packageName) + { + $composerLockFileContents = json_decode(file_get_contents($this->projectDir.'/composer.lock'), true); + + foreach ($composerLockFileContents['packages'] as $packageConfig) { + if ($packageName === $packageConfig['name']) { + return $packageConfig['version']; + } + } + } + + /** + * Generates a good Composer project name based on the application name + * and on the user name. + * + * @param $projectName + * + * @return string The generated Composer package name + */ + public function createPackageName($projectName) + { + if (!empty($_SERVER['USERNAME'])) { + $packageName = $_SERVER['USERNAME'].'/'.$projectName; + } elseif (true === extension_loaded('posix') && $user = posix_getpwuid(posix_getuid())) { + $packageName = $user['name'].'/'.$projectName; + } elseif (get_current_user()) { + $packageName = get_current_user().'/'.$projectName; + } else { + // package names must be in the format foo/bar + $packageName = $projectName.'/'.$projectName; + } + + return $this->fixPackageName($packageName); + } + + /** + * It returns the project's Composer config as a PHP array. + * + * @return array + */ + private function getProjectConfig() + { + $composerJsonPath = $this->projectDir.'/composer.json'; + if (!is_writable($composerJsonPath)) { + return []; + } + + return json_decode(file_get_contents($composerJsonPath), true); + } + + /** + * It saves the given PHP array as the project's Composer config. In addition + * to JSON-serializing the contents, it synchronizes the composer.lock file to + * avoid out-of-sync Composer errors. + * + * @param array $config + */ + private function saveProjectConfig(array $config) + { + $composerJsonPath = $this->projectDir.'/composer.json'; + $this->fs->dumpFile($composerJsonPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"); + + $this->syncComposerLockFile(); + } + + /** + * Updates the hash values stored in composer.lock to avoid out-of-sync + * problems when the composer.json file contents are changed. + */ + private function syncComposerLockFile() + { + $composerJsonFileContents = file_get_contents($this->projectDir.'/composer.json'); + $composerLockFileContents = json_decode(file_get_contents($this->projectDir.'/composer.lock'), true); + + if (array_key_exists('hash', $composerLockFileContents)) { + $composerLockFileContents['hash'] = md5($composerJsonFileContents); + } + + if (array_key_exists('content-hash', $composerLockFileContents)) { + $composerLockFileContents['content-hash'] = $this->getComposerContentHash($composerJsonFileContents); + } + + $this->fs->dumpFile($this->projectDir.'/composer.lock', json_encode($composerLockFileContents, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"); + } + + /** + * Returns the md5 hash of the sorted content of the composer file. + * + * @see https://github.com/composer/composer/blob/master/src/Composer/Package/Locker.php (getContentHash() method) + * + * @param string $composerJsonFileContents The contents of the composer.json file. + * + * @return string The hash of the composer file content. + */ + private function getComposerContentHash($composerJsonFileContents) + { + $composerConfig = json_decode($composerJsonFileContents, true); + + $relevantKeys = array( + 'name', + 'version', + 'require', + 'require-dev', + 'conflict', + 'replace', + 'provide', + 'minimum-stability', + 'prefer-stable', + 'repositories', + 'extra', + ); + + $relevantComposerConfig = array(); + + foreach (array_intersect($relevantKeys, array_keys($composerConfig)) as $key) { + $relevantComposerConfig[$key] = $composerConfig[$key]; + } + + if (isset($composerConfig['config']['platform'])) { + $relevantComposerConfig['config']['platform'] = $composerConfig['config']['platform']; + } + + ksort($relevantComposerConfig); + + return md5(json_encode($relevantComposerConfig)); + } + + /** + * Transforms a project name into a valid Composer package name. + * + * @param string $name The project name to transform + * + * @return string The valid Composer package name + */ + private function fixPackageName($name) + { + $name = str_replace( + ['à', 'á', 'â', 'ä', 'æ', 'ã', 'å', 'ā', 'é', 'è', 'ê', 'ë', 'ę', 'ė', 'ē', 'ī', 'į', 'í', 'ì', 'ï', 'î', 'ō', 'ø', 'œ', 'õ', 'ó', 'ò', 'ö', 'ô', 'ū', 'ú', 'ù', 'ü', 'û', 'ç', 'ć', 'č', 'ł', 'ñ', 'ń', 'ß', 'ś', 'š', 'ŵ', 'ŷ', 'ÿ', 'ź', 'ž', 'ż'], + ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'i', 'i', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'u', 'u', 'u', 'u', 'u', 'c', 'c', 'c', 'l', 'n', 'n', 's', 's', 's', 'w', 'y', 'y', 'z', 'z', 'z'], + $name + ); + $name = preg_replace('#[^A-Za-z0-9_./-]+#', '', $name); + + return strtolower($name); + } +} diff --git a/src/Symfony/Installer/NewCommand.php b/src/Symfony/Installer/NewCommand.php index f6df447..23e00cc 100644 --- a/src/Symfony/Installer/NewCommand.php +++ b/src/Symfony/Installer/NewCommand.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Installer\Exception\AbortException; +use Symfony\Installer\Manager\ComposerManager; /** * This command creates new Symfony projects for the given Symfony version. @@ -48,6 +49,8 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->version = trim($input->getArgument('version')); $this->projectDir = $this->fs->isAbsolutePath($directory) ? $directory : getcwd().DIRECTORY_SEPARATOR.$directory; $this->projectName = basename($directory); + + $this->composerManager = new ComposerManager($this->projectDir); } /** @@ -66,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ->cleanUp() ->dumpReadmeFile() ->updateParameters() - ->updateComposerJson() + ->updateComposerConfig() ->createGitIgnore() ->checkSymfonyRequirements() ->displayInstallationResult() @@ -322,71 +325,19 @@ protected function updateParameters() * * @return $this */ - protected function updateComposerJson() + protected function updateComposerConfig() { - parent::updateComposerJson(); - - $composerConfig = $this->getProjectComposerConfig(); - - $composerConfig['name'] = $this->generateComposerProjectName(); - $composerConfig['license'] = 'proprietary'; - - if (isset($composerConfig['description'])) { - unset($composerConfig['description']); - } - - if (isset($composerConfig['extra']['branch-alias'])) { - unset($composerConfig['extra']['branch-alias']); - } - - $this->saveProjectComposerConfig($composerConfig); + parent::updateComposerConfig(); + $this->composerManager->updateProjectConfig([ + 'name' => $this->composerManager->createPackageName($this->projectName), + 'license' => 'proprietary', + 'description' => null, + 'extra' => ['branch-alias' => null], + ]); return $this; } - /** - * Generates a good Composer project name based on the application name - * and on the user name. - * - * @return string The generated Composer project name - */ - protected function generateComposerProjectName() - { - $name = $this->projectName; - - if (!empty($_SERVER['USERNAME'])) { - $name = $_SERVER['USERNAME'].'/'.$name; - } elseif (true === extension_loaded('posix') && $user = posix_getpwuid(posix_getuid())) { - $name = $user['name'].'/'.$name; - } elseif (get_current_user()) { - $name = get_current_user().'/'.$name; - } else { - // package names must be in the format foo/bar - $name = $name.'/'.$name; - } - - return $this->fixComposerPackageName($name); - } - - /** - * Transforms uppercase strings into dash-separated strings - * (e.g. FooBar -> foo-bar) to comply with Composer rules for package names. - * - * @param string $name The project name to transform - * - * @return string The fixed Composer project name - */ - private function fixComposerPackageName($name) - { - return strtolower( - preg_replace( - array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), - array('\\1-\\2', '\\1-\\2'), - strtr($name, '-', '.') - ) - ); - } - /** * {@inheritdoc} */ diff --git a/tests/Symfony/Installer/Tests/Manager/ComposerManagerTest.php b/tests/Symfony/Installer/Tests/Manager/ComposerManagerTest.php new file mode 100644 index 0000000..153abba --- /dev/null +++ b/tests/Symfony/Installer/Tests/Manager/ComposerManagerTest.php @@ -0,0 +1,32 @@ +setAccessible(true); + + $fixedName = $method->invoke($composerManager, $originalName); + $this->assertSame($expectedName, $fixedName); + } + + public function getProjectNames() + { + return [ + ['foo/bar', 'foo/bar'], + ['áèî/øū', 'aei/ou'], + ['çñß/łŵž', 'cns/lwz'], + ['foo#bar\foo?bar=foo!bar{foo]bar', 'foobarfoobarfoobarfoobar'], + ['FOO/bar', 'foo/bar'], + ]; + } +}