Port Bash scripts to PHP

It makes more sense to use PHP to generate PHP code than to use Bash. I
love writing Bash scripts, but I know it's not for everyone, and they
can become quite complex. Porting them to PHP code also lowers the
barrier for people to change them.

While I was making those changes, I also noticed a problem with how we
save the domain suffixes. We're converting all of them to ASCII, so we
are not preserving languages such as Chinese, Thai, and Hebrew, which
use non-ASCII characters.
This commit is contained in:
Henrique Moody 2026-01-05 12:40:00 +01:00
commit 7892a7c902
No known key found for this signature in database
GPG key ID: 221E9281655813A6
16 changed files with 1120 additions and 556 deletions

12
.gitattributes vendored
View file

@ -1,6 +1,6 @@
/*.dist export-ignore
/*.yml export-ignore
/.* export-ignore
/bin export-ignore
/docs export-ignore
/tests export-ignore
/* export-ignore
/composer.json -export-ignore
/data -export-ignore
/library -export-ignore
/LICENSE -export-ignore
/README.md -export-ignore

View file

@ -25,10 +25,13 @@ jobs:
ref: ${{ secrets.LAST_MINOR_VERSION }}
- name: Update top level domains
run: bin/update-domain-toplevel
run: bin/console update:domain-toplevel
- name: Update public domain suffixes
run: bin/update-domain-suffixes
run: bin/console update:domain-suffixes
- name: Update postal codes
run: bin/console update:postal-codes
- name: Create pull request
id: cpr

29
bin/console Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Respect\Dev\Commands\CreateMixinCommand;
use Respect\Dev\Commands\UpdateDocLinksCommand;
use Respect\Dev\Commands\UpdateDomainSuffixesCommand;
use Respect\Dev\Commands\UpdateDomainToplevelCommand;
use Respect\Dev\Commands\UpdatePostalCodesCommand;
use Symfony\Component\Console\Application;
return (static function () {
$application = new Application('Respect/Validation', '3.0');
$application->addCommand(new CreateMixinCommand());
$application->addCommand(new UpdateDocLinksCommand());
$application->addCommand(new UpdateDomainSuffixesCommand());
$application->addCommand(new UpdateDomainToplevelCommand());
$application->addCommand(new UpdatePostalCodesCommand());
return $application->run();
})();

View file

@ -1,248 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Nette\PhpGenerator\InterfaceType;
use Nette\PhpGenerator\PhpNamespace;
use Nette\PhpGenerator\Printer;
use Respect\Validation\Mixins\AllBuilder;
use Respect\Validation\Mixins\AllChain;
use Respect\Validation\Mixins\Chain;
use Respect\Validation\Mixins\KeyBuilder;
use Respect\Validation\Mixins\KeyChain;
use Respect\Validation\Mixins\LengthBuilder;
use Respect\Validation\Mixins\LengthChain;
use Respect\Validation\Mixins\MaxBuilder;
use Respect\Validation\Mixins\MaxChain;
use Respect\Validation\Mixins\MinBuilder;
use Respect\Validation\Mixins\MinChain;
use Respect\Validation\Mixins\NotBuilder;
use Respect\Validation\Mixins\NotChain;
use Respect\Validation\Mixins\NullOrBuilder;
use Respect\Validation\Mixins\NullOrChain;
use Respect\Validation\Mixins\PropertyBuilder;
use Respect\Validation\Mixins\PropertyChain;
use Respect\Validation\Mixins\UndefOrBuilder;
use Respect\Validation\Mixins\UndefOrChain;
use Respect\Validation\Validator;
use Respect\Validation\ValidatorBuilder;
function addMethodToInterface(
string $originalName,
InterfaceType $interfaceType,
ReflectionClass $reflection,
string|null $prefix,
array $allowList,
array $denyList,
): void {
if ($allowList !== [] && !in_array($reflection->getShortName(), $allowList, true)) {
return;
}
if ($denyList !== [] && in_array($reflection->getShortName(), $denyList, true)) {
return;
}
$name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName);
$method = $interfaceType->addMethod($name)->setPublic()->setReturnType(Chain::class);
if (str_contains($interfaceType->getName(), 'Builder')) {
$method->setStatic();
}
if ($prefix === 'key') {
$method->addParameter('key')->setType('int|string');
}
if ($prefix === 'property') {
$method->addParameter('propertyName')->setType('string');
}
$reflrectionConstructor = $reflection->getConstructor();
if ($reflrectionConstructor === null) {
return;
}
$commend = $reflrectionConstructor->getDocComment();
if ($commend) {
$method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $commend));
}
foreach ($reflrectionConstructor->getParameters() as $reflectionParameter) {
if ($reflectionParameter->isVariadic()) {
$method->setVariadic();
}
$type = $reflectionParameter->getType();
$types = [];
if ($type instanceof ReflectionUnionType) {
foreach ($type->getTypes() as $type) {
$types[] = $type->getName();
}
} elseif ($type instanceof ReflectionNamedType) {
$types[] = $type->getName();
if (
str_starts_with($type->getName(), 'Sokil')
|| str_starts_with($type->getName(), 'Egulias')
|| $type->getName() === 'finfo'
) {
continue;
}
}
$parameter = $method->addParameter($reflectionParameter->getName());
$parameter->setType(implode('|', $types));
if (!$reflectionParameter->isDefaultValueAvailable()) {
$parameter->setNullable($reflectionParameter->isOptional());
}
if (count($types) > 1 || $reflectionParameter->isVariadic()) {
$parameter->setNullable(false);
}
if (!$reflectionParameter->isDefaultValueAvailable()) {
continue;
}
$defaultValue = $reflectionParameter->getDefaultValue();
if (is_object($defaultValue)) {
$parameter->setDefaultValue(null);
$parameter->setNullable(true);
continue;
}
$parameter->setDefaultValue($defaultValue);
$parameter->setNullable(false);
}
}
function overwriteFile(string $content, string $basename): void
{
file_put_contents(sprintf('%s/../library/Mixins/%s.php', __DIR__, $basename), implode(PHP_EOL . PHP_EOL, [
'<?php',
file_get_contents(__DIR__ . '/../.docheader'),
'declare(strict_types=1);',
preg_replace('/extends (.+, )+/', 'extends' . PHP_EOL . '\1', $content),
]));
}
(static function (): void {
$numberRelatedValidators = [
'Between',
'BetweenExclusive',
'Equals',
'Equivalent',
'Even',
'Factor',
'Fibonacci',
'Finite',
'GreaterThan',
'Identical',
'In',
'Infinite',
'LessThan',
'LessThanOrEqual',
'GreaterThanOrEqual',
'Multiple',
'Odd',
'PerfectSquare',
'Positive',
'PrimeNumber',
];
$structureRelatedValidators = [
'Exists',
'Key',
'KeyExists',
'KeyOptional',
'KeySet',
'NullOr',
'UndefOr',
'Property',
'PropertyExists',
'PropertyOptional',
'Attributes',
'Templated',
'Named',
];
$mixins = [
['All', 'all', [], ['All', ...$structureRelatedValidators]],
['Key', 'key', [], $structureRelatedValidators],
['Length', 'length', $numberRelatedValidators, []],
['Max', 'max', $numberRelatedValidators, []],
['Min', 'min', $numberRelatedValidators, []],
['Not', 'not', [], ['Not', 'NullOr', 'UndefOr', 'Attributes', 'Templated', 'Named']],
['NullOr', 'nullOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Templated', 'Named']],
['Property', 'property', [], $structureRelatedValidators],
['UndefOr', 'undefOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Attributes', 'Templated', 'Named']],
['', null, [], []],
];
$names = [];
foreach (new DirectoryIterator(__DIR__ . '/../library/Validators') as $file) {
if (!$file->isFile()) {
continue;
}
$className = 'Respect\\Validation\\Validators\\' . $file->getBasename('.php');
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract()) {
continue;
}
$names[$reflection->getShortName()] = $reflection;
}
ksort($names);
foreach ($mixins as [$name, $prefix, $allowList, $denyList]) {
$chainedNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
$chainedNamespace->addUse(Validator::class);
$chainedInterface = $chainedNamespace->addInterface($name . 'Chain');
$staticNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
$staticNamespace->addUse(Validator::class);
$staticInterface = $staticNamespace->addInterface($name . 'Builder');
if ($name === '') {
$chainedInterface->addExtend(Validator::class);
$chainedInterface->addExtend(AllChain::class);
$chainedInterface->addExtend(KeyChain::class);
$chainedInterface->addExtend(LengthChain::class);
$chainedInterface->addExtend(MaxChain::class);
$chainedInterface->addExtend(MinChain::class);
$chainedInterface->addExtend(NotChain::class);
$chainedInterface->addExtend(NullOrChain::class);
$chainedInterface->addExtend(PropertyChain::class);
$chainedInterface->addExtend(UndefOrChain::class);
$chainedInterface->addComment('@mixin \\' . ValidatorBuilder::class);
$staticInterface->addExtend(AllBuilder::class);
$staticInterface->addExtend(KeyBuilder::class);
$staticInterface->addExtend(LengthBuilder::class);
$staticInterface->addExtend(MaxBuilder::class);
$staticInterface->addExtend(MinBuilder::class);
$staticInterface->addExtend(NotBuilder::class);
$staticInterface->addExtend(NullOrBuilder::class);
$staticInterface->addExtend(PropertyBuilder::class);
$staticInterface->addExtend(UndefOrBuilder::class);
}
foreach ($names as $originalName => $reflection) {
addMethodToInterface($originalName, $staticInterface, $reflection, $prefix, $allowList, $denyList);
addMethodToInterface($originalName, $chainedInterface, $reflection, $prefix, $allowList, $denyList);
}
$printer = new Printer();
$printer->wrapLength = 115;
overwriteFile($printer->printNamespace($staticNamespace), $staticInterface->getName());
overwriteFile($printer->printNamespace($chainedNamespace), $chainedInterface->getName());
}
shell_exec(__DIR__ . '/../vendor/bin/phpcbf ' . __DIR__ . '/../library/Mixins');
})();

View file

@ -1,124 +0,0 @@
#!/usr/bin/env bash
# Usage: {script} DOCS_DIREECTORY
# Update list of rules and link related rules.
set -eo pipefail
declare -r IFS=$'\n'
declare -r DOCS_DIREECTORY="${1}"
declare -A RULES_BY_CATEGORIZATION=()
declare -A RELATED_RULES=()
list_rules()
{
find "${DOCS_DIREECTORY}/rules" -type f |
sed "s,${DOCS_DIREECTORY}/,," |
cut -d '/' -f 2 |
cut -d '.' -f 1 |
sort
}
list_categories()
{
while read rule; do
declare filename="${DOCS_DIREECTORY}/rules/${rule}.md"
sed -n '/## Categorization/,/## Changelog/p' "${filename}"
done < <(list_rules) |
grep '^-' |
sort -u |
sed 's,- ,,'
}
create_rule_documentation()
{
# Usage: create_rule_documentation RULE RELATED_RULES
local rule=${1}
local related_rules=${2}
local filename="${DOCS_DIREECTORY}/rules/${rule}.md"
local links=$(ggrep -E '^\[.+\]: .+' "${filename}")
local related_links=$(
tr ':' '\n' <<< ${related_rules} |
sort -u |
grep -v '^$' |
grep -v "^${rule}$" |
sed -E "s,(.+),- [\1](\1.md),g"
)
# "Description" section
sed -nE '/^# /,/^## Changelog/p' "${filename}" | ggrep -E -v '^## Changelog'
# "Changelog" section
sed -nE '/^## Changelog/,/^---/p' "${filename}"
# "See also" section
echo
echo "See also:"
echo
echo "${related_links}"
# Index of links
if [[ ! -z "${links}" ]]; then
echo
echo "${links}"
fi
}
create_list_of_rules()
{
echo "Building list of rules per categorization"
while read rule; do
declare filename="${DOCS_DIREECTORY}/rules/${rule}.md"
for category in $(sed -n '/## Categorization/,/## Changelog/p' "${filename}" |
grep '^-' |
sed 's,- ,,'); do
RULES_BY_CATEGORIZATION[${category}]=${RULES_BY_CATEGORIZATION[${category}]}:${rule}
done
done < <(list_rules)
echo "Creating list of rules"
{
echo "# List of rules by category"
echo
for category in $(list_categories); do
echo "## ${category}"
sed -E 's,:,\n- ,g' <<< "${RULES_BY_CATEGORIZATION[${category}]}" |
sort -u |
sed -E 's,- (.+),- [\1](rules/\1.md),'
echo
done
echo "## Alphabetically"
echo
ls -1 "${DOCS_DIREECTORY}/rules/" | sort | sed -E 's,^(.+).md$,- [\1](rules/\1.md),'
} > "${DOCS_DIREECTORY}/09-list-of-validators-by-category.md"
}
link_related_rules()
{
local temporaty=$(mktemp)
echo "Building list of related rules"
for rule in $(list_rules); do
declare filename="${DOCS_DIREECTORY}/rules/${rule}.md"
declare related_rules=$(ggrep -E '\[.+\]\(.+\.md\)' "${filename}" |
sed -E 's,.*\[.+\]\((.+)\.md\).*,\1,' |
grep -v 'comparable-values')
for related_rule in ${related_rules}; do
RELATED_RULES[${related_rule}]=${RELATED_RULES[${related_rule}]}:${rule}
done
RELATED_RULES[${rule}]="${RELATED_RULES[${rule}]}:$(tr '\n' ':' <<< ${related_rules})"
done
echo "Recreating rule documentations with related rules"
for rule in $(list_rules); do
echo "- ${rule}"
create_rule_documentation "${rule}" "${RELATED_RULES[${rule}]}" > "${temporaty}"
cat "${temporaty}" > "${DOCS_DIREECTORY}/rules/${rule}.md"
done
}
create_list_of_rules
link_related_rules

View file

@ -1,79 +0,0 @@
#!/usr/bin/env bash
# Usage: {script} RULE_FILENAME
# Update list of TLD
set -euo pipefail
declare -r IFS=$'\n'
declare -r LIST_URL="https://publicsuffix.org/list/public_suffix_list.dat"
declare -r LIST_FILENAME=$(mktemp)
declare -r RULE_FILENAME_TEMPORARY=$(mktemp)
echo "- Downloading list"
curl --silent --location "${LIST_URL}" --output "${LIST_FILENAME}"
echo "- Removing old data"
rm -Rf data/domain/*
mkdir -p data/domain/public-suffix
parse_tlds_list () {
sed '/^\/\/*/d' |
idn2 |
tr '[:lower:]' '[:upper:]' |
sed '/^$/d' | while read -r suffix
do
suffix="${suffix#\*\.}"
suffix="${suffix#\!}"
tld="${suffix##*\.}"
if test "$tld" != "$suffix"
then
prefix="${suffix%.$tld}"
echo "$tld $prefix"
else
prefix=""
fi
done
}
echo "- Creating files"
cat "$LIST_FILENAME" |
parse_tlds_list |
cut -d" " -f1 |
sort -u | while read -r tld_with_suffix
do
suffixlist="$(cat "$LIST_FILENAME" | while read -r line
do
if test "// ===END ICANN DOMAINS===" = "$line"
then break
else echo "$line"
fi
done |
parse_tlds_list |
grep "^$tld_with_suffix " |
cut -d" " -f2 |
tr '[:lower:]' '[:upper:]' |
LC_ALL=C sort | while read -r suffix
do
echo " '$suffix.$tld_with_suffix',"
done | LC_ALL=C sort || :)"
if test -n "$suffixlist"
then
echo "- Creating public-suffix/$tld_with_suffix.php"
echo "<?php declare(strict_types=1);
// Copyright (c) https://publicsuffix.org
// SPDX-License-Identifier: MPL-2.0-no-copyleft-exception
return [
$suffixlist
];" > "data/domain/public-suffix/$tld_with_suffix.php"
else
echo "- Skipping public-suffix/$tld_with_suffix.php"
fi
done
wait
echo "Finished!"

View file

@ -1,42 +0,0 @@
#!/usr/bin/env bash
# Usage: {script} RULE_FILENAME
# Update list of TLD
set -euo pipefail
declare -r IFS=$'\n'
declare -r LIST_URL="https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
declare -r LIST_FILENAME=$(mktemp)
declare -r RULE_FILENAME=$(dirname "${BASH_SOURCE}")/../library/Validators/Tld.php
declare -r RULE_FILENAME_TEMPORARY=$(mktemp)
echo "- Downloading list"
curl --silent --location "${LIST_URL}" --output "${LIST_FILENAME}"
echo "- Creating temporary file"
{
sed --silent --regexp-extended "/^</,/^\{/p" "${RULE_FILENAME}"
echo " /**"
echo " * List extracted from ${LIST_URL}"
echo " */"
echo " private const TLD_LIST = ["
grep --invert-match "^#" "${LIST_FILENAME}" |
sed --regexp-extended "s,^,',; s/$/', /" |
tr --delete "\n" |
fold --width=72 --spaces |
sed "s,^, ,g; s, $,,g"
echo
echo " ];"
echo
echo " /**"
echo " * {@inheritDoc}"
echo " */"
sed --silent --regexp-extended "/^ public function/,/^}/p" "${RULE_FILENAME}"
} > "${RULE_FILENAME_TEMPORARY}"
echo "- Updating content of '$(basename ${RULE_FILENAME})'"
mv "${RULE_FILENAME_TEMPORARY}" "${RULE_FILENAME}"
echo "Finished!"

View file

@ -1,48 +0,0 @@
#!/usr/bin/env bash
# Usage: {script}
# Update the list of currency codes
set -euo pipefail
declare -r IFS=$'\n'
declare -r LIST_URL="https://download.geonames.org/export/dump/countryInfo.txt"
declare -r LIST_FILENAME=$(mktemp)
declare -r RULE_FILENAME=$(dirname "${BASH_SOURCE}")/../library/Validators/PostalCode.php
declare -r RULE_FILENAME_TEMPORARY=$(mktemp)
echo "- Downloading list"
curl --silent --location "${LIST_URL}" --output "${LIST_FILENAME}"
declare -r CURRENCY_CODES_COUNT=$(grep "<CcyNtry>" "${LIST_FILENAME}" | wc --lines)
echo "- Creating temporary file"
{
sed -n "/^</,/private const POSTAL_CODES = \[/p" "${RULE_FILENAME}"
echo ' // phpcs:disable Generic.Files.LineLength.TooLong'
cat "$LIST_FILENAME" |
sed '/^#/d' |
sed '/^$/d' |
cut -f1,14,15 |
sort -u | while read -r country_postal_code
do
country_code="${country_postal_code%% *}"
country_postal="${country_postal_code#$country_code }"
country_format="${country_postal%% *}"
country_regex="${country_postal#$country_format }"
country_regex="${country_regex%% }"
country_format="$(echo "$country_format" | sed 's/#/\\d/g' | sed 's/@/\\w/g')"
if test -n "$country_regex"
then
echo " '$country_code' => ['/^$country_format$/', '/$country_regex/'],"
fi
done
echo ' // phpcs:disable Generic.Files.LineLength.TooLong'
sed --silent '/^ \];\/\/end/,/^}/p' "${RULE_FILENAME}"
} > "${RULE_FILENAME_TEMPORARY}"
echo "- Updating content of '$(basename ${RULE_FILENAME})'"
mv "${RULE_FILENAME_TEMPORARY}" "${RULE_FILENAME}"
echo "Finished!"

