Skip to content

Commit e636746

Browse files
committed
Initial safe space for nulls exercise
1 parent 5ea074b commit e636746

File tree

6 files changed

+268
-1
lines changed

6 files changed

+268
-1
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
];

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
],
2222
"require": {
2323
"php": "^8.0",
24-
"php-school/php-workshop": "dev-master"
24+
"php-school/php-workshop": "dev-file-comparison-check"
2525
},
2626
"require-dev": {
2727
"phpunit/phpunit": "^9",

exercises/a-safe-space-for-nulls/problem/problem.md

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

0 commit comments

Comments
 (0)