Skip to content

Named arguments exercise #15

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 9 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <<<ART
_ __ _
Expand Down
5 changes: 5 additions & 0 deletions app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use PhpSchool\PHP8Appreciate\AstService;
use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
use Psr\Container\ContainerInterface;
use function DI\create;
use function DI\factory;
Expand All @@ -14,4 +15,8 @@
AMatchMadeInHeaven::class => function (ContainerInterface $c) {
return new AMatchMadeInHeaven($c->get(PhpParser\Parser::class));
},
HaveTheLastSay::class => function (ContainerInterface $c) {
return new HaveTheLastSay($c->get(PhpParser\Parser::class));
},

];
32 changes: 16 additions & 16 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions exercises/have-the-last-say/problem/problem.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 10 additions & 0 deletions exercises/have-the-last-say/solution/solution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

$fp = fopen($argv[1], 'r');

while (!feof($fp)) {
$row = fgetcsv($fp, separator: "|");
echo "Country: {$row[0]}, Capital: {$row[1]}\n";
}

fclose($fp);
9 changes: 9 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
parameters:
treatPhpDocTypesAsCertain: false
ignoreErrors:
- '#Cannot access property \$args on PhpParser\\Node\|null#'
- '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:toString\(\)#'
- '#Parameter \#1 \$array of function array_flip expects array, array\<int, int\|string\>\|int\|string given\.#'

excludes_analyse:
- src/TestUtils/WorkshopExerciseTest.php
159 changes: 159 additions & 0 deletions src/Exercise/HaveTheLastSay.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PHP8Appreciate\Exercise;

use PhpParser\Node\Expr\FuncCall;
use PhpParser\NodeFinder;
use PhpParser\Parser;
use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck;
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
use PhpSchool\PhpWorkshop\Input\Input;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\Result\FailureInterface;
use PhpSchool\PhpWorkshop\Result\ResultInterface;
use PhpSchool\PhpWorkshop\Result\Success;

class HaveTheLastSay extends AbstractExercise implements
ExerciseInterface,
CliExercise,
FunctionRequirementsExerciseCheck,
SelfCheck
{
use TemporaryDirectoryTrait;

public function __construct(private Parser $parser)
{
}

public function getName(): string
{
return 'Have the Last Say';
}

public function getDescription(): string
{
return 'Use named arguments to specify the last to a specific parameter';
}

public function getArgs(): array
{
$file = $this->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<array{0: string, 1: string}> $countries
* @return array<array{0: string, 1: string}> $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());
}
}
Loading