View file

@ -44,6 +44,7 @@
"sokil/php-isocodes": "^4.2.1",
"sokil/php-isocodes-db-only": "^4.0",
"squizlabs/php_codesniffer": "^4.0",
"symfony/console": "^7.4",
"symfony/var-exporter": "^6.4 || ^7.0"
},
"suggest": {
@ -64,6 +65,7 @@
},
"autoload-dev": {
"psr-4": {
"Respect\\Dev\\": "src-dev/",
"Respect\\Validation\\": "tests/unit/",
"Respect\\Validation\\Test\\": "tests/library/"
}

View file

@ -102,6 +102,13 @@
- [Uploaded](validators/Uploaded.md)
- [Writable](validators/Writable.md)
## ISO codes
- [CountryCode](validators/CountryCode.md)
- [CurrencyCode](validators/CurrencyCode.md)
- [LanguageCode](validators/LanguageCode.md)
- [SubdivisionCode](validators/SubdivisionCode.md)
## Identifications
- [Bsn](validators/Bsn.md)
@ -131,13 +138,6 @@
- [Url](validators/Url.md)
- [VideoUrl](validators/VideoUrl.md)
## ISO codes
- [CountryCode](validators/CountryCode.md)
- [CurrencyCode](validators/CurrencyCode.md)
- [LanguageCode](validators/LanguageCode.md)
- [SubdivisionCode](validators/SubdivisionCode.md)
## Localization
- [CountryCode](validators/CountryCode.md)

View file

@ -12,6 +12,7 @@
<arg value="s" />
<file>library/</file>
<file>src-dev/</file>
<file>tests/</file>
<rule ref="Respect" />

View file

@ -0,0 +1,363 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Dev\Commands;
use DirectoryIterator;
use Nette\PhpGenerator\InterfaceType;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\PhpNamespace;
use Nette\PhpGenerator\Printer;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
use Respect\Validation\Mixins\AllBuilder;
use Respect\Validation\Mixins\AllChain;
use Respect\Validation\Mixins\Chain;
use Respect\Validation\Mixins\KeyBuilder;
use Respect\Validation\Mixins\KeyChain;
use Respect\Validation\Mixins\LengthBuilder;
use Respect\Validation\Mixins\LengthChain;
use Respect\Validation\Mixins\MaxBuilder;
use Respect\Validation\Mixins\MaxChain;
use Respect\Validation\Mixins\MinBuilder;
use Respect\Validation\Mixins\MinChain;
use Respect\Validation\Mixins\NotBuilder;
use Respect\Validation\Mixins\NotChain;
use Respect\Validation\Mixins\NullOrBuilder;
use Respect\Validation\Mixins\NullOrChain;
use Respect\Validation\Mixins\PropertyBuilder;
use Respect\Validation\Mixins\PropertyChain;
use Respect\Validation\Mixins\UndefOrBuilder;
use Respect\Validation\Mixins\UndefOrChain;
use Respect\Validation\Validator;
use Respect\Validation\ValidatorBuilder;
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 function array_filter;
use function array_merge;
use function count;
use function dirname;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function implode;
use function in_array;
use function is_object;
use function ksort;
use function lcfirst;
use function preg_replace;
use function shell_exec;
use function sprintf;
use function str_contains;
use function str_starts_with;
use function ucfirst;
#[AsCommand(
name: 'create:mixin',
description: 'Generate mixin interfaces from validators',
)]
final class CreateMixinCommand extends Command
{
private const array NUMBER_RELATED_VALIDATORS = [
'Between',
'BetweenExclusive',
'Equals',
'Equivalent',
'Even',
'Factor',
'Fibonacci',
'Finite',
'GreaterThan',
'Identical',
'In',
'Infinite',
'LessThan',
'LessThanOrEqual',
'GreaterThanOrEqual',
'Multiple',
'Odd',
'PerfectSquare',
'Positive',
'PrimeNumber',
];
private const array STRUCTURE_RELATED_VALIDATORS = [
'Exists',
'Key',
'KeyExists',
'KeyOptional',
'KeySet',
'NullOr',
'UndefOr',
'Property',
'PropertyExists',
'PropertyOptional',
'Attributes',
'Templated',
'Named',
];
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Generating mixin interfaces');
// Scan validators directory
$libraryDir = dirname(__DIR__, 2) . '/library';
$validatorsDir = $libraryDir . '/Validators';
$validators = $this->scanValidators($validatorsDir);
$io->text(sprintf('Found %d validators', count($validators)));
// Define mixins
$mixins = [
['All', 'all', [], array_merge(['All'], self::STRUCTURE_RELATED_VALIDATORS)],
['Key', 'key', [], self::STRUCTURE_RELATED_VALIDATORS],
['Length', 'length', self::NUMBER_RELATED_VALIDATORS, []],
['Max', 'max', self::NUMBER_RELATED_VALIDATORS, []],
['Min', 'min', self::NUMBER_RELATED_VALIDATORS, []],
['Not', 'not', [], ['Not', 'NullOr', 'UndefOr', 'Attributes', 'Templated', 'Named']],
['NullOr', 'nullOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Templated', 'Named']],
['Property', 'property', [], self::STRUCTURE_RELATED_VALIDATORS],
['UndefOr', 'undefOr', [], ['NullOr', 'Blank', 'Undef', 'UndefOr', 'Attributes', 'Templated', 'Named']],
['', null, [], []],
];
$io->section('Generating mixin interfaces');
foreach ($mixins as [$name, $prefix, $allowList, $denyList]) {
$io->text(sprintf('Generating %sBuilder and %sChain', $name ?: 'Base', $name ?: 'Base'));
$chainedNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
$chainedNamespace->addUse(Validator::class);
$chainedInterface = $chainedNamespace->addInterface($name . 'Chain');
$staticNamespace = new PhpNamespace('Respect\\Validation\\Mixins');
$staticNamespace->addUse(Validator::class);
$staticInterface = $staticNamespace->addInterface($name . 'Builder');
if ($name === '') {
$chainedInterface->addExtend(Validator::class);
$chainedInterface->addExtend(AllChain::class);
$chainedInterface->addExtend(KeyChain::class);
$chainedInterface->addExtend(LengthChain::class);
$chainedInterface->addExtend(MaxChain::class);
$chainedInterface->addExtend(MinChain::class);
$chainedInterface->addExtend(NotChain::class);
$chainedInterface->addExtend(NullOrChain::class);
$chainedInterface->addExtend(PropertyChain::class);
$chainedInterface->addExtend(UndefOrChain::class);
$chainedInterface->addComment('@mixin \\' . ValidatorBuilder::class);
$staticInterface->addExtend(AllBuilder::class);
$staticInterface->addExtend(KeyBuilder::class);
$staticInterface->addExtend(LengthBuilder::class);
$staticInterface->addExtend(MaxBuilder::class);
$staticInterface->addExtend(MinBuilder::class);
$staticInterface->addExtend(NotBuilder::class);
$staticInterface->addExtend(NullOrBuilder::class);
$staticInterface->addExtend(PropertyBuilder::class);
$staticInterface->addExtend(UndefOrBuilder::class);
}
foreach ($validators as $originalName => $reflection) {
$this->addMethodToInterface(
$originalName,
$staticInterface,
$reflection,
$prefix,
$allowList,
$denyList,
);
$this->addMethodToInterface(
$originalName,
$chainedInterface,
$reflection,
$prefix,
$allowList,
$denyList,
);
}
$printer = new Printer();
$printer->wrapLength = 115;
$this->overwriteFile($printer->printNamespace($staticNamespace), $staticInterface->getName());
$this->overwriteFile($printer->printNamespace($chainedNamespace), $chainedInterface->getName());
}
// Run code beautifier
$io->section('Running code beautifier');
$mixinsDir = $libraryDir . '/Mixins';
$phpcbfPath = dirname(__DIR__, 2) . '/vendor/bin/phpcbf';
if (file_exists($phpcbfPath)) {
shell_exec($phpcbfPath . ' ' . $mixinsDir);
$io->success('Code beautified');
} else {
$io->warning('phpcbf not found, skipping code beautification');
}
$io->success('Mixin interfaces generated successfully');
return Command::SUCCESS;
}
/** @return array<string, ReflectionClass> */
private function scanValidators(string $directory): array
{
$names = [];
foreach (new DirectoryIterator($directory) as $file) {
if (!$file->isFile()) {
continue;
}
$className = 'Respect\\Validation\\Validators\\' . $file->getBasename('.php');
$reflection = new ReflectionClass($className);
if ($reflection->isAbstract()) {
continue;
}
$names[$reflection->getShortName()] = $reflection;
}
ksort($names);
return $names;
}
/**
* @param array<string> $allowList
* @param array<string> $denyList
*/
private function addMethodToInterface(
string $originalName,
InterfaceType $interfaceType,
ReflectionClass $reflection,
string|null $prefix,
array $allowList,
array $denyList,
): void {
if ($allowList !== [] && !in_array($reflection->getShortName(), $allowList, true)) {
return;
}
if ($denyList !== [] && in_array($reflection->getShortName(), $denyList, true)) {
return;
}
$name = $prefix ? $prefix . ucfirst($originalName) : lcfirst($originalName);
$method = $interfaceType->addMethod($name)->setPublic()->setReturnType(Chain::class);
if (str_contains($interfaceType->getName(), 'Builder')) {
$method->setStatic();
}
if ($prefix === 'key') {
$method->addParameter('key')->setType('int|string');
}
if ($prefix === 'property') {
$method->addParameter('propertyName')->setType('string');
}
$reflectionConstructor = $reflection->getConstructor();
if ($reflectionConstructor === null) {
return;
}
$comment = $reflectionConstructor->getDocComment();
if ($comment) {
$method->addComment(preg_replace('@(/\*\* *| +\* +| +\*/)@', '', $comment));
}
foreach ($reflectionConstructor->getParameters() as $reflectionParameter) {
$this->addParameterToMethod($method, $reflectionParameter);
}
}
private function addParameterToMethod(Method $method, ReflectionParameter $reflectionParameter): void
{
if ($reflectionParameter->isVariadic()) {
$method->setVariadic();
}
$type = $reflectionParameter->getType();
$types = [];
if ($type instanceof ReflectionUnionType) {
foreach ($type->getTypes() as $subType) {
$types[] = $subType->getName();
}
} elseif ($type instanceof ReflectionNamedType) {
$types[] = $type->getName();
if (
str_starts_with($type->getName(), 'Sokil')
|| str_starts_with($type->getName(), 'Egulias')
|| $type->getName() === 'finfo'
) {
return;
}
}
$parameter = $method->addParameter($reflectionParameter->getName());
$parameter->setType(implode('|', $types));
if (!$reflectionParameter->isDefaultValueAvailable()) {
$parameter->setNullable($reflectionParameter->isOptional());
}
if (count($types) > 1 || $reflectionParameter->isVariadic()) {
$parameter->setNullable(false);
}
if (!$reflectionParameter->isDefaultValueAvailable()) {
return;
}
$defaultValue = $reflectionParameter->getDefaultValue();
if (is_object($defaultValue)) {
$parameter->setDefaultValue(null);
$parameter->setNullable(true);
return;
}
$parameter->setDefaultValue($defaultValue);
$parameter->setNullable(false);
}
private function overwriteFile(string $content, string $basename): void
{
$libraryDir = dirname(__DIR__, 2) . '/library';
$docheaderPath = dirname(__DIR__, 2) . '/.docheader';
$docheader = file_exists($docheaderPath) ? file_get_contents($docheaderPath) : '';
$finalContent = implode("\n\n", array_filter([
'<?php',
$docheader,
'declare(strict_types=1);',
preg_replace('/extends (.+, )+/', 'extends' . "\n" . '\1', $content),
]));
file_put_contents(
sprintf('%s/Mixins/%s.php', $libraryDir, $basename),
$finalContent,
);
}
}

