Skip to content

Commit 349540d

Browse files
committed
Add StringEmail type with tryFromString and comprehensive tests
- Implemented `StringEmail` class for representing validated email addresses. - Added `fromString` and `tryFromString` methods to validate and parse emails, returning `Undefined` for invalid cases. - Introduced `EmailStringTypeException` to handle invalid email inputs. - Updated `Usage` examples to demonstrate `StringEmail` usage and `tryFromString` behavior. - Included robust unit tests to validate creation, exception handling, and `Undefined` behavior for invalid inputs.
1 parent 320e217 commit 349540d

File tree

5 files changed

+121
-0
lines changed

5 files changed

+121
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
22
/vendor/
3+
/cache/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\Exception;
6+
7+
class EmailStringTypeException extends TypeException
8+
{
9+
}

src/String/StringEmail.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\String;
6+
7+
use const FILTER_VALIDATE_EMAIL;
8+
9+
use PhpTypedValues\Abstract\String\StrType;
10+
use PhpTypedValues\Exception\EmailStringTypeException;
11+
use PhpTypedValues\Exception\TypeException;
12+
use PhpTypedValues\Undefined\Alias\Undefined;
13+
14+
use function filter_var;
15+
use function sprintf;
16+
17+
/**
18+
* Email address string (pragmatic validation).
19+
*
20+
* Example "user@example.com"
21+
*
22+
* @psalm-immutable
23+
*/
24+
readonly class StringEmail extends StrType
25+
{
26+
/** @var non-empty-string */
27+
protected string $value;
28+
29+
/**
30+
* @throws EmailStringTypeException
31+
*/
32+
public function __construct(string $value)
33+
{
34+
if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
35+
throw new EmailStringTypeException(sprintf('Expected valid email address, got "%s"', $value));
36+
}
37+
38+
/** @var non-empty-string $value */
39+
$this->value = $value;
40+
}
41+
42+
/**
43+
* @throws EmailStringTypeException
44+
*/
45+
public static function fromString(string $value): static
46+
{
47+
return new static($value);
48+
}
49+
50+
public static function tryFromString(string $value): static|Undefined
51+
{
52+
try {
53+
return static::fromString($value);
54+
} catch (TypeException) {
55+
return Undefined::create();
56+
}
57+
}
58+
59+
/** @return non-empty-string */
60+
public function value(): string
61+
{
62+
return $this->value;
63+
}
64+
}

src/Usage/String.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpTypedValues\String\Alias\StrType;
88
use PhpTypedValues\String\Json;
99
use PhpTypedValues\String\MariaDb\StringVarChar255;
10+
use PhpTypedValues\String\StringEmail;
1011
use PhpTypedValues\String\StringNonBlank;
1112
use PhpTypedValues\String\StringNonEmpty;
1213
use PhpTypedValues\String\StringStandard;
@@ -42,6 +43,13 @@
4243
echo json_encode(Json::fromString('{"a": 1}')->toObject(), \JSON_THROW_ON_ERROR) . \PHP_EOL;
4344
echo Json::tryFromString('{}')->toString() . \PHP_EOL;
4445

46+
// Email (usage and try* for Psalm visibility)
47+
echo StringEmail::fromString('User@Example.COM')->toString() . \PHP_EOL; // normalized to lowercase
48+
$em = StringEmail::tryFromString('not-an-email');
49+
if (!($em instanceof Undefined)) {
50+
echo $em->toString() . \PHP_EOL;
51+
}
52+
4553
/**
4654
* Artificial functions.
4755
*/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpTypedValues\Exception\EmailStringTypeException;
6+
use PhpTypedValues\String\StringEmail;
7+
use PhpTypedValues\Undefined\Alias\Undefined;
8+
9+
it('accepts valid email, preserves value/toString', function (): void {
10+
$e = new StringEmail('User.Name+tag@Example.COM');
11+
12+
expect($e->value())
13+
->toBe('User.Name+tag@Example.COM')
14+
->and($e->toString())
15+
->toBe('User.Name+tag@Example.COM')
16+
->and((string) $e)
17+
->toBe('User.Name+tag@Example.COM');
18+
});
19+
20+
it('throws EmailStringTypeException on empty or invalid emails', function (): void {
21+
expect(fn() => new StringEmail(''))
22+
->toThrow(EmailStringTypeException::class, 'Expected valid email address, got ""')
23+
->and(fn() => StringEmail::fromString(' User.Name+tag@Example.COM '))
24+
->toThrow(EmailStringTypeException::class, 'Expected valid email address, got " User.Name+tag@Example.COM "')
25+
->and(fn() => StringEmail::fromString('not-an-email'))
26+
->toThrow(EmailStringTypeException::class, 'Expected valid email address, got "not-an-email"');
27+
});
28+
29+
it('tryFromString returns instance for valid and Undefined for invalid', function (): void {
30+
$ok = StringEmail::tryFromString('admin@Example.org');
31+
$bad = StringEmail::tryFromString('invalid');
32+
33+
expect($ok)
34+
->toBeInstanceOf(StringEmail::class)
35+
->and($ok->value())
36+
->toBe('admin@Example.org')
37+
->and($bad)
38+
->toBeInstanceOf(Undefined::class);
39+
});

0 commit comments

Comments
 (0)