Skip to content

Unite the types #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 7, 2021
2 changes: 2 additions & 0 deletions app/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
use PhpSchool\PhpWorkshop\Application;

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

$art = <<<ART
_ __ _
Expand Down
5 changes: 4 additions & 1 deletion app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
use PhpSchool\PHP8Appreciate\Exercise\LordOfTheStrings;
use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
use Psr\Container\ContainerInterface;

use function DI\create;
use function DI\factory;
use function DI\object;

return [
'basePath' => __DIR__ . '/../',
Expand All @@ -30,4 +30,7 @@
LordOfTheStrings::class => function (ContainerInterface $c) {
return new LordOfTheStrings($c->get(\Faker\Generator::class));
},
UniteTheTypes::class => function (ContainerInterface $c) {
return new UniteTheTypes($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
},
];
41 changes: 41 additions & 0 deletions exercises/unite-the-types/problem/problem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
For a long time in PHP the types have been independent & solitary, it's now time for the uprising, and the uniting of types.

Create a program which adds up and prints the result of all the arguments passed to the program (not including the program name).

In the process you should create a function named `adder` which accepts these numbers as a variadic parameter.

The type of the parameter should be a union of all the types of numbers we might pass to your program.

We will pass to your program any amount of random numbers which could be integers, floats or strings. Your `adder` function
should only accept these types. Regardless of the type, every argument will be a number.

You should output the sum of the numbers followed by a new line.

How you print and add the numbers is up to you.

### The advantages of union types

* Allows us to represent more complex types in a simpler manner, such as the pseudo `Number` type we are inventing here in this exercise.
* The types are enforced by PHP so `TypeError`'s will be thrown when attempting to pass non-valid types.
* Allows us to move information from phpdoc into function signatures.
* It prevents incorrect function information. phpdocs can often go stale when they are not updated with the function itself.


----------------------------------------------------------------------
## HINTS

Remember the first argument will be the programs file path and not an argument passed to the program.

The function you implement must be called `adder`.

It is up to you to pass the numbers to your function.

Documentation on union types can be found by pointing your browser here:
[https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.union]()

----------------------------------------------------------------------
## EXTRA

You should access `$argv` directly to fetch the numbers (we have casted the arguments from strings to their respective types)

Think about the return type of your `adder` function - you could declare it as a float.
12 changes: 12 additions & 0 deletions exercises/unite-the-types/solution/solution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

function adder(string|float|int ...$numbers): float
{
return array_sum($numbers);
}

$nums = $argv;
array_shift($nums);

echo adder(...$nums) . "\n";

Empty file added solution.php
Empty file.
148 changes: 148 additions & 0 deletions src/Exercise/UniteTheTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace PhpSchool\PHP8Appreciate\Exercise;

use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\UnionType;
use PhpParser\NodeFinder;
use PhpParser\Parser;
use PhpSchool\PhpWorkshop\CodeInsertion;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
use PhpSchool\PhpWorkshop\Input\Input;
use PhpSchool\PhpWorkshop\Patch;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\Result\ResultInterface;
use PhpSchool\PhpWorkshop\Result\Success;
use Faker\Generator as FakerGenerator;

class UniteTheTypes extends AbstractExercise implements
ExerciseInterface,
CliExercise,
SelfCheck,
SubmissionPatchable
{
public function __construct(private Parser $parser, private FakerGenerator $faker)
{
}

public function getName(): string
{
return 'Unite The Types';
}

public function getDescription(): string
{
return 'PHP 8\'s union types';
}

public function getType(): ExerciseType
{
return ExerciseType::CLI();
}

public function getArgs(): array
{
$numbers = array_map(
function (): string {
if ($this->faker->boolean()) {
return (string) $this->faker->numberBetween(0, 50);
}
return (string) $this->faker->randomFloat(3, 0, 50);
},
range(0, random_int(5, 15))
);

return [$numbers];
}

public function getPatch(): Patch
{
$code = <<<'CODE'
$first = array_shift($argv);
$argv = array_merge([$first], array_map(function ($value) {
return match (true) {
(int) $value != (float) $value => (float) $value,
(bool) random_int(0, 1) => (int) $value,
default => (string) $value
};
}, $argv));
CODE;

$casterInsertion = new CodeInsertion(CodeInsertion::TYPE_BEFORE, $code);

return (new Patch())
->withTransformer(new Patch\ForceStrictTypes())
->withInsertion($casterInsertion);
}

public function check(Input $input): ResultInterface
{
/** @var array<Stmt> $statements */
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));

/** @var Function_|null $adder */
$adder = (new NodeFinder())->findFirst($statements, function (\PhpParser\Node $node) {
return $node instanceof Function_ && $node->name->toString() === 'adder';
});

if (null === $adder) {
return Failure::fromNameAndReason($this->getName(), 'No function named adder was found');
}

if (!isset($adder->params[0])) {
return Failure::fromNameAndReason($this->getName(), 'Function adder has no parameters');
}

/** @var \PhpParser\Node\Param $firstParam */
$firstParam = $adder->params[0];

if (!$firstParam->type instanceof UnionType) {
return Failure::fromNameAndReason(
$this->getName(),
'Function adder does not use a union type for it\'s first param'
);
}

$incorrectTypes = array_filter(
$firstParam->type->types,
fn ($type) => !$type instanceof Identifier
);

if (count($incorrectTypes)) {
return Failure::fromNameAndReason(
$this->getName(),
'Union type is incorrect, it should only accept the required types'
);
}

$types = array_map(
fn (Identifier $type) => $type->__toString(),
$firstParam->type->types
);

sort($types);

if ($types !== ['float', 'int', 'string']) {
return Failure::fromNameAndReason(
$this->getName(),
'Union type is incorrect, it should only accept the required types'
);
}

if (!$firstParam->variadic) {
return Failure::fromNameAndReason(
$this->getName(),
'Function adder\'s first parameter should be variadic in order to accept multiple arguments'
);
}

return new Success('Union type for adder is correct');
}
}
127 changes: 127 additions & 0 deletions test/Exercise/UniteTheTypesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

