Skip to content

Commit 62e564f

Browse files
authored
Member names validation (#69)
1 parent 825b828 commit 62e564f

11 files changed

+166
-99
lines changed

.travis.yml

-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,3 @@ script:
1111
- vendor/bin/php-cs-fixer fix -v --dry-run
1212
- phpunit --coverage-clover build/logs/clover.xml
1313
- vendor/bin/doc2test && vendor/bin/phpunit -c doc-test/phpunit.xml
14-
15-
matrix:
16-
exclude:
17-
- php: '7.0'
18-
script: vendor/bin/doc2test && vendor/bin/phpunit -c doc-test/phpunit.xml

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
}
3030
},
3131
"scripts": {
32-
"test": "php-cs-fixer fix -v --dry-run --ansi && phpunit --colors=always --coverage-text"
32+
"test": "php-cs-fixer fix -v --dry-run --ansi && phpunit --colors=always --coverage-text && vendor/bin/doc2test && vendor/bin/phpunit -c doc-test/phpunit.xml --coverage-text"
3333
}
3434
}

src/Document/Container.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace JsonApiPhp\JsonApi\Document;
5+
6+
class Container implements \JsonSerializable, \IteratorAggregate
7+
{
8+
private $data;
9+
10+
public function set(MemberName $name, $value)
11+
{
12+
if (! $this->data) {
13+
$this->data = (object) [];
14+
}
15+
$this->data->$name = $value;
16+
}
17+
18+
public function getIterator(): \Traversable
19+
{
20+
return $this->data;
21+
}
22+
23+
public function jsonSerialize()
24+
{
25+
return $this->data;
26+
}
27+
}

src/Document/LinksTrait.php

+12-3
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,26 @@
99
trait LinksTrait
1010
{
1111
/**
12-
* @var LinkInterface[]
12+
* @var Container|null
1313
*/
1414
protected $links;
1515

1616
public function setLink(string $name, string $url)
1717
{
18-
$this->links[$name] = new Link($url);
18+
$this->init();
19+
$this->links->set(new MemberName($name), new Link($url));
1920
}
2021

2122
public function setLinkObject(string $name, LinkInterface $link)
2223
{
23-
$this->links[$name] = $link;
24+
$this->init();
25+
$this->links->set(new MemberName($name), $link);
26+
}
27+
28+
private function init()
29+
{
30+
if (! $this->links) {
31+
$this->links = new Container();
32+
}
2433
}
2534
}

src/Document/MemberName.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace JsonApiPhp\JsonApi\Document;
5+
6+
class MemberName extends SpecialValue
7+
{
8+
public function __construct(string $type)
9+
{
10+
parent::__construct($type, "Invalid member name '%'");
11+
}
12+
}

src/Document/Meta.php

+33-10
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
class Meta implements \JsonSerializable
77
{
8+
/**
9+
* @var Container
10+
*/
811
private $data;
912

1013
public function __construct(\stdClass $data)
1114
{
12-
$this->validateObject($data);
13-
14-
$this->data = $data;
15+
$this->data = $this->toContainer($data);
1516
}
1617

1718
public static function fromArray(array $array): self
@@ -24,21 +25,43 @@ public function jsonSerialize()
2425
return $this->data;
2526
}
2627

