Skip to content

Commit 599bf0b

Browse files
authored
Merge pull request #15 from php-school/named-args
Named arguments exercise
2 parents a8d7001 + a6b5ec4 commit 599bf0b

18 files changed

+484
-16
lines changed

app/bootstrap.php

+2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
}
2121

2222
use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
23+
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
2324
use PhpSchool\PhpWorkshop\Application;
2425

2526
$app = new Application('PHP8 Appreciate', __DIR__ . '/config.php');
2627

2728
$app->addExercise(AMatchMadeInHeaven::class);
29+
$app->addExercise(HaveTheLastSay::class);
2830

2931
$art = <<<ART
3032
_ __ _

app/config.php

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use PhpSchool\PHP8Appreciate\AstService;
44
use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
5+
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
56
use Psr\Container\ContainerInterface;
67
use function DI\create;
78
use function DI\factory;
@@ -14,4 +15,8 @@
1415
AMatchMadeInHeaven::class => function (ContainerInterface $c) {
1516
return new AMatchMadeInHeaven($c->get(PhpParser\Parser::class));
1617
},
18+
HaveTheLastSay::class => function (ContainerInterface $c) {
19+
return new HaveTheLastSay($c->get(PhpParser\Parser::class));
20+
},
21+
1722
];

composer.lock