namespace PhpSchool\PHP8AppreciateTest\Exercise;

use PhpSchool\PHP8Appreciate\Exercise\UniteTheTypes;
use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;

class UniteTheTypesTest extends WorkshopExerciseTest
{
public function getExerciseClass(): string
{
return UniteTheTypes::class;
}

public function getApplication(): Application
{
return require __DIR__ . '/../../app/bootstrap.php';
}

public function testFailureWhenNoFunctionNamedAdder(): void
{
$this->runExercise('no-adder-function.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(Failure::class, 'No function named adder was found');
}

public function testFailureWhenAdderFunctionHasNoParams(): void
{
$this->runExercise('no-function-params.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(Failure::class, 'Function adder has no parameters');
}

public function testFailureWhenAdderFunctionHasNoUnionTypeParam(): void
{
$this->runExercise('no-union-type-param.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(
Failure::class,
'Function adder does not use a union type for it\'s first param'
);
}

public function testFailureWhenAdderFunctionHasClassTypeInUnion(): void
{
$this->runExercise('incorrect-union-class-type.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(
Failure::class,
'Union type is incorrect, it should only accept the required types'
);
}

public function testFailureWhenAdderFunctionHasIncorrectUnion(): void
{
$this->runExercise('incorrect-union-scalar-type.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(
Failure::class,
'Union type is incorrect, it should only accept the required types'
);
}

public function testFailureWhenAdderFunctionHasCorrectUnionWithExtraTypes(): void
{
$this->runExercise('incorrect-union-extra-type.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(
Failure::class,
'Union type is incorrect, it should only accept the required types'
);
}

public function testFailureWhenAdderFunctionParamIsNotVariadic(): void
{
$this->runExercise('union-type-param-not-variadic.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(
Failure::class,
'Function adder\'s first parameter should be variadic in order to accept multiple arguments'
);
}

public function testSuccessfulSolution(): void
{
$this->runExercise('correct-union-type-same-order.php');

$this->assertVerifyWasSuccessful();
}

public function testSuccessfulSolutionWithDifferentOrderUnion(): void
{
$this->runExercise('correct-union-type-diff-order.php');

$this->assertVerifyWasSuccessful();
}

public function testSuccessfulSolutionWithFloatReturnType(): void
{
$this->runExercise('correct-union-type-float-return.php');

$this->assertVerifyWasSuccessful();
}

public function testSuccessfulSolutionWithStrictTypes(): void
{
$this->runExercise('correct-union-type-strict-types.php');

$this->assertVerifyWasSuccessful();
}
}
10 changes: 10 additions & 0 deletions test/solutions/unite-the-types/correct-union-type-diff-order.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

function adder(float|string|int ...$numbers) {
return array_sum($numbers);
}

$nums = $argv;
array_shift($nums);

echo adder(...$nums) . "\n";
10 changes: 10 additions & 0 deletions test/solutions/unite-the-types/correct-union-type-float-return.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

function adder(string|float|int ...$numbers): float {
return array_sum($numbers);
}

$nums = $argv;
array_shift($nums);

echo adder(...$nums) . "\n";
Loading