View file

@ -0,0 +1,275 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Dev\Commands;
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 function array_unique;
use function basename;
use function count;
use function dirname;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function glob;
use function implode;
use function in_array;
use function preg_match;
use function preg_match_all;
use function preg_split;
use function sort;
use function sprintf;
use function trim;
use const PHP_EOL;
#[AsCommand(
name: 'update:doc-links',
description: 'Update list of validators and link related validators in documentation',
)]
final class UpdateDocLinksCommand extends Command
{
/** @var array<string, array<string>> */
private array $validatorsByCategory = [];
/** @var array<string, array<string>> */
private array $relatedRules = [];
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$docsDirectory = dirname(__DIR__, 2) . '/docs';
if (!file_exists($docsDirectory)) {
$io->error(sprintf('Documentation directory not found: %s', $docsDirectory));
return Command::FAILURE;
}
$validators = $this->listRules($docsDirectory);
$this->createListOfRules($docsDirectory, $validators);
$this->linkRelatedRules($docsDirectory, $validators, $io);
$io->success('Documentation updated successfully');
return Command::SUCCESS;
}
/** @return array<string> */
private function listRules(string $docsDirectory): array
{
$files = glob($docsDirectory . '/validators/*.md');
if ($files === false) {
return [];
}
$validators = [];
foreach ($files as $file) {
$validators[] = basename($file, '.md');
}
sort($validators);
return $validators;
}
/**
* @param array<string> $validators
*
* @return array<string>
*/
private function listCategories(string $docsDirectory, array $validators): array
{
$categories = [];
foreach ($validators as $validator) {
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
$content = file_get_contents($filename);
if ($content === false) {
continue;
}
// Extract categories between "## Categorization" and "## Changelog"
if (!preg_match('/## Categorization\s*(.*?)\s*## Changelog/s', $content, $matches)) {
continue;
}
preg_match_all('/^-\s*(.+)$/m', $matches[1], $categoryMatches);
foreach ($categoryMatches[1] as $category) {
$categories[] = trim($category);
}
}
$categories = array_unique($categories);
sort($categories);
return $categories;
}
/** @param array<string> $validators */
private function createListOfRules(string $docsDirectory, array $validators): void
{
// Build validators by category
foreach ($validators as $validator) {
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
$content = file_get_contents($filename);
if ($content === false) {
continue;
}
if (!preg_match('/## Categorization\s*(.*?)\s*## Changelog/s', $content, $matches)) {
continue;
}
preg_match_all('/^-\s*(.+)$/m', $matches[1], $categoryMatches);
foreach ($categoryMatches[1] as $category) {
$category = trim($category);
if (!isset($this->validatorsByCategory[$category])) {
$this->validatorsByCategory[$category] = [];
}
$this->validatorsByCategory[$category][] = $validator;
}
}
// Generate the list file
$categories = $this->listCategories($docsDirectory, $validators);
$lines = ['# List of validators by category', ''];
foreach ($categories as $category) {
$lines[] = sprintf('## %s', $category);
$lines[] = '';
if (isset($this->validatorsByCategory[$category])) {
$categoryRules = $this->validatorsByCategory[$category];
sort($categoryRules);
foreach ($categoryRules as $validator) {
$lines[] = sprintf('- [%s](validators/%s.md)', $validator, $validator);
}
}
$lines[] = '';
}
$lines[] = '## Alphabetically';
$lines[] = '';
foreach ($validators as $validator) {
$lines[] = sprintf('- [%1$s](validators/%1$s.md)', $validator);
}
$outputFile = sprintf('%s/09-list-of-validators-by-category.md', $docsDirectory);
file_put_contents($outputFile, trim(implode("\n", $lines)) . PHP_EOL);
}
/** @param array<string> $validators */
private function linkRelatedRules(string $docsDirectory, array $validators, SymfonyStyle $io): void
{
// Build list of related validators
foreach ($validators as $validator) {
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
$content = file_get_contents($filename);
if ($content === false) {
continue;
}
// Find all markdown links
preg_match_all('/\[([^\]]+)\]\(([^\)]+)\.md\)/', $content, $matches);
foreach ($matches[2] as $relatedRule) {
$relatedRule = basename($relatedRule);
if ($relatedRule === '08-comparable-values' || $relatedRule === 'comparing-empty-values') {
continue;
}
if (!isset($this->relatedRules[$relatedRule])) {
$this->relatedRules[$relatedRule] = [];
}
if (!in_array($validator, $this->relatedRules[$relatedRule], true)) {
$this->relatedRules[$relatedRule][] = $validator;
}
if (!isset($this->relatedRules[$validator])) {
$this->relatedRules[$validator] = [];
}
if (in_array($relatedRule, $this->relatedRules[$validator], true)) {
continue;
}
$this->relatedRules[$validator][] = $relatedRule;
}
}
// Update each validator documentation
$io->progressStart(count($validators));
foreach ($validators as $validator) {
$this->createRuleDocumentation($docsDirectory, $validator);
$io->progressAdvance();
}
$io->progressFinish();
}
private function createRuleDocumentation(string $docsDirectory, string $validator): void
{
$filename = sprintf('%s/validators/%s.md', $docsDirectory, $validator);
$content = file_get_contents($filename);
if ($content === false) {
return;
}
// Extract links at the bottom
preg_match_all('/^\[.+\]: .+$/m', $content, $linkMatches);
$links = $linkMatches[0] ?? [];
// Get content before "See also" or "---"
$parts = preg_split('/(^## See also:|^---)/m', $content);
$mainContent = $parts[0] ?? $content;
// Build "See also" section
$relatedLinks = [];
if (isset($this->relatedRules[$validator])) {
$related = array_unique($this->relatedRules[$validator]);
sort($related);
foreach ($related as $relatedRule) {
if ($relatedRule === $validator) {
continue;
}
$relatedLinks[] = sprintf('- [%s](%s.md)', $relatedRule, $relatedRule);
}
}
// Rebuild the document
$lines = [
trim($mainContent),
'',
'---',
'',
];
if ($relatedLinks !== []) {
$lines[] = 'See also:';
$lines[] = '';
$lines = [...$lines, ...$relatedLinks];
}
if ($links !== []) {
$lines[] = '';
$lines = [...$lines, ...$links];
}
file_put_contents($filename, trim(implode("\n", $lines)) . PHP_EOL);
}
}

