From c4b3402b3b16e25d0f7bb1148aca7898c61fee03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Fri, 19 Jun 2026 11:05:37 +0200 Subject: [PATCH 1/6] add array and string parse --- src/Context/Name/GermanTypeName.php | 4 +- src/Parser/Name/AbstractParser.php | 68 +++++++++++++ src/Parser/Name/ArrayParser.php | 52 ++++++++++ .../Name/Rule/Array/EnglishKeyNameRule.php | 31 ++++++ .../Name/Rule/Array/GermanKeyNameRule.php | 31 ++++++ src/Parser/Name/Rule/Result.php | 37 ++++++++ src/Parser/Name/Rule/RowValue.php | 19 ++++ src/Parser/Name/Rule/RuleInterface.php | 11 +++ src/Parser/Name/Rule/Value.php | 15 +++ src/Parser/Name/StringParser.php | 95 +++++++++++++++++++ 10 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 src/Parser/Name/AbstractParser.php create mode 100644 src/Parser/Name/ArrayParser.php create mode 100644 src/Parser/Name/Rule/Array/EnglishKeyNameRule.php create mode 100644 src/Parser/Name/Rule/Array/GermanKeyNameRule.php create mode 100644 src/Parser/Name/Rule/Result.php create mode 100644 src/Parser/Name/Rule/RowValue.php create mode 100644 src/Parser/Name/Rule/RuleInterface.php create mode 100644 src/Parser/Name/Rule/Value.php create mode 100644 src/Parser/Name/StringParser.php diff --git a/src/Context/Name/GermanTypeName.php b/src/Context/Name/GermanTypeName.php index 79d557f..8f877a3 100644 --- a/src/Context/Name/GermanTypeName.php +++ b/src/Context/Name/GermanTypeName.php @@ -5,8 +5,8 @@ class GermanTypeName extends AbstractName { public function __construct( - private readonly string $firstName, - private readonly string $lastName, + public string $firstName, + public string $lastName, ) { } diff --git a/src/Parser/Name/AbstractParser.php b/src/Parser/Name/AbstractParser.php new file mode 100644 index 0000000..0f1a04d --- /dev/null +++ b/src/Parser/Name/AbstractParser.php @@ -0,0 +1,68 @@ +[] + */ + protected array $rules, + /** @var class-string */ + protected string $format, + ) { + if (!(is_a($this->format, AbstractName::class, true))) { + throw new \InvalidArgumentException(sprintf('Format class "%s" does not exist or is not a subclass of "%s".', $this->format, AbstractName::class)); + } + } + + public static function create(string $format): static + { + static::validateFormat($format); + $rules = static::defaultRules($format); + + $instance = new static($rules, $format); + return $instance; + } + + public function allowIncomplete(bool $allow): static + { + $this->allowIncomplete = $allow; + return $this; + } + + public static function validateFormat(string $targetFormat): void + { + if (!class_exists($targetFormat) || !is_subclass_of($targetFormat, AbstractName::class)) { + throw new \InvalidArgumentException(sprintf('Target format class "%s" does not exist or is not a subclass of "%s".', $targetFormat, AbstractName::class)); + } + } + + protected function buildNameFromResult(Result $result): ?AbstractName + { + if (!$result->isComplete() && !$this->allowIncomplete) { + return null; + } + + if ($result->isEmpty()) { + return null; + } + + /** @var GermanTypeName $name */ + $name = new ($this->format)(); + $name->firstName = $result->firstName; + $name->lastName = $result->lastName; + + return $name; + } + + abstract public static function defaultRules(string $format): array; +} \ No newline at end of file diff --git a/src/Parser/Name/ArrayParser.php b/src/Parser/Name/ArrayParser.php new file mode 100644 index 0000000..0394ea5 --- /dev/null +++ b/src/Parser/Name/ArrayParser.php @@ -0,0 +1,52 @@ + $value) { + $entry = new RowValue($key, $value); + foreach ($this->rules as $rule) { + if (!(is_a($rule, RuleInterface::class, true))) { + continue; + } + $result->with((new $rule)->apply($entry)); + if ($result->isComplete()) { + break 2; + } + } + } + + return $this->buildNameFromResult($result); + } + + /** + * @param string $format + * @return class-string[] + */ + public static function defaultRules(string $format): array + { + return match ($format) { + GermanTypeName::class => [ + GermanKeyNameRule::class, + EnglishKeyNameRule::class, + ], + default => [ + EnglishKeyNameRule::class, + ], + }; + } +} \ No newline at end of file diff --git a/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php b/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php new file mode 100644 index 0000000..4bfe86f --- /dev/null +++ b/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php @@ -0,0 +1,31 @@ +originalValue)) { + return new Result(false); + } + + if ($value->normalizedKey === 'firstname') { + return new Result(true, firstName: $value->originalValue, origin: $value); + } + if (\in_array($value->normalizedKey, ['lastname', 'name'])) { + return new Result(true, lastName: $value->originalValue, origin: $value); + } + + return new Result(false); + } +} \ No newline at end of file diff --git a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php new file mode 100644 index 0000000..78f2025 --- /dev/null +++ b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php @@ -0,0 +1,31 @@ +originalValue)) { + return new Result(false); + } + + if (in_array($value->normalizedKey, ['vorname'])) { + return new Result(true, firstName: $value->originalValue, origin: $value); + } + if (in_array($value->normalizedKey, ['nachname', 'zuname', 'name'])) { + return new Result(true, lastName: $value->originalValue, origin: $value); + } + + return new Result(false); + } +} \ No newline at end of file diff --git a/src/Parser/Name/Rule/Result.php b/src/Parser/Name/Rule/Result.php new file mode 100644 index 0000000..d2253cd --- /dev/null +++ b/src/Parser/Name/Rule/Result.php @@ -0,0 +1,37 @@ +matched) { + return $this; + } + + return new self( + true, + $result->firstName !== '' ? $result->firstName : $this->firstName, + $result->lastName !== '' ? $result->lastName : $this->lastName, + $this->origin // oder $result->origin, je nach gewünschter Semantik + ); + } + + public function isComplete(): bool + { + return $this->matched && $this->firstName !== '' && $this->lastName !== ''; + } + + public function isEmpty(): bool + { + return $this->firstName === '' && $this->lastName === ''; + } +} \ No newline at end of file diff --git a/src/Parser/Name/Rule/RowValue.php b/src/Parser/Name/Rule/RowValue.php new file mode 100644 index 0000000..347def6 --- /dev/null +++ b/src/Parser/Name/Rule/RowValue.php @@ -0,0 +1,19 @@ +originalKey = (string) $key; + $this->normalizedKey = strtolower((string) preg_replace('/[^a-z0-9]/i', '', $this->originalKey)); + } +} \ No newline at end of file diff --git a/src/Parser/Name/Rule/RuleInterface.php b/src/Parser/Name/Rule/RuleInterface.php new file mode 100644 index 0000000..d1d37c1 --- /dev/null +++ b/src/Parser/Name/Rule/RuleInterface.php @@ -0,0 +1,11 @@ +originalValue = $originalValue; + } + + +} \ No newline at end of file diff --git a/src/Parser/Name/StringParser.php b/src/Parser/Name/StringParser.php new file mode 100644 index 0000000..f15bcea --- /dev/null +++ b/src/Parser/Name/StringParser.php @@ -0,0 +1,95 @@ +buildNameFromResult(new Result(true, $firstName, $lastName)); + } + + $parts = preg_split('/\s+/u', $name, -1, PREG_SPLIT_NO_EMPTY); + $count = count($parts); + + if ($count === 1) { + return $this->buildNameFromResult(new Result(true, '', $parts[0])); + } + + if ($count === 2) { + return $this->buildNameFromResult(new Result(true, $parts[0], $parts[1])); + } + + $surnamePrefixes = [ + 'von', + 'vom', + 'van', + 'de', + 'del', + 'della', + 'der', + 'den', + 'zu', + 'zur', + 'zum', + 'da', + 'di', + 'du', + 'la', + 'le', + 'dos', + 'das', + 'do', + 'bin', + 'al', + ]; + + // Startpunkt des Nachnamens suchen + $lastNameStartIndex = $count - 1; + + while ( + $lastNameStartIndex > 0 + && in_array(mb_strtolower($parts[$lastNameStartIndex - 1]), $surnamePrefixes, true) + ) { + $lastNameStartIndex--; + } + + if ($lastNameStartIndex < $count - 1) { + return $this->buildNameFromResult( + new Result( + true, + implode(' ', array_slice($parts, 0, $lastNameStartIndex)), + implode(' ', array_slice($parts, $lastNameStartIndex)), + ) + ); + } + + // at least we have only wild guessing :) + $splitIndex = (int) ceil($count / 2); + return $this->buildNameFromResult( + new Result( + true, + implode(' ', array_slice($parts, 0, $splitIndex)), + implode(' ', array_slice($parts, $splitIndex)) + ) + ); + } +} \ No newline at end of file From 3388260516a1b5325eed84b808d917f9b1639f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Fri, 19 Jun 2026 11:08:47 +0200 Subject: [PATCH 2/6] run rector --- src/Parser/Name/ArrayParser.php | 2 -- src/Parser/Name/Rule/Array/GermanKeyNameRule.php | 1 - src/Parser/Name/Rule/Value.php | 5 +---- src/Parser/Name/StringParser.php | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Parser/Name/ArrayParser.php b/src/Parser/Name/ArrayParser.php index 0394ea5..c4d894d 100644 --- a/src/Parser/Name/ArrayParser.php +++ b/src/Parser/Name/ArrayParser.php @@ -9,8 +9,6 @@ use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RowValue; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RuleInterface; -use HeimrichHannot\SalutationCreator\Parser\Name\ValueObject\ArrayRow; -use HeimrichHannot\SalutationCreator\Parser\Name\ValueObject\Name; class ArrayParser extends AbstractParser { diff --git a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php index 78f2025..a0941c9 100644 --- a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php +++ b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php @@ -3,7 +3,6 @@ namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule\Array; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result; -use HeimrichHannot\SalutationCreator\Parser\Name\ValueObject\ArrayRow; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RowValue; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Value; diff --git a/src/Parser/Name/Rule/Value.php b/src/Parser/Name/Rule/Value.php index 3c6103c..5dbf07a 100644 --- a/src/Parser/Name/Rule/Value.php +++ b/src/Parser/Name/Rule/Value.php @@ -4,11 +4,8 @@ class Value { - public mixed $originalValue; - - public function __construct(mixed $originalValue) + public function __construct(public mixed $originalValue) { - $this->originalValue = $originalValue; } diff --git a/src/Parser/Name/StringParser.php b/src/Parser/Name/StringParser.php index f15bcea..6eded20 100644 --- a/src/Parser/Name/StringParser.php +++ b/src/Parser/Name/StringParser.php @@ -23,7 +23,7 @@ public function parse(string $name): ?AbstractName } if (str_contains($name, ',')) { - [$lastName, $firstName] = array_map('trim', explode(',', $name, 2)); + [$lastName, $firstName] = array_map(trim(...), explode(',', $name, 2)); return $this->buildNameFromResult(new Result(true, $firstName, $lastName)); } From bb8a6fec96cc7d85a52a9ec8b1ab249ac7d4a9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Fri, 19 Jun 2026 11:24:41 +0200 Subject: [PATCH 3/6] fix phpstan reports --- src/Parser/Name/AbstractParser.php | 16 +++++++++++----- src/Parser/Name/ArrayParser.php | 5 ++++- src/Parser/Name/Rule/Array/GermanKeyNameRule.php | 3 ++- src/Parser/Name/Rule/RowValue.php | 2 +- src/Parser/Name/StringParser.php | 4 +++- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Parser/Name/AbstractParser.php b/src/Parser/Name/AbstractParser.php index 0f1a04d..0dba3ac 100644 --- a/src/Parser/Name/AbstractParser.php +++ b/src/Parser/Name/AbstractParser.php @@ -11,12 +11,12 @@ abstract class AbstractParser { private bool $allowIncomplete = false; - public function __construct( - /** - * @var class-string[] - */ + /** + * @param class-string[] $rules + * @param class-string $format + */ + final public function __construct( protected array $rules, - /** @var class-string */ protected string $format, ) { if (!(is_a($this->format, AbstractName::class, true))) { @@ -24,6 +24,9 @@ public function __construct( } } + /** + * @param class-string $format + */ public static function create(string $format): static { static::validateFormat($format); @@ -64,5 +67,8 @@ protected function buildNameFromResult(Result $result): ?AbstractName return $name; } + /** + * @return class-string[] + */ abstract public static function defaultRules(string $format): array; } \ No newline at end of file diff --git a/src/Parser/Name/ArrayParser.php b/src/Parser/Name/ArrayParser.php index c4d894d..e011775 100644 --- a/src/Parser/Name/ArrayParser.php +++ b/src/Parser/Name/ArrayParser.php @@ -12,6 +12,9 @@ class ArrayParser extends AbstractParser { + /** + * @param array $data + */ public function parse(array $data): ?AbstractName { $result = new Result(false); @@ -32,7 +35,7 @@ public function parse(array $data): ?AbstractName } /** - * @param string $format + * @param class-string $format * @return class-string[] */ public static function defaultRules(string $format): array diff --git a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php index a0941c9..560bfe6 100644 --- a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php +++ b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php @@ -4,9 +4,10 @@ use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RowValue; +use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RuleInterface; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Value; -class GermanKeyNameRule +class GermanKeyNameRule implements RuleInterface { public function apply(Value $value): Result { diff --git a/src/Parser/Name/Rule/RowValue.php b/src/Parser/Name/Rule/RowValue.php index 347def6..bdb4463 100644 --- a/src/Parser/Name/Rule/RowValue.php +++ b/src/Parser/Name/Rule/RowValue.php @@ -8,7 +8,7 @@ class RowValue extends Value public string $normalizedKey; public function __construct( - mixed $key, + string|int $key, mixed $value ) { parent::__construct($value); diff --git a/src/Parser/Name/StringParser.php b/src/Parser/Name/StringParser.php index 6eded20..008333a 100644 --- a/src/Parser/Name/StringParser.php +++ b/src/Parser/Name/StringParser.php @@ -28,7 +28,9 @@ public function parse(string $name): ?AbstractName } $parts = preg_split('/\s+/u', $name, -1, PREG_SPLIT_NO_EMPTY); - $count = count($parts); + if (false === $parts || ($count = count($parts)) === 0) { + return null; + } if ($count === 1) { return $this->buildNameFromResult(new Result(true, '', $parts[0])); From ababeae49f68f74a0fc91b8c039f873dd2605a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Fri, 19 Jun 2026 11:27:03 +0200 Subject: [PATCH 4/6] run ecs --- src/Context/Gender/Gender.php | 2 +- src/Context/Name/GermanTypeName.php | 2 +- src/Parser/Name/AbstractParser.php | 8 +++++--- src/Parser/Name/ArrayParser.php | 7 ++++--- .../Name/Rule/Array/EnglishKeyNameRule.php | 8 ++++---- .../Name/Rule/Array/GermanKeyNameRule.php | 4 ++-- src/Parser/Name/Rule/Result.php | 17 +++++++++-------- src/Parser/Name/Rule/RowValue.php | 4 ++-- src/Parser/Name/Rule/RuleInterface.php | 5 +---- src/Parser/Name/Rule/Value.php | 9 ++++----- src/Parser/Name/StringParser.php | 10 ++++++---- src/SalutationCreator.php | 7 ++----- 12 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/Context/Gender/Gender.php b/src/Context/Gender/Gender.php index 1c4a44d..0ab7c38 100644 --- a/src/Context/Gender/Gender.php +++ b/src/Context/Gender/Gender.php @@ -13,7 +13,7 @@ enum Gender: string implements GenderInterface public function build(): SalutationPartResult { return new SalutationPartResult( - 'gender.' . $this->value, + 'gender.'.$this->value, domain: 'salutation_creator', fallback: ucfirst($this->value), ); diff --git a/src/Context/Name/GermanTypeName.php b/src/Context/Name/GermanTypeName.php index 8f877a3..d65e63d 100644 --- a/src/Context/Name/GermanTypeName.php +++ b/src/Context/Name/GermanTypeName.php @@ -12,7 +12,7 @@ public function __construct( public function getFullName(): string { - return $this->firstName . ' ' . $this->lastName; + return $this->firstName.' '.$this->lastName; } public function getFormalName(): string diff --git a/src/Parser/Name/AbstractParser.php b/src/Parser/Name/AbstractParser.php index 0dba3ac..338160d 100644 --- a/src/Parser/Name/AbstractParser.php +++ b/src/Parser/Name/AbstractParser.php @@ -13,13 +13,13 @@ abstract class AbstractParser /** * @param class-string[] $rules - * @param class-string $format + * @param class-string $format */ final public function __construct( protected array $rules, protected string $format, ) { - if (!(is_a($this->format, AbstractName::class, true))) { + if (!is_a($this->format, AbstractName::class, true)) { throw new \InvalidArgumentException(sprintf('Format class "%s" does not exist or is not a subclass of "%s".', $this->format, AbstractName::class)); } } @@ -33,12 +33,14 @@ public static function create(string $format): static $rules = static::defaultRules($format); $instance = new static($rules, $format); + return $instance; } public function allowIncomplete(bool $allow): static { $this->allowIncomplete = $allow; + return $this; } @@ -71,4 +73,4 @@ protected function buildNameFromResult(Result $result): ?AbstractName * @return class-string[] */ abstract public static function defaultRules(string $format): array; -} \ No newline at end of file +} diff --git a/src/Parser/Name/ArrayParser.php b/src/Parser/Name/ArrayParser.php index e011775..474ff5d 100644 --- a/src/Parser/Name/ArrayParser.php +++ b/src/Parser/Name/ArrayParser.php @@ -21,10 +21,10 @@ public function parse(array $data): ?AbstractName foreach ($data as $key => $value) { $entry = new RowValue($key, $value); foreach ($this->rules as $rule) { - if (!(is_a($rule, RuleInterface::class, true))) { + if (!is_a($rule, RuleInterface::class, true)) { continue; } - $result->with((new $rule)->apply($entry)); + $result->with((new $rule())->apply($entry)); if ($result->isComplete()) { break 2; } @@ -36,6 +36,7 @@ public function parse(array $data): ?AbstractName /** * @param class-string $format + * * @return class-string[] */ public static function defaultRules(string $format): array @@ -50,4 +51,4 @@ public static function defaultRules(string $format): array ], }; } -} \ No newline at end of file +} diff --git a/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php b/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php index 4bfe86f..22ac8ee 100644 --- a/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php +++ b/src/Parser/Name/Rule/Array/EnglishKeyNameRule.php @@ -3,15 +3,15 @@ namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule\Array; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result; -use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RuleInterface; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RowValue; +use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RuleInterface; use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Value; class EnglishKeyNameRule implements RuleInterface { public function apply(Value $value): Result { - if (!($value instanceof RowValue)) { + if (!$value instanceof RowValue) { return new Result(false); } @@ -19,7 +19,7 @@ public function apply(Value $value): Result return new Result(false); } - if ($value->normalizedKey === 'firstname') { + if ('firstname' === $value->normalizedKey) { return new Result(true, firstName: $value->originalValue, origin: $value); } if (\in_array($value->normalizedKey, ['lastname', 'name'])) { @@ -28,4 +28,4 @@ public function apply(Value $value): Result return new Result(false); } -} \ No newline at end of file +} diff --git a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php index 560bfe6..5a69408 100644 --- a/src/Parser/Name/Rule/Array/GermanKeyNameRule.php +++ b/src/Parser/Name/Rule/Array/GermanKeyNameRule.php @@ -11,7 +11,7 @@ class GermanKeyNameRule implements RuleInterface { public function apply(Value $value): Result { - if (!($value instanceof RowValue)) { + if (!$value instanceof RowValue) { return new Result(false); } @@ -28,4 +28,4 @@ public function apply(Value $value): Result return new Result(false); } -} \ No newline at end of file +} diff --git a/src/Parser/Name/Rule/Result.php b/src/Parser/Name/Rule/Result.php index d2253cd..bcbd550 100644 --- a/src/Parser/Name/Rule/Result.php +++ b/src/Parser/Name/Rule/Result.php @@ -5,11 +5,12 @@ class Result { public function __construct( - public readonly bool $matched, + public readonly bool $matched, public readonly string $firstName = '', public readonly string $lastName = '', - public readonly mixed $origin = null, - ) {} + public readonly mixed $origin = null, + ) { + } public function with(Result $result): Result { @@ -19,19 +20,19 @@ public function with(Result $result): Result return new self( true, - $result->firstName !== '' ? $result->firstName : $this->firstName, - $result->lastName !== '' ? $result->lastName : $this->lastName, + '' !== $result->firstName ? $result->firstName : $this->firstName, + '' !== $result->lastName ? $result->lastName : $this->lastName, $this->origin // oder $result->origin, je nach gewünschter Semantik ); } public function isComplete(): bool { - return $this->matched && $this->firstName !== '' && $this->lastName !== ''; + return $this->matched && '' !== $this->firstName && '' !== $this->lastName; } public function isEmpty(): bool { - return $this->firstName === '' && $this->lastName === ''; + return '' === $this->firstName && '' === $this->lastName; } -} \ No newline at end of file +} diff --git a/src/Parser/Name/Rule/RowValue.php b/src/Parser/Name/Rule/RowValue.php index bdb4463..d0e5797 100644 --- a/src/Parser/Name/Rule/RowValue.php +++ b/src/Parser/Name/Rule/RowValue.php @@ -9,11 +9,11 @@ class RowValue extends Value public function __construct( string|int $key, - mixed $value + mixed $value, ) { parent::__construct($value); $this->originalKey = (string) $key; $this->normalizedKey = strtolower((string) preg_replace('/[^a-z0-9]/i', '', $this->originalKey)); } -} \ No newline at end of file +} diff --git a/src/Parser/Name/Rule/RuleInterface.php b/src/Parser/Name/Rule/RuleInterface.php index d1d37c1..05c498d 100644 --- a/src/Parser/Name/Rule/RuleInterface.php +++ b/src/Parser/Name/Rule/RuleInterface.php @@ -2,10 +2,7 @@ namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule; -use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result; -use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Value; - interface RuleInterface { public function apply(Value $value): Result; -} \ No newline at end of file +} diff --git a/src/Parser/Name/Rule/Value.php b/src/Parser/Name/Rule/Value.php index 5dbf07a..80d6090 100644 --- a/src/Parser/Name/Rule/Value.php +++ b/src/Parser/Name/Rule/Value.php @@ -4,9 +4,8 @@ class Value { - public function __construct(public mixed $originalValue) - { + public function __construct( + public mixed $originalValue, + ) { } - - -} \ No newline at end of file +} diff --git a/src/Parser/Name/StringParser.php b/src/Parser/Name/StringParser.php index 008333a..87ffd5b 100644 --- a/src/Parser/Name/StringParser.php +++ b/src/Parser/Name/StringParser.php @@ -24,6 +24,7 @@ public function parse(string $name): ?AbstractName if (str_contains($name, ',')) { [$lastName, $firstName] = array_map(trim(...), explode(',', $name, 2)); + return $this->buildNameFromResult(new Result(true, $firstName, $lastName)); } @@ -32,11 +33,11 @@ public function parse(string $name): ?AbstractName return null; } - if ($count === 1) { + if (1 === $count) { return $this->buildNameFromResult(new Result(true, '', $parts[0])); } - if ($count === 2) { + if (2 === $count) { return $this->buildNameFromResult(new Result(true, $parts[0], $parts[1])); } @@ -71,7 +72,7 @@ public function parse(string $name): ?AbstractName $lastNameStartIndex > 0 && in_array(mb_strtolower($parts[$lastNameStartIndex - 1]), $surnamePrefixes, true) ) { - $lastNameStartIndex--; + --$lastNameStartIndex; } if ($lastNameStartIndex < $count - 1) { @@ -86,6 +87,7 @@ public function parse(string $name): ?AbstractName // at least we have only wild guessing :) $splitIndex = (int) ceil($count / 2); + return $this->buildNameFromResult( new Result( true, @@ -94,4 +96,4 @@ public function parse(string $name): ?AbstractName ) ); } -} \ No newline at end of file +} diff --git a/src/SalutationCreator.php b/src/SalutationCreator.php index c539195..13c22d8 100644 --- a/src/SalutationCreator.php +++ b/src/SalutationCreator.php @@ -20,9 +20,8 @@ public function generate(SalutationContext $context): string if (Position::PREFIX === $title->getPosition()) { $prefixTitleList[$title->getPriority()][] = $title; continue; - } else { - $suffixTitleList[$title->getPriority()][] = $title; } + $suffixTitleList[$title->getPriority()][] = $title; } $prefixTitles = $this->buildString($prefixTitleList); @@ -52,9 +51,7 @@ private function buildString(array $array): string { $string = ''; array_walk_recursive($array, function ($value) use (&$string) { - /** - * @var SalutationPartInterface $value - */ + /** @var SalutationPartInterface $value */ $string .= $value->build()->trans($this->translator); }); From 5976c9a71fe26472017e7d61c8bee7cf5cec02f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Fri, 19 Jun 2026 11:42:45 +0200 Subject: [PATCH 5/6] fix review reports --- src/Context/Name/GermanTypeName.php | 6 +++--- src/Parser/Name/AbstractParser.php | 3 +++ src/Parser/Name/ArrayParser.php | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Context/Name/GermanTypeName.php b/src/Context/Name/GermanTypeName.php index d65e63d..e65dadf 100644 --- a/src/Context/Name/GermanTypeName.php +++ b/src/Context/Name/GermanTypeName.php @@ -5,14 +5,14 @@ class GermanTypeName extends AbstractName { public function __construct( - public string $firstName, - public string $lastName, + public string $firstName = '', + public string $lastName = '', ) { } public function getFullName(): string { - return $this->firstName.' '.$this->lastName; + return trim($this->firstName.' '.$this->lastName); } public function getFormalName(): string diff --git a/src/Parser/Name/AbstractParser.php b/src/Parser/Name/AbstractParser.php index 338160d..21851e4 100644 --- a/src/Parser/Name/AbstractParser.php +++ b/src/Parser/Name/AbstractParser.php @@ -61,6 +61,9 @@ protected function buildNameFromResult(Result $result): ?AbstractName return null; } + /** + * @todo We need to abstract the naming even more + */ /** @var GermanTypeName $name */ $name = new ($this->format)(); $name->firstName = $result->firstName; diff --git a/src/Parser/Name/ArrayParser.php b/src/Parser/Name/ArrayParser.php index 474ff5d..c361f7d 100644 --- a/src/Parser/Name/ArrayParser.php +++ b/src/Parser/Name/ArrayParser.php @@ -24,7 +24,7 @@ public function parse(array $data): ?AbstractName if (!is_a($rule, RuleInterface::class, true)) { continue; } - $result->with((new $rule())->apply($entry)); + $result = $result->with((new $rule())->apply($entry)); if ($result->isComplete()) { break 2; } From 22bd7ab135a486c91098066b03dee9474a7cb733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20K=C3=B6rner?= Date: Fri, 19 Jun 2026 11:54:25 +0200 Subject: [PATCH 6/6] fix review issue, update readme --- README.md | 62 ++++++++++++++++++++++++++++++--- src/Parser/Name/Rule/Result.php | 2 +- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a4a9290..d07d27e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ composer require heimrichhannot/salutation-creator ## Requirements -- PHP ^8.1 +- PHP ^8.2 - symfony/translation-contracts ^1 || ^2 || ^3 ## Features @@ -22,6 +22,7 @@ composer require heimrichhannot/salutation-creator - ✅ Support for academic titles (Dr., Prof., etc.) - ✅ Gender-sensitive salutations - ✅ Prefix and suffix titles with priorities +- ✅ Name parsing from strings and arrays - ✅ Multilingual translations via Symfony Translation - ✅ Extensible with custom name, gender, and title classes @@ -58,9 +59,8 @@ $context = (new SalutationContext()) ->setGender(Gender::FEMALE) ->addTitle(new GermanDoctorTitle()) ->addTitle(new GermanProfessorTitle()) - ->addTitle(new PrefixTitle('Hero') + ->addTitle(new PrefixTitle('Hero')) ->addTitles(Titles::fromString('dr phd unknown')); // Adding titles from string - ; echo $salutationCreator->generate($context); @@ -84,6 +84,7 @@ Gender from string: use HeimrichHannot\SalutationCreator\SalutationCreator; use HeimrichHannot\SalutationCreator\SalutationContext; use HeimrichHannot\SalutationCreator\Context\Gender\Gender; +use HeimrichHannot\SalutationCreator\Context\Name\GermanTypeName; $data = [ 'firstname' => 'Lisa', @@ -93,12 +94,58 @@ $data = [ $salutationContext = (new SalutationContext()) ->setName(new GermanTypeName($data['firstname'], $data['lastname'])) - ->addTitles(Gender::tryFromString($data['gender']))); + ->setGender(Gender::tryFromString($data['gender'])); echo (new SalutationCreator($this->translator))->generate($salutationContext); // Outputs "Sehr geehrte Frau Müller" ``` +## Name Parsing + +Use the bundled name parsers to create `AbstractName` instances from strings or arrays before passing them to a `SalutationContext`. + +### Parse a name string + +```php +use HeimrichHannot\SalutationCreator\Context\Name\GermanTypeName; +use HeimrichHannot\SalutationCreator\Parser\Name\StringParser; + +$name = StringParser::create(GermanTypeName::class)->parse('Max Mustermann'); + +echo $name?->getFullName(); +// Output: "Max Mustermann" +``` + +The string parser supports common formats such as `Max Mustermann` and `Mustermann, Max`. For single-part names, parsing returns `null` by default because the result is incomplete. + +If incomplete names are acceptable for your use case, enable them explicitly: + +```php +$name = StringParser::create(GermanTypeName::class) + ->allowIncomplete(true) + ->parse('Mustermann'); + +echo $name?->getFormalName(); +// Output: "Mustermann" +``` + +### Parse name data from arrays + +```php +use HeimrichHannot\SalutationCreator\Context\Name\GermanTypeName; +use HeimrichHannot\SalutationCreator\Parser\Name\ArrayParser; + +$name = ArrayParser::create(GermanTypeName::class)->parse([ + 'firstname' => 'Max', + 'lastname' => 'Mustermann', +]); + +echo $name?->getFullName(); +// Output: "Max Mustermann" +``` + +For `GermanTypeName`, the array parser supports English keys (`firstname`, `lastname`, `name`) and German keys (`vorname`, `nachname`, `zuname`, `name`). Non-string values are ignored. + ## Translations / Translation files Since this library in framework-agnostic, you need to register the translation files manually or copy them to your project's translation directory. @@ -196,6 +243,11 @@ The following parameters are available in translations: - `GermanTypeName`: German names with first and last name +### Name Parser Classes + +- `StringParser`: Parses full names from plain strings +- `ArrayParser`: Parses first and last names from array data + ### Gender - `Gender::MALE` - Male @@ -310,4 +362,4 @@ class IcelandTypeName extends AbstractName return $this->firstName; } } -``` \ No newline at end of file +``` diff --git a/src/Parser/Name/Rule/Result.php b/src/Parser/Name/Rule/Result.php index bcbd550..0d70add 100644 --- a/src/Parser/Name/Rule/Result.php +++ b/src/Parser/Name/Rule/Result.php @@ -22,7 +22,7 @@ public function with(Result $result): Result true, '' !== $result->firstName ? $result->firstName : $this->firstName, '' !== $result->lastName ? $result->lastName : $this->lastName, - $this->origin // oder $result->origin, je nach gewünschter Semantik + $result->origin ); }