Skip to content

Commit 501df7d

Browse files
authored
Merge pull request #20 from php-school/unite-the-types
Unite the types
2 parents f0c39a8 + 9b70bdd commit 501df7d

18 files changed

+410
-1
lines changed

app/bootstrap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
2525
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
2626
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
27+
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
2728
use PhpSchool\PhpWorkshop\Application;
2829

2930
$app = new Application('PHP8 Appreciate', __DIR__ . '/config.php');
@@ -33,6 +34,7 @@
3334
$app->addExercise(PhpPromotion::class);
3435
$app->addExercise(CautionWithCatches::class);
3536
$app->addExercise(LordOfTheStrings::class);
37+
$app->addExercise(UniteTheTypes::class);
3638

3739
$art = <<<ART
3840
_ __ _

app/config.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
66
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
77
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
8+
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
89
use Psr\Container\ContainerInterface;
910

1011
use function DI\create;
1112
use function DI\factory;
12-
use function DI\object;
1313

1414
return [
1515
'basePath' => __DIR__ . '/../',
@@ -30,4 +30,7 @@
3030
LordOfTheStrings::class => function (ContainerInterface $c) {
3131
return new LordOfTheStrings($c->get(\Faker\Generator::class));
3232
},
33+
UniteTheTypes::class => function (ContainerInterface $c) {
34+
return new UniteTheTypes($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
35+
},
3336
];
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
For a long time in PHP the types have been independent & solitary, it's now time for the uprising, and the uniting of types.
2+
3+
Create a program which adds up and prints the result of all the arguments passed to the program (not including the program name).
4+
5+
In the process you should create a function named `adder` which accepts these numbers as a variadic parameter.
6+
7+
The type of the parameter should be a union of all the types of numbers we might pass to your program.
8+
9+
We will pass to your program any amount of random numbers which could be integers, floats or strings. Your `adder` function
10+
should only accept these types. Regardless of the type, every argument will be a number.
11+
12+
You should output the sum of the numbers followed by a new line.
13+
14+
How you print and add the numbers is up to you.
15+
16+
### The advantages of union types
17+
18+
* Allows us to represent more complex types in a simpler manner, such as the pseudo `Number` type we are inventing here in this exercise.
19+
* The types are enforced by PHP so `TypeError`'s will be thrown when attempting to pass non-valid types.
20+
* Allows us to move information from phpdoc into function signatures.
21+
* It prevents incorrect function information. phpdocs can often go stale when they are not updated with the function itself.
22+
23+
24+
----------------------------------------------------------------------
25+
## HINTS
26+
27+
Remember the first argument will be the programs file path and not an argument passed to the program.
28+
29+
The function you implement must be called `adder`.
30+
31+
It is up to you to pass the numbers to your function.
32+
33+
Documentation on union types can be found by pointing your browser here:
34+
[https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.union]()
35+
36+
----------------------------------------------------------------------
37+
## EXTRA
38+
39+
You should access `$argv` directly to fetch the numbers (we have casted the arguments from strings to their respective types)
40+
41+
Think about the return type of your `adder` function - you could declare it as a float.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
function adder(string|float|int ...$numbers): float
4+
{
5+
return array_sum($numbers);
6+
}
7+
8+
$nums = $argv;
9+
array_shift($nums);
10+
11+
echo adder(...$nums) . "\n";
12+

solution.php

Whitespace-only changes.

src/Exercise/UniteTheTypes.php

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
namespace PhpSchool\PHP8Appreciate\Exercise;
4+
5+
use PhpParser\Node\Identifier;
6+
use PhpParser\Node\Stmt;
7+
use PhpParser\Node\Stmt\Function_;
8+
use PhpParser\Node\UnionType;
9+
use PhpParser\NodeFinder;
10+
use PhpParser\Parser;
11+
use PhpSchool\PhpWorkshop\CodeInsertion;
12+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
13+
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
14+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
15+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
16+
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
17+
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
18+
use PhpSchool\PhpWorkshop\Input\Input;
19+
use PhpSchool\PhpWorkshop\Patch;
20+
use PhpSchool\PhpWorkshop\Result\Failure;
21+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
22+
use PhpSchool\PhpWorkshop\Result\Success;
23+
use Faker\Generator as FakerGenerator;
24+
25+
class UniteTheTypes extends AbstractExercise implements
26+
ExerciseInterface,
27+
CliExercise,
28+
SelfCheck,
29+
SubmissionPatchable
30+
{
31+
public function __construct(private Parser $parser, private FakerGenerator $faker)
32+
{
33+
}
34+
35+
public function getName(): string
36+
{
37+
return 'Unite The Types';
38+
}
39+
40+
public function getDescription(): string
41+
{
42+
return 'PHP 8\'s union types';
43+
}
44+
45+
public function getType(): ExerciseType
46+
{
47+
return ExerciseType::CLI();
48+
}
49+
50+
public function getArgs(): array
51+
{
52+
$numbers = array_map(
53+
function (): string {
54+
if ($this->faker->boolean()) {
55+
return (string) $this->faker->numberBetween(0, 50);
56+
}
57+
return (string) $this->faker->randomFloat(3, 0, 50);
58+
},
59+
range(0, random_int(5, 15))
60+
);
61+
62+
return [$numbers];
63+
}
64+
65+
public function getPatch(): Patch
66+
{
67+
$code = <<<'CODE'
68+
$first = array_shift($argv);
69+
$argv = array_merge([$first], array_map(function ($value) {
70+
return match (true) {
71+
(int) $value != (float) $value => (float) $value,
72+
(bool) random_int(0, 1) => (int) $value,
73+
default => (string) $value
74+
};
75+
}, $argv));
76+
CODE;
77+
78+
$casterInsertion = new CodeInsertion(CodeInsertion::TYPE_BEFORE, $code);
79+
80+
return (new Patch())
81+
->withTransformer(new Patch\ForceStrictTypes())
82+
->withInsertion($casterInsertion);
83+
}
84+
85+
public function check(Input $input): ResultInterface
86+
{
87+
/** @var array<Stmt> $statements */
88+
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
89+
90+
/** @var Function_|null $adder */
91+
$adder = (new NodeFinder())->findFirst($statements, function (\PhpParser\Node $node) {
92+
return $node instanceof Function_ && $node->name->toString() === 'adder';
93+
});
94+
95+
if (null === $adder) {
96+
return Failure::fromNameAndReason($this->getName(), 'No function named adder was found');
97+
}
98+
99+
if (!isset($adder->params[0])) {
100+
return Failure::fromNameAndReason($this->getName(), 'Function adder has no parameters');
101+
}
102+
103+
/** @var \PhpParser\Node\Param $firstParam */
104+
$firstParam = $adder->params[0];
105+
106+
if (!$firstParam->type instanceof UnionType) {
107+
return Failure::fromNameAndReason(
108+
$this->getName(),
109+
'Function adder does not use a union type for it\'s first param'
110+
);
111+
}
112+
113+
$incorrectTypes = array_filter(
114+
$firstParam->type->types,
115+
fn ($type) => !$type instanceof Identifier
116+
);
117+
118+
if (count($incorrectTypes)) {
119+
return Failure::fromNameAndReason(
120+
$this->getName(),
121+
'Union type is incorrect, it should only accept the required types'
122+
);
123+
}
124+
125+
$types = array_map(
126+
fn (Identifier $type) => $type->__toString(),
127+
$firstParam->type->types
128+
);
129+
130+
sort($types);
131+
132+
if ($types !== ['float', 'int', 'string']) {
133+
return Failure::fromNameAndReason(
134+
$this->getName(),
135+
'Union type is incorrect, it should only accept the required types'
136+
);
137+
}
138+
139+
if (!$firstParam->variadic) {
140+
return Failure::fromNameAndReason(
141+
$this->getName(),
142+
'Function adder\'s first parameter should be variadic in order to accept multiple arguments'
143+
);
144+
}
145+
146+
return new Success('Union type for adder is correct');
147+
}
148+
}

test/Exercise/UniteTheTypesTest.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace PhpSchool\PHP8AppreciateTest\Exercise;
4+
5+
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
6+
use PhpSchool\PhpWorkshop\Application;
7+
use PhpSchool\PhpWorkshop\Result\Failure;
8+
use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
9+
10+
class UniteTheTypesTest extends WorkshopExerciseTest
11+
{
12+
public function getExerciseClass(): string
13+
{
14+
return UniteTheTypes::class;
15+
}
16+
17+
public function getApplication(): Application
18+
{
19+
return require __DIR__ . '/../../app/bootstrap.php';
20+
}
21+
22+
public function testFailureWhenNoFunctionNamedAdder(): void
23+
{
24+
$this->runExercise('no-adder-function.php');
25+
26+
$this->assertVerifyWasNotSuccessful();
27+
28+
$this->assertResultsHasFailure(Failure::class, 'No function named adder was found');
29+
}
30+
31+
public function testFailureWhenAdderFunctionHasNoParams(): void
32+
{
33+
$this->runExercise('no-function-params.php');
34+
35+
$this->assertVerifyWasNotSuccessful();
36+
37+
$this->assertResultsHasFailure(Failure::class, 'Function adder has no parameters');
38+
}
39+
40+
public function testFailureWhenAdderFunctionHasNoUnionTypeParam(): void
41+
{
42+
$this->runExercise('no-union-type-param.php');
43+
44+
$this->assertVerifyWasNotSuccessful();
45+
46+
$this->assertResultsHasFailure(
47+
Failure::class,
48+
'Function adder does not use a union type for it\'s first param'
49+
);
50+
}
51+
52+
public function testFailureWhenAdderFunctionHasClassTypeInUnion(): void
53+
{
54+
$this->runExercise('incorrect-union-class-type.php');
55+
56+
$this->assertVerifyWasNotSuccessful();
57+
58+
$this->assertResultsHasFailure(
59+
Failure::class,
60+
'Union type is incorrect, it should only accept the required types'
61+
);
62+
}
63+
64+
public function testFailureWhenAdderFunctionHasIncorrectUnion(): void
65+
{
66+
$this->runExercise('incorrect-union-scalar-type.php');
67+
68+
$this->assertVerifyWasNotSuccessful();
69+
70+
$this->assertResultsHasFailure(
71+
Failure::class,
72+
'Union type is incorrect, it should only accept the required types'
73+
);
74+
}
75+
76+
public function testFailureWhenAdderFunctionHasCorrectUnionWithExtraTypes(): void
77+
{
78+
$this->runExercise('incorrect-union-extra-type.php');
79+
80+
$this->assertVerifyWasNotSuccessful();
81+
82+
$this->assertResultsHasFailure(
83+
Failure::class,
84+
'Union type is incorrect, it should only accept the required types'
85+
);
86+
}
87+
88+
public function testFailureWhenAdderFunctionParamIsNotVariadic(): void
89+
{
90+
$this->runExercise('union-type-param-not-variadic.php');
91+
92+
$this->assertVerifyWasNotSuccessful();
93+
94+
$this->assertResultsHasFailure(
95+
Failure::class,
96+
'Function adder\'s first parameter should be variadic in order to accept multiple arguments'
97+
);
98+
}
99+
100+
public function testSuccessfulSolution(): void
101+
{
102+
$this->runExercise('correct-union-type-same-order.php');
103+
104+
$this->assertVerifyWasSuccessful();
105+
}
106+
107+
public function testSuccessfulSolutionWithDifferentOrderUnion(): void
108+
{
109+
$this->runExercise('correct-union-type-diff-order.php');
110+
111+
$this->assertVerifyWasSuccessful();
112+
}
113+
114+
public function testSuccessfulSolutionWithFloatReturnType(): void
115+
{
116+
$this->runExercise('correct-union-type-float-return.php');
117+
118+
$this->assertVerifyWasSuccessful();
119+
}
120+
121+
public function testSuccessfulSolutionWithStrictTypes(): void
122+
{
123+
$this->runExercise('correct-union-type-strict-types.php');
124+
125+
$this->assertVerifyWasSuccessful();
126+
}
127+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
function adder(float|string|int ...$numbers) {
4+
return array_sum($numbers);
5+
}
6+
7+
$nums = $argv;
8+
array_shift($nums);
9+
10+
echo adder(...$nums) . "\n";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
function adder(string|float|int ...$numbers): float {
4+
return array_sum($numbers);
5+
}
6+
7+
$nums = $argv;
8+
array_shift($nums);
9+
10+
echo adder(...$nums) . "\n";

0 commit comments

Comments
 (0)