27-
private function validateObject($object)
28+
private function toContainer($object): Container
2829
{
30+
$c = new Container();
2931
foreach ($object as $name => $value) {
30-
if (is_string($name) && !$this->isValidMemberName($name)) {
31-
throw new \OutOfBoundsException("Not a valid attribute name '$name'");
32+
if (is_object($object)) {
33+
$name = (string) $name;
34+
}
35+
if ($this->canConvert($value)) {
36+
$value = $this->toContainer($value);
37+
} else {
38+
$value = $this->traverse($value);
3239
}
40+
$c->set(new MemberName($name), $value);
41+
}
42+
return $c;
43+
}
3344

34-
if (is_array($value) || $value instanceof \stdClass) {
35-
$this->validateObject($value);
45+
private function traverse($value)
46+
{
47+
if ($this->canConvert($value)) {
48+
return $this->toContainer($value);
49+
}
50+
if (is_array($value)) {
51+
foreach ($value as $k => $v) {
52+
$value[$k] = $this->traverse($v);
3653
}
3754
}
55+
return $value;
3856
}
3957

40-
private function isValidMemberName(string $name): bool
58+
private function canConvert($v): bool
4159
{
42-
return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1;
60+
return is_object($v)
61+
|| (
62+
is_array($v)
63+
&& $v !== []
64+
&& array_keys($v) !== range(0, count($v) - 1)
65+
);
4366
}
4467
}

src/Document/ReservedName.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace JsonApiPhp\JsonApi\Document;
5+
6+
class ReservedName extends MemberName
7+
{
8+
public function __construct(string $name)
9+
{
10+
parent::__construct($name);
11+
if ($this->isReservedName($name)) {
12+
throw new \InvalidArgumentException("Can not use a reserved name '$name'");
13+
}
14+
}
15+
16+
private function isReservedName(string $name): bool
17+
{
18+
return in_array($name, ['id', 'type']);
19+
}
20+
}

src/Document/Resource/ResourceObject.php

+5-24
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use JsonApiPhp\JsonApi\Document\LinksTrait;
77
use JsonApiPhp\JsonApi\Document\Meta;
8+
use JsonApiPhp\JsonApi\Document\ReservedName;
89

910
class ResourceObject implements \JsonSerializable
1011
{
@@ -22,7 +23,7 @@ class ResourceObject implements \JsonSerializable
2223

2324
public function __construct(string $type, string $id = null)
2425
{
25-
$this->type = $type;
26+
$this->type = new ResourceType($type);
2627
$this->id = $id;
2728
}
2829

@@ -33,12 +34,7 @@ public function setMeta(Meta $meta)
3334

3435
public function setAttribute(string $name, $value)
3536
{
36-
if ($this->isReservedName($name)) {
37-
throw new \InvalidArgumentException("Can not use a reserved name '$name'");
38-
}
39-
if (!$this->isValidMemberName($name)) {
40-
throw new \OutOfBoundsException("Not a valid attribute name '$name'");
41-
}
37+
$name = (string) new ReservedName($name);
4238
if (isset($this->relationships[$name])) {
4339
throw new \LogicException("Field '$name' already exists in relationships");
4440
}
@@ -47,12 +43,7 @@ public function setAttribute(string $name, $value)
4743

4844
public function setRelationship(string $name, Relationship $relationship)
4945
{
50-
if ($this->isReservedName($name)) {
51-
throw new \InvalidArgumentException("Can not use a reserved name '$name'");
52-
}
53-
if (!$this->isValidMemberName($name)) {
54-
throw new \OutOfBoundsException("Not a valid attribute name '$name'");
55-
}
46+
$name = (string) new ReservedName($name);
5647
if (isset($this->attributes[$name])) {
5748
throw new \LogicException("Field '$name' already exists in attributes");
5849
}
@@ -61,7 +52,7 @@ public function setRelationship(string $name, Relationship $relationship)
6152

6253
public function toIdentifier(): ResourceIdentifier
6354
{
64-
return new ResourceIdentifier($this->type, $this->id);
55+
return new ResourceIdentifier((string) $this->type, $this->id);
6556
}
6657

6758
public function jsonSerialize()
@@ -92,14 +83,4 @@ public function identifies(ResourceObject $resource): bool
9283
}
9384
return false;
9485
}
95-
96-
private function isReservedName(string $name): bool
97-
{
98-
return in_array($name, ['id', 'type']);
99-
}
100-
101-
private function isValidMemberName(string $name): bool
102-
{
103-
return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1;
104-
}
10586
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace JsonApiPhp\JsonApi\Document\Resource;
5+
6+
use JsonApiPhp\JsonApi\Document\SpecialValue;
7+
8+
class ResourceType extends SpecialValue
9+
{
10+
public function __construct(string $type)
11+
{
12+
parent::__construct($type, "Invalid resource type '%'");
13+
}
14+
}

src/Document/SpecialValue.php

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace JsonApiPhp\JsonApi\Document;
5+
6+
abstract class SpecialValue implements \JsonSerializable
7+
{
8+
private $val;
9+
10+
public function __construct(string $val, string $errorMessage = "Invalid value '%s'")
11+
{
12+
if (!$this->isValidMemberNameOrTypeValue($val)) {
13+
throw new \OutOfBoundsException(sprintf($errorMessage, $val));
14+
}
15+
$this->val = $val;
16+
}
17+
18+
public function jsonSerialize()
19+
{
20+
return $this->val;
21+
}
22+
23+
public function __toString()
24+
{
25+
return $this->val;
26+
}
27+
28+
protected function isValidMemberNameOrTypeValue(string $name): bool
29+
{
30+
return preg_match('/^(?=[^-_ ])[a-zA-Z0-9\x{0080}-\x{FFFF}-_ ]*(?<=[^-_ ])$/u', $name) === 1;
31+
}
32+
}

0 commit comments

Comments
 (0)