Skip to content

Commit 8dd8a23

Browse files
authored
Merge pull request #23 from php-school/a-safe-space-for-nulls
A safe space for nulls exercise
2 parents 5ea074b + 2fc1467 commit 8dd8a23

File tree

14 files changed

+524
-0
lines changed

14 files changed

+524
-0
lines changed

app/bootstrap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
}
2121

2222
use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
23+
use PhpSchool\PHP8Appreciate\Exercise\ASafeSpaceForNulls;
2324
use PhpSchool\PHP8Appreciate\Exercise\CautionWithCatches;
2425
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
2526
use PhpSchool\PHP8Appreciate\Exercise\InfiniteDivisions;
@@ -37,6 +38,7 @@
3738
$app->addExercise(LordOfTheStrings::class);
3839
$app->addExercise(UniteTheTypes::class);
3940
$app->addExercise(InfiniteDivisions::class);
41+
$app->addExercise(ASafeSpaceForNulls::class);
4042

4143
$art = <<<ART
4244
_ __ _

app/config.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
4+
use PhpSchool\PHP8Appreciate\Exercise\ASafeSpaceForNulls;
45
use PhpSchool\PHP8Appreciate\Exercise\CautionWithCatches;
56
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
67
use PhpSchool\PHP8Appreciate\Exercise\InfiniteDivisions;
@@ -37,4 +38,7 @@
3738
InfiniteDivisions::class => function (ContainerInterface $c) {
3839
return new InfiniteDivisions($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
3940
},
41+
ASafeSpaceForNulls::class => function (ContainerInterface $c) {
42+
return new ASafeSpaceForNulls($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
43+
},
4044
];
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
Create a program that exports a `User` object instance to a CSV file using the null safe operator to access it's member properties.
2+
3+
You will have a variable named `$user` available in your PHP script. It is placed there automatically each time your program runs. It is populated with random data.
4+
5+
Sometimes, properties won't have a value set and will be null.
6+
7+
With the null safe operator it is possible to access variables like so:
8+
9+
```php
10+
$capitalPopulation = $country?->capital?->population;
11+
```
12+
13+
If the `capital` property is null, the variable `$capitalPopulation` will also be null. Previously, without the null safe operator, this would be achieved like so:
14+
15+
```php
16+
$capitalPopulation = null;
17+
if ($city->capital !== null) {
18+
$capitalPopulation = $country->capital->population;
19+
}
20+
```
21+
22+
The `User` class, for which the `$user` variable holds an instance of, has the following signature:
23+
24+
```php
25+
class User
26+
{
27+
public string $firstName;
28+
public string $lastName;
29+
public ?int $age = null;
30+
public ?Address $address = null;
31+
}
32+
33+
class Address
34+
{
35+
public int $number;
36+
public string $addressLine1;
37+
public ?string $addressLine2 = null;
38+
}
39+
```
40+
41+
Note also the `Address` class which the property `$user->address` may be an instance of, or it may be null.
42+
43+
Export the `$user` data to a CSV with the following columns:
44+
45+
`First Name`, `Last Name`, `Age`, `House num`, `Addr 1`, `Addr 2`
46+
47+
* The CSV should be comma delimited
48+
* The columns should read exactly as above, any mistake will trigger a failure
49+
* There should be one row for the column headers and one for the data
50+
* Any properties which are null on the user should be printed as empty fields in the CSV
51+
* The file should be named `users.csv` and exist next to your submission file (eg in the same directory)
52+
53+
And finally, the most important part, all properties which may be `NULL` should be accessed using the null safe operator!
54+
55+
### Advantages of the null safe operator
56+
57+
* Much less code for simple operations where null is a valid value
58+
* If the operator is part of a chain anything to the right of the null will not be executed, the statements will be short-circuited.
59+
* Can be used on methods where null coalescing cannot `$user->getCreatedAt()->format() ?? null` where `getCreatedAt()` could return null or a `\DateTime` instance
60+
61+
----------------------------------------------------------------------
62+
## HINTS
63+
64+
Remember your program will be passed no arguments. There will be a `User` object populated for you under the variable `$user`.
65+
It is available at the beginning of your script.
66+
67+
Documentation on the Null Safe Operator can be found by pointing your browser here:
68+
[https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.nullsafe]()
69+
70+
----------------------------------------------------------------------
71+
## EXTRA
72+
73+
We have not given any hints regarding writing to a CSV file, as we are not testing you on that. How you achieve that (`fputcsv`, `file_put_contents`, etc) is up to you.
74+
75+
Therefore, it is up to you to figure out how to write a CSV if you don't already know :)
76+
77+
Okay... just one hint: Check back over the exercise "Have the Last Say". You might find some pointers there!
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
$fp = fopen('users.csv', 'w+');
4+
fputcsv($fp, ['First Name', 'Last Name', 'Age', 'House num', 'Addr 1', 'Addr 2']);
5+
fputcsv(
6+
$fp,
7+
[
8+
$user->firstName,
9+
$user->lastName,
10+
$user?->age,
11+
$user?->address?->number,
12+
$user?->address?->addressLine1,
13+
$user?->address?->addressLine2
14+
]
15+
);
16+
fclose($fp);

src/Exercise/ASafeSpaceForNulls.php

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
<?php
2+
3+
namespace PhpSchool\PHP8Appreciate\Exercise;
4+
5+
use Faker\Generator as FakerGenerator;
6+
use Faker\Provider\en_US\Address;
7+
use Faker\Provider\en_US\Person;
8+
use PhpParser\BuilderFactory;
9+
use PhpParser\Node;
10+
use PhpParser\Node\Expr\Assign;
11+
use PhpParser\Node\Expr\NullsafePropertyFetch;
12+
use PhpParser\Node\Expr\Variable;
13+
use PhpParser\Node\Identifier;
14+
use PhpParser\Node\NullableType;
15+
use PhpParser\Node\Stmt;
16+
use PhpParser\Node\Stmt\Expression;
17+
use PhpParser\NodeFinder;
18+
use PhpParser\Parser;
19+
use PhpSchool\PhpWorkshop\Check\FileComparisonCheck;
20+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
21+
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
22+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
23+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
24+
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
25+
use PhpSchool\PhpWorkshop\ExerciseCheck\FileComparisonExerciseCheck;
26+
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
27+
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
28+
use PhpSchool\PhpWorkshop\Input\Input;
29+
use PhpSchool\PhpWorkshop\Patch;
30+
use PhpSchool\PhpWorkshop\Result\Failure;
31+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
32+
use PhpSchool\PhpWorkshop\Result\Success;
33+
34+
class ASafeSpaceForNulls extends AbstractExercise implements
35+
ExerciseInterface,
36+
CliExercise,
37+
SubmissionPatchable,
38+
FileComparisonExerciseCheck,
39+
SelfCheck
40+
{
41+
42+
private ?Patch $patch = null;
43+
44+
public function __construct(private Parser $parser, private FakerGenerator $faker)
45+
{
46+
}
47+
48+
public function getName(): string
49+
{
50+
return 'A Safe Space For Nulls';
51+
}
52+
53+
public function getDescription(): string
54+
{
55+
return 'PHP 8\'s Null Safe Operator';
56+
}
57+
58+
public function getType(): ExerciseType
59+
{
60+
return new ExerciseType(ExerciseType::CLI);
61+
}
62+
63+
public function getArgs(): array
64+
{
65+
return [];
66+
}
67+
68+
public function configure(ExerciseDispatcher $dispatcher): void
69+
{
70+
$dispatcher->requireCheck(FileComparisonCheck::class);
71+
}
72+
73+
public function getPatch(): Patch
74+
{
75+
if ($this->patch) {
76+
return $this->patch;
77+
}
78+
79+
80+
$factory = new BuilderFactory();
81+
82+
$statements = [];
83+
$statements[] = $factory->class('User')
84+
->addStmt($factory->property('firstName')->setType('string')->makePublic())
85+
->addStmt($factory->property('lastName')->setType('string')->makePublic())
86+
->addStmt($factory->property('age')->setType(new NullableType('int'))->makePublic()->setDefault(null))
87+
->addStmt($factory->property('address')
88+
->setType(new NullableType('Address'))
89+
->makePublic()
90+
->setDefault(null))
91+
->getNode();
92+
93+
$statements[] = $factory->class('Address')
94+
->addStmt($factory->property('number')->setType('int')->makePublic())
95+
->addStmt($factory->property('addressLine1')->setType('string')->makePublic())
96+
->addStmt($factory->property('addressLine2')
97+
->setType(new NullableType('string'))
98+
->makePublic()
99+
->setDefault(null))
100+
->getNode();
101+
102+
$addressFaker = new Address($this->faker);
103+
$personFaker = new Person($this->faker);
104+
105+
$statements[] = new Expression(
106+
new Assign($factory->var('user'), $factory->new('User'))
107+
);
108+
$statements[] = new Expression(
109+
new Assign(
110+
$factory->propertyFetch($factory->var('user'), 'firstName'),
111+
$factory->val($personFaker->firstName())
112+
)
113+
);
114+
$statements[] = new Expression(
115+
new Assign(
116+
$factory->propertyFetch($factory->var('user'), 'lastName'),
117+
$factory->val($personFaker->lastName())
118+
)
119+
);
120+
121+
if ($this->faker->boolean()) {
122+
$statements[] = new Expression(
123+
new Assign(
124+
$factory->propertyFetch($factory->var('user'), 'age'),
125+
$factory->val($this->faker->numberBetween(18, 100))
126+
)
127+
);
128+
}
129+
130+
if ($this->faker->boolean()) {
131+
$statements[] = new Expression(
132+
new Assign(
133+
$factory->propertyFetch($factory->var('user'), 'address'),
134+
$factory->new('Address')
135+
)
136+
);
137+
$statements[] = new Expression(
138+
new Assign(
139+
$factory->propertyFetch(
140+
$factory->propertyFetch($factory->var('user'), 'address'),
141+
'number'
142+
),
143+
$factory->val($addressFaker->buildingNumber())
144+
)
145+
);
146+
$statements[] = new Expression(
147+
new Assign(
148+
$factory->propertyFetch(
149+
$factory->propertyFetch($factory->var('user'), 'address'),
150+
'addressLine1'
151+
),
152+
$factory->val($addressFaker->streetName())
153+
)
154+
);
155+
156+
if ($this->faker->boolean()) {
157+
$statements[] = new Expression(
158+
new Assign(
159+
$factory->propertyFetch(
160+
$factory->propertyFetch($factory->var('user'), 'address'),
161+
'addressLine2'
162+
),
163+
$factory->val($addressFaker->secondaryAddress())
164+
)
165+
);
166+
}
167+
}
168+
169+
return $this->patch = (new Patch())
170+
->withTransformer(function (array $originalStatements) use ($statements) {
171+
return array_merge($statements, $originalStatements);
172+
});
173+
}
174+
175+
public function check(Input $input): ResultInterface
176+
{
177+
/** @var array<Stmt> $statements */
178+
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
179+
180+
$ageFetch = $this->findNullSafePropFetch($statements, 'user', 'age');
181+
$addressFetch = $this->findAllNullSafePropertyFetch($statements, 'user', 'address');
182+
183+
if ($ageFetch === null) {
184+
return new Failure(
185+
$this->getName(),
186+
'The $user->age property should be accessed with the null safe operator'
187+
);
188+
}
189+
190+
if (count($addressFetch) < 3) {
191+
return new Failure(
192+
$this->getName(),
193+
'The $user->address property should always be accessed with the null safe operator'
194+
);
195+
}
196+
197+
$props = [
198+
'$user->address->number' => $this->findNestedNullSafePropFetch($statements, 'user', 'number'),
199+
'$user->address->addressLine1' => $this->findNestedNullSafePropFetch($statements, 'user', 'addressLine1'),
200+
'$user->address->addressLine2' => $this->findNestedNullSafePropFetch($statements, 'user', 'addressLine2'),
201+
];
202+
203+
foreach ($props as $prop => $node) {
204+
if ($node === null) {
205+
return new Failure(
206+
$this->getName(),
207+
"The $prop property should be accessed with the null safe operator"
208+
);
209+
}
210+
}
211+
212+
return new Success($this->getName());
213+
}
214+
215+
/**
216+
* @param array<Node> $statements
217+
*/
218+
private function findNullSafePropFetch(array $statements, string $variableName, string $propName): ?Node
219+
{
220+
$nodes = $this->findAllNullSafePropertyFetch($statements, $variableName, $propName);
221+
return count($nodes) > 0 ? $nodes[0] : null;
222+
}
223+
224+
/**
225+
* @param array<Node> $statements
226+
* @return array<Node>
227+
*/
228+
private function findAllNullSafePropertyFetch(array $statements, string $variableName, string $propName): array
229+
{
230+
return (new NodeFinder())->find($statements, function (Node $node) use ($variableName, $propName) {
231+
return $node instanceof NullsafePropertyFetch
232+
&& $node->var instanceof Variable
233+
&& $node->var->name === $variableName
234+
&& $node->name instanceof Identifier
235+
&& $node->name->name === $propName;
236+
});
237+
}
238+
239+
/**
240+
* @param array<Node> $statements
241+
*/
242+
private function findNestedNullSafePropFetch(array $statements, string $variableName, string $propName): ?Node
243+
{
244+
return (new NodeFinder())->findFirst($statements, function (Node $node) use ($variableName, $propName) {
245+
return $node instanceof NullsafePropertyFetch
246+
&& $node->var instanceof NullsafePropertyFetch
247+
&& $node->var->var instanceof Variable
248+
&& $node->var->var->name === $variableName
249+
&& $node->name instanceof Identifier
250+
&& $node->name->name === $propName;
251+
});
252+
}
253+
254+
public function getFilesToCompare(): array
255+
{
256+
return [
257+
'users.csv'
258+
];
259+
}
260+
}

0 commit comments

Comments
 (0)