View file

@ -0,0 +1,207 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Dev\Commands;
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;
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;
use function preg_match;
use function rmdir;
use function sort;
use function sprintf;
use function str_replace;
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',
)]
final class UpdateDomainSuffixesCommand extends Command
{
private const string LIST_URL = 'https://publicsuffix.org/list/public_suffix_list.dat';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Updating domain suffixes');
// Download the list
$io->section('Downloading list');
$io->text(sprintf('Fetching from: %s', self::LIST_URL));
$listContent = file_get_contents(self::LIST_URL);
if ($listContent === false) {
$io->error('Failed to download public suffix list');
return Command::FAILURE;
}
$io->success('Downloaded successfully');
// Clean old data
$io->section('Removing old data');
$dataDir = dirname(__DIR__, 2) . '/data/domain';
$this->removeDirectory($dataDir . '/public-suffix');
if (!is_dir($dataDir)) {
mkdir($dataDir, 0777, true);
}
mkdir($dataDir . '/public-suffix', 0777, true);
$io->success('Old data removed');
// Process the list
$io->section('Processing public suffix list');
$suffixes = $this->parseTldsList($listContent);
$tlds = array_unique(array_keys($suffixes));
sort($tlds);
$io->text(sprintf('Found %d TLDs with suffixes', count($tlds)));
// Create files
$io->section('Creating suffix files');
$progressBar = $io->createProgressBar(count($tlds));
$progressBar->start();
foreach ($tlds as $tld) {
$suffixList = $suffixes[$tld];
if ($suffixList === []) {
$progressBar->advance();
continue;
}
sort($suffixList);
$fileContent = implode(PHP_EOL, [
'<?php declare(strict_types=1);',
'// Copyright (c) https://publicsuffix.org',
'// 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);
$progressBar->advance();
}
$progressBar->finish();
$io->newLine(2);
$io->success('Domain suffixes updated successfully');
return Command::SUCCESS;
}
/** @return array<string, array<string>> */
private function parseTldsList(string $content): array
{
$lines = explode("\n", $content);
$suffixes = [];
$icannOnly = true;
foreach ($lines as $line) {
$line = trim($line);
// Check if we've reached the end of ICANN domains
if ($line === '// ===END ICANN DOMAINS===') {
$icannOnly = false;
}
// Skip comments and empty lines
if ($line === '' || str_starts_with($line, '//')) {
continue;
}
// Process the suffix
$suffix = $line;
// Remove wildcards and exceptions
$suffix = str_replace('*.', '', $suffix);
$suffix = str_replace('!', '', $suffix);
// Convert to uppercase (using multibyte for international characters)
$suffix = mb_strtoupper($suffix, 'UTF-8');
// Split into TLD and prefix
if (!preg_match('/^([^.]+)$|^(.+)\.([^.]+)$/', $suffix, $matches)) {
continue;
}
if (isset($matches[3])) {
// Has a prefix
$tld = $matches[3];
$prefix = $matches[2];
if (!isset($suffixes[$tld])) {
$suffixes[$tld] = [];
}
// Only add ICANN domains
if ($icannOnly) {
$suffixes[$tld][] = $prefix . '.' . $tld;
}
} else {
// Just a TLD
$tld = $matches[1];
if (!isset($suffixes[$tld])) {
$suffixes[$tld] = [];
}
}
}
return $suffixes;
}
private function removeDirectory(string $directory): void
{
if (!is_dir($directory)) {
return;
}
$files = glob($directory . '/*');
if ($files === false) {
return;
}
foreach ($files as $file) {
if (is_dir($file)) {
$this->removeDirectory($file);
} else {
@unlink($file);
}
}
@rmdir($directory);
}
}

View file

@ -0,0 +1,95 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Dev\Commands;
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',
)]
final class UpdateDomainToplevelCommand extends Command
{
private const string LIST_URL = 'https://data.iana.org/TLD/tlds-alpha-by-domain.txt';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Updating TLD list');
// Download the list
$io->section('Downloading list');
$io->text(sprintf('Fetching from: %s', self::LIST_URL));
$listContent = file_get_contents(self::LIST_URL);
if ($listContent === false) {
$io->error('Failed to download TLD list');
return Command::FAILURE;
}
$io->success('Downloaded successfully');
// Process the list
$io->section('Processing TLD list');
$lines = explode("\n", trim($listContent));
$tlds = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
$tlds[] = $line;
}
// Create the data file
$dataFilename = dirname(__DIR__, 2) . '/data/domain/tld.php';
$fileContent = implode(PHP_EOL, [
'<?php declare(strict_types=1);',
'// Copyright (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->text(sprintf('Total TLDs: %d', count($tlds)));
return Command::SUCCESS;
}
}

