diff --git a/app/bootstrap.php b/app/bootstrap.php index 63cfcf0..3416fdb 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -20,11 +20,13 @@ } use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven; +use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay; use PhpSchool\PhpWorkshop\Application; $app = new Application('PHP8 Appreciate', __DIR__ . '/config.php'); $app->addExercise(AMatchMadeInHeaven::class); +$app->addExercise(HaveTheLastSay::class); $art = << function (ContainerInterface $c) { return new AMatchMadeInHeaven($c->get(PhpParser\Parser::class)); }, + HaveTheLastSay::class => function (ContainerInterface $c) { + return new HaveTheLastSay($c->get(PhpParser\Parser::class)); + }, + ]; diff --git a/composer.lock b/composer.lock index 84c6a29..4746b99 100644 --- a/composer.lock +++ b/composer.lock @@ -121,16 +121,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.12.0", + "version": "v1.12.1", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "9aa6c9e289860951e6b4d010c7a841802d015cd8" + "reference": "841e8bdde345cc1ea9f98e776959e7531cadea0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/9aa6c9e289860951e6b4d010c7a841802d015cd8", - "reference": "9aa6c9e289860951e6b4d010c7a841802d015cd8", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/841e8bdde345cc1ea9f98e776959e7531cadea0e", + "reference": "841e8bdde345cc1ea9f98e776959e7531cadea0e", "shasum": "" }, "require": { @@ -167,9 +167,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.12.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.12.1" }, - "time": "2020-11-23T09:33:08+00:00" + "time": "2020-12-11T10:39:41+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1035,12 +1035,12 @@ "source": { "type": "git", "url": "https://github.com/php-school/php-workshop.git", - "reference": "e129a8750277a6110bda87c57e4a94f32d73492a" + "reference": "dc62895c36b876e042b803bab46d0b260d7407dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-school/php-workshop/zipball/e129a8750277a6110bda87c57e4a94f32d73492a", - "reference": "e129a8750277a6110bda87c57e4a94f32d73492a", + "url": "https://api.github.com/repos/php-school/php-workshop/zipball/dc62895c36b876e042b803bab46d0b260d7407dc", + "reference": "dc62895c36b876e042b803bab46d0b260d7407dc", "shasum": "" }, "require": { @@ -1110,7 +1110,7 @@ "issues": "https://github.com/php-school/php-workshop/issues", "source": "https://github.com/php-school/php-workshop/tree/master" }, - "time": "2020-12-07T19:02:12+00:00" + "time": "2020-12-13T16:54:31+00:00" }, { "name": "php-school/terminal", @@ -2174,16 +2174,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.59", + "version": "0.12.62", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "cf4107257c8ca2ad967efdd6a00f12b21acbb779" + "reference": "632393159335bbbdd7ca07d19b3ad50d76aa7fd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cf4107257c8ca2ad967efdd6a00f12b21acbb779", - "reference": "cf4107257c8ca2ad967efdd6a00f12b21acbb779", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/632393159335bbbdd7ca07d19b3ad50d76aa7fd8", + "reference": "632393159335bbbdd7ca07d19b3ad50d76aa7fd8", "shasum": "" }, "require": { @@ -2214,7 +2214,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.59" + "source": "https://github.com/phpstan/phpstan/tree/0.12.62" }, "funding": [ { @@ -2230,7 +2230,7 @@ "type": "tidelift" } ], - "time": "2020-12-07T14:46:03+00:00" + "time": "2020-12-13T13:59:38+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/exercises/have-the-last-say/problem/problem.md b/exercises/have-the-last-say/problem/problem.md new file mode 100644 index 0000000..fca4483 --- /dev/null +++ b/exercises/have-the-last-say/problem/problem.md @@ -0,0 +1,72 @@ +Create a program that reads a CSV file using `fgetcsv` and change the delimiter argument using named arguments. + +The first argument is the file name of the CSV which you should read. + +The CSV file contains two columns separated with `|` (the pipe operator). The first column is `country` +and the second column is `capital`. You should print each row to the console like the +following (with a new line after each row): + +``` +Country: Austria, Capital: Vienna +``` + +The list of countries will be picked at random, so the CSV will be different each time your program runs. + +When using `fgetcsv` there's a bunch of arguments which change the behaviour of the way the CSV is parsed. In our case we +want to change the `separator` argument, which defaults to a comma `,`. We need it to be the +pipe `|` character. + +We don't want to specify the rest of the arguments, so aside from the file pointer which is the first argument, +we only want to specify the `separator` argument. + +Named arguments are a great way to change argument default values without having to specify all the defaults again. +For example, if you only want to change the value of the last argument to a function, +you can do so, without specifying all the other arguments. For example: + +```php +htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false); +``` + +We only want to change the last argument (double_encode) of the function to false (the default is true). However, +we are forced to specify all the other arguments, but they have not changed from the defaults. + +Named arguments allows to write the same, but in a more succinct fashion: + +```php +htmlspecialchars($string, double_encode: false); +``` + +Note: only the values changed from the defaults are specified! + +### Advantages of named arguments + +* Possible to skip defaults in between the arguments you want to change +* The code is better documented since the argument label is specified with the value, very useful for booleans + +---------------------------------------------------------------------- +## HINTS + +You will need to open the file for writing before using `fgetcsv` you can do that using `fopen`. + +`fgetcsv` will return *one* line at a time + +You will most likely need a loop to process all the data in the file. + +You will need to keep reading from the file until it has been fully read. `feof` is your friend here to know +whether there is any data left to read. + +Documentation on the `fopen` function can be found by pointing your browser here: +[https://www.php.net/manual/en/function.fopen.php]() + +Documentation on the `fgetcsv` function can be found by pointing your browser here: +[https://www.php.net/manual/en/function.fgetcsv.php]() + +Documentation on the `feof` function can be found by pointing your browser here: +[https://www.php.net/manual/en/function.feof.php]() + +---------------------------------------------------------------------- +## EXTRA + +Although not entirely necessary for a small script, it is good practise to close +any open file handles so that other processes can access them, you can use `fclose` +for that. diff --git a/exercises/have-the-last-say/solution/solution.php b/exercises/have-the-last-say/solution/solution.php new file mode 100644 index 0000000..001d6f7 --- /dev/null +++ b/exercises/have-the-last-say/solution/solution.php @@ -0,0 +1,10 @@ +\|int\|string given\.#' + + excludes_analyse: + - src/TestUtils/WorkshopExerciseTest.php diff --git a/src/Exercise/HaveTheLastSay.php b/src/Exercise/HaveTheLastSay.php new file mode 100644 index 0000000..19f18a9 --- /dev/null +++ b/src/Exercise/HaveTheLastSay.php @@ -0,0 +1,159 @@ +getTemporaryPath(); + + $countries = [ + ['UK', 'London'], + ['Austria', 'Vienna'], + ['France', 'Paris'], + ['Turkey', 'Istanbul'], + ['Morocco', 'Rabat'], + ['Georgia', 'Tbilisi'], + ['Kyrgyzstan', 'Bishkek'], + ['Serbia', 'Belgrade'], + ['Uzbekistan', 'Tashkent'], + ['Belarus', 'Minsk'], + ]; + + file_put_contents( + $file, + collect($this->getRandomCountries($countries))->map(fn ($row) => implode("|", $row))->implode("\n") + ); + + return [ + [$file] + ]; + } + + /** + * @param array $countries + * @return array $countries + */ + private function getRandomCountries(array $countries): array + { + return array_intersect_key( + $countries, + array_flip(array_rand($countries, random_int(3, 7))) + ); + } + + public function tearDown(): void + { + unlink($this->getTemporaryPath()); + } + + public function getType(): ExerciseType + { + return new ExerciseType(ExerciseType::CLI); + } + + public function getRequiredFunctions(): array + { + return ['fopen', 'fgetcsv']; + } + + public function getBannedFunctions(): array + { + return []; + } + + public function check(Input $input): ResultInterface + { + $statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program'))); + + if (null === $statements || empty($statements)) { + return Failure::fromNameAndReason($this->getName(), 'No code was found'); + } + + $check = new FunctionRequirementsCheck($this->parser); + $result = $check->check($this, $input); + + if ($result instanceof FailureInterface) { + return $result; + } + + $funcCall = (new NodeFinder())->findFirst( + $statements, + fn ($node) => $node instanceof FuncCall && $node->name->toString() === 'fgetcsv' + ); + + if ($funcCall->args[0]->name !== null) { + return Failure::fromNameAndReason( + $this->getName(), + 'The stream argument must be specified using a positional parameter' + ); + } + + if (count($funcCall->args) > 2) { + return Failure::fromNameAndReason( + $this->getName(), + 'You should only specify the stream and separator arguments, no others' + ); + } + + if (!isset($funcCall->args[1])) { + return Failure::fromNameAndReason($this->getName(), 'The separator argument has not been specified'); + } + + if (!$funcCall->args[1]->name) { + return Failure::fromNameAndReason( + $this->getName(), + 'The second positional argument should not be specified' + ); + } + + if ($funcCall->args[1]->name->name !== 'separator') { + return Failure::fromNameAndReason( + $this->getName(), + 'A named argument has been used, but not for the separator argument' + ); + } + + return new Success($this->getName()); + } +} diff --git a/test/Exercise/HaveTheLastSayTest.php b/test/Exercise/HaveTheLastSayTest.php new file mode 100644 index 0000000..7c13dac --- /dev/null +++ b/test/Exercise/HaveTheLastSayTest.php @@ -0,0 +1,138 @@ +runExercise('solution-no-code.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure(Failure::class, 'No code was found'); + } + + public function testWithNoStreamArgument(): void + { + $this->runExercise('solution-no-stream-arg.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure( + Failure::class, + 'The stream argument must be specified using a positional parameter' + ); + } + + public function testWithStreamAsNamedArgument(): void + { + $this->runExercise('solution-stream-as-named-arg.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure( + Failure::class, + 'The stream argument must be specified using a positional parameter' + ); + } + + public function testWithSecondArgumentSpecified(): void + { + $this->runExercise('solution-second-arg-specified.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure( + Failure::class, + 'The second positional argument should not be specified' + ); + } + + public function testWithMoreThanTwoArguments(): void + { + $this->runExercise('solution-three-args-specified.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure( + Failure::class, + 'You should only specify the stream and separator arguments, no others' + ); + } + + public function testWithNoSeparatorArgumentSpecified(): void + { + $this->runExercise('solution-no-separator-arg-specified.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure( + Failure::class, + 'The separator argument has not been specified' + ); + } + + public function testWithWrongNamedArgument(): void + { + $this->runExercise('solution-wrong-named-arg.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailure( + Failure::class, + 'A named argument has been used, but not for the separator argument' + ); + } + + public function testWithNoFgetCsv(): void + { + $this->runExercise('solution-without-fgetcsv.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertResultsHasFailureAndMatches( + FunctionRequirementsFailure::class, + function (FunctionRequirementsFailure $failure) { + return ['fgetcsv'] === $failure->getMissingFunctions(); + } + ); + + $this->assertOutputWasCorrect(); + } + + public function testWithIncorrectOutput(): void + { + $this->runExercise('solution-wrong-output.php'); + + $this->assertVerifyWasNotSuccessful(); + + $this->assertOutputWasIncorrect(); + } + + public function testWithCorrectSolution(): void + { + $this->runExercise('solution-correct.php'); + + $this->assertVerifyWasSuccessful(); + } +} diff --git a/test/solutions/have-the-last-say/solution-correct.php b/test/solutions/have-the-last-say/solution-correct.php new file mode 100644 index 0000000..d48a90a --- /dev/null +++ b/test/solutions/have-the-last-say/solution-correct.php @@ -0,0 +1,8 @@ +