Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ composer require heimrichhannot/salutation-creator

## Requirements

- PHP ^8.1
- PHP ^8.2
- symfony/translation-contracts ^1 || ^2 || ^3

## Features
Expand All @@ -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

Expand Down Expand Up @@ -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);
Expand All @@ -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',
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -310,4 +362,4 @@ class IcelandTypeName extends AbstractName
return $this->firstName;
}
}
```
```
2 changes: 1 addition & 1 deletion src/Context/Gender/Gender.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
Expand Down
6 changes: 3 additions & 3 deletions src/Context/Name/GermanTypeName.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
class GermanTypeName extends AbstractName
{
public function __construct(
private readonly string $firstName,
private readonly 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
Expand Down
79 changes: 79 additions & 0 deletions src/Parser/Name/AbstractParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace HeimrichHannot\SalutationCreator\Parser\Name;

use HeimrichHannot\SalutationCreator\Context\Name\AbstractName;
use HeimrichHannot\SalutationCreator\Context\Name\GermanTypeName;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RuleInterface;

abstract class AbstractParser
{
private bool $allowIncomplete = false;

/**
* @param class-string<RuleInterface>[] $rules
* @param class-string<AbstractName> $format
*/
final public function __construct(
protected array $rules,
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));
}
}

/**
* @param class-string<AbstractName> $format
*/
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;
}

/**
* @todo We need to abstract the naming even more
*/
/** @var GermanTypeName $name */
$name = new ($this->format)();
$name->firstName = $result->firstName;
$name->lastName = $result->lastName;

return $name;
}

/**
* @return class-string<RuleInterface>[]
*/
abstract public static function defaultRules(string $format): array;
}
54 changes: 54 additions & 0 deletions src/Parser/Name/ArrayParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace HeimrichHannot\SalutationCreator\Parser\Name;

use HeimrichHannot\SalutationCreator\Context\Name\AbstractName;
use HeimrichHannot\SalutationCreator\Context\Name\GermanTypeName;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Array\EnglishKeyNameRule;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Array\GermanKeyNameRule;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\Result;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RowValue;
use HeimrichHannot\SalutationCreator\Parser\Name\Rule\RuleInterface;

class ArrayParser extends AbstractParser
{
/**
* @param array<mixed, mixed> $data
*/
public function parse(array $data): ?AbstractName
{
$result = new Result(false);
foreach ($data as $key => $value) {
$entry = new RowValue($key, $value);
foreach ($this->rules as $rule) {
if (!is_a($rule, RuleInterface::class, true)) {
continue;
}
$result = $result->with((new $rule())->apply($entry));
if ($result->isComplete()) {
break 2;
}
}
}

return $this->buildNameFromResult($result);
}

/**
* @param class-string<AbstractName> $format
*
* @return class-string<RuleInterface>[]
*/
public static function defaultRules(string $format): array
{
return match ($format) {
GermanTypeName::class => [
GermanKeyNameRule::class,
EnglishKeyNameRule::class,
],
default => [
EnglishKeyNameRule::class,
],
};
}
}
31 changes: 31 additions & 0 deletions src/Parser/Name/Rule/Array/EnglishKeyNameRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule\Array;

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 EnglishKeyNameRule implements RuleInterface
{
public function apply(Value $value): Result
{
if (!$value instanceof RowValue) {
return new Result(false);
}

if (!is_string($value->originalValue)) {
return new Result(false);
}

if ('firstname' === $value->normalizedKey) {
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);
}
}
31 changes: 31 additions & 0 deletions src/Parser/Name/Rule/Array/GermanKeyNameRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule\Array;

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 implements RuleInterface
{
public function apply(Value $value): Result
{
if (!$value instanceof RowValue) {
return new Result(false);
}

if (!is_string($value->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);
}
}
38 changes: 38 additions & 0 deletions src/Parser/Name/Rule/Result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule;

class Result
{
public function __construct(
public readonly bool $matched,
public readonly string $firstName = '',
public readonly string $lastName = '',
public readonly mixed $origin = null,
) {
}

public function with(Result $result): Result
{
if (!$result->matched) {
return $this;
}

return new self(
true,
'' !== $result->firstName ? $result->firstName : $this->firstName,
'' !== $result->lastName ? $result->lastName : $this->lastName,
$result->origin
);
}

public function isComplete(): bool
{
return $this->matched && '' !== $this->firstName && '' !== $this->lastName;
}

public function isEmpty(): bool
{
return '' === $this->firstName && '' === $this->lastName;
}
}
19 changes: 19 additions & 0 deletions src/Parser/Name/Rule/RowValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace HeimrichHannot\SalutationCreator\Parser\Name\Rule;

class RowValue extends Value
{
public string $originalKey;
public string $normalizedKey;

public function __construct(
string|int $key,
mixed $value,
) {
parent::__construct($value);

$this->originalKey = (string) $key;
$this->normalizedKey = strtolower((string) preg_replace('/[^a-z0-9]/i', '', $this->originalKey));
}
}
Loading
Loading