diff --git a/app/bootstrap.php b/app/bootstrap.php index d22e768..282a2c5 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -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'); @@ -33,6 +34,7 @@ $app->addExercise(PhpPromotion::class); $app->addExercise(CautionWithCatches::class); $app->addExercise(LordOfTheStrings::class); +$app->addExercise(UniteTheTypes::class); $art = << __DIR__ . '/../', @@ -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)); + }, ]; diff --git a/exercises/unite-the-types/problem/problem.md b/exercises/unite-the-types/problem/problem.md new file mode 100644 index 0000000..5995f71 --- /dev/null +++ b/exercises/unite-the-types/problem/problem.md @@ -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. diff --git a/exercises/unite-the-types/solution/solution.php b/exercises/unite-the-types/solution/solution.php new file mode 100644 index 0000000..209d9bc --- /dev/null +++ b/exercises/unite-the-types/solution/solution.php @@ -0,0 +1,12 @@ +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 $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'); + } +} diff --git a/test/Exercise/UniteTheTypesTest.php b/test/Exercise/UniteTheTypesTest.php new file mode 100644 index 0000000..11dfd17 --- /dev/null +++ b/test/Exercise/UniteTheTypesTest.php @@ -0,0 +1,127 @@ +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(); + } +} diff --git a/test/solutions/unite-the-types/correct-union-type-diff-order.php b/test/solutions/unite-the-types/correct-union-type-diff-order.php new file mode 100644 index 0000000..29f41f3 --- /dev/null +++ b/test/solutions/unite-the-types/correct-union-type-diff-order.php @@ -0,0 +1,10 @@ +