diff --git a/module/CLI/src/Command/Db/AbstractDatabaseCommand.php b/module/CLI/src/Command/Db/AbstractDatabaseCommand.php
index a85cb999..a38abc72 100644
--- a/module/CLI/src/Command/Db/AbstractDatabaseCommand.php
+++ b/module/CLI/src/Command/Db/AbstractDatabaseCommand.php
@@ -4,23 +4,26 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;
-use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
-use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
+use Shlinkio\Shlink\CLI\Command\Util\CommandUtils;
+use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Process\PhpExecutableFinder;
-abstract class AbstractDatabaseCommand extends AbstractLockedCommand
+abstract class AbstractDatabaseCommand extends Command
{
private string $phpBinary;
public function __construct(
- LockFactory $locker,
+ private readonly LockFactory $locker,
private readonly ProcessRunnerInterface $processRunner,
PhpExecutableFinder $phpFinder,
) {
- parent::__construct($locker);
+ parent::__construct();
$this->phpBinary = $phpFinder->find(false) ?: 'php';
}
@@ -30,8 +33,15 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand
$this->processRunner->run($output, $command);
}
- protected function getLockConfig(): LockedCommandConfig
+ final protected function execute(InputInterface $input, OutputInterface $output): int
{
- return LockedCommandConfig::blocking($this->getName() ?? static::class);
+ return CommandUtils::executeWithLock(
+ $this->locker,
+ LockConfig::blocking($this->getName() ?? static::class),
+ new SymfonyStyle($input, $output),
+ fn () => $this->lockedExecute($input, $output),
+ );
}
+
+ abstract protected function lockedExecute(InputInterface $input, OutputInterface $output): int;
}
diff --git a/module/CLI/src/Command/Util/AbstractLockedCommand.php b/module/CLI/src/Command/Util/AbstractLockedCommand.php
deleted file mode 100644
index a4c3ef5d..00000000
--- a/module/CLI/src/Command/Util/AbstractLockedCommand.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getLockConfig();
- $lock = $this->locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
-
- if (! $lock->acquire($lockConfig->isBlocking)) {
- $output->writeln(
- sprintf('Command "%s" is already in progress. Skipping.', $lockConfig->lockName),
- );
- return self::INVALID;
- }
-
- try {
- return $this->lockedExecute($input, $output);
- } finally {
- $lock->release();
- }
- }
-
- abstract protected function lockedExecute(InputInterface $input, OutputInterface $output): int;
-
- abstract protected function getLockConfig(): LockedCommandConfig;
-}
diff --git a/module/CLI/src/Command/Util/CommandUtils.php b/module/CLI/src/Command/Util/CommandUtils.php
index 76085f1a..69158275 100644
--- a/module/CLI/src/Command/Util/CommandUtils.php
+++ b/module/CLI/src/Command/Util/CommandUtils.php
@@ -6,6 +6,9 @@ namespace Shlinkio\Shlink\CLI\Command\Util;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\Lock\LockFactory;
+
+use function sprintf;
class CommandUtils
{
@@ -25,4 +28,31 @@ class CommandUtils
return $callback();
}
+
+ /**
+ * Runs a callback with a lock, making sure the lock is released after running the callback, and the callback does
+ * not run if the lock is already acquired.
+ *
+ * @param callable(): int $callback
+ */
+ public static function executeWithLock(
+ LockFactory $locker,
+ LockConfig $lockConfig,
+ SymfonyStyle $io,
+ callable $callback,
+ ): int {
+ $lock = $locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
+ if (! $lock->acquire($lockConfig->isBlocking)) {
+ $io->writeln(
+ sprintf('Command "%s" is already in progress. Skipping.', $lockConfig->lockName),
+ );
+ return Command::INVALID;
+ }
+
+ try {
+ return $callback();
+ } finally {
+ $lock->release();
+ }
+ }
}
diff --git a/module/CLI/src/Command/Util/LockedCommandConfig.php b/module/CLI/src/Command/Util/LockConfig.php
similarity index 56%
rename from module/CLI/src/Command/Util/LockedCommandConfig.php
rename to module/CLI/src/Command/Util/LockConfig.php
index a8834d92..8f8fb09c 100644
--- a/module/CLI/src/Command/Util/LockedCommandConfig.php
+++ b/module/CLI/src/Command/Util/LockConfig.php
@@ -4,24 +4,24 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;
-final class LockedCommandConfig
+final readonly class LockConfig
{
public const float DEFAULT_TTL = 600.0; // 10 minutes
private function __construct(
- public readonly string $lockName,
- public readonly bool $isBlocking,
- public readonly float $ttl = self::DEFAULT_TTL,
+ public string $lockName,
+ public bool $isBlocking,
+ public float $ttl = self::DEFAULT_TTL,
) {
}
public static function blocking(string $lockName): self
{
- return new self($lockName, true);
+ return new self($lockName, isBlocking: true);
}
public static function nonBlocking(string $lockName): self
{
- return new self($lockName, false);
+ return new self($lockName, isBlocking: false);
}
}
diff --git a/module/CLI/src/Command/Visit/LocateVisitsCommand.php b/module/CLI/src/Command/Visit/LocateVisitsCommand.php
index 3ed2edf9..fa1b13b4 100644
--- a/module/CLI/src/Command/Visit/LocateVisitsCommand.php
+++ b/module/CLI/src/Command/Visit/LocateVisitsCommand.php
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
-use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
-use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
+use Shlinkio\Shlink\CLI\Command\Util\CommandUtils;
+use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocatorInterface;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
@@ -26,7 +27,7 @@ use Throwable;
use function sprintf;
-class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocationHelperInterface
+class LocateVisitsCommand extends Command implements VisitGeolocationHelperInterface
{
public const string NAME = 'visit:locate';
@@ -35,9 +36,9 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
public function __construct(
private readonly VisitLocatorInterface $visitLocator,
private readonly VisitToLocationHelperInterface $visitToLocation,
- LockFactory $locker,
+ private readonly LockFactory $locker,
) {
- parent::__construct($locker);
+ parent::__construct();
}
protected function configure(): void
@@ -97,7 +98,17 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
return $this->io->confirm('Do you want to proceed?', false);
}
- protected function lockedExecute(InputInterface $input, OutputInterface $output): int
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ return CommandUtils::executeWithLock(
+ $this->locker,
+ LockConfig::nonBlocking(self::NAME),
+ new SymfonyStyle($input, $output),
+ fn () => $this->locateVisits($input),
+ );
+ }
+
+ private function locateVisits(InputInterface $input): int
{
$retry = $input->getOption('retry');
$all = $retry && $input->getOption('all');
@@ -174,9 +185,4 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
}
}
-
- protected function getLockConfig(): LockedCommandConfig
- {
- return LockedCommandConfig::nonBlocking(self::NAME);
- }
}
diff --git a/module/CLI/src/Util/ProcessRunner.php b/module/CLI/src/Util/ProcessRunner.php
index af9577ea..7e650f9d 100644
--- a/module/CLI/src/Util/ProcessRunner.php
+++ b/module/CLI/src/Util/ProcessRunner.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;
use Closure;
-use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
+use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -24,7 +24,7 @@ class ProcessRunner implements ProcessRunnerInterface
{
$this->createProcess = $createProcess !== null
? $createProcess(...)
- : static fn (array $cmd) => new Process($cmd, timeout: LockedCommandConfig::DEFAULT_TTL);
+ : static fn (array $cmd) => new Process($cmd, timeout: LockConfig::DEFAULT_TTL);
}
public function run(OutputInterface $output, array $cmd): void