diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index d617f9f4..536b4744 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -8,7 +8,6 @@
'@Symfony' => true,
'@Symfony:risky' => true,
'fopen_flags' => false,
- 'modernize_strpos' => false, // requires PHP 8
'protected_to_private' => false,
])
->setRiskyAllowed(true)
diff --git a/src/Configurator/AbstractConfigurator.php b/src/Configurator/AbstractConfigurator.php
index 7608cab2..e88defaf 100644
--- a/src/Configurator/AbstractConfigurator.php
+++ b/src/Configurator/AbstractConfigurator.php
@@ -56,7 +56,7 @@ protected function write($messages, $verbosity = IOInterface::VERBOSE)
protected function isFileMarked(Recipe $recipe, string $file): bool
{
- return is_file($file) && false !== strpos(file_get_contents($file), \sprintf('###> %s ###', $recipe->getName()));
+ return is_file($file) && str_contains(file_get_contents($file), \sprintf('###> %s ###', $recipe->getName()));
}
protected function markData(Recipe $recipe, string $data): string
@@ -66,7 +66,7 @@ protected function markData(Recipe $recipe, string $data): string
protected function isFileXmlMarked(Recipe $recipe, string $file): bool
{
- return is_file($file) && false !== strpos(file_get_contents($file), \sprintf('###+ %s ###', $recipe->getName()));
+ return is_file($file) && str_contains(file_get_contents($file), \sprintf('###+ %s ###', $recipe->getName()));
}
protected function markXmlData(Recipe $recipe, string $data): string
@@ -104,7 +104,7 @@ protected function updateDataString(string $contents, string $data): ?string
$startMark = trim(reset($pieces));
$endMark = trim(end($pieces));
- if (false === strpos($contents, $startMark) || false === strpos($contents, $endMark)) {
+ if (!str_contains($contents, $startMark) || !str_contains($contents, $endMark)) {
return null;
}
diff --git a/src/Configurator/AddLinesConfigurator.php b/src/Configurator/AddLinesConfigurator.php
index 8a4bc6d6..59d7b6bb 100644
--- a/src/Configurator/AddLinesConfigurator.php
+++ b/src/Configurator/AddLinesConfigurator.php
@@ -168,7 +168,7 @@ private function getPatchedContents(string $file, string $value, string $positio
{
$fileContents = $this->readFile($file);
- if (false !== strpos($fileContents, $value)) {
+ if (str_contains($fileContents, $value)) {
return $fileContents; // already includes value, skip
}
@@ -185,7 +185,7 @@ private function getPatchedContents(string $file, string $value, string $positio
$lines = explode("\n", $fileContents);
$targetFound = false;
foreach ($lines as $key => $line) {
- if (false !== strpos($line, $target)) {
+ if (str_contains($line, $target)) {
array_splice($lines, $key + 1, 0, $value);
$targetFound = true;
@@ -214,13 +214,13 @@ private function getUnPatchedContents(string $file, $value): string
{
$fileContents = $this->readFile($file);
- if (false === strpos($fileContents, $value)) {
+ if (!str_contains($fileContents, $value)) {
return $fileContents; // value already gone!
}
- if (false !== strpos($fileContents, "\n".$value)) {
+ if (str_contains($fileContents, "\n".$value)) {
$value = "\n".$value;
- } elseif (false !== strpos($fileContents, $value."\n")) {
+ } elseif (str_contains($fileContents, $value."\n")) {
$value .= "\n";
}
@@ -249,7 +249,7 @@ private function isPackageInstalled($packages): bool
private function relativize(string $path): string
{
$rootDir = $this->options->get('root-dir');
- if (0 === strpos($path, $rootDir)) {
+ if (str_starts_with($path, $rootDir)) {
$path = substr($path, \strlen($rootDir) + 1);
}
diff --git a/src/Configurator/CopyFromRecipeConfigurator.php b/src/Configurator/CopyFromRecipeConfigurator.php
index 004c0653..6acf6377 100644
--- a/src/Configurator/CopyFromRecipeConfigurator.php
+++ b/src/Configurator/CopyFromRecipeConfigurator.php
@@ -31,7 +31,13 @@ public function configure(Recipe $recipe, $config, Lock $lock, array $options =
public function unconfigure(Recipe $recipe, $config, Lock $lock)
{
$this->write('Removing files from recipe');
- $this->removeFiles($config, $this->getRemovableFilesFromRecipeAndLock($recipe, $lock), $this->options->get('root-dir'));
+ $rootDir = $this->options->get('root-dir');
+
+ foreach ($this->options->getRemovableFiles($recipe, $lock) as $file) {
+ if ('.git' !== $file) { // never remove the main Git directory, even if it was created by a recipe
+ $this->removeFile($this->path->concatenate([$rootDir, $file]));
+ }
+ }
}
public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array $newConfig): void
@@ -58,7 +64,7 @@ public function update(RecipeUpdate $recipeUpdate, array $originalConfig, array
private function resolveTargetFolder(string $path, array $config): string
{
foreach ($config as $key => $target) {
- if (0 === strpos($path, $key)) {
+ if (str_starts_with($path, $key)) {
return $this->options->expandTargetDir($target).substr($path, \strlen($key));
}
}
@@ -66,32 +72,6 @@ private function resolveTargetFolder(string $path, array $config): string
return $path;
}
- private function getRemovableFilesFromRecipeAndLock(Recipe $recipe, Lock $lock): array
- {
- $lockedFiles = array_unique(
- array_reduce(
- array_column($lock->all(), 'files'),
- function (array $carry, array $package) {
- return array_merge($carry, $package);
- },
- []
- )
- );
-
- $removableFiles = $recipe->getFiles();
-
- $lockedFiles = array_map('realpath', $lockedFiles);
-
- // Compare file paths by their real path to abstract OS differences
- foreach (array_keys($removableFiles) as $file) {
- if (\in_array(realpath($file), $lockedFiles)) {
- unset($removableFiles[$file]);
- }
- }
-
- return $removableFiles;
- }
-
private function copyFiles(array $manifest, array $files, array $options): array
{
$copiedFiles = [];
@@ -116,7 +96,7 @@ private function copyDir(string $source, string $target, array $files, array $op
{
$copiedFiles = [];
foreach ($files as $file => $data) {
- if (0 === strpos($file, $source)) {
+ if (str_starts_with($file, $source)) {
$file = $this->path->concatenate([$target, substr($file, \strlen($source))]);
$copiedFiles[] = $this->copyFile($file, $data['contents'], $data['executable'], $options);
}
@@ -148,28 +128,6 @@ private function copyFile(string $to, string $contents, bool $executable, array
return $copiedFile;
}
- private function removeFiles(array $manifest, array $files, string $to)
- {
- foreach ($manifest as $source => $target) {
- $target = $this->options->expandTargetDir($target);
-
- if ('.git' === $target) {
- // never remove the main Git directory, even if it was created by a recipe
- continue;
- }
-
- if ('/' === substr($source, -1)) {
- foreach (array_keys($files) as $file) {
- if (0 === strpos($file, $source)) {
- $this->removeFile($this->path->concatenate([$to, $target, substr($file, \strlen($source))]));
- }
- }
- } else {
- $this->removeFile($this->path->concatenate([$to, $target]));
- }
- }
- }
-
private function removeFile(string $to)
{
if (!file_exists($to)) {
diff --git a/src/Configurator/DockerComposeConfigurator.php b/src/Configurator/DockerComposeConfigurator.php
index 4f471ed6..9a13906e 100644
--- a/src/Configurator/DockerComposeConfigurator.php
+++ b/src/Configurator/DockerComposeConfigurator.php
@@ -260,7 +260,7 @@ private function configureDockerCompose(Recipe $recipe, array $config, bool $upd
}
// Skip blank lines and comments
- if (('' !== $ltrimedLine && 0 === strpos($ltrimedLine, '#')) || '' === trim($line)) {
+ if (('' !== $ltrimedLine && str_starts_with($ltrimedLine, '#')) || '' === trim($line)) {
continue;
}
@@ -349,7 +349,7 @@ private function getContentsAfterApplyingRecipe(string $rootDir, Recipe $recipe,
$updatedContents = [];
foreach ($files as $file) {
$localPath = $file;
- if (0 === strpos($file, $rootDir)) {
+ if (str_starts_with($file, $rootDir)) {
$localPath = substr($file, \strlen($rootDir) + 1);
}
$localPath = ltrim($localPath, '/\\');
diff --git a/src/Downloader.php b/src/Downloader.php
index e6bb5673..a0b971ee 100644
--- a/src/Downloader.php
+++ b/src/Downloader.php
@@ -60,9 +60,9 @@ public function __construct(Composer $composer, IOInterface $io, HttpDownloader
if (null === $endpoint = $composer->getPackage()->getExtra()['symfony']['endpoint'] ?? null) {
$this->endpoints = self::DEFAULT_ENDPOINTS;
- } elseif (\is_array($endpoint) || false !== strpos($endpoint, '.json') || 'flex://defaults' === $endpoint) {
+ } elseif (\is_array($endpoint) || str_contains($endpoint, '.json') || 'flex://defaults' === $endpoint) {
$this->endpoints = array_values((array) $endpoint);
- if (\is_string($endpoint) && false !== strpos($endpoint, '.json')) {
+ if (\is_string($endpoint) && str_contains($endpoint, '.json')) {
$this->endpoints[] = 'flex://defaults';
}
} else {
@@ -71,7 +71,7 @@ public function __construct(Composer $composer, IOInterface $io, HttpDownloader
if (false === $endpoint = getenv('SYMFONY_ENDPOINT')) {
// no-op
- } elseif (false !== strpos($endpoint, '.json') || 'flex://defaults' === $endpoint) {
+ } elseif (str_contains($endpoint, '.json') || 'flex://defaults' === $endpoint) {
$this->endpoints ?? $this->endpoints = self::DEFAULT_ENDPOINTS;
array_unshift($this->endpoints, $endpoint);
$this->legacyEndpoint = null;
@@ -174,7 +174,7 @@ public function getRecipes(array $operations): array
if ($operation instanceof InformationOperation && $operation->getVersion()) {
$version = $operation->getVersion();
}
- if (0 === strpos($version, 'dev-') && isset($package->getExtra()['branch-alias'])) {
+ if (str_starts_with($version, 'dev-') && isset($package->getExtra()['branch-alias'])) {
$branchAliases = $package->getExtra()['branch-alias'];
if (
(isset($branchAliases[$version]) && $alias = $branchAliases[$version])
diff --git a/src/Flex.php b/src/Flex.php
index a17aedec..fdaaf003 100644
--- a/src/Flex.php
+++ b/src/Flex.php
@@ -111,13 +111,19 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
$this->composer = $composer;
$this->io = $io;
$this->config = $composer->getConfig();
+
+ $composerFile = Factory::getComposerFile();
+ $composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock';
+ $symfonyLock = str_replace('composer', 'symfony', basename($composerLock));
+
+ $this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock'));
$this->options = $this->initOptions();
// if Flex is being upgraded, the original operations from the original Flex
// instance are stored in the static property, so we can reuse them now.
- if (property_exists(self::class, 'storedOperations') && self::$storedOperations) {
- $this->operations = self::$storedOperations;
- self::$storedOperations = [];
+ if (property_exists(Flex::class, 'storedOperations') && Flex::$storedOperations) {
+ $this->operations = Flex::$storedOperations;
+ Flex::$storedOperations = [];
}
$symfonyRequire = preg_replace('/\.x$/', '.x-dev', getenv('SYMFONY_REQUIRE') ?: ($composer->getPackage()->getExtra()['symfony']['require'] ?? ''));
@@ -130,12 +136,7 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
$this->filter = new PackageFilter($io, $symfonyRequire, $this->downloader);
}
- $composerFile = Factory::getComposerFile();
- $composerLock = 'json' === pathinfo($composerFile, \PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile.'.lock';
- $symfonyLock = str_replace('composer', 'symfony', basename($composerLock));
-
$this->configurator = new Configurator($composer, $io, $this->options);
- $this->lock = new Lock(getenv('SYMFONY_LOCKFILE') ?: \dirname($composerLock).'/'.(basename($composerLock) !== $symfonyLock ? $symfonyLock : 'symfony.lock'));
$disable = true;
foreach (array_merge($composer->getPackage()->getRequires() ?? [], $composer->getPackage()->getDevRequires() ?? []) as $link) {
@@ -210,8 +211,9 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
*/
public function deactivate(Composer $composer, IOInterface $io)
{
- // store operations in case Flex is being upgraded
- self::$storedOperations = $this->operations;
+ // Using `Flex::` instead of `self::` to avoid issues when
+ // composer renames plugin classes when upgrading them
+ Flex::$storedOperations = $this->operations;
self::$activated = false;
}
@@ -351,7 +353,7 @@ public function install(Event $event)
$runtime = $this->options->get('runtime');
$dotenvPath = $rootDir.'/'.($runtime['dotenv_path'] ?? '.env');
- if (!file_exists($dotenvPath) && !file_exists($dotenvPath.'.local') && file_exists($dotenvPath.'.dist') && false === strpos(file_get_contents($dotenvPath.'.dist'), '.env.local')) {
+ if (!file_exists($dotenvPath) && !file_exists($dotenvPath.'.local') && file_exists($dotenvPath.'.dist') && !str_contains(file_get_contents($dotenvPath.'.dist'), '.env.local')) {
copy($dotenvPath.'.dist', $dotenvPath);
}
@@ -707,7 +709,7 @@ private function initOptions(): Options
'runtime' => $extra['runtime'] ?? [],
], $extra);
- return new Options($options, $this->io);
+ return new Options($options, $this->io, $this->lock);
}
private function formatOrigin(Recipe $recipe): string
diff --git a/src/GithubApi.php b/src/GithubApi.php
index 61d4edef..e33c671d 100644
--- a/src/GithubApi.php
+++ b/src/GithubApi.php
@@ -142,7 +142,7 @@ public function getPullRequestForCommit(string $commit, string $repo): ?array
$bestItem = null;
foreach ($data['items'] as $item) {
// make sure the PR referenced isn't from a different repository
- if (false === strpos($item['html_url'], \sprintf('%s/pull', $repositoryName))) {
+ if (!str_contains($item['html_url'], \sprintf('%s/pull', $repositoryName))) {
continue;
}
@@ -186,7 +186,7 @@ private function requestGitHubApi(string $path)
private function getRepositoryName(string $repo): ?string
{
// only supports public repository placement
- if (0 !== strpos($repo, 'github.com')) {
+ if (!str_starts_with($repo, 'github.com')) {
return null;
}
diff --git a/src/Options.php b/src/Options.php
index ee0bb3b3..55aeae7a 100644
--- a/src/Options.php
+++ b/src/Options.php
@@ -22,11 +22,13 @@ class Options
private $options;
private $writtenFiles = [];
private $io;
+ private $lockData;
- public function __construct(array $options = [], ?IOInterface $io = null)
+ public function __construct(array $options = [], ?IOInterface $io = null, ?Lock $lock = null)
{
$this->options = $options;
$this->io = $io;
+ $this->lockData = $lock?->all() ?? [];
}
public function get(string $name)
@@ -101,6 +103,38 @@ public function shouldWriteFile(string $file, bool $overwrite, bool $skipQuestio
return $this->io && $this->io->askConfirmation(\sprintf('File "%s" has uncommitted changes, overwrite? [y/N] ', $name), false);
}
+ public function getRemovableFiles(Recipe $recipe, Lock $lock): array
+ {
+ if (null === $removableFiles = $this->lockData[$recipe->getName()]['files'] ?? null) {
+ $removableFiles = [];
+ foreach (array_keys($recipe->getFiles()) as $source => $target) {
+ if (str_ends_with($source, '/')) {
+ $removableFiles[] = $this->expandTargetDir($target);
+ }
+ }
+ }
+
+ unset($this->lockData[$recipe->getName()]);
+ $lockedFiles = array_count_values(array_merge(...array_column($lock->all(), 'files')));
+
+ $nonRemovableFiles = [];
+ foreach ($removableFiles as $i => $file) {
+ if (isset($lockedFiles[$file])) {
+ $nonRemovableFiles[] = $file;
+ unset($removableFiles[$i]);
+ }
+ }
+
+ if ($nonRemovableFiles && $this->io) {
+ $this->io?->writeError(' The following files are still referenced by other recipes, you might need to adjust them manually:');
+ foreach ($nonRemovableFiles as $file) {
+ $this->io?->writeError(' - '.$file);
+ }
+ }
+
+ return array_values($removableFiles);
+ }
+
public function toArray(): array
{
return $this->options;
diff --git a/src/PackageResolver.php b/src/PackageResolver.php
index 37987fe8..5286074a 100644
--- a/src/PackageResolver.php
+++ b/src/PackageResolver.php
@@ -70,7 +70,7 @@ public function parseVersion(string $package, string $version, bool $isRequire):
{
$guess = 'guess' === ($version ?: 'guess');
- if (0 !== strpos($package, 'symfony/')) {
+ if (!str_starts_with($package, 'symfony/')) {
return $guess ? '' : ':'.$version;
}
@@ -108,7 +108,7 @@ private function resolvePackageName(string $argument, int $position, bool $isReq
$skippedPackages[] = 'lock';
}
- if (false !== strpos($argument, '/') || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) || preg_match('{(?<=[a-z0-9_/-])\*|\*(?=[a-z0-9_/-])}i', $argument) || \in_array($argument, $skippedPackages)) {
+ if (str_contains($argument, '/') || preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $argument) || preg_match('{(?<=[a-z0-9_/-])\*|\*(?=[a-z0-9_/-])}i', $argument) || \in_array($argument, $skippedPackages)) {
return $argument;
}
@@ -140,7 +140,7 @@ private function throwAlternatives(string $argument, int $position)
$alternatives = [];
foreach ($this->downloader->getAliases() as $alias => $package) {
$lev = levenshtein($argument, $alias);
- if ($lev <= \strlen($argument) / 3 || ('' !== $argument && false !== strpos($alias, $argument))) {
+ if ($lev <= \strlen($argument) / 3 || ('' !== $argument && str_contains($alias, $argument))) {
$alternatives[$package][] = $alias;
}
}
diff --git a/src/SymfonyBundle.php b/src/SymfonyBundle.php
index 7d1d8a1c..ed6a187f 100644
--- a/src/SymfonyBundle.php
+++ b/src/SymfonyBundle.php
@@ -109,7 +109,7 @@ private function isBundleClass(string $class, string $path, bool $isPsr4): bool
// heuristic that should work in almost all cases
$classContents = file_get_contents($classPath);
- return (false !== strpos($classContents, 'Symfony\Component\HttpKernel\Bundle\Bundle'))
- || (false !== strpos($classContents, 'Symfony\Component\HttpKernel\Bundle\AbstractBundle'));
+ return str_contains($classContents, 'Symfony\Component\HttpKernel\Bundle\Bundle')
+ || str_contains($classContents, 'Symfony\Component\HttpKernel\Bundle\AbstractBundle');
}
}
diff --git a/src/Update/RecipePatcher.php b/src/Update/RecipePatcher.php
index 69773a06..680fb0f3 100644
--- a/src/Update/RecipePatcher.php
+++ b/src/Update/RecipePatcher.php
@@ -233,7 +233,7 @@ private function _applyPatchFile(RecipePatch $patch)
return true;
}
- if (false !== strpos($this->processExecutor->getErrorOutput(), 'with conflicts')) {
+ if (str_contains($this->processExecutor->getErrorOutput(), 'with conflicts')) {
// successful with conflicts
return false;
}
diff --git a/tests/Configurator/CopyFromRecipeConfiguratorTest.php b/tests/Configurator/CopyFromRecipeConfiguratorTest.php
index 95f01656..2d6ba3c5 100644
--- a/tests/Configurator/CopyFromRecipeConfiguratorTest.php
+++ b/tests/Configurator/CopyFromRecipeConfiguratorTest.php
@@ -61,7 +61,7 @@ public function testConfigureLocksFiles()
public function testConfigureAndOverwriteFiles()
{
if (!file_exists($this->targetDirectory)) {
- mkdir($this->targetDirectory);
+ @mkdir($this->targetDirectory, 0777, true);
}
file_put_contents($this->targetFile, '-');
$lock = $this->getMockBuilder(Lock::class)->disableOriginalConstructor()->getMock();
@@ -99,17 +99,22 @@ public function testConfigure()
public function testUnconfigureKeepsLockedFiles()
{
if (!file_exists($this->sourceDirectory)) {
- mkdir($this->sourceDirectory);
+ @mkdir($this->sourceDirectory, 0777, true);
+ }
+ if (!file_exists($this->targetDirectory)) {
+ @mkdir($this->targetDirectory, 0777, true);
}
+ file_put_contents($this->targetFile, '');
file_put_contents($this->sourceFile, '-');
- $this->assertFileExists($this->sourceFile);
$lock = new Lock(FLEX_TEST_DIR.'/test.lock');
- $lock->set('other-recipe', ['files' => ['./'.$this->targetFileRelativePath]]);
+ $lock->set('other-recipe', ['files' => [$this->targetFileRelativePath]]);
+ $this->recipe->method('getName')->willReturn('test-recipe');
$this->createConfigurator()->unconfigure($this->recipe, [$this->targetFileRelativePath], $lock);
$this->assertFileExists($this->sourceFile);
+ $this->assertFileExists($this->targetFile);
}
public function testUnconfigure()
@@ -118,11 +123,12 @@ public function testUnconfigure()
$this->io->expects($this->at(1))->method('writeError')->with([' Removed "./config/file">']);
if (!file_exists($this->targetDirectory)) {
- mkdir($this->targetDirectory);
+ @mkdir($this->targetDirectory, 0777, true);
}
file_put_contents($this->targetFile, '');
$this->assertFileExists($this->targetFile);
$lock = $this->getMockBuilder(Lock::class)->disableOriginalConstructor()->getMock();
+ $this->recipe->method('getName')->willReturn('test-recipe');
$this->createConfigurator()->unconfigure($this->recipe, [$this->targetFileRelativePath], $lock);
$this->assertFileDoesNotExist($this->targetFile);
}
@@ -270,8 +276,6 @@ public function testUpdateResolveDirectories()
protected function setUp(): void
{
- parent::setUp();
-
$this->sourceDirectory = FLEX_TEST_DIR.'/source';
$this->sourceFileRelativePath = 'source/file';
$this->sourceFile = $this->sourceDirectory.'/file';
@@ -294,14 +298,16 @@ protected function setUp(): void
protected function tearDown(): void
{
- parent::tearDown();
-
$this->cleanUpTargetFiles();
}
private function createConfigurator(): CopyFromRecipeConfigurator
{
- return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config'], $this->io));
+ $lock = new Lock(FLEX_TEST_DIR.'/test.lock');
+ $lock->set('test-recipe', ['files' => [$this->targetFileRelativePath]]);
+ $options = new Options(['root-dir' => FLEX_TEST_DIR, 'config-dir' => 'config'], $this->io, $lock);
+
+ return new CopyFromRecipeConfigurator($this->getMockBuilder(Composer::class)->getMock(), $this->io, $options);
}
private function cleanUpTargetFiles()