+16-16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
Create a program that reads a CSV file using `fgetcsv` and change the delimiter argument using named arguments.
2+
3+
The first argument is the file name of the CSV which you should read.
4+
5+
The CSV file contains two columns separated with `|` (the pipe operator). The first column is `country`
6+
and the second column is `capital`. You should print each row to the console like the
7+
following (with a new line after each row):
8+
9+
```
10+
Country: Austria, Capital: Vienna
11+
```
12+
13+
The list of countries will be picked at random, so the CSV will be different each time your program runs.
14+
15+
When using `fgetcsv` there's a bunch of arguments which change the behaviour of the way the CSV is parsed. In our case we
16+
want to change the `separator` argument, which defaults to a comma `,`. We need it to be the
17+
pipe `|` character.
18+
19+
We don't want to specify the rest of the arguments, so aside from the file pointer which is the first argument,
20+
we only want to specify the `separator` argument.
21+
22+
Named arguments are a great way to change argument default values without having to specify all the defaults again.
23+
For example, if you only want to change the value of the last argument to a function,
24+
you can do so, without specifying all the other arguments. For example:
25+
26+
```php
27+
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);
28+
```
29+
30+
We only want to change the last argument (double_encode) of the function to false (the default is true). However,
31+
we are forced to specify all the other arguments, but they have not changed from the defaults.
32+
33+
Named arguments allows to write the same, but in a more succinct fashion:
34+
35+
```php
36+
htmlspecialchars($string, double_encode: false);
37+
```
38+
39+
Note: only the values changed from the defaults are specified!
40+
41+
### Advantages of named arguments
42+
43+
* Possible to skip defaults in between the arguments you want to change
44+
* The code is better documented since the argument label is specified with the value, very useful for booleans
45+
46+
----------------------------------------------------------------------
47+
## HINTS
48+
49+
You will need to open the file for writing before using `fgetcsv` you can do that using `fopen`.
50+
51+
`fgetcsv` will return *one* line at a time
52+
53+
You will most likely need a loop to process all the data in the file.
54+
55+
You will need to keep reading from the file until it has been fully read. `feof` is your friend here to know
56+
whether there is any data left to read.
57+
58+
Documentation on the `fopen` function can be found by pointing your browser here:
59+
[https://www.php.net/manual/en/function.fopen.php]()
60+
61+
Documentation on the `fgetcsv` function can be found by pointing your browser here:
62+
[https://www.php.net/manual/en/function.fgetcsv.php]()
63+
64+
Documentation on the `feof` function can be found by pointing your browser here:
65+
[https://www.php.net/manual/en/function.feof.php]()
66+
67+
----------------------------------------------------------------------
68+
## EXTRA
69+
70+
Although not entirely necessary for a small script, it is good practise to close
71+
any open file handles so that other processes can access them, you can use `fclose`
72+
for that.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
$fp = fopen($argv[1], 'r');
4+
5+
while (!feof($fp)) {
6+
$row = fgetcsv($fp, separator: "|");
7+
echo "Country: {$row[0]}, Capital: {$row[1]}\n";
8+
}
9+
10+
fclose($fp);

phpstan.neon

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
parameters:
2+
treatPhpDocTypesAsCertain: false
3+
ignoreErrors:
4+
- '#Cannot access property \$args on PhpParser\\Node\|null#'
5+
- '#Call to an undefined method PhpParser\\Node\\Expr\|PhpParser\\Node\\Name\:\:toString\(\)#'
6+
- '#Parameter \#1 \$array of function array_flip expects array, array\<int, int\|string\>\|int\|string given\.#'
7+
8+
excludes_analyse:
9+
- src/TestUtils/WorkshopExerciseTest.php

src/Exercise/HaveTheLastSay.php

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PHP8Appreciate\Exercise;
6+
7+
use PhpParser\Node\Expr\FuncCall;
8+
use PhpParser\NodeFinder;
9+
use PhpParser\Parser;
10+
use PhpSchool\PhpWorkshop\Check\FunctionRequirementsCheck;
11+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
12+
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
13+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
14+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
15+
use PhpSchool\PhpWorkshop\Exercise\TemporaryDirectoryTrait;
16+
use PhpSchool\PhpWorkshop\ExerciseCheck\FunctionRequirementsExerciseCheck;
17+
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
18+
use PhpSchool\PhpWorkshop\Input\Input;
19+
use PhpSchool\PhpWorkshop\Result\Failure;
20+
use PhpSchool\PhpWorkshop\Result\FailureInterface;
21+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
22+
use PhpSchool\PhpWorkshop\Result\Success;
23+
24+
class HaveTheLastSay extends AbstractExercise implements
25+
ExerciseInterface,
26+
CliExercise,
27+
FunctionRequirementsExerciseCheck,
28+
SelfCheck
29+
{
30+
use TemporaryDirectoryTrait;
31+
32+
public function __construct(private Parser $parser)
33+
{
34+
}
35+
36+
public function getName(): string
37+
{
38+
return 'Have the Last Say';
39+
}
40+
41+
public function getDescription(): string
42+
{
43+
return 'Use named arguments to specify the last to a specific parameter';
44+
}
45+
46+
public function getArgs(): array
47+
{
48+
$file = $this->getTemporaryPath();
49+
50+
$countries = [
51+
['UK', 'London'],
52+
['Austria', 'Vienna'],
53+
['France', 'Paris'],
54+
['Turkey', 'Istanbul'],
55+
['Morocco', 'Rabat'],
56+
['Georgia', 'Tbilisi'],
57+
['Kyrgyzstan', 'Bishkek'],
58+
['Serbia', 'Belgrade'],
59+
['Uzbekistan', 'Tashkent'],
60+
['Belarus', 'Minsk'],
61+
];
62+
63+
file_put_contents(
64+
$file,
65+
collect($this->getRandomCountries($countries))->map(fn ($row) => implode("|", $row))->implode("\n")
66+
);
67+
68+
return [
69+
[$file]
70+
];
71+
}
72+
73+
/**
74+
* @param array<array{0: string, 1: string}> $countries
75+
* @return array<array{0: string, 1: string}> $countries
76+
*/
77+
private function getRandomCountries(array $countries): array
78+
{
79+
return array_intersect_key(
80+
$countries,
81+
array_flip(array_rand($countries, random_int(3, 7)))
82+
);
83+
}
84+
85+
public function tearDown(): void
86+
{
87+
unlink($this->getTemporaryPath());
88+
}
89+
90+
public function getType(): ExerciseType
91+
{
92+
return new ExerciseType(ExerciseType::CLI);
93+
}
94+
95+
public function getRequiredFunctions(): array
96+
{
97+
return ['fopen', 'fgetcsv'];
98+
}
99+
100+
public function getBannedFunctions(): array
101+
{
102+
return [];
103+
}
104+
105+
public function check(Input $input): ResultInterface
106+
{
107+
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
108+
109+
if (null === $statements || empty($statements)) {
110+
return Failure::fromNameAndReason($this->getName(), 'No code was found');
111+
}
112+
113+
$check = new FunctionRequirementsCheck($this->parser);
114+
$result = $check->check($this, $input);
115+
116+
if ($result instanceof FailureInterface) {
117+
return $result;
118+
}
119+
120+
$funcCall = (new NodeFinder())->findFirst(
121+
$statements,
122+
fn ($node) => $node instanceof FuncCall && $node->name->toString() === 'fgetcsv'
123+
);
124+
125+
if ($funcCall->args[0]->name !== null) {
126+
return Failure::fromNameAndReason(
127+
$this->getName(),
128+
'The stream argument must be specified using a positional parameter'
129+
);
130+
}
131+
132+
if (count($funcCall->args) > 2) {
133+
return Failure::fromNameAndReason(
134+
$this->getName(),
135+
'You should only specify the stream and separator arguments, no others'
136+
);
137+
}
138+
139+
if (!isset($funcCall->args[1])) {
140+
return Failure::fromNameAndReason($this->getName(), 'The separator argument has not been specified');
141+
}
142+
143+
if (!$funcCall->args[1]->name) {
144+
return Failure::fromNameAndReason(
145+
$this->getName(),
146+
'The second positional argument should not be specified'
147+
);
148+
}
149+
150+
if ($funcCall->args[1]->name->name !== 'separator') {
151+
return Failure::fromNameAndReason(
152+
$this->getName(),
153+
'A named argument has been used, but not for the separator argument'
154+
);
155+
}
156+
157+
return new Success($this->getName());
158+
}
159+
}

0 commit comments

Comments
 (0)