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.
This commit is contained in:
Henrique Moody 2026-01-10 05:13:04 +01:00
commit 4390e4feb6
No known key found for this signature in database
GPG key ID: 221E9281655813A6
11 changed files with 205 additions and 158 deletions

View file

@ -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();

View file

@ -10,12 +10,12 @@ declare(strict_types=1);
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 count;
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 str_starts_with;
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 @@ final class UpdateDomainSuffixesCommand extends Command
continue;
}
sort($suffixList);
$SPDX = '// SPDX';
$fileContent = implode(PHP_EOL, [
'<?php declare(strict_types=1);',
$SPDX . '-FileCopyrightText: 200722 Mozilla Foundation',
$SPDX . '-License-Identifier: MPL-2.0-no-copyleft-exception',
'return ' . VarExporter::export($suffixList) . ';' . PHP_EOL,
]);
// Convert IDN TLD to ASCII (Punycode) for filename
$filename = sprintf('%s/public-suffix/%s.php', $dataDir, $tld);
file_put_contents($filename, $fileContent);
$this->dataSaver->save(
$suffixList,
'200722 Mozilla Foundation',
'MPL-2.0-no-copyleft-exception',
sprintf('domain/public-suffix/%s.php', $tld),
);
$progressBar->advance();
}

View file

@ -10,26 +10,20 @@ declare(strict_types=1);
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 @@ final class UpdateDomainToplevelCommand extends Command
$tlds[] = $line;
}
// Create the data file
$dataFilename = dirname(__DIR__, 2) . '/data/domain/tld.php';
$this->dataSaver->save(
$tlds,
'(c) https://data.iana.org/TLD/',
'MPL-2.0',
'domain/tld.php',
);
$SPDX = '// SPDX';
$fileContent = implode(PHP_EOL, [
'<?php declare(strict_types=1);',
$SPDX . '-FileCopyrightText: (c) https://data.iana.org/TLD/',
$SPDX . '-License-Identifier: MPL-2.0',
'return ' . VarExporter::export($tlds) . ';' . PHP_EOL,
]);
// Write the data file
if (file_put_contents($dataFilename, $fileContent) === false) {
$io->error('Failed to write data file');
return Command::FAILURE;
}
$io->success(sprintf('Updated %s successfully', basename($dataFilename)));
$io->success('Updated successfully');
$io->text(sprintf('Total TLDs: %d', count($tlds)));
return Command::SUCCESS;

View file

@ -10,22 +10,16 @@ declare(strict_types=1);
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 str_starts_with;
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 @@ final class UpdatePostalCodesCommand extends Command
$postalCodes[$countryCode] = ['/^' . $countryFormat . '$/', '/' . $countryRegex . '/'];
}
ksort($postalCodes);
$this->dataSaver->save(
$postalCodes,
'(c) https://download.geonames.org/export/dump/countryInfo.txt',
'CC-BY-4.0',
'postal-code.php',
);
// Create the data file
$dataFilename = dirname(__DIR__, 2) . '/data/postal-code.php';
$SPDX = '// SPDX';
$fileContent = implode(PHP_EOL, [
'<?php declare(strict_types=1);',
$SPDX . '-FileCopyrightText: (c) https://download.geonames.org/export/dump/countryInfo.txt',
$SPDX . '-License-Identifier: CC-BY-4.0',
'return ' . preg_replace('/\\\([dws])/', '\\1', VarExporter::export($postalCodes)) . ';' . PHP_EOL,
]);
// Write the data file
if (file_put_contents($dataFilename, $fileContent) === false) {
$io->error('Failed to write data file');
return Command::FAILURE;
}
$io->success(sprintf('Updated %s successfully', basename($dataFilename)));
$io->success('Updated successfully');
$io->text(sprintf('Total postal codes: %d', count($postalCodes)));
return Command::SUCCESS;

View file

@ -0,0 +1,50 @@
<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
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<string|int, mixed> $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
'<?php declare(strict_types=1);',
'// SPDX-FileCopyrightText: ' . $fileCopyrightText,
'// SPDX-License-Identifier: ' . $licenseIdentifier,
// REUSE-IgnoreEnd
'return ' . preg_replace('/\\\([dws])/', '\\1', VarExporter::export($data)) . ';' . PHP_EOL,
]);
$filename = str_replace('/', DIRECTORY_SEPARATOR, dirname(__DIR__, 2) . '/data/' . $path);
if (file_put_contents($filename, $fileContent) === false) {
throw new RuntimeException('Failed to write data file: ' . $filename);
}
}
}

View file

@ -0,0 +1,35 @@
<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
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<string, array<int|string, mixed>> */
private static array $runtimeCache = [];
/** @return array<string|int, mixed> */
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];
}
}

View file

@ -1,42 +0,0 @@
<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
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<string> */
public function getPublicSuffixes(): array
{
return $this->data;
}
}

View file

@ -26,11 +26,10 @@ namespace Respect\Validation\Validators;
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 @@ final class PostalCode extends Envelope
);
}
/** @return array<string, array{string, string}> */
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] ?? '/^$/';
}

View file

@ -13,7 +13,7 @@ namespace Respect\Validation\Validators;
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 array_pop;
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 @@ final class PublicDomainSuffix extends Simple
$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;
}

View file

@ -10,35 +10,19 @@ declare(strict_types=1);
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
public function __construct()
{
if (!is_scalar($input)) {
return false;
}
return in_array(mb_strtoupper((string) $input), $this->getTldList());
}
/** @return array<string> */
private function getTldList(): array
{
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'))));
}
}

View file

@ -0,0 +1,61 @@
<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
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);
}
}