View file

@ -0,0 +1,130 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-License-Identifier: MIT
*/
declare(strict_types=1);
namespace Respect\Dev\Commands;
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;
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',
)]
final class UpdatePostalCodesCommand extends Command
{
private const string LIST_URL = 'https://download.geonames.org/export/dump/countryInfo.txt';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Updating postal codes list');
// Download the list
$io->section('Downloading list');
$io->text(sprintf('Fetching from: %s', self::LIST_URL));
$listContent = file_get_contents(self::LIST_URL);
if ($listContent === false) {
$io->error('Failed to download postal codes list');
return Command::FAILURE;
}
$io->success('Downloaded successfully');
// Process the list
$io->section('Processing postal codes');
$lines = explode("\n", $listContent);
$postalCodes = [];
foreach ($lines as $line) {
$line = trim($line);
// Skip comments and empty lines
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
// Split by tab
$parts = explode("\t", $line);
if (count($parts) < 15) {
continue;
}
$countryCode = $parts[0];
$countryFormat = $parts[13];
$countryRegex = trim($parts[14]);
if ($countryRegex === '') {
continue;
}
// Convert format
$countryFormat = preg_replace_callback('/(#+|@+)/', static function ($matches) {
$length = strlen($matches[0]);
$regex = str_contains($matches[0], '#') ? '\d' : '\w';
if ($length > 1) {
$regex .= '{' . $length . '}';
}
return $regex;
}, $countryFormat);
$postalCodes[$countryCode] = ['/^' . $countryFormat . '$/', '/' . $countryRegex . '/'];
}
ksort($postalCodes);
// Create the data file
$dataFilename = dirname(__DIR__, 2) . '/data/domain/postal-code.php';
$fileContent = implode(PHP_EOL, [
'<?php declare(strict_types=1);',
'// Copyright (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->text(sprintf('Total postal codes: %d', count($postalCodes)));
return Command::SUCCESS;
}
}