Skip to content

Commit 860300e

Browse files
authored
Merge pull request #29 from php-school/attributes
Attributes
2 parents 678c104 + 1afe07f commit 860300e

File tree

66 files changed

+2819
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2819
-0
lines changed

app/bootstrap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use PhpSchool\PHP8Appreciate\Exercise\InfiniteDivisions;
2828
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
2929
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
30+
use PhpSchool\PHP8Appreciate\Exercise\TheAttributesOfSuccess;
3031
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
3132
use PhpSchool\PHP8Appreciate\Exercise\ThrowAnExpression;
3233
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
@@ -47,6 +48,7 @@
4748
$app->addExercise(TheReturnOfStatic::class);
4849
$app->addExercise(ThrowAnExpression::class);
4950
$app->addExercise(StringifyToDemystify::class);
51+
$app->addExercise(TheAttributesOfSuccess::class);
5052

5153
$art = <<<ART
5254
_ __ _

app/config.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpSchool\PHP8Appreciate\Exercise\InfiniteDivisions;
99
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
1010
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
11+
use PhpSchool\PHP8Appreciate\Exercise\TheAttributesOfSuccess;
1112
use PhpSchool\PHP8Appreciate\Exercise\TheReturnOfStatic;
1213
use PhpSchool\PHP8Appreciate\Exercise\ThrowAnExpression;
1314
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
@@ -57,4 +58,7 @@
5758
StringifyToDemystify::class => function (ContainerInterface $c) {
5859
return new StringifyToDemystify($c->get(PhpParser\Parser::class));
5960
},
61+
TheAttributesOfSuccess::class => function (ContainerInterface $c) {
62+
return new TheAttributesOfSuccess($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
63+
}
6064
];
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
#[Attribute(Attribute::TARGET_CLASS)]
4+
class Deserialize {
5+
6+
}
7+
8+
#[Attribute(Attribute::TARGET_PROPERTY)]
9+
class Map {
10+
public function __construct(public string $mapFrom)
11+
{
12+
}
13+
}
14+
15+
#[Attribute(Attribute::TARGET_PROPERTY)]
16+
class Skip {
17+
18+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
function camelCaseToSnakeCase(string $string): string
4+
{
5+
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $string));
6+
}
7+
8+
function deserialize(string $data, string $className): object
9+
{
10+
$reflectionClass = new \ReflectionClass($className);
11+
$attrs = $reflectionClass->getAttributes(Deserialize::class);
12+
13+
if (empty($attrs)) {
14+
throw new \RuntimeException('Class cannot be deserialized');
15+
}
16+
17+
$attrs[0]->newInstance();
18+
19+
$object = new $className();
20+
21+
$data = json_decode($data, true);
22+
23+
$obfuscators = array_filter(
24+
$reflectionClass->getMethods(),
25+
fn (ReflectionMethod $m) => count($m->getAttributes(Obfuscate::class)) > 0
26+
);
27+
28+
$obfuscators = array_combine(
29+
array_map(
30+
fn(ReflectionMethod $m) => $m->getAttributes(Obfuscate::class)[0]->newInstance()->key,
31+
$obfuscators
32+
),
33+
$obfuscators
34+
);
35+
36+
foreach ($data as $key => $value) {
37+
if (isset($obfuscators[$key])) {
38+
$data[$key] = $object->{$obfuscators[$key]->getName()}($value);
39+
}
40+
}
41+
42+
foreach ($reflectionClass->getProperties() as $property) {
43+
if ($map = $property->getAttributes(Map::class)) {
44+
$key = $map[0]->newInstance()->mapFrom;
45+
46+
if (isset($data[$key])) {
47+
$object->{$property->getName()} = $data[$key];
48+
}
49+
50+
} elseif ($skip = $property->getAttributes(Skip::class)) {
51+
continue;
52+
} elseif (isset($data[camelCaseToSnakeCase($property->getName())])) {
53+
$object->{$property->getName()} = $data[camelCaseToSnakeCase($property->getName())];
54+
}
55+
}
56+
57+
return $object;
58+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
require_once __DIR__ . '/deserialize.php';
4+
require_once __DIR__ . '/attributes.php';
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
You have been given a few pieces of code (look for `attributes.php`, `deserialize.php` & `the-attributes-of-success.php` in your working directory).
2+
3+
Your entry point is `the-attributes-of-success.php`. This is the file you should edit and work on. The other files should not be modified. However, they are included by `the-attributes-of-success.php`.
4+
5+
You can run and verify your program like so:
6+
7+
```sh
8+
$ {appname} run the-attributes-of-success.php
9+
$ {appname} verify the-attributes-of-success.php
10+
```
11+
12+
Your task is split into two sections. The overall task is to write a class using properties and attributes which describe how to map data to an instance of the class.
13+
14+
The data will be passed to you in a JSON encoded string via the first command line argument.
15+
16+
The data will represent a product review.
17+
18+
You pass the `JSON` data and the name of your class to a function named `deserialize` which is provided to you in the file `deserialize.php`. For reference, its signature is:
19+
20+
```php
21+
function deserialize(string $data, string $className): object;
22+
```
23+
24+
It will return to you an instance of `$className` with the data from the `JSON` string `$data` mapped to its properties.
25+
26+
You should dump out the object using `var_dump`.
27+
28+
### Task 1 - Annotate a class with existing attributes
29+
30+
Create a class named `Review` with five public properties which represent the data of the review. The properties should all be `string` types and should be named `comment`, `starRating`, `date`, `id` and `reviewer`.
31+
32+
The class should use the Attribute `Deserialize` so that our `deserialize` function knows that this is a valid class to use.
33+
34+
By default, our `deserialize` function will use the names of the class properties to locate the field value in the `JSON` data.
35+
36+
For example, when passing the following class to our `deserialise` function it would look for the `sku` key in the `JSON` data and set it on the `sku` property.
37+
38+
```php
39+
#[Deserialize]
40+
class Product {
41+
public string $sku;
42+
}
43+
```
44+
45+
#### Mapping properties
46+
47+
However, the `starRating` key does not exist in the `JSON` data. It exists as `rating`. So here, we need to use the `Map` attribute. By using the `Map` attribute we can tell our `deserialize` function to fetch the star rating from a different key in the `JSON` data.
48+
49+
Use it like so:
50+
51+
```php
52+
#[Deserialize]
53+
class Product {
54+
#[Map('reference')]
55+
public string $sku;
56+
}
57+
```
58+
59+
Where `reference` is the key in the `JSON` data you want to use rather than `sku`.
60+
61+
#### Skipping properties
62+
63+
We don't care about the ID value, this relates to a 3rd party system and is not relevant in our code.
64+
65+
Use the `Skip` attribute on the `id` property of our `Review` class to tell our `deserialize` function to skip this piece of data.
66+
67+
### Task 2 - Create your own attribute
68+
69+
By now you should be able to call the `deserialize` function with the `JSON` data and your class name.
70+
71+
When executing your program with
72+
73+
```sh
74+
$ {appname} run the-attributes-of-success.php
75+
```
76+
77+
You should see a dump of your `Review` instance.
78+
79+
Here comes our problem: The reviewers name is not anonymous. We have to comply with strict privacy laws, we cannot display this data without the reviewer's permission.
80+
81+
For now, we will have to obfuscate this data. We can accomplish this using a custom attribute.
82+
83+
#### Create the obfuscate method
84+
85+
Create a method on your `Review` class named `obfuscateReviewer`. It should take a string input, run it through the `md5` function and return it.
86+
87+
#### Create an attribute
88+
89+
Create an attribute named `Obfuscate`. It should have a public property named `key` and it's constructor should assign the first passed in string value to this property.
90+
91+
The attribute must be designated as an attribute by using the `Attribute` attribute (confused much??) and it should target methods only.
92+
93+
Targets designate where an attribute can be used, on classes, methods or properties and so on. For reference, the `Deserialize` attribute can only be used on classes and hence its target is `Attribute::TARGET_CLASS`.
94+
95+
See below for an example of designating a class as an attribute and configuring its target as classes only.
96+
97+
```php
98+
#[Attribute(Attribute::TARGET_CLASS)] //This is how we designate `MyAttribute` as an attribute with its target.
99+
class MyAttribute {
100+
public function __construct(string $someValue) {
101+
}
102+
}
103+
```
104+
105+
#### Use the attribute
106+
107+
The last step is to use the attribute on your `obfuscateReviewer` method. We need to tell our `deserialize` function that this method should be called when accessing the `reviewer` key from the `JSON` data.
108+
109+
Use the `Obfuscate` attribute on the method and pass to it the name of the key in the `JSON` data we want the obfuscator to run over, which is the `reviewer` key.
110+
111+
When the `deserialize` function sees a method with the `Obfuscate` attribute it will use the key found in the public property named `key` of the attribute. It will find the value referenced by that key in the `JSON` data and pass it to the obfuscator method.
112+
113+
Finally, the returned data will be set on the `Review` object instance.
114+
115+
### Dump your object
116+
117+
The last task is to dump your object instance out using the PHP function `var_dump` - we use this output to verify the structure and data in your `Review` instance.
118+
119+
### The advantages of Attributes
120+
121+
* Add Metadata to classes, methods, properties and arguments and so on
122+
* They can replace PHP doc blocks, each with custom parsers and rules to a unified standard supported by PHP
123+
* The data can be introspected using PHP's Reflection API's
124+
* PHP Core and extensions can provide new behaviour and runtime configuration which is opt-in, such as conditionally declaring functions, deprecating features and so on
125+
126+
----------------------------------------------------------------------
127+
## HINTS
128+
129+
Documentation on the Attributes feature can be found by pointing your browser here:
130+
[https://www.php.net/manual/en/language.attributes.overview.php]()
131+
132+
Remember, do not edit `attributes.php` or `deserialize.php` - verification will fail if you do. Feel free to read the files to get a better understanding of the deserialization process.
133+
134+
You must call the `deserialize` function and you must use the `var_dump` function to output your deserialized object.
135+
136+
If you want to see the `JSON` data - use `var_dump` to dump it out.
137+
138+
139+
## Extra
140+
141+
If you're not sure how to access command line arguments - you should maybe try a different workshop which covers that topic. Try `learnyouphp`.
142+
143+
`json_decode` can fail if it is passed a malformed string. Wrap the decode in a `try\catch` statement and pass the `JSON_THROW_ON_ERROR` flag to `json_decode`'s fourth parameter.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
#[Attribute(Attribute::TARGET_CLASS)]
4+
class Deserialize {
5+
6+
}
7+
8+
#[Attribute(Attribute::TARGET_PROPERTY)]
9+
class Map {
10+
public function __construct(public string $mapFrom)
11+
{
12+
}
13+
}
14+
15+
#[Attribute(Attribute::TARGET_PROPERTY)]
16+
class Skip {
17+
18+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
function camelCaseToSnakeCase(string $string): string
4+
{
5+
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', $string));
6+
}
7+
8+
function deserialize(string $data, string $className): object
9+
{
10+
$reflectionClass = new \ReflectionClass($className);
11+
$attrs = $reflectionClass->getAttributes(Deserialize::class);
12+
13+
if (empty($attrs)) {
14+
throw new \RuntimeException('Class cannot be deserialized');
15+
}
16+
17+
$attrs[0]->newInstance();
18+
19+
$object = new $className();
20+
21+
$data = json_decode($data, true);
22+
23+
$obfuscators = array_filter(
24+
$reflectionClass->getMethods(),
25+
fn (ReflectionMethod $m) => count($m->getAttributes(Obfuscate::class)) > 0
26+
);
27+
28+
$obfuscators = array_combine(
29+
array_map(
30+
fn(ReflectionMethod $m) => $m->getAttributes(Obfuscate::class)[0]->newInstance()->key,
31+
$obfuscators
32+
),
33+
$obfuscators
34+
);
35+
36+
foreach ($data as $key => $value) {
37+
if (isset($obfuscators[$key])) {
38+
$data[$key] = $object->{$obfuscators[$key]->getName()}($value);
39+
}
40+
}
41+
42+
foreach ($reflectionClass->getProperties() as $property) {
43+
if ($map = $property->getAttributes(Map::class)) {
44+
$key = $map[0]->newInstance()->mapFrom;
45+
46+
if (isset($data[$key])) {
47+
$object->{$property->getName()} = $data[$key];
48+
}
49+
50+
} elseif ($skip = $property->getAttributes(Skip::class)) {
51+
continue;
52+
} elseif (isset($data[camelCaseToSnakeCase($property->getName())])) {
53+
$object->{$property->getName()} = $data[camelCaseToSnakeCase($property->getName())];
54+
}
55+
}
56+
57+
return $object;
58+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
require_once __DIR__ . '/deserialize.php';
4+
require_once __DIR__ . '/attributes.php';
5+
6+
7+
#[Attribute(Attribute::TARGET_METHOD)]
8+
class Obfuscate {
9+
public function __construct(public string $key)
10+
{
11+
}
12+
}
13+
14+
#[Deserialize]
15+
class Review {
16+
public string $comment;
17+
18+
#[Map('rating')]
19+
public string $starRating;
20+
21+
public string $date;
22+
23+
#[Skip()]
24+
public string $id;
25+
26+
public ?string $reviewer = null;
27+
28+
#[Obfuscate('reviewer')]
29+
public function obfuscateReviewer(string $reviewer): string
30+
{
31+
return md5($reviewer);
32+
}
33+
}
34+
35+
$object = deserialize($argv[1], Review::class);
36+
37+
var_dump($object);

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ parameters:
44
- '#Cannot access property \$args on PhpParser\\Node\|null#'
55
- '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:toString\(\)#'
66
- '#Parameter \#1 \$array of function array_flip expects array<int\|string>, array<int, int\|string>\|int\|string given.#'
7+
- '#Call to an undefined method Faker\\Generator\:\:userName\(\).#'
78

89
excludes_analyse:
910
- src/TestUtils/WorkshopExerciseTest.php

0 commit comments

Comments
 (0)