From 4390e4feb6109904531c125478c9238fa24fcbeb Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Sat, 10 Jan 2026 05:13:04 +0100 Subject: [PATCH] Simplify how we load and save files in `data/` We had different ways of saving and loading files from `data/`, so I decided to unify them to simplify things. I repurposed the `DomainInfo` class and named it `DataLoader`, so we can use the same class to load anything from the `data/` directory. --- bin/console | 12 ++-- .../Commands/UpdateDomainSuffixesCommand.php | 32 ++++------ .../Commands/UpdateDomainToplevelCommand.php | 40 +++++------- src-dev/Commands/UpdatePostalCodesCommand.php | 44 +++++-------- src-dev/Helpers/DataSaver.php | 50 +++++++++++++++ src/Helpers/DataLoader.php | 35 +++++++++++ src/Helpers/DomainInfo.php | 42 ------------- src/Validators/PostalCode.php | 13 +--- src/Validators/PublicDomainSuffix.php | 8 +-- src/Validators/Tld.php | 26 ++------ tests/unit/Helpers/DataLoaderTest.php | 61 +++++++++++++++++++ 11 files changed, 205 insertions(+), 158 deletions(-) create mode 100644 src-dev/Helpers/DataSaver.php create mode 100644 src/Helpers/DataLoader.php delete mode 100644 src/Helpers/DomainInfo.php create mode 100644 tests/unit/Helpers/DataLoaderTest.php diff --git a/bin/console b/bin/console index dca33e3a6..f8daf5e7b 100755 --- a/bin/console +++ b/bin/console @@ -12,25 +12,27 @@ require __DIR__ . '/../vendor/autoload.php'; use Respect\Dev\Commands\LintDocsCommand; use Respect\Dev\Commands\LintMixinCommand; -use Respect\Dev\Commands\SmokeTestsCheckCompleteCommand; use Respect\Dev\Commands\LintSpdxCommand; +use Respect\Dev\Commands\SmokeTestsCheckCompleteCommand; use Respect\Dev\Commands\UpdateDomainSuffixesCommand; use Respect\Dev\Commands\UpdateDomainToplevelCommand; use Respect\Dev\Commands\UpdatePostalCodesCommand; use Respect\Dev\Differ\ConsoleDiffer; +use Respect\Dev\Helpers\DataSaver; use Respect\Dev\Markdown\CompositeLinter; use Respect\Dev\Markdown\Linters\AssertionMessageLinter; +use Respect\Dev\Markdown\Linters\ValidatorChangelogLinter; use Respect\Dev\Markdown\Linters\ValidatorHeaderLinter; use Respect\Dev\Markdown\Linters\ValidatorIndexLinter; use Respect\Dev\Markdown\Linters\ValidatorRelatedLinter; use Respect\Dev\Markdown\Linters\ValidatorTemplatesLinter; -use Respect\Dev\Markdown\Linters\ValidatorChangelogLinter; use SebastianBergmann\Diff\Differ; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; use Symfony\Component\Console\Application; return (static function () { $differ = new ConsoleDiffer(new Differ(new UnifiedDiffOutputBuilder('', addLineNumbers: true))); + $dataSaver = new DataSaver(); $application = new Application('Respect/Validation', '3.0'); $application->addCommand(new LintDocsCommand($differ, new CompositeLinter( @@ -43,9 +45,9 @@ return (static function () { ))); $application->addCommand(new LintMixinCommand($differ)); $application->addCommand(new LintSpdxCommand()); - $application->addCommand(new UpdateDomainSuffixesCommand()); - $application->addCommand(new UpdateDomainToplevelCommand()); - $application->addCommand(new UpdatePostalCodesCommand()); + $application->addCommand(new UpdateDomainSuffixesCommand($dataSaver)); + $application->addCommand(new UpdateDomainToplevelCommand($dataSaver)); + $application->addCommand(new UpdatePostalCodesCommand($dataSaver)); $application->addCommand(new SmokeTestsCheckCompleteCommand()); return $application->run(); diff --git a/src-dev/Commands/UpdateDomainSuffixesCommand.php b/src-dev/Commands/UpdateDomainSuffixesCommand.php index 80d030e2e..dc077c1cc 100644 --- a/src-dev/Commands/UpdateDomainSuffixesCommand.php +++ b/src-dev/Commands/UpdateDomainSuffixesCommand.php @@ -10,12 +10,12 @@ namespace Respect\Dev\Commands; +use Respect\Dev\Helpers\DataSaver; use Symfony\Component\Console\Attribute\AsCommand; 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\VarExporter\VarExporter; use function array_keys; use function array_unique; @@ -23,9 +23,7 @@ use function dirname; use function explode; use function file_get_contents; -use function file_put_contents; use function glob; -use function implode; use function is_dir; use function mb_strtoupper; use function mkdir; @@ -38,8 +36,6 @@ use function trim; use function unlink; -use const PHP_EOL; - #[AsCommand( name: 'update:domain-suffixes', description: 'Update list of public domain suffixes', @@ -48,6 +44,12 @@ final class UpdateDomainSuffixesCommand extends Command { private const string LIST_URL = 'https://publicsuffix.org/list/public_suffix_list.dat'; + public function __construct( + private readonly DataSaver $dataSaver, + ) { + parent::__construct(); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -100,20 +102,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int continue; } - sort($suffixList); - - $SPDX = '// SPDX'; - - $fileContent = implode(PHP_EOL, [ - 'dataSaver->save( + $suffixList, + '2007–22 Mozilla Foundation', + 'MPL-2.0-no-copyleft-exception', + sprintf('domain/public-suffix/%s.php', $tld), + ); $progressBar->advance(); } diff --git a/src-dev/Commands/UpdateDomainToplevelCommand.php b/src-dev/Commands/UpdateDomainToplevelCommand.php index bad58d5bc..8c24c1669 100644 --- a/src-dev/Commands/UpdateDomainToplevelCommand.php +++ b/src-dev/Commands/UpdateDomainToplevelCommand.php @@ -10,26 +10,20 @@ namespace Respect\Dev\Commands; +use Respect\Dev\Helpers\DataSaver; use Symfony\Component\Console\Attribute\AsCommand; 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\VarExporter\VarExporter; -use function basename; use function count; -use function dirname; use function explode; use function file_get_contents; -use function file_put_contents; -use function implode; use function sprintf; use function str_starts_with; use function trim; -use const PHP_EOL; - #[AsCommand( name: 'update:domain-toplevel', description: 'Update list of Top Level Domains (TLD) in the Tld validator', @@ -38,6 +32,12 @@ final class UpdateDomainToplevelCommand extends Command { private const string LIST_URL = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt'; + public function __construct( + private readonly DataSaver $dataSaver, + ) { + parent::__construct(); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -71,26 +71,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $tlds[] = $line; } - // Create the data file - $dataFilename = dirname(__DIR__, 2) . '/data/domain/tld.php'; - - $SPDX = '// SPDX'; - - $fileContent = implode(PHP_EOL, [ - 'error('Failed to write data file'); - - return Command::FAILURE; - } + $this->dataSaver->save( + $tlds, + '(c) https://data.iana.org/TLD/', + 'MPL-2.0', + 'domain/tld.php', + ); - $io->success(sprintf('Updated %s successfully', basename($dataFilename))); + $io->success('Updated successfully'); $io->text(sprintf('Total TLDs: %d', count($tlds))); return Command::SUCCESS; diff --git a/src-dev/Commands/UpdatePostalCodesCommand.php b/src-dev/Commands/UpdatePostalCodesCommand.php index 092ee6e67..bcbc707e5 100644 --- a/src-dev/Commands/UpdatePostalCodesCommand.php +++ b/src-dev/Commands/UpdatePostalCodesCommand.php @@ -10,22 +10,16 @@ namespace Respect\Dev\Commands; +use Respect\Dev\Helpers\DataSaver; use Symfony\Component\Console\Attribute\AsCommand; 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\VarExporter\VarExporter; -use function basename; use function count; -use function dirname; use function explode; use function file_get_contents; -use function file_put_contents; -use function implode; -use function ksort; -use function preg_replace; use function preg_replace_callback; use function sprintf; use function str_contains; @@ -33,8 +27,6 @@ use function strlen; use function trim; -use const PHP_EOL; - #[AsCommand( name: 'update:postal-codes', description: 'Update the list of postal codes in the PostalCode validator', @@ -43,6 +35,12 @@ final class UpdatePostalCodesCommand extends Command { private const string LIST_URL = 'https://download.geonames.org/export/dump/countryInfo.txt'; + public function __construct( + private readonly DataSaver $dataSaver, + ) { + parent::__construct(); + } + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -104,28 +102,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $postalCodes[$countryCode] = ['/^' . $countryFormat . '$/', '/' . $countryRegex . '/']; } - ksort($postalCodes); - - // Create the data file - $dataFilename = dirname(__DIR__, 2) . '/data/postal-code.php'; - - $SPDX = '// SPDX'; - - $fileContent = implode(PHP_EOL, [ - 'error('Failed to write data file'); - - return Command::FAILURE; - } + $this->dataSaver->save( + $postalCodes, + '(c) https://download.geonames.org/export/dump/countryInfo.txt', + 'CC-BY-4.0', + 'postal-code.php', + ); - $io->success(sprintf('Updated %s successfully', basename($dataFilename))); + $io->success('Updated successfully'); $io->text(sprintf('Total postal codes: %d', count($postalCodes))); return Command::SUCCESS; diff --git a/src-dev/Helpers/DataSaver.php b/src-dev/Helpers/DataSaver.php new file mode 100644 index 000000000..920baea4f --- /dev/null +++ b/src-dev/Helpers/DataSaver.php @@ -0,0 +1,50 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Dev\Helpers; + +use RuntimeException; +use Symfony\Component\VarExporter\VarExporter; + +use function array_is_list; +use function dirname; +use function file_put_contents; +use function implode; +use function ksort; +use function preg_replace; +use function str_replace; + +use const DIRECTORY_SEPARATOR; +use const PHP_EOL; + +final class DataSaver +{ + /** @param array $data */ + public function save(array $data, string $fileCopyrightText, string $licenseIdentifier, string $path): void + { + if (!array_is_list($data)) { + ksort($data); + } + + $fileContent = implode(PHP_EOL, [ + // REUSE-IgnoreStart + ' + */ + +declare(strict_types=1); + +namespace Respect\Validation\Helpers; + +use function dirname; +use function file_exists; +use function str_replace; + +use const DIRECTORY_SEPARATOR; + +final class DataLoader +{ + /** @var array> */ + private static array $runtimeCache = []; + + /** @return array */ + public static function load(string $basePath): array + { + $basePath = str_replace('/', DIRECTORY_SEPARATOR, $basePath); + $path = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . $basePath; + if (!isset(static::$runtimeCache[$basePath])) { + static::$runtimeCache[$basePath] = file_exists($path) ? require $path : []; + } + + return static::$runtimeCache[$basePath]; + } +} diff --git a/src/Helpers/DomainInfo.php b/src/Helpers/DomainInfo.php deleted file mode 100644 index 0266a75d6..000000000 --- a/src/Helpers/DomainInfo.php +++ /dev/null @@ -1,42 +0,0 @@ - - * SPDX-FileContributor: Henrique Moody - */ - -declare(strict_types=1); - -namespace Respect\Validation\Helpers; - -use function file_exists; -use function mb_strtoupper; - -final class DomainInfo -{ - /** @var mixed[] */ - private readonly array $data; - - /** @var mixed[] */ - private static array $runtimeCache = []; - - public function __construct(string $tld) - { - $tld = mb_strtoupper($tld); - - if (!isset(static::$runtimeCache[$tld])) { - $filename = __DIR__ . '/../../data/domain/public-suffix/' . $tld . '.php'; - static::$runtimeCache[$tld] = file_exists($filename) ? require $filename : []; - } - - $this->data = static::$runtimeCache[$tld]; - } - - /** @return array */ - public function getPublicSuffixes(): array - { - return $this->data; - } -} diff --git a/src/Validators/PostalCode.php b/src/Validators/PostalCode.php index 130bdd9fe..1217d272d 100644 --- a/src/Validators/PostalCode.php +++ b/src/Validators/PostalCode.php @@ -26,11 +26,10 @@ use Attribute; use Respect\Validation\Exceptions\InvalidValidatorException; +use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Envelope; -use function dirname; - /** @see http://download.geonames.org/export/dump/countryInfo.txt */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( @@ -66,18 +65,10 @@ public function __construct(string $countryCode, bool $formatted = false) ); } - /** @return array */ - private function getPostalCodes(): array - { - static $postalCodes = null; - - return $postalCodes ??= require dirname(__DIR__, 2) . '/data/postal-code.php'; - } - private function buildRegex(string $countryCode, bool $formatted): string { $index = $formatted ? 0 : 1; - $postalCodes = $this->getPostalCodes(); + $postalCodes = DataLoader::load('postal-code.php'); return self::POSTAL_CODES_EXTRA[$countryCode][$index] ?? $postalCodes[$countryCode][$index] ?? '/^$/'; } diff --git a/src/Validators/PublicDomainSuffix.php b/src/Validators/PublicDomainSuffix.php index 98794c919..5e0cbf931 100644 --- a/src/Validators/PublicDomainSuffix.php +++ b/src/Validators/PublicDomainSuffix.php @@ -13,7 +13,7 @@ use Attribute; use Respect\Validation\Helpers\CanValidateUndefined; -use Respect\Validation\Helpers\DomainInfo; +use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; use Respect\Validation\Validators\Core\Simple; @@ -21,6 +21,7 @@ use function explode; use function in_array; use function is_scalar; +use function mb_strtoupper; use function strtoupper; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] @@ -41,9 +42,8 @@ public function isValid(mixed $input): bool $parts = explode('.', (string) $input); $tld = array_pop($parts); - $domainInfo = new DomainInfo($tld); - $dataSource = $domainInfo->getPublicSuffixes(); - if ($this->isUndefined($input) && empty($dataSource)) { + $dataSource = DataLoader::load('domain/public-suffix/' . mb_strtoupper($tld) . '.php'); + if ($this->isUndefined($input) && $dataSource === []) { return true; } diff --git a/src/Validators/Tld.php b/src/Validators/Tld.php index c85bc872a..490bdb090 100644 --- a/src/Validators/Tld.php +++ b/src/Validators/Tld.php @@ -10,35 +10,19 @@ namespace Respect\Validation\Validators; use Attribute; +use Respect\Validation\Helpers\DataLoader; use Respect\Validation\Message\Template; -use Respect\Validation\Validators\Core\Simple; - -use function dirname; -use function in_array; -use function is_scalar; -use function mb_strtoupper; +use Respect\Validation\Validators\Core\Envelope; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( '{{subject}} must be a valid top-level domain name', '{{subject}} must not be a valid top-level domain name', )] -final class Tld extends Simple +final class Tld extends Envelope { - public function isValid(mixed $input): bool - { - if (!is_scalar($input)) { - return false; - } - - return in_array(mb_strtoupper((string) $input), $this->getTldList()); - } - - /** @return array */ - private function getTldList(): array + public function __construct() { - static $tldList = null; - - return $tldList ??= require dirname(__DIR__, 2) . '/data/domain/tld.php'; + parent::__construct(new Call('mb_strtoupper', new In(DataLoader::load('domain/tld.php')))); } } diff --git a/tests/unit/Helpers/DataLoaderTest.php b/tests/unit/Helpers/DataLoaderTest.php new file mode 100644 index 000000000..e5239442e --- /dev/null +++ b/tests/unit/Helpers/DataLoaderTest.php @@ -0,0 +1,61 @@ + + */ + +declare(strict_types=1); + +namespace Respect\Validation\Helpers; + +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use ReflectionClass; + +#[CoversClass(DataLoader::class)] +final class DataLoaderTest extends TestCase +{ + protected function setUp(): void + { + // Clear the runtime cache before each test + $reflection = new ReflectionClass(DataLoader::class); + $property = $reflection->getProperty('runtimeCache'); + $property->setValue(null, []); + } + + #[Test] + public function shouldLoadDataWhenExistingFileIsProvided(): void + { + $data = DataLoader::load('postal-code.php'); + + self::assertNotEmpty($data); + } + + #[Test] + public function shouldReturnEmptyArrayWhenNonExistingFileIsProvided(): void + { + $data = DataLoader::load('non-existing-file.php'); + + self::assertEmpty($data); + } + + #[Test] + public function shouldLoadDataWhenSubdirectoryPathIsProvided(): void + { + $data = DataLoader::load('domain/tld.php'); + + self::assertNotEmpty($data); + } + + #[Test] + public function shouldReturnDifferentDataWhenDifferentFilesAreLoaded(): void + { + $data1 = DataLoader::load('postal-code.php'); + $data2 = DataLoader::load('domain/tld.php'); + + self::assertNotSame($data1, $data2); + } +}