diff --git a/.travis.yml b/.travis.yml index ed70733..11424c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,7 @@ matrix: - php: 5.5 - php: 5.6 - php: 7.0 - - php: hhvm - allow_failures: - - php: hhvm + - php: 7.1 before_install: - composer self-update @@ -24,7 +22,7 @@ before_install: install: - composer install - - php box build + - ~/.phpenv/versions/5.6/bin/php box build script: - - phpunit + - ./vendor/bin/simple-phpunit diff --git a/README.md b/README.md index 82d2257..aa9f633 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ Symfony Installer ================= -**This is the official installer to start new projects based on the Symfony -full-stack framework.** +This is the official installer to start new projects based on the Symfony +full-stack framework. The installer is only compatible with Symfony 2 and 3. + +Creating Symfony 4 projects +--------------------------- + +**This installer is not compatible with Symfony 4** and newer versions. Instead, +use [Composer](https://getcomposer.org/) and create your Symfony 4 project as follows: + +```bash +$ composer create-project symfony/skeleton my_project_name +``` + +See the [Symfony Installation article](https://symfony.com/doc/current/setup.html) +on the official Symfony Documentation for more details. Installing the installer ------------------------ diff --git a/composer.json b/composer.json index f2d3582..7f924b6 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "authors": [ { "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "homepage": "https://symfony.com/contributors" } ], "autoload": { @@ -20,7 +20,8 @@ "raulfraile/distill": "~0.9,!=0.9.3,!=0.9.4" }, "require-dev": { - "symfony/process": "~2.5" + "symfony/process": "~2.5", + "symfony/phpunit-bridge": "^4.0" }, "extra": { "branch-alias": { diff --git a/composer.lock b/composer.lock index 32c1a8a..b82669d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "7587a63f96b596f379d4d830bee56119", - "content-hash": "3c97ce42899ed9440a9ef2cc5d78847f", + "content-hash": "8f72036c453520314de3967a312a6733", "packages": [ { "name": "guzzlehttp/guzzle", @@ -57,7 +56,7 @@ "rest", "web service" ], - "time": "2016-07-15 19:28:39" + "time": "2016-07-15T19:28:39+00:00" }, { "name": "guzzlehttp/ringphp", @@ -108,7 +107,7 @@ } ], "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", - "time": "2015-05-20 03:37:09" + "time": "2015-05-20T03:37:09+00:00" }, { "name": "guzzlehttp/streams", @@ -158,7 +157,7 @@ "Guzzle", "stream" ], - "time": "2014-10-12 19:18:40" + "time": "2014-10-12T19:18:40+00:00" }, { "name": "pimple/pimple", @@ -204,7 +203,7 @@ "container", "dependency injection" ], - "time": "2015-09-11 15:10:35" + "time": "2015-09-11T15:10:35+00:00" }, { "name": "raulfraile/distill", @@ -273,7 +272,7 @@ "xz", "zip" ], - "time": "2015-10-17 06:41:00" + "time": "2015-10-17T06:41:00+00:00" }, { "name": "react/promise", @@ -317,7 +316,7 @@ } ], "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "time": "2015-07-03 13:48:55" + "time": "2015-07-03T13:48:55+00:00" }, { "name": "symfony/console", @@ -374,7 +373,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2015-09-25 08:32:23" + "time": "2015-09-25T08:32:23+00:00" }, { "name": "symfony/filesystem", @@ -423,7 +422,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2015-09-09 17:42:36" + "time": "2015-09-09T17:42:36+00:00" }, { "name": "symfony/process", @@ -472,10 +471,73 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2015-09-19 19:59:23" + "time": "2015-09-19T19:59:23+00:00" + } + ], + "packages-dev": [ + { + "name": "symfony/phpunit-bridge", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/phpunit-bridge.git", + "reference": "61c84ebdce0d4c289413a222ee545f0114e60120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/61c84ebdce0d4c289413a222ee545f0114e60120", + "reference": "61c84ebdce0d4c289413a222ee545f0114e60120", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, + "suggest": { + "ext-zip": "Zip support is required when using bin/simple-phpunit", + "symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" + }, + "bin": [ + "bin/simple-phpunit" + ], + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Bridge\\PhpUnit\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony PHPUnit Bridge", + "homepage": "https://symfony.com", + "time": "2017-12-14T19:48:22+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], diff --git a/src/Symfony/Installer/DemoCommand.php b/src/Symfony/Installer/DemoCommand.php index 8944c31..c941a78 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() @@ -145,13 +148,22 @@ private function displayInstallationResult() )); } + $serverRunCommand = extension_loaded('pcntl') ? 'server:start' : 'server:run'; + $this->output->writeln(sprintf( " 1. Change your current directory to %s\n\n". - " 2. Execute the php bin/console server:run command to run the demo application.\n\n". + " 2. Execute the php bin/console %s command to run the demo application.\n\n". " 3. Browse to the http://localhost:8000 URL to see the demo application in action.\n\n", - $this->projectDir + $this->projectDir, $serverRunCommand )); + $this->output->writeln( + " WARNING \n\n". + " This installer downloads the old Symfony Demo version based on Symfony 3.\n". + " If you prefer to install the new version based on Symfony 4 and Symfony Flex,\n". + " execute the following command:\n\n". + " composer create-project symfony/symfony-demo\n"); + return $this; } @@ -168,6 +180,6 @@ protected function getDownloadedApplicationType() */ protected function getRemoteFileUrl() { - return 'http://symfony.com/download?v=Symfony_Demo'; + return 'https://symfony.com/download?v=Symfony_Demo'; } } diff --git a/src/Symfony/Installer/DownloadCommand.php b/src/Symfony/Installer/DownloadCommand.php index 2ac118d..b4634aa 100644 --- a/src/Symfony/Installer/DownloadCommand.php +++ b/src/Symfony/Installer/DownloadCommand.php @@ -21,6 +21,7 @@ use GuzzleHttp\Event\ProgressEvent; use GuzzleHttp\Utils; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -28,6 +29,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 +84,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. @@ -129,7 +134,7 @@ protected function download() $symfonyArchiveFile = $distill ->getChooser() ->setStrategy(new MinimumSize()) - ->addFilesWithDifferentExtensions($this->getRemoteFileUrl(), ['tgz', 'zip']) + ->addFilesWithDifferentExtensions($this->getRemoteFileUrl(), array('tgz', 'zip')) ->getPreferredFile() ; @@ -146,10 +151,10 @@ protected function download() if (null === $progressBar) { ProgressBar::setPlaceholderFormatterDefinition('max', function (ProgressBar $bar) { - return $this->formatSize($bar->getMaxSteps()); + return Helper::formatMemory($bar->getMaxSteps()); }); ProgressBar::setPlaceholderFormatterDefinition('current', function (ProgressBar $bar) { - return str_pad($this->formatSize($bar->getProgress()), 11, ' ', STR_PAD_LEFT); + return str_pad(Helper::formatMemory($bar->getProgress()), 11, ' ', STR_PAD_LEFT); }); $progressBar = new ProgressBar($this->output, $downloadSize); @@ -179,7 +184,7 @@ protected function download() $request->getEmitter()->on('progress', $downloadCallback); $response = $client->send($request); } catch (ClientException $e) { - if ('new' === $this->getName() && ($e->getCode() === 403 || $e->getCode() === 404)) { + if ('new' === $this->getName() && (403 === $e->getCode() || 404 === $e->getCode())) { throw new \RuntimeException(sprintf( "The selected version (%s) cannot be installed because it does not exist.\n". "Execute the following command to install the latest stable Symfony release:\n". @@ -224,6 +229,10 @@ protected function checkProjectName() )); } + if ('demo' === $this->projectName && 'new' === $this->getName()) { + $this->output->writeln("\n TIP If you want to download the Symfony Demo app, execute 'symfony demo' instead of 'symfony new demo'"); + } + return $this; } @@ -363,23 +372,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 +413,13 @@ protected function createGitIgnore() */ protected function getInstalledSymfonyVersion() { - $composer = json_decode(file_get_contents($this->projectDir.'/composer.lock'), true); - - foreach ($composer['packages'] as $package) { - if ('symfony/symfony' === $package['name']) { - if ('v' === substr($package['version'], 0, 1)) { - return substr($package['version'], 1); - }; + $symfonyVersion = $this->composerManager->getPackageVersion('symfony/symfony'); - return $package['version']; - } + if (!empty($symfonyVersion) && 'v' === substr($symfonyVersion, 0, 1)) { + return substr($symfonyVersion, 1); } + + return $symfonyVersion; } /** @@ -449,26 +440,6 @@ protected function checkPermissions() return $this; } - /** - * Utility method to show the number of bytes in a readable format. - * - * @param int $bytes The number of bytes to format - * - * @return string The human readable string of bytes (e.g. 4.32MB) - */ - protected function formatSize($bytes) - { - $units = array('B', 'KB', 'MB', 'GB', 'TB'); - - $bytes = max($bytes, 0); - $pow = $bytes ? floor(log($bytes, 1024)) : 0; - $pow = min($pow, count($units) - 1); - - $bytes /= pow(1024, $pow); - - return number_format($bytes, 2).' '.$units[$pow]; - } - /** * Formats the error message contained in the given Requirement item * using the optional line length provided. @@ -600,111 +571,10 @@ 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. * - * @throws AbortException If the execution has been aborted with SIGINT signal. + * @throws AbortException if the execution has been aborted with SIGINT signal */ private function enableSignalHandler() { @@ -712,7 +582,7 @@ private function enableSignalHandler() return; } - declare(ticks = 1); + declare(ticks=1); pcntl_signal(SIGINT, function () { error_reporting(0); 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..b3bf403 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() @@ -94,7 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output) /** * Checks whether the given Symfony version is installable by the installer. * Due to the changes introduced in the Icu/Intl components - * (see http://symfony.com/blog/new-in-symfony-2-6-farewell-to-icu-component) + * (see https://symfony.com/blog/new-in-symfony-2-6-farewell-to-icu-component) * not all the previous Symfony versions are installable by the installer. * * The rules to decide if the version is installable are as follows: @@ -121,12 +124,12 @@ protected function checkSymfonyVersionIsInstallable() // Get the full list of Symfony versions to check if it's installable $client = $this->getGuzzleClient(); - $symfonyVersions = $client->get('http://symfony.com/versions.json')->json(); + $symfonyVersions = $client->get('https://symfony.com/versions.json')->json(); if (empty($symfonyVersions)) { throw new \RuntimeException( "There was a problem while downloading the list of Symfony versions from\n". "symfony.com. Check that you are online and the following URL is accessible:\n\n". - 'http://symfony.com/versions.json' + 'https://symfony.com/versions.json' ); } @@ -136,7 +139,7 @@ protected function checkSymfonyVersionIsInstallable() throw new \RuntimeException(sprintf( "The selected branch (%s) does not exist, or is not maintained.\n". "To solve this issue, install Symfony with the latest stable release:\n\n". - '%s %s %s', $this->version, $_SERVER['PHP_SELF'], $this->getName(), $this->projectDir + '%s %s %s', $this->version, $_SERVER['PHP_SELF'], $this->getName(), $this->projectDir )); } @@ -170,11 +173,21 @@ protected function checkSymfonyVersionIsInstallable() } // check that the system has the PHP version required by the Symfony version to be installed - if (version_compare($this->version, '3.0.0', '>=') && version_compare(phpversion(), '5.5.9', '<')) { + if (version_compare($this->version, '3.0.0', '>=') && version_compare(PHP_VERSION, '5.5.9', '<')) { throw new \RuntimeException(sprintf( "The selected version (%s) cannot be installed because it requires\n". "PHP 5.5.9 or higher and your system has PHP %s installed.\n", - $this->version, phpversion() + $this->version, PHP_VERSION + )); + } + + // check that the Symfony version to be installed is not 4.x, which is incompatible with this installer + if (version_compare($this->version, '4.0.0', '>=')) { + throw new \RuntimeException(sprintf( + "The Symfony Installer is not compatible with Symfony 4.x or newer versions.\n". + "Run this other command to install Symfony using Composer instead:\n\n". + 'composer create-project symfony/skeleton %s', + $this->projectName )); } @@ -256,14 +269,15 @@ protected function displayInstallationResult() } $consoleDir = ($this->isSymfony3() ? 'bin' : 'app'); + $serverRunCommand = version_compare($this->version, '2.6.0', '>=') && extension_loaded('pcntl') ? 'server:start' : 'server:run'; $this->output->writeln(sprintf( " * Configure your application in app/config/parameters.yml file.\n\n". " * Run your application:\n". - " 1. Execute the php %s/console server:run command.\n". + " 1. Execute the php %s/console %s command.\n". " 2. Browse to the http://localhost:8000 URL.\n\n". - " * Read the documentation at http://symfony.com/doc\n", - $consoleDir + " * Read the documentation at https://symfony.com/doc\n", + $consoleDir, $serverRunCommand )); return $this; @@ -322,71 +336,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(array( + 'name' => $this->composerManager->createPackageName($this->projectName), + 'license' => 'proprietary', + 'description' => null, + 'extra' => array('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} */ @@ -400,6 +362,6 @@ protected function getDownloadedApplicationType() */ protected function getRemoteFileUrl() { - return 'http://symfony.com/download?v=Symfony_Standard_Vendors_'.$this->version; + return 'https://symfony.com/download?v=Symfony_Standard_Vendors_'.$this->version; } } diff --git a/src/Symfony/Installer/SelfUpdateCommand.php b/src/Symfony/Installer/SelfUpdateCommand.php index 0b1810e..2a93865 100644 --- a/src/Symfony/Installer/SelfUpdateCommand.php +++ b/src/Symfony/Installer/SelfUpdateCommand.php @@ -89,7 +89,7 @@ public function isEnabled() protected function initialize(InputInterface $input, OutputInterface $output) { parent::initialize($input, $output); - $this->remoteInstallerFile = 'http://symfony.com/installer'; + $this->remoteInstallerFile = 'https://symfony.com/installer'; $this->currentInstallerFile = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; $this->tempDir = sys_get_temp_dir(); $this->currentInstallerBackupFile = basename($this->currentInstallerFile, '.phar').'-backup.phar'; @@ -136,6 +136,8 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($this->output->isVeryVerbose()) { $this->output->writeln($e->getMessage()); } + + return 1; } } @@ -148,11 +150,11 @@ private function downloadNewVersion() { // check for permissions in local filesystem before start downloading files if (!is_writable($this->currentInstallerFile)) { - throw new \RuntimeException('Symfony Installer update failed: the "'.$this->currentInstallerFile.'" file could not be written'); + throw new IOException('Symfony Installer update failed: the "'.$this->currentInstallerFile.'" file could not be written'); } if (!is_writable($this->tempDir)) { - throw new \RuntimeException('Symfony Installer update failed: the "'.$this->tempDir.'" directory used to download files temporarily could not be written'); + throw new IOException('Symfony Installer update failed: the "'.$this->tempDir.'" directory used to download files temporarily could not be written'); } if (false === $newInstaller = $this->getUrlContents($this->remoteInstallerFile)) { @@ -225,7 +227,7 @@ private function rollback() { $this->output->writeln(array( '', - 'There was an error while updating the installer.', + 'There was an error while updating the installer.', 'The previous Symfony Installer version has been restored.', '', )); @@ -250,6 +252,6 @@ protected function getDownloadedApplicationType() */ protected function getRemoteFileUrl() { - return 'http://symfony.com/installer'; + return 'https://symfony.com/installer'; } } diff --git a/symfony b/symfony index 14fd272..b9f2d8d 100755 --- a/symfony +++ b/symfony @@ -14,11 +14,22 @@ if (PHP_VERSION_ID < 50400) { exit(1); } +if (extension_loaded('suhosin')) { + file_put_contents('php://stderr', + "Symfony Installer is not compatible with the 'suhosin' PHP extension.\n". + "Disable that extension before running the installer.\n\n". + "Alternatively, install Symfony manually executing the following command:\n\n". + "composer create-project symfony/framework-standard-edition \n\n" + ); + + exit(1); +} + require file_exists(__DIR__.'/vendor/autoload.php') ? __DIR__.'/vendor/autoload.php' : __DIR__.'/../../autoload.php'; -$appVersion = '1.5.7-DEV'; +$appVersion = '1.5.12-DEV'; // Windows uses Path instead of PATH if (!isset($_SERVER['PATH']) && isset($_SERVER['Path'])) { diff --git a/tests/Symfony/Installer/Tests/IntegrationTest.php b/tests/Symfony/Installer/Tests/IntegrationTest.php index 90a8d5a..7ade375 100644 --- a/tests/Symfony/Installer/Tests/IntegrationTest.php +++ b/tests/Symfony/Installer/Tests/IntegrationTest.php @@ -52,11 +52,7 @@ public function testDemoApplicationInstallation() $this->assertContains('Downloading the Symfony Demo Application', $output); $this->assertContains('Symfony Demo Application was successfully installed.', $output); - $output = $this->runCommand('php bin/console --version', $projectDir); - $this->assertRegExp('/Symfony version 3\.\d+\.\d+(-DEV)? - app\/dev\/debug/', $output); - $composerConfig = json_decode(file_get_contents($projectDir.'/composer.json'), true); - $this->assertArrayNotHasKey('platform', $composerConfig['config'], 'The composer.json file does not define any platform configuration.'); } @@ -65,7 +61,7 @@ public function testDemoApplicationInstallation() */ public function testSymfonyInstallation($versionToInstall, $messageRegexp, $versionRegexp, $requiredPhpVersion) { - if (version_compare(phpversion(), $requiredPhpVersion, '<')) { + if (version_compare(PHP_VERSION, $requiredPhpVersion, '<')) { $this->markTestSkipped(sprintf('This test requires PHP %s or higher.', $requiredPhpVersion)); } @@ -76,14 +72,10 @@ public function testSymfonyInstallation($versionToInstall, $messageRegexp, $vers $this->assertContains('Downloading Symfony...', $output); $this->assertRegExp($messageRegexp, $output); - if ('3' === substr($versionToInstall, 0, 1) || '' === $versionToInstall) { - if (PHP_VERSION_ID < 50500) { - $this->markTestSkipped('Symfony 3 requires PHP 5.5.9 or higher.'); - } - - $output = $this->runCommand('php bin/console --version', $projectDir); - } else { + if (file_exists($projectDir.'/app/console')) { $output = $this->runCommand('php app/console --version', $projectDir); + } else { + $output = $this->runCommand('php bin/console --version', $projectDir); } $this->assertRegExp($versionRegexp, $output); @@ -109,6 +101,19 @@ public function testSymfonyRequiresNewerPhpVersion() $this->runCommand(sprintf('php %s/symfony.phar new my_test_project 3.0.0', $this->rootDir)); } + /** + * @expectedException \RuntimeException + * @expectedExceptionMessageRegExp /.+The Symfony Installer is not compatible with Symfony 4\.x or newer versions.*Run this other command to install Symfony using Composer instead:.*composer create-project symfony\/skeleton .+/s + */ + public function testUseComposerToInstallSymfony4() + { + if (PHP_VERSION_ID < 50500) { + $this->markTestSkipped('This test requires PHP 5.5 or newer.'); + } + + $this->runCommand(sprintf('php %s/symfony.phar new my_test_project', $this->rootDir)); + } + public function testSymfonyInstallationInCurrentDirectory() { $projectDir = sprintf('%s/my_test_project', sys_get_temp_dir()); @@ -122,6 +127,17 @@ public function testSymfonyInstallationInCurrentDirectory() $this->assertContains('Symfony version 2.7.5 - app/dev/debug', $output); } + public function testSymfonyDemoInstallationWithNewCommand() + { + if (PHP_VERSION_ID < 50500) { + $this->markTestSkipped('This test requires PHP 5.5 or higher.'); + } + + $output = $this->runCommand(sprintf('php %s/symfony.phar new demo 3.4', $this->rootDir)); + $this->assertContains("If you want to download the Symfony Demo app, execute 'symfony demo' instead of 'symfony new demo'", $output); + $this->fs->remove('demo'); + } + /** * Runs the given string as a command and returns the resulting output. * The CWD is set to the root project directory to simplify command paths. @@ -150,13 +166,6 @@ private function runCommand($command, $workingDirectory = null) public function provideSymfonyInstallationData() { return array( - array( - '', - '/.*Symfony 3\.1\.\d+ was successfully installed.*/', - '/Symfony version 3\.1\.\d+(-DEV)? - app\/dev\/debug/', - '5.5.9', - ), - array( '3.0', '/.*Symfony 3\.0\.\d+ was successfully installed.*/', @@ -166,9 +175,9 @@ public function provideSymfonyInstallationData() array( 'lts', - '/.*Symfony 2\.8\.\d+ was successfully installed.*/', - '/Symfony version 2\.8\.\d+(-DEV)? - app\/dev\/debug/', - '5.3.9', + '/.*Symfony 3\.4\.\d+ was successfully installed.*/', + '/Symfony 3\.4\.\d+ \(kernel: app, env: dev, debug: true\)/', + '5.5.9', ), array( diff --git a/tests/Symfony/Installer/Tests/Manager/ComposerManagerTest.php b/tests/Symfony/Installer/Tests/Manager/ComposerManagerTest.php new file mode 100644 index 0000000..6b8a14e --- /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'], + ]; + } +}