Skip to content

Commit 6584c59

Browse files
committed
Add StringUrl type with tryFromString, alias, and unit tests
- Implemented `StringUrl` class to represent validated absolute URLs, using `FILTER_VALIDATE_URL` for pragmatic validation. - Added `fromString` and `tryFromString` methods, returning `Undefined` for invalid URLs. - Introduced `UrlStringTypeException` to handle invalid URL inputs. - Created alias `Url` as a readonly extension of `StringUrl`. - Updated `Usage` examples to demonstrate `StringUrl` and `tryFromString` behavior. - Included unit tests to validate creation, exception handling, alias behavior, and the `Undefined` return for invalid cases.
1 parent 85e1d6d commit 6584c59

File tree

6 files changed

+135
-1
lines changed

6 files changed

+135
-1
lines changed
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 UrlStringTypeException extends TypeException
8+
{
9+
}

src/String/Alias/Url.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpTypedValues\String\Alias;
6+
7+
use PhpTypedValues\String\StringUrl;
8+
9+
/**
10+
* @psalm-immutable
11+
*/
12+
readonly class Url extends StringUrl
13+
{
14+
}

src/String/StringEmail.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use function sprintf;
1616

1717
/**
18-
* Email address string (pragmatic validation).
18+
* Email address string RFC 5322 (pragmatic validation).
1919
*
2020
* Example "user@example.com"
2121
*

src/String/StringUrl.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_URL;
8+
9+
use PhpTypedValues\Abstract\String\StrType;
10+
use PhpTypedValues\Exception\TypeException;
11+
use PhpTypedValues\Exception\UrlStringTypeException;
12+
use PhpTypedValues\Undefined\Alias\Undefined;
13+
14+
use function filter_var;
15+
use function sprintf;
16+
17+
/**
18+
* Absolute URL string (http/https recommended; uses FILTER_VALIDATE_URL for pragmatic validation).
19+
*
20+
* Example "https://example.com/path?x=1"
21+
*
22+
* @psalm-immutable
23+
*/
24+
readonly class StringUrl extends StrType
25+
{
26+
/** @var non-empty-string */
27+
protected string $value;
28+
29+
/**
30+
* @throws UrlStringTypeException
31+
*/
32+
public function __construct(string $value)
33+
{
34+
if (filter_var($value, FILTER_VALIDATE_URL) === false) {
35+
throw new UrlStringTypeException(sprintf('Expected valid URL, got "%s"', $value));
36+
}
37+
38+
/** @var non-empty-string $value */
39+
$this->value = $value;
40+
}
41+
42+
/**
43+
* @throws UrlStringTypeException
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
use PhpTypedValues\String\Alias\NonEmptyStr;
77
use PhpTypedValues\String\Alias\Str;
88
use PhpTypedValues\String\Alias\StrType;
9+
use PhpTypedValues\String\Alias\Url;
910
use PhpTypedValues\String\Json;
1011
use PhpTypedValues\String\MariaDb\StringVarChar255;
1112
use PhpTypedValues\String\StringEmail;
1213
use PhpTypedValues\String\StringNonBlank;
1314
use PhpTypedValues\String\StringNonEmpty;
1415
use PhpTypedValues\String\StringStandard;
16+
use PhpTypedValues\String\StringUrl;
1517
use PhpTypedValues\String\StringUuidV4;
1618
use PhpTypedValues\String\StringUuidV7;
1719
use PhpTypedValues\Undefined\Alias\Undefined;
@@ -52,6 +54,14 @@
5254
echo $em->toString() . \PHP_EOL;
5355
}
5456

57+
// URL (usage and try* for Psalm visibility)
58+
echo Url::fromString('https://example.com/path?x=1')->toString() . \PHP_EOL;
59+
echo StringUrl::fromString('https://example.com/path?x=1')->toString() . \PHP_EOL;
60+
$url = StringUrl::tryFromString('notaurl');
61+
if (!($url instanceof Undefined)) {
62+
echo $url->toString() . \PHP_EOL;
63+
}
64+
5565
/**
5666
* Artificial functions.
5767
*/
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PhpTypedValues\Exception\UrlStringTypeException;
6+
use PhpTypedValues\String\StringUrl;
7+
use PhpTypedValues\Undefined\Alias\Undefined;
8+
9+
it('accepts valid URL, preserves value/toString and casts via __toString', function (): void {
10+
$u = new StringUrl('https://example.com/path?x=1#anchor');
11+
12+
expect($u->value())
13+
->toBe('https://example.com/path?x=1#anchor')
14+
->and($u->toString())
15+
->toBe('https://example.com/path?x=1#anchor')
16+
->and((string) $u)
17+
->toBe('https://example.com/path?x=1#anchor');
18+
});
19+
20+
it('throws UrlStringTypeException on empty or invalid URLs', function (): void {
21+
expect(fn() => new StringUrl(''))
22+
->toThrow(UrlStringTypeException::class, 'Expected valid URL, got ""')
23+
->and(fn() => StringUrl::fromString('not-a-url'))
24+
->toThrow(UrlStringTypeException::class, 'Expected valid URL, got "not-a-url"');
25+
});
26+
27+
it('tryFromString returns instance for valid and Undefined for invalid', function (): void {
28+
$ok = StringUrl::tryFromString('https://www.example.org');
29+
$bad = StringUrl::tryFromString('notaurl');
30+
31+
expect($ok)
32+
->toBeInstanceOf(StringUrl::class)
33+
->and($ok->value())
34+
->toBe('https://www.example.org')
35+
->and($bad)
36+
->toBeInstanceOf(Undefined::class);
37+
});

0 commit comments

Comments
 (0)