diff --git a/.gitattributes b/.gitattributes
index e4c272f..c481848 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,12 +5,11 @@ test-resources export-ignore
.github export-ignore
.gitattributes export-ignore
.gitignore export-ignore
-.phpstorm.meta.php export-ignore
CHANGELOG.md export-ignore
docker-compose.yml export-ignore
Dockerfile export-ignore
indocker export-ignore
-phpcs.xml export-ignore
+mago.toml export-ignore
phpstan.neon export-ignore
phpunit.xml.dist export-ignore
shlink-installer.gif export-ignore
diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php
deleted file mode 100644
index 9e34cb7..0000000
--- a/.phpstorm.meta.php
+++ /dev/null
@@ -1,23 +0,0 @@
- [
- '' == '@',
- ],
- ServiceLocatorInterface::build('') => [
- '' == '@',
- ],
-];
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d01417..14921bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,23 @@ 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).
+## [10.1.0] - 2026-06-11
+### Added
+* *Nothing*
+
+### Changed
+* Migrate form PHP_CodeSniffer to Mago for code linting and formatting.
+
+### Deprecated
+* *Nothing*
+
+### Removed
+* *Nothing*
+
+### Fixed
+* *Nothing*
+
+
## [10.0.0] - 2026-02-08
### Added
* Add support for Symfony 8.0
diff --git a/composer.json b/composer.json
index b93d016..2997f0a 100644
--- a/composer.json
+++ b/composer.json
@@ -23,12 +23,12 @@
"webimpress/safe-writer": "^2.2"
},
"require-dev": {
+ "carthage-software/mago": "^1.28",
"devster/ubench": "^2.1",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^13.0",
- "roave/security-advisories": "dev-master",
- "shlinkio/php-coding-standard": "~2.5.0",
+ "shlinkio/shlink-mago-config": "^1.0",
"symfony/var-dumper": "^8.0"
},
"autoload": {
@@ -47,8 +47,12 @@
"@stan",
"@test:ci"
],
- "cs": "phpcs",
- "cs:fix": "phpcbf",
+ "lint": "mago lint",
+ "lint:fix": "mago lint --fix",
+ "format": "mago format",
+ "format:check": "mago format --check",
+ "cs": ["@lint", "@format:check"],
+ "cs:fix": ["@lint:fix", "@format"],
"stan": "phpstan analyse",
"test": "phpunit --order-by=random --testdox --testdox-summary",
"test:ci": [
diff --git a/config/config.php b/config/config.php
index 1a048c9..90eabee 100644
--- a/config/config.php
+++ b/config/config.php
@@ -13,7 +13,6 @@
use Symfony\Component\Process\PhpExecutableFinder;
return [
-
'dependencies' => [
'factories' => [
Console\Application::class => Factory\ApplicationFactory::class,
@@ -47,32 +46,32 @@
'DATABASE' => [
'Database > Driver' => Config\Option\Database\DatabaseDriverConfigOption::class,
'Database > Name' => Config\Option\Database\DatabaseNameConfigOption::class,
- 'Database > Host (or unix socket for PostgreSQL)'
- => Config\Option\Database\DatabaseHostConfigOption::class,
+ 'Database > Host (or unix socket for PostgreSQL)' =>
+ Config\Option\Database\DatabaseHostConfigOption::class,
'Database > Port' => Config\Option\Database\DatabasePortConfigOption::class,
'Database > User' => Config\Option\Database\DatabaseUserConfigOption::class,
'Database > Password' => Config\Option\Database\DatabasePasswordConfigOption::class,
- 'Database > Unix socket (Mysql/MariaDB)'
- => Config\Option\Database\DatabaseUnixSocketConfigOption::class,
+ 'Database > Unix socket (Mysql/MariaDB)' =>
+ Config\Option\Database\DatabaseUnixSocketConfigOption::class,
'Database > Use encryption' => Config\Option\Database\DatabaseUseEncryptionConfigOption::class,
],
'URL SHORTENER' => [
'URL shortener > Short domain' => Config\Option\UrlShortener\ShortDomainHostConfigOption::class,
'URL shortener > Schema' => Config\Option\UrlShortener\ShortDomainSchemaConfigOption::class,
'URL shortener > Short codes length' => Config\Option\UrlShortener\ShortCodeLengthOption::class,
- 'URL shortener > Auto resolve titles'
- => Config\Option\UrlShortener\AutoResolveTitlesConfigOption::class,
+ 'URL shortener > Auto resolve titles' =>
+ Config\Option\UrlShortener\AutoResolveTitlesConfigOption::class,
'URL shortener > Extra path mode' => Config\Option\UrlShortener\ExtraPathModeConfigOption::class,
- 'URL shortener > Multi-segment slugs'
- => Config\Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
+ 'URL shortener > Multi-segment slugs' =>
+ Config\Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
'URL shortener > Trailing slashes' => Config\Option\UrlShortener\EnableTrailingSlashConfigOption::class,
'URL shortener > Mode' => Config\Option\UrlShortener\ShortUrlModeConfigOption::class,
'GeoLite2 license key' => Config\Option\UrlShortener\GeoLiteLicenseKeyConfigOption::class,
- 'Redirects > Status code (301/302/307/308)'
- => Config\Option\UrlShortener\RedirectStatusCodeConfigOption::class,
+ 'Redirects > Status code (301/302/307/308)' =>
+ Config\Option\UrlShortener\RedirectStatusCodeConfigOption::class,
'Redirects > Caching life time' => Config\Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
- 'Redirects > Caching visibility'
- => Config\Option\UrlShortener\RedirectCacheVisibilityConfigOption::class,
+ 'Redirects > Caching visibility' =>
+ Config\Option\UrlShortener\RedirectCacheVisibilityConfigOption::class,
],
'TRACKING' => [
'Tracking > Orphan visits tracking' => Config\Option\Tracking\OrphanVisitsTrackingConfigOption::class,
@@ -82,8 +81,8 @@
'Tracking > Disable IP address tracking' => Config\Option\Tracking\DisableIpTrackingConfigOption::class,
'Tracking > IP Anonymization' => Config\Option\Tracking\IpAnonymizationConfigOption::class,
'Tracking > Disable user agent tracking' => Config\Option\Tracking\DisableUaTrackingConfigOption::class,
- 'Tracking > Disable referrer tracking'
- => Config\Option\Tracking\DisableReferrerTrackingConfigOption::class,
+ 'Tracking > Disable referrer tracking' =>
+ Config\Option\Tracking\DisableReferrerTrackingConfigOption::class,
],
'REDIRECTS' => [
'Redirects > Base URL' => Config\Option\Redirect\BaseUrlRedirectConfigOption::class,
@@ -300,5 +299,4 @@
],
],
],
-
];
diff --git a/config/container.php b/config/container.php
index 79ecbce..b278802 100644
--- a/config/container.php
+++ b/config/container.php
@@ -24,7 +24,7 @@
$shlinkConfigLoader = static function () {
$appConfigPath = __DIR__ . '/../../../../config/config.php';
- if (! file_exists($appConfigPath)) {
+ if (!file_exists($appConfigPath)) {
return [];
}
@@ -35,12 +35,12 @@
return $appConfig;
};
-$config = (new ConfigAggregator([
+$config = new ConfigAggregator([
Config\ConfigProvider::class,
- new PhpFileProvider(__DIR__ . '/config.php'), // Installer config
- $shlinkConfigLoader, // Overwritten config coming from Shlink
+ new PhpFileProvider(__DIR__ . '/config.php'), // Installer config
+ $shlinkConfigLoader, // Overwritten config coming from Shlink
new PhpFileProvider(__DIR__ . '/config.local.php'), // Local config
-]))->getMergedConfig();
+])->getMergedConfig();
$container = new ServiceManager($config['dependencies']);
$container->setService('config', $config);
diff --git a/mago.toml b/mago.toml
new file mode 100644
index 0000000..c55cb50
--- /dev/null
+++ b/mago.toml
@@ -0,0 +1,19 @@
+#:schema https://mago.carthage.software/1.27.1/schema.json
+# Welcome to Mago!
+# For full documentation, see https://mago.carthage.software/tools/overview
+version = "1"
+php-version = "8.4.0"
+extends = "vendor/shlinkio/shlink-mago-config/mago.toml"
+
+[source]
+workspace = "."
+paths = ["config", "src", "test"]
+excludes = ["vendor"]
+
+[source.glob]
+literal-separator = true
+
+[linter.rules]
+no-literal-password = { exclude = ["test"] }
+no-empty-catch-clause = { exclude = ["test"] }
+no-empty-loop = { exclude = ["test"] }
diff --git a/phpcs.xml b/phpcs.xml
deleted file mode 100644
index d08b686..0000000
--- a/phpcs.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
- Coding standard
-
-
-
-
-
-
-
-
-
- bin
- config
- src
- test
-
-
- config/params/*
-
diff --git a/src/Command/InitCommand.php b/src/Command/InitCommand.php
index 9cabc75..a0fc35f 100644
--- a/src/Command/InitCommand.php
+++ b/src/Command/InitCommand.php
@@ -18,7 +18,7 @@
#[AsCommand(
name: InitCommand::NAME,
description: 'Initializes external dependencies required for Shlink to properly work, like DB, cache warmup, '
- . 'initial GeoLite DB download, etc',
+ . 'initial GeoLite DB download, etc',
)]
class InitCommand extends Command
{
@@ -33,16 +33,25 @@ public function __invoke(SymfonyStyle $io, InputInterface $input, #[MapInput] In
{
$commands = [...$inputData->resolveCommands()];
- return array_reduce($commands, function (bool $carry, array $commandInfo) use ($input, $io): bool {
- /** @var array{InstallationCommand, string|null} $commandInfo */
- [$command, $arg] = $commandInfo;
-
- return $this->commandsRunner->execPhpCommand(
- name: $command->value,
- io: $io,
- interactive: $input->isInteractive(),
- args: $arg !== null ? [$arg] : [],
- ) && $carry;
- }, initial: true) ? self::SUCCESS : self::FAILURE;
+ return array_reduce(
+ $commands,
+ function (bool $carry, array $commandInfo) use ($input, $io): bool {
+ /** @var array{InstallationCommand, string|null} $commandInfo */
+ [$command, $arg] = $commandInfo;
+
+ return (
+ $this->commandsRunner->execPhpCommand(
+ name: $command->value,
+ io: $io,
+ interactive: $input->isInteractive(),
+ args: $arg !== null ? [$arg] : [],
+ )
+ && $carry
+ );
+ },
+ initial: true,
+ )
+ ? self::SUCCESS
+ : self::FAILURE;
}
}
diff --git a/src/Command/Model/InitCommandInput.php b/src/Command/Model/InitCommandInput.php
index 1da335f..4d40f1b 100644
--- a/src/Command/Model/InitCommandInput.php
+++ b/src/Command/Model/InitCommandInput.php
@@ -42,7 +42,7 @@ final class InitCommandInput
*/
public function resolveCommands(): iterable
{
- if (! $this->skipInitializeDb) {
+ if (!$this->skipInitializeDb) {
yield [InstallationCommand::DB_CREATE_SCHEMA, null];
}
@@ -53,7 +53,7 @@ public function resolveCommands(): iterable
yield [InstallationCommand::ORM_CLEAR_CACHE, null];
}
- if (! $this->skipDownloadGeolite) {
+ if (!$this->skipDownloadGeolite) {
yield [InstallationCommand::GEOLITE_DOWNLOAD_DB, null];
}
diff --git a/src/Command/Model/InitOption.php b/src/Command/Model/InitOption.php
index 1e51c14..b59b0ea 100644
--- a/src/Command/Model/InitOption.php
+++ b/src/Command/Model/InitOption.php
@@ -1,5 +1,7 @@
groups = array_filter(
iterator_to_array($this->flattenGroupsWithTitle($groups)),
- static fn (string $configOption) => $enabledOptions === null || ArrayUtils::contains(
+ static fn (string $configOption) => $enabledOptions === null
+ || ArrayUtils::contains(
$configOption,
$enabledOptions,
),
@@ -56,7 +57,7 @@ private function flattenGroupsWithTitle(iterable $groups): Generator
foreach ($groups as $key => $value) {
if (is_iterable($value)) {
yield from $this->flattenGroupsWithTitle($value);
- } elseif (! is_numeric($key)) {
+ } elseif (!is_numeric($key)) {
yield $key => $value;
}
}
@@ -64,7 +65,7 @@ private function flattenGroupsWithTitle(iterable $groups): Generator
public function __invoke(SymfonyStyle $io): int
{
- if (! $this->filesystem->exists($this->generatedConfigPath)) {
+ if (!$this->filesystem->exists($this->generatedConfigPath)) {
throw InvalidShlinkPathException::forCurrentPath();
}
diff --git a/src/Config/ConfigGenerator.php b/src/Config/ConfigGenerator.php
index d18a966..8669e69 100644
--- a/src/Config/ConfigGenerator.php
+++ b/src/Config/ConfigGenerator.php
@@ -21,8 +21,7 @@ public function __construct(
private readonly ConfigOptionsManagerInterface $configOptionsManager,
private readonly array $configOptionsGroups,
private readonly array|null $enabledOptions,
- ) {
- }
+ ) {}
public function generateConfigInteractively(StyleInterface $io, array $previousConfig): array
{
@@ -33,12 +32,13 @@ public function generateConfigInteractively(StyleInterface $io, array $previousC
// FIXME Improve code quality on these nested loops
foreach ($pluginsGroups as $title => $configOptions) {
foreach ($configOptions as $configOption => $plugin) {
- $optionIsEnabled = $this->enabledOptions === null || ArrayUtils::contains(
+ $optionIsEnabled = $this->enabledOptions === null
+ || ArrayUtils::contains(
$configOption,
$this->enabledOptions,
);
$shouldAsk = $optionIsEnabled && $plugin->shouldBeAsked($answers);
- if (! $shouldAsk) {
+ if (!$shouldAsk) {
if ($plugin instanceof ConfigOptionMigratorInterface && isset($answers[$plugin->getEnvVar()])) {
$answers[$plugin->getEnvVar()] = $plugin->tryToMigrateValue($answers[$plugin->getEnvVar()]);
}
@@ -47,7 +47,7 @@ public function generateConfigInteractively(StyleInterface $io, array $previousC
}
// Render every title only once, and only as soon as we find a plugin that should be asked
- if (! ArrayUtils::contains($title, $alreadyRenderedTitles)) {
+ if (!ArrayUtils::contains($title, $alreadyRenderedTitles)) {
$alreadyRenderedTitles[] = $title;
$io->title($title);
}
@@ -73,17 +73,23 @@ private function resolveAndSortOptions(): array
// Sort plugins based on which other plugins they depend on
usort(
$plugins,
- static fn (ConfigOptionInterface $a, ConfigOptionInterface $b): int =>
- $a instanceof DependentConfigOptionInterface && $a->getDependentOption() === $b::class ? 1 : 0,
+ static fn (ConfigOptionInterface $a, ConfigOptionInterface $b): int => $a
+ instanceof DependentConfigOptionInterface
+ && $a->getDependentOption() === $b::class
+ ? 1
+ : 0,
);
return array_combine($configOptions, $plugins);
};
$filterDisabledOptions = fn (array $configOptions) => array_filter(
$configOptions,
- fn (string $option) => $this->enabledOptions === null || ArrayUtils::contains(
- $option,
- $this->enabledOptions,
+ fn (string $option) => (
+ $this->enabledOptions === null
+ || ArrayUtils::contains(
+ $option,
+ $this->enabledOptions,
+ )
),
);
diff --git a/src/Config/ConfigOptionsManager.php b/src/Config/ConfigOptionsManager.php
index 91082fd..6b0ab4f 100644
--- a/src/Config/ConfigOptionsManager.php
+++ b/src/Config/ConfigOptionsManager.php
@@ -4,20 +4,19 @@
namespace Shlinkio\Shlink\Installer\Config;
-use Laminas\ServiceManager\AbstractPluginManager;
+use Laminas\ServiceManager\AbstractSingleInstancePluginManager;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use function get_debug_type;
use function sprintf;
/**
- * @extends AbstractPluginManager