Skip to content

Commit 4435bf4

Browse files
authored
Merge pull request #19 from php-school/caution-with-catches
Caution with Catches excercise
2 parents 3451ff4 + fbc67dd commit 4435bf4

File tree

13 files changed

+212
-11
lines changed

13 files changed

+212
-11
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
php-version: ${{ matrix.php }}
2626
tools: composer:v2,pecl
2727
extensions: pdo_sqlite
28+
ini-values: opcache.enable=0
2829

2930
- name: Install Dependencies
3031
run: composer update

app/bootstrap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
}
2121

2222
use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
23+
use PhpSchool\PHP8Appreciate\Exercise\CautionWithCatches;
2324
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
2425
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
2526
use PhpSchool\PhpWorkshop\Application;
@@ -29,6 +30,7 @@
2930
$app->addExercise(AMatchMadeInHeaven::class);
3031
$app->addExercise(HaveTheLastSay::class);
3132
$app->addExercise(PhpPromotion::class);
33+
$app->addExercise(CautionWithCatches::class);
3234

3335
$art = <<<ART
3436
_ __ _

app/config.php

Lines changed: 4 additions & 0 deletions
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\CautionWithCatches;
56
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
67
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
78
use Psr\Container\ContainerInterface;
@@ -22,4 +23,7 @@
2223
PhpPromotion::class => function (ContainerInterface $c) {
2324
return new PhpPromotion($c->get(PhpParser\Parser::class));
2425
},
26+
CautionWithCatches::class => function (ContainerInterface $c) {
27+
return new CautionWithCatches($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
28+
},
2529
];

composer.lock

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
You're writing a login validation service for a password provided as a command line argument. In this service you have access to the function `verify_password`.
2+
3+
The signature of this function is as follows:
4+
5+
```php
6+
/**
7+
* @throws InvalidPasswordException When the password is invalid
8+
*/
9+
function verify_password(string $password): bool;
10+
```
11+
12+
For this exercise this function will always throw an exception but unfortunately the exception message contains the password in plain text!
13+
14+
To pass this exercise you will need to call the `verify_password` function with the password provided, handle the exception and output `"Given password is invalid, please try again`.
15+
16+
PHP 8 allows you to handle the exception without capturing the exception itself which will ensure this message is not leaked further.
17+
18+
### The advantages of non capturing catches
19+
20+
* No unused variables
21+
* A clear way to show you don't want to make use of the exception itself
22+
* Is compatible with Catching Multiple Exception Types / Union Types
23+
24+
----------------------------------------------------------------------
25+
## HINTS
26+
27+
Documentation on the non-capturing catches feature is sparse without examples, so the RFC has the most amount of detail:
28+
[https://wiki.php.net/rfc/non-capturing_catches]()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
try {
4+
verify_password($argv[1]);
5+
} catch (InvalidPasswordException) {
6+
echo 'Given password is invalid, please try again';
7+
}

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ parameters:
33
ignoreErrors:
44
- '#Cannot access property \$args on PhpParser\\Node\|null#'
55
- '#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\.#'
6+
- '#Parameter \#1 \$array of function array_flip expects array<int\|string>, array<int, int\|string>\|int\|string given.#'
77

88
excludes_analyse:
99
- src/TestUtils/WorkshopExerciseTest.php

src/Exercise/CautionWithCatches.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PHP8Appreciate\Exercise;
6+
7+
use Faker\Generator;
8+
use PhpParser\Node\Stmt;
9+
use PhpParser\Node\Stmt\TryCatch;
10+
use PhpParser\NodeFinder;
11+
use PhpParser\Parser;
12+
use PhpSchool\PhpWorkshop\CodeInsertion;
13+
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
14+
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
15+
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
16+
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
17+
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
18+
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
19+
use PhpSchool\PhpWorkshop\Input\Input;
20+
use PhpSchool\PhpWorkshop\Patch;
21+
use PhpSchool\PhpWorkshop\Result\Failure;
22+
use PhpSchool\PhpWorkshop\Result\ResultInterface;
23+
use PhpSchool\PhpWorkshop\Result\Success;
24+
25+
class CautionWithCatches extends AbstractExercise implements
26+
ExerciseInterface,
27+
CliExercise,
28+
SelfCheck,
29+
SubmissionPatchable
30+
{
31+
private string $password = '';
32+
33+
public function __construct(private Parser $parser, private Generator $faker)
34+
{
35+
}
36+
37+
public function getName(): string
38+
{
39+
return 'Caution with Catches';
40+
}
41+
42+
public function getDescription(): string
43+
{
44+
return 'PHP 8\'s Non-capturing Catches';
45+
}
46+
47+
public function getType(): ExerciseType
48+
{
49+
return ExerciseType::CLI();
50+
}
51+
52+
public function getArgs(): array
53+
{
54+
$this->password = $this->faker->password();
55+
return [[$this->password]];
56+
}
57+
58+
public function check(Input $input): ResultInterface
59+
{
60+
/** @var array<Stmt> $statements */
61+
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));
62+
63+
/** @var TryCatch|null $tryCatch */
64+
$tryCatch = (new NodeFinder())->findFirstInstanceOf($statements, TryCatch::class);
65+
66+
if (null === $tryCatch) {
67+
return Failure::fromNameAndReason($this->getName(), 'No try/catch statement was found');
68+
}
69+
70+
if (count($tryCatch->catches) > 0 && $tryCatch->catches[0]->var !== null) {
71+
return Failure::fromNameAndReason($this->getName(), 'Exception variable was captured');
72+
}
73+
74+
return new Success($this->getName());
75+
}
76+
77+
public function getPatch(): Patch
78+
{
79+
$code = <<<CODE
80+
class InvalidPasswordException extends \RuntimeException {}
81+
function verify_password(string \$password) {
82+
throw new InvalidPasswordException(sprintf('The password "%s" is invalid', \$password));
83+
}
84+
CODE;
85+
86+
$passwordVerifyInsertion = new CodeInsertion(CodeInsertion::TYPE_BEFORE, $code);
87+
88+
return (new Patch())->withInsertion($passwordVerifyInsertion);
89+
}
90+
}

test/Exercise/.gitkeep

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PHP8AppreciateTest\Exercise;
6+
7+
use PhpSchool\PHP8Appreciate\Exercise\CautionWithCatches;
8+
use PhpSchool\PhpWorkshop\Application;
9+
use PhpSchool\PhpWorkshop\Result\Failure;
10+
use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;
11+
12+
class CautionWithCatchesTest extends WorkshopExerciseTest
13+
{
14+
15+
public function getExerciseClass(): string
16+
{
17+
return CautionWithCatches::class;
18+
}
19+
20+
public function getApplication(): Application
21+
{
22+
return require __DIR__ . '/../../app/bootstrap.php';
23+
}
24+
25+
public function testFailureWhenNoTryCatch()
26+
{
27+
$this->runExercise('no-try-catch.php');
28+
29+
$this->assertVerifyWasNotSuccessful();
30+
31+
$this->assertResultsHasFailure(Failure::class, 'No try/catch statement was found');
32+
}
33+
34+
public function testFailureWhenCapturingException()
35+
{
36+
$this->runExercise('captures-exception.php');
37+
38+
$this->assertVerifyWasNotSuccessful();
39+
40+
$this->assertResultsHasFailure(Failure::class, 'Exception variable was captured');
41+
}
42+
43+
public function testSuccessfulSolution()
44+
{
45+
$this->runExercise('solution.php');
46+
47+
$this->assertVerifyWasSuccessful();
48+
}
49+
}

0 commit comments

Comments
 (0)