mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-14 04:00:57 +03:00
Merge pull request #440 from acelaya/feature/locked-installation
Feature/locked installation
This commit is contained in:
commit
1341d4fe57
22 changed files with 643 additions and 79 deletions
|
@ -41,6 +41,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
* True-Client-IP
|
||||
* X-Real-IP
|
||||
|
||||
* [#440](https://github.com/shlinkio/shlink/pull/440) Created `db:create` command, which improves how the shlink database is created, with these benefits:
|
||||
|
||||
* It sets up a lock which prevents the command to be run multiple times.
|
||||
* It checks of the database does not exist, and creates it in that case.
|
||||
* It checks if the database tables already exist, exiting gracefully in that case.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#430](https://github.com/shlinkio/shlink/issues/430) Updated to [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) 1.2.2
|
||||
|
|
|
@ -268,6 +268,8 @@ Available commands:
|
|||
config
|
||||
config:generate-charset [DEPRECATED] Generates a character set sample just by shuffling the default one, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
|
||||
config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption
|
||||
db
|
||||
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists
|
||||
short-url
|
||||
short-url:delete [short-code:delete] Deletes a short URL
|
||||
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it
|
||||
|
|
|
@ -31,41 +31,41 @@
|
|||
"monolog/monolog": "^1.21",
|
||||
"ocramius/proxy-manager": "^2.0",
|
||||
"phly/phly-event-dispatcher": "^1.0",
|
||||
"shlinkio/shlink-installer": "^1.1",
|
||||
"symfony/console": "^4.2",
|
||||
"symfony/filesystem": "^4.2",
|
||||
"symfony/lock": "^4.2",
|
||||
"symfony/process": "^4.2",
|
||||
"shlinkio/shlink-installer": "^1.2.1",
|
||||
"symfony/console": "^4.3",
|
||||
"symfony/filesystem": "^4.3",
|
||||
"symfony/lock": "^4.3",
|
||||
"symfony/process": "^4.3",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
"zendframework/zend-diactoros": "^2.1.1",
|
||||
"zendframework/zend-expressive": "^3.0",
|
||||
"zendframework/zend-config": "^3.3",
|
||||
"zendframework/zend-config-aggregator": "^1.1",
|
||||
"zendframework/zend-diactoros": "^2.1.3",
|
||||
"zendframework/zend-expressive": "^3.2",
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.3",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.1",
|
||||
"zendframework/zend-expressive-swoole": "^2.4",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"zendframework/zend-servicemanager": "^3.2",
|
||||
"zendframework/zend-stdlib": "^3.0"
|
||||
"zendframework/zend-i18n": "^2.9",
|
||||
"zendframework/zend-inputfilter": "^2.10",
|
||||
"zendframework/zend-paginator": "^2.8",
|
||||
"zendframework/zend-servicemanager": "^3.4",
|
||||
"zendframework/zend-stdlib": "^3.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"devster/ubench": "^2.0",
|
||||
"doctrine/data-fixtures": "^1.3",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"filp/whoops": "^2.0",
|
||||
"filp/whoops": "^2.4",
|
||||
"infection/infection": "^0.12.2",
|
||||
"phpstan/phpstan": "^0.11.2",
|
||||
"phpunit/phpcov": "^6.0",
|
||||
"phpunit/phpunit": "^8.0",
|
||||
"phpunit/phpunit": "^8.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~1.2.2",
|
||||
"symfony/dotenv": "^4.2",
|
||||
"symfony/var-dumper": "^4.2",
|
||||
"symfony/dotenv": "^4.3",
|
||||
"symfony/var-dumper": "^4.3",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
"zendframework/zend-expressive-tooling": "^1.0"
|
||||
"zendframework/zend-expressive-tooling": "^1.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
@ -36,4 +36,13 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
'db_create_schema' => [
|
||||
'command' => 'bin/cli db:create',
|
||||
],
|
||||
// 'db_migrate' => [
|
||||
// 'command' => 'bin/cli db:migrate',
|
||||
// ],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -28,6 +28,8 @@ return [
|
|||
Command\Tag\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||
|
||||
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -3,15 +3,18 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use GeoIp2\Database\Reader;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Lock;
|
||||
use Symfony\Component\Console as SymfonyCli;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
|
@ -19,7 +22,9 @@ return [
|
|||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Application::class => Factory\ApplicationFactory::class,
|
||||
SymfonyCli\Application::class => Factory\ApplicationFactory::class,
|
||||
SymfonyCli\Helper\ProcessHelper::class => Factory\ProcessHelperFactory::class,
|
||||
PhpExecutableFinder::class => InvokableFactory::class,
|
||||
|
||||
GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||
|
||||
|
@ -44,11 +49,13 @@ return [
|
|||
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, Lock\Factory::class],
|
||||
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, Locker::class],
|
||||
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||
|
@ -60,7 +67,7 @@ return [
|
|||
Command\Visit\LocateVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpLocationResolverInterface::class,
|
||||
Lock\Factory::class,
|
||||
Locker::class,
|
||||
GeolocationDbUpdater::class,
|
||||
],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||
|
@ -73,6 +80,14 @@ return [
|
|||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class],
|
||||
|
||||
Command\Db\CreateDatabaseCommand::class => [
|
||||
Locker::class,
|
||||
SymfonyCli\Helper\ProcessHelper::class,
|
||||
PhpExecutableFinder::class,
|
||||
Connection::class,
|
||||
NoDbNameConnectionFactory::SERVICE_NAME,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
33
module/CLI/src/Command/Db/AbstractDatabaseCommand.php
Normal file
33
module/CLI/src/Command/Db/AbstractDatabaseCommand.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function array_unshift;
|
||||
|
||||
abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||
{
|
||||
/** @var ProcessHelper */
|
||||
private $processHelper;
|
||||
/** @var string */
|
||||
private $phpBinary;
|
||||
|
||||
public function __construct(Locker $locker, ProcessHelper $processHelper, PhpExecutableFinder $phpFinder)
|
||||
{
|
||||
parent::__construct($locker);
|
||||
$this->processHelper = $processHelper;
|
||||
$this->phpBinary = $phpFinder->find(false) ?: 'php';
|
||||
}
|
||||
|
||||
protected function runPhpCommand(OutputInterface $output, array $command): void
|
||||
{
|
||||
array_unshift($command, $this->phpBinary);
|
||||
$this->processHelper->run($output, $command);
|
||||
}
|
||||
}
|
98
module/CLI/src/Command/Db/CreateDatabaseCommand.php
Normal file
98
module/CLI/src/Command/Db/CreateDatabaseCommand.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
use function Functional\contains;
|
||||
|
||||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
{
|
||||
public const NAME = 'db:create';
|
||||
public const DOCTRINE_HELPER_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
||||
public const DOCTRINE_HELPER_COMMAND = 'orm:schema-tool:create';
|
||||
|
||||
/** @var Connection */
|
||||
private $regularConn;
|
||||
/** @var Connection */
|
||||
private $noDbNameConn;
|
||||
|
||||
public function __construct(
|
||||
Locker $locker,
|
||||
ProcessHelper $processHelper,
|
||||
PhpExecutableFinder $phpFinder,
|
||||
Connection $conn,
|
||||
Connection $noDbNameConn
|
||||
) {
|
||||
parent::__construct($locker, $processHelper, $phpFinder);
|
||||
$this->regularConn = $conn;
|
||||
$this->noDbNameConn = $noDbNameConn;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(
|
||||
'Creates the database needed for shlink to work. It will do nothing if the database already exists'
|
||||
);
|
||||
}
|
||||
|
||||
protected function lockedExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->checkDbExists();
|
||||
|
||||
if ($this->schemaExists()) {
|
||||
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
// Create database
|
||||
$io->writeln('<fg=blue>Creating database tables...</>');
|
||||
$this->runPhpCommand($output, [self::DOCTRINE_HELPER_SCRIPT, self::DOCTRINE_HELPER_COMMAND]);
|
||||
$io->success('Database properly created!');
|
||||
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function checkDbExists(): void
|
||||
{
|
||||
if ($this->regularConn->getDatabasePlatform()->getName() === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// In order to create the new database, we have to use a connection where the dbname was not set.
|
||||
// Otherwise, it will fail to connect and will not be able to create the new database
|
||||
$schemaManager = $this->noDbNameConn->getSchemaManager();
|
||||
$databases = $schemaManager->listDatabases();
|
||||
$shlinkDatabase = $this->regularConn->getDatabase();
|
||||
|
||||
if (! contains($databases, $shlinkDatabase)) {
|
||||
$schemaManager->createDatabase($shlinkDatabase);
|
||||
}
|
||||
}
|
||||
|
||||
private function schemaExists(): bool
|
||||
{
|
||||
// If at least one of the shlink tables exist, we will consider the database exists somehow.
|
||||
// Any inconsistency will be taken care by the migrations
|
||||
$schemaManager = $this->regularConn->getSchemaManager();
|
||||
return ! empty($schemaManager->listTableNames());
|
||||
}
|
||||
|
||||
protected function getLockConfig(): LockedCommandConfig
|
||||
{
|
||||
return new LockedCommandConfig($this->getName(), true);
|
||||
}
|
||||
}
|
47
module/CLI/src/Command/Util/AbstractLockedCommand.php
Normal file
47
module/CLI/src/Command/Util/AbstractLockedCommand.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
abstract class AbstractLockedCommand extends Command
|
||||
{
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
|
||||
public function __construct(Locker $locker)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->locker = $locker;
|
||||
}
|
||||
|
||||
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$lockConfig = $this->getLockConfig();
|
||||
$lock = $this->locker->createLock($lockConfig->lockName(), $lockConfig->ttl(), $lockConfig->isBlocking());
|
||||
|
||||
if (! $lock->acquire($lockConfig->isBlocking())) {
|
||||
$output->writeln(
|
||||
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName())
|
||||
);
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->lockedExecute($input, $output);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function lockedExecute(InputInterface $input, OutputInterface $output): int;
|
||||
|
||||
abstract protected function getLockConfig(): LockedCommandConfig;
|
||||
}
|
38
module/CLI/src/Command/Util/LockedCommandConfig.php
Normal file
38
module/CLI/src/Command/Util/LockedCommandConfig.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
||||
|
||||
final class LockedCommandConfig
|
||||
{
|
||||
private const DEFAULT_TTL = 90.0; // 1.5 minutes
|
||||
|
||||
/** @var string */
|
||||
private $lockName;
|
||||
/** @var bool */
|
||||
private $isBlocking;
|
||||
/** @var float */
|
||||
private $ttl;
|
||||
|
||||
public function __construct(string $lockName, bool $isBlocking = false, float $ttl = self::DEFAULT_TTL)
|
||||
{
|
||||
$this->lockName = $lockName;
|
||||
$this->isBlocking = $isBlocking;
|
||||
$this->ttl = $ttl;
|
||||
}
|
||||
|
||||
public function lockName(): string
|
||||
{
|
||||
return $this->lockName;
|
||||
}
|
||||
|
||||
public function isBlocking(): bool
|
||||
{
|
||||
return $this->isBlocking;
|
||||
}
|
||||
|
||||
public function ttl(): float
|
||||
{
|
||||
return $this->ttl;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Exception;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
||||
|
@ -15,16 +17,16 @@ use Shlinkio\Shlink\Core\Entity\Visit;
|
|||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class LocateVisitsCommand extends Command
|
||||
class LocateVisitsCommand extends AbstractLockedCommand
|
||||
{
|
||||
public const NAME = 'visit:locate';
|
||||
public const ALIASES = ['visit:process'];
|
||||
|
@ -33,8 +35,6 @@ class LocateVisitsCommand extends Command
|
|||
private $visitService;
|
||||
/** @var IpLocationResolverInterface */
|
||||
private $ipLocationResolver;
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
/** @var GeolocationDbUpdaterInterface */
|
||||
private $dbUpdater;
|
||||
|
||||
|
@ -49,10 +49,9 @@ class LocateVisitsCommand extends Command
|
|||
Locker $locker,
|
||||
GeolocationDbUpdaterInterface $dbUpdater
|
||||
) {
|
||||
parent::__construct();
|
||||
parent::__construct($locker);
|
||||
$this->visitService = $visitService;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->locker = $locker;
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
}
|
||||
|
||||
|
@ -64,16 +63,10 @@ class LocateVisitsCommand extends Command
|
|||
->setDescription('Resolves visits origin locations.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
protected function lockedExecute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
$lock = $this->locker->createLock(self::NAME);
|
||||
if (! $lock->acquire()) {
|
||||
$this->io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->checkDbUpdate();
|
||||
|
||||
|
@ -90,15 +83,13 @@ class LocateVisitsCommand extends Command
|
|||
|
||||
$this->io->success('Finished processing all IPs');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
$this->io->error($e->getMessage());
|
||||
if ($this->io->isVerbose()) {
|
||||
if ($e instanceof Exception && $this->io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $this->io);
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,4 +151,9 @@ class LocateVisitsCommand extends Command
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLockConfig(): LockedCommandConfig
|
||||
{
|
||||
return new LockedCommandConfig($this->getName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,32 +4,13 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class ApplicationFactory implements FactoryInterface
|
||||
class ApplicationFactory
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return CliApp
|
||||
* @throws NotFoundExceptionInterface
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): CliApp
|
||||
public function __invoke(ContainerInterface $container): CliApp
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
|
|
20
module/CLI/src/Factory/ProcessHelperFactory.php
Normal file
20
module/CLI/src/Factory/ProcessHelperFactory.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Symfony\Component\Console\Helper;
|
||||
|
||||
class ProcessHelperFactory
|
||||
{
|
||||
public function __invoke(): Helper\ProcessHelper
|
||||
{
|
||||
$processHelper = new Helper\ProcessHelper();
|
||||
$processHelper->setHelperSet(new Helper\HelperSet([
|
||||
new Helper\FormatterHelper(),
|
||||
new Helper\DebugFormatterHelper(),
|
||||
]));
|
||||
|
||||
return $processHelper;
|
||||
}
|
||||
}
|
155
module/CLI/test/Command/Db/CreateDatabaseCommandTest.php
Normal file
155
module/CLI/test/Command/Db/CreateDatabaseCommandTest.php
Normal file
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Symfony\Component\Lock\LockInterface;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
class CreateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $processHelper;
|
||||
/** @var ObjectProphecy */
|
||||
private $regularConn;
|
||||
/** @var ObjectProphecy */
|
||||
private $noDbNameConn;
|
||||
/** @var ObjectProphecy */
|
||||
private $schemaManager;
|
||||
/** @var ObjectProphecy */
|
||||
private $databasePlatform;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$locker = $this->prophesize(Locker::class);
|
||||
$lock = $this->prophesize(LockInterface::class);
|
||||
$lock->acquire(Argument::any())->willReturn(true);
|
||||
$lock->release()->will(function () {
|
||||
});
|
||||
$locker->createLock(Argument::cetera())->willReturn($lock->reveal());
|
||||
|
||||
$phpExecutableFinder = $this->prophesize(PhpExecutableFinder::class);
|
||||
$phpExecutableFinder->find(false)->willReturn('/usr/local/bin/php');
|
||||
|
||||
$this->processHelper = $this->prophesize(ProcessHelper::class);
|
||||
$this->schemaManager = $this->prophesize(AbstractSchemaManager::class);
|
||||
$this->databasePlatform = $this->prophesize(AbstractPlatform::class);
|
||||
|
||||
$this->regularConn = $this->prophesize(Connection::class);
|
||||
$this->regularConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
$this->regularConn->getDatabasePlatform()->willReturn($this->databasePlatform->reveal());
|
||||
$this->noDbNameConn = $this->prophesize(Connection::class);
|
||||
$this->noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
|
||||
|
||||
$command = new CreateDatabaseCommand(
|
||||
$locker->reveal(),
|
||||
$this->processHelper->reveal(),
|
||||
$phpExecutableFinder->reveal(),
|
||||
$this->regularConn->reveal(),
|
||||
$this->noDbNameConn->reveal()
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function successMessageIsPrintedIfDatabaseAlreadyExists(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function databaseIsCreatedIfItDoesNotExist(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldHaveBeenCalledOnce();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function tablesAreCreatedIfDatabaseIsEMpty(): void
|
||||
{
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn([]);
|
||||
$runCommand = $this->processHelper->run(Argument::type(OutputInterface::class), [
|
||||
'/usr/local/bin/php',
|
||||
CreateDatabaseCommand::DOCTRINE_HELPER_SCRIPT,
|
||||
CreateDatabaseCommand::DOCTRINE_HELPER_COMMAND,
|
||||
]);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Creating database tables...', $output);
|
||||
$this->assertStringContainsString('Database properly created!', $output);
|
||||
$getDatabase->shouldHaveBeenCalledOnce();
|
||||
$listDatabases->shouldHaveBeenCalledOnce();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
$runCommand->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function databaseCheckIsSkippedForSqlite(): void
|
||||
{
|
||||
$this->databasePlatform->getName()->willReturn('sqlite');
|
||||
|
||||
$shlinkDatabase = 'shlink_database';
|
||||
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
|
||||
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
|
||||
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () {
|
||||
});
|
||||
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$getDatabase->shouldNotHaveBeenCalled();
|
||||
$listDatabases->shouldNotHaveBeenCalled();
|
||||
$createDatabase->shouldNotHaveBeenCalled();
|
||||
$listTables->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -49,10 +49,10 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
$this->locker = $this->prophesize(Lock\Factory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire()->willReturn(true);
|
||||
$this->lock->acquire(false)->willReturn(true);
|
||||
$this->lock->release()->will(function () {
|
||||
});
|
||||
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
$this->locker->createLock(Argument::type('string'), 90.0, false)->willReturn($this->lock->reveal());
|
||||
|
||||
$command = new LocateVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
|
@ -162,9 +162,9 @@ class LocateVisitsCommandTest extends TestCase
|
|||
}
|
||||
|
||||
/** @test */
|
||||
public function noActionIsPerformedIfLockIsAcquired()
|
||||
public function noActionIsPerformedIfLockIsAcquired(): void
|
||||
{
|
||||
$this->lock->acquire()->willReturn(false);
|
||||
$this->lock->acquire(false)->willReturn(false);
|
||||
|
||||
$locateVisits = $this->visitService->locateUnlocatedVisits(Argument::cetera())->will(function () {
|
||||
});
|
||||
|
@ -174,7 +174,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
sprintf('There is already an instance of the "%s" command', LocateVisitsCommand::NAME),
|
||||
sprintf('Command "%s" is already in progress. Skipping.', LocateVisitsCommand::NAME),
|
||||
$output
|
||||
);
|
||||
$locateVisits->shouldNotHaveBeenCalled();
|
||||
|
|
|
@ -25,14 +25,7 @@ class ApplicationFactoryTest extends TestCase
|
|||
}
|
||||
|
||||
/** @test */
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = ($this->factory)($this->createServiceManager(), '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function allCommandsWhichAreServicesAreAdded()
|
||||
public function allCommandsWhichAreServicesAreAdded(): void
|
||||
{
|
||||
$sm = $this->createServiceManager([
|
||||
'commands' => [
|
||||
|
@ -45,8 +38,7 @@ class ApplicationFactoryTest extends TestCase
|
|||
$sm->setService('bar', $this->createCommandMock('bar')->reveal());
|
||||
|
||||
/** @var Application $instance */
|
||||
$instance = ($this->factory)($sm, '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
$instance = ($this->factory)($sm);
|
||||
|
||||
$this->assertTrue($instance->has('foo'));
|
||||
$this->assertTrue($instance->has('bar'));
|
||||
|
|
29
module/CLI/test/Factory/ProcessHelperFactoryTest.php
Normal file
29
module/CLI/test/Factory/ProcessHelperFactoryTest.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
||||
|
||||
class ProcessHelperFactoryTest extends TestCase
|
||||
{
|
||||
/** @var ProcessHelperFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->factory = new ProcessHelperFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function createsTheServiceWithTheProperSetOfHelpers(): void
|
||||
{
|
||||
$processHelper = ($this->factory)();
|
||||
$helperSet = $processHelper->getHelperSet();
|
||||
|
||||
$this->assertCount(2, $helperSet);
|
||||
$this->assertTrue($helperSet->has('formatter'));
|
||||
$this->assertTrue($helperSet->has('debug_formatter'));
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
return [
|
||||
|
@ -18,6 +19,8 @@ return [
|
|||
'dependencies' => [
|
||||
'factories' => [
|
||||
EntityManager::class => Doctrine\EntityManagerFactory::class,
|
||||
Connection::class => Doctrine\ConnectionFactory::class,
|
||||
Doctrine\NoDbNameConnectionFactory::SERVICE_NAME => Doctrine\NoDbNameConnectionFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
|
|
17
module/Common/src/Doctrine/ConnectionFactory.php
Normal file
17
module/Common/src/Doctrine/ConnectionFactory.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class ConnectionFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container): Connection
|
||||
{
|
||||
$em = $container->get(EntityManager::class);
|
||||
return $em->getConnection();
|
||||
}
|
||||
}
|
21
module/Common/src/Doctrine/NoDbNameConnectionFactory.php
Normal file
21
module/Common/src/Doctrine/NoDbNameConnectionFactory.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class NoDbNameConnectionFactory
|
||||
{
|
||||
public const SERVICE_NAME = 'Shlinkio\Shlink\Common\Doctrine\NoDbNameConnection';
|
||||
|
||||
public function __invoke(ContainerInterface $container): Connection
|
||||
{
|
||||
$conn = $container->get(Connection::class);
|
||||
$params = $conn->getParams();
|
||||
unset($params['dbname']);
|
||||
|
||||
return new Connection($params, $conn->getDriver(), $conn->getConfiguration(), $conn->getEventManager());
|
||||
}
|
||||
}
|
44
module/Common/test/Doctrine/ConnectionFactoryTest.php
Normal file
44
module/Common/test/Doctrine/ConnectionFactoryTest.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Doctrine\ConnectionFactory;
|
||||
|
||||
class ConnectionFactoryTest extends TestCase
|
||||
{
|
||||
/** @var ConnectionFactory */
|
||||
private $factory;
|
||||
/** @var ObjectProphecy */
|
||||
private $container;
|
||||
/** @var ObjectProphecy */
|
||||
private $em;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->container = $this->prophesize(ContainerInterface::class);
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->container->get(EntityManager::class)->willReturn($this->em->reveal());
|
||||
|
||||
$this->factory = new ConnectionFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function properServiceFallbackOccursWhenInvoked(): void
|
||||
{
|
||||
$connection = $this->prophesize(Connection::class)->reveal();
|
||||
$getConnection = $this->em->getConnection()->willReturn($connection);
|
||||
|
||||
$result = ($this->factory)($this->container->reveal());
|
||||
|
||||
$this->assertSame($connection, $result);
|
||||
$getConnection->shouldHaveBeenCalledOnce();
|
||||
$this->container->get(EntityManager::class)->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Doctrine;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
|
||||
class NoDbNameConnectionFactoryTest extends TestCase
|
||||
{
|
||||
/** @var NoDbNameConnectionFactory */
|
||||
private $factory;
|
||||
/** @var ObjectProphecy */
|
||||
private $container;
|
||||
/** @var ObjectProphecy */
|
||||
private $originalConn;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->container = $this->prophesize(ContainerInterface::class);
|
||||
$this->originalConn = $this->prophesize(Connection::class);
|
||||
$this->container->get(Connection::class)->willReturn($this->originalConn->reveal());
|
||||
|
||||
$this->factory = new NoDbNameConnectionFactory();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function createsNewConnectionRemovingDbNameFromOriginalConnectionParams(): void
|
||||
{
|
||||
$params = [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'dbname' => 'something',
|
||||
];
|
||||
$getOriginalParams = $this->originalConn->getParams()->willReturn($params);
|
||||
$getOriginalDriver = $this->originalConn->getDriver()->willReturn($this->prophesize(Driver::class)->reveal());
|
||||
$getOriginalConfig = $this->originalConn->getConfiguration()->willReturn(null);
|
||||
$getOriginalEvents = $this->originalConn->getEventManager()->willReturn(null);
|
||||
|
||||
$conn = ($this->factory)($this->container->reveal());
|
||||
|
||||
$this->assertEquals([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
], $conn->getParams());
|
||||
$getOriginalParams->shouldHaveBeenCalledOnce();
|
||||
$getOriginalDriver->shouldHaveBeenCalledOnce();
|
||||
$getOriginalConfig->shouldHaveBeenCalledOnce();
|
||||
$getOriginalEvents->shouldHaveBeenCalledOnce();
|
||||
$this->container->get(Connection::class)->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue