Skip to content
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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
php-version: ${{ matrix.php }}
tools: composer:v2,pecl
extensions: pdo_sqlite
ini-values: opcache.enable=0

- name: Install Dependencies
run: composer update
Expand Down
2 changes: 2 additions & 0 deletions app/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
}

use PhpSchool\PHP8Appreciate\Exercise\AMatchMadeInHeaven;
use PhpSchool\PHP8Appreciate\Exercise\CautionWithCatches;
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
use PhpSchool\PhpWorkshop\Application;
Expand All @@ -29,6 +30,7 @@
$app->addExercise(AMatchMadeInHeaven::class);
$app->addExercise(HaveTheLastSay::class);
$app->addExercise(PhpPromotion::class);
$app->addExercise(CautionWithCatches::class);

$art = <<<ART
_ __ _
Expand Down
4 changes: 4 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\CautionWithCatches;
use PhpSchool\PHP8Appreciate\Exercise\HaveTheLastSay;
use PhpSchool\PHP8Appreciate\Exercise\PhpPromotion;
use Psr\Container\ContainerInterface;
Expand All @@ -22,4 +23,7 @@
PhpPromotion::class => function (ContainerInterface $c) {
return new PhpPromotion($c->get(PhpParser\Parser::class));
},
CautionWithCatches::class => function (ContainerInterface $c) {
return new CautionWithCatches($c->get(PhpParser\Parser::class), $c->get(\Faker\Generator::class));
},
];
21 changes: 11 additions & 10 deletions composer.lock

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

28 changes: 28 additions & 0 deletions exercises/caution-with-catches/problem/problem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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`.

The signature of this function is as follows:

```php
/**
* @throws InvalidPasswordException When the password is invalid
*/
function verify_password(string $password): bool;
```

For this exercise this function will always throw an exception but unfortunately the exception message contains the password in plain text!

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`.

PHP 8 allows you to handle the exception without capturing the exception itself which will ensure this message is not leaked further.

### The advantages of non capturing catches

* No unused variables
* A clear way to show you don't want to make use of the exception itself
* Is compatible with Catching Multiple Exception Types / Union Types

----------------------------------------------------------------------
## HINTS

Documentation on the non-capturing catches feature is sparse without examples, so the RFC has the most amount of detail:
[https://wiki.php.net/rfc/non-capturing_catches]()
7 changes: 7 additions & 0 deletions exercises/caution-with-catches/solution/solution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

try {
verify_password($argv[1]);
} catch (InvalidPasswordException) {
echo 'Given password is invalid, please try again';
}
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ parameters:
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\.#'
- '#Parameter \#1 \$array of function array_flip expects array<int\|string>, array<int, int\|string>\|int\|string given.#'

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

declare(strict_types=1);

namespace PhpSchool\PHP8Appreciate\Exercise;

use Faker\Generator;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\TryCatch;
use PhpParser\NodeFinder;
use PhpParser\Parser;
use PhpSchool\PhpWorkshop\CodeInsertion;
use PhpSchool\PhpWorkshop\Exercise\AbstractExercise;
use PhpSchool\PhpWorkshop\Exercise\CliExercise;
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Exercise\ExerciseType;
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
use PhpSchool\PhpWorkshop\ExerciseCheck\SelfCheck;
use PhpSchool\PhpWorkshop\Input\Input;
use PhpSchool\PhpWorkshop\Patch;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\Result\ResultInterface;
use PhpSchool\PhpWorkshop\Result\Success;

class CautionWithCatches extends AbstractExercise implements
ExerciseInterface,
CliExercise,
SelfCheck,
SubmissionPatchable
{
private string $password = '';

public function __construct(private Parser $parser, private Generator $faker)
{
}

public function getName(): string
{
return 'Caution with Catches';
}

public function getDescription(): string
{
return 'PHP 8\'s Non-capturing Catches';
}

public function getType(): ExerciseType
{
return ExerciseType::CLI();
}

public function getArgs(): array
{
$this->password = $this->faker->password();
return [[$this->password]];
}

public function check(Input $input): ResultInterface
{
/** @var array<Stmt> $statements */
$statements = $this->parser->parse((string) file_get_contents($input->getRequiredArgument('program')));

/** @var TryCatch|null $tryCatch */
$tryCatch = (new NodeFinder())->findFirstInstanceOf($statements, TryCatch::class);

if (null === $tryCatch) {
return Failure::fromNameAndReason($this->getName(), 'No try/catch statement was found');
}

if (count($tryCatch->catches) > 0 && $tryCatch->catches[0]->var !== null) {
return Failure::fromNameAndReason($this->getName(), 'Exception variable was captured');
}

return new Success($this->getName());
}

public function getPatch(): Patch
{
$code = <<<CODE
class InvalidPasswordException extends \RuntimeException {}
function verify_password(string \$password) {
throw new InvalidPasswordException(sprintf('The password "%s" is invalid', \$password));
}
CODE;

$passwordVerifyInsertion = new CodeInsertion(CodeInsertion::TYPE_BEFORE, $code);

return (new Patch())->withInsertion($passwordVerifyInsertion);
}
}
Empty file removed test/Exercise/.gitkeep
Empty file.
49 changes: 49 additions & 0 deletions test/Exercise/CautionWithCatchesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PHP8AppreciateTest\Exercise;

use PhpSchool\PHP8Appreciate\Exercise\CautionWithCatches;
use PhpSchool\PhpWorkshop\Application;
use PhpSchool\PhpWorkshop\Result\Failure;
use PhpSchool\PhpWorkshop\TestUtils\WorkshopExerciseTest;

class CautionWithCatchesTest extends WorkshopExerciseTest
{

public function getExerciseClass(): string
{
return CautionWithCatches::class;
}

public function getApplication(): Application
{
return require __DIR__ . '/../../app/bootstrap.php';
}

public function testFailureWhenNoTryCatch()
{
$this->runExercise('no-try-catch.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(Failure::class, 'No try/catch statement was found');
}

public function testFailureWhenCapturingException()
{
$this->runExercise('captures-exception.php');

$this->assertVerifyWasNotSuccessful();

$this->assertResultsHasFailure(Failure::class, 'Exception variable was captured');
}

public function testSuccessfulSolution()
{
$this->runExercise('solution.php');

$this->assertVerifyWasSuccessful();
}
}
7 changes: 7 additions & 0 deletions test/solutions/caution-with-catches/captures-exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

try {
verify_password($argv[1]);
} catch (InvalidPasswordException $e) {
echo $e->getMessage();
}
5 changes: 5 additions & 0 deletions test/solutions/caution-with-catches/no-try-catch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

if (!verify_password($argv[1])) {
echo 'Given password is invalid, please try again';
}
7 changes: 7 additions & 0 deletions test/solutions/caution-with-catches/solution.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

try {
verify_password($argv[1]);
} catch (InvalidPasswordException) {
echo 'Given password is invalid, please try again';
}