Skip to content

Commit 1c50041

Browse files
author
Nil Portugués
committed
Strict check on class mappings
1 parent 0cdb9e1 commit 1c50041

File tree

8 files changed

+242
-9
lines changed

8 files changed

+242
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace NilPortugues\Api\Mapping\ClassMapping;
4+
5+
interface ApiMappingInterface
6+
{
7+
/**
8+
* Returns a string with the full class name, including namespace.
9+
*
10+
* @return string
11+
*/
12+
public function getClass();
13+
14+
/**
15+
* Returns a string representing the resource name as it will be shown after the mapping.
16+
*
17+
* @return string
18+
*/
19+
public function getAlias();
20+
21+
/**
22+
* Returns an array of properties that will be renamed.
23+
* Key is current property from the class. Value is the property's alias name.
24+
*
25+
* @return array
26+
*/
27+
public function getAliasedProperties();
28+
29+
/**
30+
* List of properties in the class that will be ignored by the mapping.
31+
*
32+
* @return array
33+
*/
34+
public function getHideProperties();
35+
36+
/**
37+
* Returns an array of properties that are used as an ID value.
38+
*
39+
* @return array
40+
*/
41+
public function getIdProperties();
42+
43+
/**
44+
* Returns a list of URLs. This urls must have placeholders to be replaced with the getIdProperties() values.
45+
*
46+
* @return array
47+
*/
48+
public function getUrls();
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace NilPortugues\Api\Mapping\ClassMapping;
4+
5+
interface HalJsonMapping extends ApiMappingInterface
6+
{
7+
/**
8+
* Returns an array of curies.
9+
*
10+
* @return array
11+
*/
12+
public function getCuries();
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace NilPortugues\Api\Mapping\ClassMapping;
4+
5+
interface JsonApiMapping
6+
{
7+
/**
8+
* Returns an array containing the relationship mappings as an array.
9+
* Key for each relationship defined must match a property of the mapped class.
10+
*
11+
* @return array
12+
*/
13+
public function getRelationships();
14+
}

src/Mapping/Mapper.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public function __construct(array $mappings = null)
4141
);
4242
}
4343

44-
$this->classMap[ltrim($mapping->getClassName(), "\\")] = $mapping;
45-
$this->aliasMap[ltrim($mapping->getClassAlias(), "\\")] = $mapping->getClassName();
44+
$this->classMap[ltrim($mapping->getClassName(), '\\')] = $mapping;
45+
$this->aliasMap[ltrim($mapping->getClassAlias(), '\\')] = $mapping->getClassName();
4646
}
4747
}
4848
}

src/Mapping/Mapping.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ class Mapping
88
{
99
/**
1010
* @var string
11-
*/
11+
*/
1212
private $className = '';
1313
/**
1414
* @var string
15-
*/
16-
private $resourceUrlPattern = '';
15+
*/
16+
private $resourceUrlPattern = '';
1717
/**
1818
* @var string
1919
*/

src/Mapping/MappingFactory.php

+50-3
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,38 @@ public static function fromArray(array &$mappedClass)
2727
$idProperties = self::getIdProperties($mappedClass);
2828

2929
$mapping = new Mapping($className, $resourceUrl, $idProperties);
30-
3130
$mapping->setClassAlias((empty($mappedClass['alias'])) ? $className : $mappedClass['alias']);
3231

3332
if (false === empty($mappedClass['aliased_properties'])) {
3433
$mapping->setPropertyNameAliases($mappedClass['aliased_properties']);
34+
foreach (array_keys($mapping->getAliasedProperties()) as $propertyName) {
35+
if (false === in_array($propertyName, self::getClassProperties($className), true)) {
36+
throw new MappingException(
37+
sprintf('Could not alias property %s in class %s because it does not exist.', $propertyName, $className)
38+
);
39+
}
40+
}
3541
}
3642

3743
if (false === empty($mappedClass['hide_properties'])) {
3844
$mapping->setHiddenProperties($mappedClass['hide_properties']);
45+
foreach ($mapping->getHiddenProperties() as $propertyName) {
46+
if (false === in_array($propertyName, self::getClassProperties($className), true)) {
47+
throw new MappingException(
48+
sprintf('Could not hide property %s in class %s because it does not exist.', $propertyName, $className)
49+
);
50+
}
51+
}
3952
}
4053

4154
if (!empty($mappedClass['relationships'])) {
4255
foreach ($mappedClass['relationships'] as $propertyName => $urls) {
56+
if (false === in_array($propertyName, self::getClassProperties($className), true)) {
57+
throw new MappingException(
58+
sprintf('Could not find property %s in class %s because it does not exist.', $propertyName, $className)
59+
);
60+
}
61+
4362
$mapping->setRelationshipUrls($propertyName, $urls);
4463
}
4564
}
@@ -99,8 +118,7 @@ private static function getSelfUrl(array &$mappedClass)
99118
*/
100119
private static function getIdProperties(array &$mappedClass)
101120
{
102-
103-
return (!empty($mappedClass['id_properties']))? $mappedClass['id_properties'] : [];
121+
return (!empty($mappedClass['id_properties'])) ? $mappedClass['id_properties'] : [];
104122
}
105123

106124
/**
@@ -116,4 +134,33 @@ private static function getOtherUrls(array $mappedClass)
116134

117135
return $mappedClass['urls'];
118136
}
137+
138+
/**
139+
* Recursive function to get an associative array of class properties by
140+
* property name, including inherited ones from extended classes.
141+
*
142+
* @param string $className Class name
143+
*
144+
* @return array
145+
*
146+
* @link http://php.net/manual/es/reflectionclass.getproperties.php#88405
147+
*/
148+
private static function getClassProperties($className)
149+
{
150+
$ref = new \ReflectionClass($className);
151+
$properties = array();
152+
foreach ($ref->getProperties() as $prop) {
153+
$f = $prop->getName();
154+
$properties[$f] = $prop;
155+
}
156+
157+
if ($parentClass = $ref->getParentClass()) {
158+
$parentPropsArr = self::getClassProperties($parentClass->getName());
159+
if (count($parentPropsArr) > 0) {
160+
$properties = array_merge($parentPropsArr, $properties);
161+
}
162+
}
163+
164+
return array_keys($properties);
165+
}
119166
}

