diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6dca9..138bfbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [7.1.0] - 2022-04-23 +### Added +* [#157](https://github.com/shlinkio/shlink-installer/issues/157) Added support for the timezone config option. + +### Changed +* *Nothing* + +### Changed +* *Nothing* + +### Deprecated +* Deprecated webhook-related config options. + +### Removed +* *Nothing* + +### Fixed +* [#155](https://github.com/shlinkio/shlink-installer/issues/155) Fixed router config cache not getting deleted when editing config options. + + ## [7.0.2] - 2022-02-19 ### Added * *Nothing* diff --git a/composer.json b/composer.json index 4f79035..410e9c7 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,12 @@ ], "require": { "php": "^8.0", - "laminas/laminas-config": "^3.5", - "laminas/laminas-config-aggregator": "^1.5", - "laminas/laminas-servicemanager": "^3.7", - "laminas/laminas-stdlib": "^3.4", + "laminas/laminas-config": "^3.7", + "laminas/laminas-config-aggregator": "^1.7", + "laminas/laminas-servicemanager": "^3.11.2", + "laminas/laminas-stdlib": "^3.7", "lstrojny/functional-php": "^1.17", - "shlinkio/shlink-config": "^1.5", + "shlinkio/shlink-config": "^1.6", "symfony/console": "^6.0", "symfony/filesystem": "^6.0", "symfony/process": "^6.0" @@ -27,7 +27,7 @@ "devster/ubench": "^2.0", "infection/infection": "^0.26", "phpspec/prophecy-phpunit": "^2.0", - "phpstan/phpstan": "^1.2", + "phpstan/phpstan": "^1.5", "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-master", "shlinkio/php-coding-standard": "~2.2.0", diff --git a/config/config.php b/config/config.php index ccab9ea..f66ffab 100644 --- a/config/config.php +++ b/config/config.php @@ -86,6 +86,7 @@ 'APPLICATION' => [ 'Delete short URLs > Visits threshold' => Config\Option\Visit\VisitsThresholdConfigOption::class, 'Base path' => Config\Option\BasePathConfigOption::class, + 'Timezone' => Config\Option\TimezoneConfigOption::class, 'Swoole > Amount of task workers' => Config\Option\Worker\TaskWorkerNumConfigOption::class, 'Swoole > Amount of web workers' => Config\Option\Worker\WebWorkerNumConfigOption::class, ], @@ -107,6 +108,7 @@ 'factories' => [ Config\Option\BasePathConfigOption::class => InvokableFactory::class, + Config\Option\TimezoneConfigOption::class => InvokableFactory::class, Config\Option\Visit\VisitsThresholdConfigOption::class => InvokableFactory::class, Config\Option\Database\DatabaseDriverConfigOption::class => InvokableFactory::class, Config\Option\Database\DatabaseNameConfigOption::class => InvokableFactory::class, diff --git a/src/Config/Option/BasePathConfigOption.php b/src/Config/Option/BasePathConfigOption.php index e057597..51ab5a6 100644 --- a/src/Config/Option/BasePathConfigOption.php +++ b/src/Config/Option/BasePathConfigOption.php @@ -22,8 +22,8 @@ public function getEnvVar(): string public function ask(StyleInterface $io, PathCollection $currentOptions): string { return $io->ask( - 'What is the path from which shlink is going to be served? (Leave empty if you plan to serve ' - . 'shlink from the root of the domain)', + 'What is the path from which shlink is going to be served? (It must include a leading bar, like "/shlink". ' + . 'Leave empty if you plan to serve shlink from the root of the domain)', ) ?? ''; } } diff --git a/src/Config/Option/TimezoneConfigOption.php b/src/Config/Option/TimezoneConfigOption.php new file mode 100644 index 0000000..d1165b2 --- /dev/null +++ b/src/Config/Option/TimezoneConfigOption.php @@ -0,0 +1,28 @@ +pathExists([$this->getEnvVar()]); + } + + public function ask(StyleInterface $io, PathCollection $currentOptions): ?string + { + return $io->ask( + 'Set the timezone in which your Shlink instance is running (leave empty to use the one set in PHP config)', + ); + } +} diff --git a/src/Config/Option/Visit/OrphanVisitsWebhooksConfigOption.php b/src/Config/Option/Visit/OrphanVisitsWebhooksConfigOption.php index b580367..fc1c85a 100644 --- a/src/Config/Option/Visit/OrphanVisitsWebhooksConfigOption.php +++ b/src/Config/Option/Visit/OrphanVisitsWebhooksConfigOption.php @@ -9,6 +9,7 @@ use Shlinkio\Shlink\Installer\Config\Option\DependentConfigOptionInterface; use Symfony\Component\Console\Style\StyleInterface; +/** @deprecated */ class OrphanVisitsWebhooksConfigOption extends AbstractSwooleDependentConfigOption implements DependentConfigOptionInterface { diff --git a/src/Config/Option/Visit/VisitsWebhooksConfigOption.php b/src/Config/Option/Visit/VisitsWebhooksConfigOption.php index a0781ea..a804b3e 100644 --- a/src/Config/Option/Visit/VisitsWebhooksConfigOption.php +++ b/src/Config/Option/Visit/VisitsWebhooksConfigOption.php @@ -11,6 +11,7 @@ use function implode; +/** @deprecated */ class VisitsWebhooksConfigOption extends AbstractSwooleDependentConfigOption { use ConfigOptionsValidatorsTrait; diff --git a/src/Service/ShlinkAssetsHandler.php b/src/Service/ShlinkAssetsHandler.php index 68c208b..c4de698 100644 --- a/src/Service/ShlinkAssetsHandler.php +++ b/src/Service/ShlinkAssetsHandler.php @@ -17,7 +17,7 @@ class ShlinkAssetsHandler implements ShlinkAssetsHandlerInterface use AskUtilsTrait; public const GENERATED_CONFIG_PATH = 'config/params/generated_config.php'; - private const CACHED_CONFIG_PATH = 'data/cache/app_config.php'; + private const CACHED_CONFIGS_PATHS = ['data/cache/app_config.php', 'data/cache/fastroute_cached_routes.php']; private const SQLITE_DB_PATH = 'data/database.sqlite'; private const GEO_LITE_DB_PATH = 'data/GeoLite2-City.mmdb'; @@ -30,17 +30,23 @@ public function __construct(private Filesystem $filesystem) */ public function dropCachedConfigIfAny(StyleInterface $io): void { - if (! $this->filesystem->exists(self::CACHED_CONFIG_PATH)) { + foreach (self::CACHED_CONFIGS_PATHS as $file) { + $this->dropCachedConfigFile($file, $io); + } + } + + private function dropCachedConfigFile(string $file, StyleInterface $io): void + { + if (! $this->filesystem->exists($file)) { return; } try { - $this->filesystem->remove(self::CACHED_CONFIG_PATH); + $this->filesystem->remove($file); } catch (IOException $e) { - $io->error(sprintf( - 'Could not delete cached config! You will have to manually delete the "%s" file.', - self::CACHED_CONFIG_PATH, - )); + $io->error( + sprintf('Could not delete cached config! You will have to manually delete the "%s" file.', $file), + ); throw $e; } } diff --git a/test/Config/Option/BasePathConfigOptionTest.php b/test/Config/Option/BasePathConfigOptionTest.php index 412fa5b..05df6aa 100644 --- a/test/Config/Option/BasePathConfigOptionTest.php +++ b/test/Config/Option/BasePathConfigOptionTest.php @@ -36,8 +36,8 @@ public function expectedQuestionIsAsked(?string $answer, string $expectedAnswer) { $io = $this->prophesize(StyleInterface::class); $ask = $io->ask( - 'What is the path from which shlink is going to be served? (Leave empty if you plan to serve ' - . 'shlink from the root of the domain)', + 'What is the path from which shlink is going to be served? (It must include a leading bar, like "/shlink". ' + . 'Leave empty if you plan to serve shlink from the root of the domain)', )->willReturn($answer); $answer = $this->configOption->ask($io->reveal(), new PathCollection()); diff --git a/test/Config/Option/TimezoneConfigOptionTest.php b/test/Config/Option/TimezoneConfigOptionTest.php new file mode 100644 index 0000000..785b4eb --- /dev/null +++ b/test/Config/Option/TimezoneConfigOptionTest.php @@ -0,0 +1,67 @@ +configOption = new TimezoneConfigOption(); + } + + /** @test */ + public function returnsExpectedEnvVar(): void + { + self::assertEquals('TIMEZONE', $this->configOption->getEnvVar()); + } + + /** + * @test + * @dataProvider provideValidAnswers + */ + public function expectedQuestionIsAsked(?string $answer, string $expectedAnswer): void + { + $io = $this->prophesize(StyleInterface::class); + $ask = $io->ask( + 'Set the timezone in which your Shlink instance is running (leave empty to use the one set in PHP config)', + )->willReturn($answer); + + $answer = $this->configOption->ask($io->reveal(), new PathCollection()); + + self::assertEquals($expectedAnswer, $answer); + $ask->shouldHaveBeenCalledOnce(); + } + + public function provideValidAnswers(): iterable + { + yield ['the_answer', 'the_answer']; + yield [null, '']; + } + + /** + * @test + * @dataProvider provideCurrentOptions + */ + public function shouldBeCalledOnlyIfItDoesNotYetExist(PathCollection $currentOptions, bool $expected): void + { + self::assertEquals($expected, $this->configOption->shouldBeAsked($currentOptions)); + } + + public function provideCurrentOptions(): iterable + { + yield 'not exists in config' => [new PathCollection(), true]; + yield 'exists in config' => [new PathCollection(['TIMEZONE' => 'America/Los_Angeles']), false]; + } +} diff --git a/test/Config/Util/ConfigOptionsValidatorsTraitTest.php b/test/Config/Util/ConfigOptionsValidatorsTraitTest.php index 56a92ff..e29a217 100644 --- a/test/Config/Util/ConfigOptionsValidatorsTraitTest.php +++ b/test/Config/Util/ConfigOptionsValidatorsTraitTest.php @@ -23,17 +23,17 @@ protected function setUp(): void * @test * @dataProvider provideValidUrls */ - public function webhooksAreProperlySplitAndValidated(?string $webhooks, array $expectedResult): void + public function urlsAreProperlySplitAndValidated(?string $urls, array $expectedResult): void { - $result = $this->validators->splitAndValidateMultipleUrls($webhooks); + $result = $this->validators->splitAndValidateMultipleUrls($urls); self::assertEquals($expectedResult, $result); } public function provideValidUrls(): iterable { - yield 'no webhooks' => [null, []]; - yield 'single webhook' => ['https://foo.com/bar', ['https://foo.com/bar']]; - yield 'multiple webhook' => ['https://foo.com/bar,http://bar.io/foo/bar', [ + yield 'no urls' => [null, []]; + yield 'single url' => ['https://foo.com/bar', ['https://foo.com/bar']]; + yield 'multiple urls' => ['https://foo.com/bar,http://bar.io/foo/bar', [ 'https://foo.com/bar', 'http://bar.io/foo/bar', ]]; @@ -43,18 +43,18 @@ public function provideValidUrls(): iterable * @test * @dataProvider provideInvalidUrls */ - public function webhooksFailWhenProvidedValueIsNotValidUrl(string $webhooks): void + public function splitUrlsFailWhenProvidedValueIsNotValidUrl(string $urls): void { $this->expectException(InvalidConfigOptionException::class); - $this->validators->splitAndValidateMultipleUrls($webhooks); + $this->validators->splitAndValidateMultipleUrls($urls); } public function provideInvalidUrls(): iterable { - yield 'single invalid webhook' => ['invalid']; - yield 'first invalid webhook' => ['invalid,http://bar.io/foo/bar']; - yield 'last invalid webhook' => ['http://bar.io/foo/bar,invalid']; - yield 'middle invalid webhook' => ['http://bar.io/foo/bar,invalid,https://foo.com/bar']; + yield 'single invalid url' => ['invalid']; + yield 'first invalid url' => ['invalid,http://bar.io/foo/bar']; + yield 'last invalid url' => ['http://bar.io/foo/bar,invalid']; + yield 'middle invalid url' => ['http://bar.io/foo/bar,invalid,https://foo.com/bar']; } /** @test */ diff --git a/test/Service/ShlinkAssetsHandlerTest.php b/test/Service/ShlinkAssetsHandlerTest.php index a5e25f9..a3bf441 100644 --- a/test/Service/ShlinkAssetsHandlerTest.php +++ b/test/Service/ShlinkAssetsHandlerTest.php @@ -35,21 +35,27 @@ public function setUp(): void * @test * @dataProvider provideConfigExists */ - public function cachedConfigIsDeletedIfExists(bool $exists, int $expectedRemoveCalls): void + public function cachedConfigIsDeletedIfExists(bool $appExists, bool $routesExist, int $expectedRemoveCalls): void { - $appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn($exists); - $appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willReturn(null); + $appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn($appExists); + $routesConfigExists = $this->filesystem->exists('data/cache/fastroute_cached_routes.php')->willReturn( + $routesExist, + ); + $configRemove = $this->filesystem->remove(Argument::containingString('data/cache'))->willReturn(null); $this->assetsHandler->dropCachedConfigIfAny($this->io->reveal()); $appConfigExists->shouldHaveBeenCalledOnce(); - $appConfigRemove->shouldHaveBeenCalledTimes($expectedRemoveCalls); + $routesConfigExists->shouldHaveBeenCalledOnce(); + $configRemove->shouldHaveBeenCalledTimes($expectedRemoveCalls); } public function provideConfigExists(): iterable { - yield 'no cached config' => [false, 0]; - yield 'cached config' => [true, 1]; + yield 'no cached app or route config' => [false, false, 0]; + yield 'cached app config' => [true, false, 1]; + yield 'cached route config' => [false, true, 1]; + yield 'both configs cached' => [true, true, 2]; } /** @test */