tests/Dummy/ComplexObject/Post.php

+21
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@
1515

1616
class Post
1717
{
18+
/**
19+
* @var PostId
20+
*/
21+
private $postId;
22+
/**
23+
* @var
24+
*/
25+
private $title;
26+
/**
27+
* @var
28+
*/
29+
private $content;
30+
/**
31+
* @var User
32+
*/
33+
private $author;
34+
/**
35+
* @var array
36+
*/
37+
private $comments;
38+
1839
/**
1940
* @param PostId $id
2041
* @param $title

tests/Mapping/MappingFactoryTest.php

+90-1
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,103 @@ public function testItCanBuildMappingsFromArray()
5454
$this->assertEquals('http://example.com/posts/{postId}/relationships/author', $mapping->getRelationshipSelfUrl('author'));
5555
}
5656

57+
public function testItCanBuildMappingsFromArrayWillThrowExceptionIfAliasPropertyDoesNotExist()
58+
{
59+
$mappedClass = [
60+
'class' => Post::class,
61+
'alias' => 'Message',
62+
'aliased_properties' => [
63+
'I_do_not_exist' => 'headline',
64+
'content' => 'body',
65+
],
66+
'hide_properties' => [
67+
'comments',
68+
],
69+
'id_properties' => [
70+
'postId',
71+
],
72+
'urls' => [
73+
'self' => 'http://example.com/posts/{postId}',
74+
],
75+
'relationships' => [
76+
'author' => [
77+
'related' => 'http://example.com/posts/{postId}/author',
78+
'self' => 'http://example.com/posts/{postId}/relationships/author',
79+
],
80+
],
81+
];
82+
83+
$this->setExpectedException(MappingException::class);
84+
MappingFactory::fromArray($mappedClass);
85+
}
86+
87+
public function testItCanBuildMappingsFromArrayWillThrowExceptionIfHidePropertyDoesNotExist()
88+
{
89+
$mappedClass = [
90+
'class' => Post::class,
91+
'alias' => 'Message',
92+
'aliased_properties' => [
93+
'title' => 'headline',
94+
'content' => 'body',
95+
],
96+
'hide_properties' => [
97+
'I_do_not_exist',
98+
],
99+
'id_properties' => [
100+
'postId',
101+
],
102+
'urls' => [
103+
'self' => 'http://example.com/posts/{postId}',
104+
],
105+
'relationships' => [
106+
'author' => [
107+
'related' => 'http://example.com/posts/{postId}/author',
108+
'self' => 'http://example.com/posts/{postId}/relationships/author',
109+
],
110+
],
111+
];
112+
113+
$this->setExpectedException(MappingException::class);
114+
MappingFactory::fromArray($mappedClass);
115+
}
116+
117+
public function testItCanBuildMappingsFromArrayWillThrowExceptionIfRelationshipPropertyDoesNotExist()
118+
{
119+
$mappedClass = [
120+
'class' => Post::class,
121+
'alias' => 'Message',
122+
'aliased_properties' => [
123+
'title' => 'headline',
124+
'content' => 'body',
125+
],
126+
'hide_properties' => [
127+
'comments',
128+
],
129+
'id_properties' => [
130+
'postId',
131+
],
132+
'urls' => [
133+
'self' => 'http://example.com/posts/{postId}',
134+
],
135+
'relationships' => [
136+
'I_do_not_exist' => [
137+
'related' => 'http://example.com/posts/{postId}/author',
138+
'self' => 'http://example.com/posts/{postId}/relationships/author',
139+
],
140+
],
141+
];
142+
143+
$this->setExpectedException(MappingException::class);
144+
MappingFactory::fromArray($mappedClass);
145+
}
146+
57147
public function testItWillThrowExceptionIfArrayHasNoClassKey()
58148
{
59149
$this->setExpectedException(MappingException::class);
60150
$mappedClass = [];
61151
MappingFactory::fromArray($mappedClass);
62152
}
63153

64-
65154
public function testItWillThrowExceptionIfArrayHasNoSelfUrlKey()
66155
{
67156
$this->setExpectedException(MappingException::class);

0 commit comments

Comments
 (0)